Chore/refractor (#4454)

* markdown files and env examples cleanup

* components update

* update jsonlines description

* server refractor

* update telemetry

* add execute custom node

* add ui refractor

* add username and password authenticate

* correctly retrieve past images in agentflowv2

* disable e2e temporarily

* add existing username and password authenticate

* update migration to default workspace

* update todo

* blob storage migrating

* throw error on agent tool call error

* add missing execution import

* add referral

* chore: add error message when importData is undefined

* migrate api keys to db

* fix: data too long for column executionData

* migrate api keys from json to db at init

* add info on account setup

* update docstore missing fields

---------

Co-authored-by: chungyau97 <chungyau97@gmail.com>
This commit is contained in:
Henry Heng
2025-05-27 14:29:42 +08:00
committed by GitHub
parent e35a126b46
commit 5a37227d14
560 changed files with 62127 additions and 4100 deletions
@@ -0,0 +1,435 @@
import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useSelector } from 'react-redux'
// material-ui
import {
Breadcrumbs,
Menu,
MenuItem,
Dialog,
DialogContent,
CircularProgress,
Typography,
Stack,
Chip,
ListItemText,
ListItemIcon,
Select
} from '@mui/material'
import { Check } from '@mui/icons-material'
import { alpha, styled, emphasize } from '@mui/material/styles'
import { IconChevronDown } from '@tabler/icons-react'
// api
import userApi from '@/api/user'
import workspaceApi from '@/api/workspace'
// hooks
import useApi from '@/hooks/useApi'
// store
import { store } from '@/store'
import { workspaceSwitchSuccess } from '@/store/reducers/authSlice'
// ==============================|| OrgWorkspaceBreadcrumbs ||============================== //
const StyledMenu = styled((props) => (
<Menu
elevation={0}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right'
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right'
}}
{...props}
/>
))(({ theme }) => ({
'& .MuiPaper-root': {
borderRadius: 6,
marginTop: theme.spacing(1),
minWidth: 180,
boxShadow:
'rgb(255, 255, 255) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 0px 0px 1px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px',
'& .MuiMenu-list': {
padding: '4px 0'
},
'& .MuiMenuItem-root': {
'& .MuiSvgIcon-root': {
fontSize: 18,
color: theme.palette.text.secondary,
marginRight: theme.spacing(1.5)
},
'&:active': {
backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity)
}
}
}
}))
const StyledBreadcrumb = styled(Chip)(({ theme, isDarkMode }) => {
const backgroundColor = isDarkMode ? theme.palette.grey[800] : theme.palette.grey[100]
return {
backgroundColor,
height: theme.spacing(3),
color: theme.palette.text.primary,
fontWeight: theme.typography.fontWeightRegular,
'&:hover, &:focus': {
backgroundColor: emphasize(backgroundColor, 0.06)
},
'&:active': {
boxShadow: theme.shadows[1],
backgroundColor: emphasize(backgroundColor, 0.12)
}
}
})
const OrgWorkspaceBreadcrumbs = () => {
const navigate = useNavigate()
const user = useSelector((state) => state.auth.user)
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated)
const customization = useSelector((state) => state.customization)
const [orgAnchorEl, setOrgAnchorEl] = useState(null)
const [workspaceAnchorEl, setWorkspaceAnchorEl] = useState(null)
const orgMenuOpen = Boolean(orgAnchorEl)
const workspaceMenuOpen = Boolean(workspaceAnchorEl)
const [assignedOrganizations, setAssignedOrganizations] = useState([])
const [activeOrganizationId, setActiveOrganizationId] = useState(undefined)
const [assignedWorkspaces, setAssignedWorkspaces] = useState([])
const [activeWorkspaceId, setActiveWorkspaceId] = useState(undefined)
const [isWorkspaceSwitching, setIsWorkspaceSwitching] = useState(false)
const [isOrganizationSwitching, setIsOrganizationSwitching] = useState(false)
const [showWorkspaceUnavailableDialog, setShowWorkspaceUnavailableDialog] = useState(false)
const getOrganizationsByUserIdApi = useApi(userApi.getOrganizationsByUserId)
const getWorkspacesByUserIdApi = useApi(userApi.getWorkspacesByUserId)
const switchWorkspaceApi = useApi(workspaceApi.switchWorkspace)
const handleOrgClick = (event) => {
setOrgAnchorEl(event.currentTarget)
}
const handleWorkspaceClick = (event) => {
setWorkspaceAnchorEl(event.currentTarget)
}
const handleOrgClose = () => {
setOrgAnchorEl(null)
}
const handleWorkspaceClose = () => {
setWorkspaceAnchorEl(null)
}
const handleOrgSwitch = async (orgId) => {
setOrgAnchorEl(null)
if (activeOrganizationId !== orgId) {
setIsOrganizationSwitching(true)
setActiveOrganizationId(orgId)
// Fetch workspaces for the new organization
getWorkspacesByUserIdApi.request(user.id)
}
}
const handleUnavailableOrgSwitch = async (orgId) => {
setOrgAnchorEl(null)
setActiveOrganizationId(orgId)
// Fetch workspaces for the new organization
try {
const response = await userApi.getWorkspacesByUserId(user.id)
const workspaces = response.data
const filteredAssignedWorkspaces = workspaces.filter((item) => item.workspace.organizationId === orgId)
const formattedAssignedWorkspaces = filteredAssignedWorkspaces.map((item) => ({
id: item.workspaceId,
name: item.workspace.name
}))
const sortedWorkspaces = [...formattedAssignedWorkspaces].sort((a, b) => a.name.localeCompare(b.name))
setAssignedWorkspaces(sortedWorkspaces)
} catch (error) {
console.error('Error fetching workspaces:', error)
}
}
const switchWorkspace = async (id) => {
setWorkspaceAnchorEl(null)
if (activeWorkspaceId !== id) {
setIsWorkspaceSwitching(true)
switchWorkspaceApi.request(id)
}
}
useEffect(() => {
// Fetch workspaces when component mounts
if (isAuthenticated && user) {
getOrganizationsByUserIdApi.request(user.id)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isAuthenticated, user])
useEffect(() => {
if (getWorkspacesByUserIdApi.data) {
const filteredAssignedWorkspaces = getWorkspacesByUserIdApi.data.filter(
(item) => item.workspace.organizationId === activeOrganizationId
)
const formattedAssignedWorkspaces = filteredAssignedWorkspaces.map((item) => ({
id: item.workspaceId,
name: item.workspace.name
}))
const sortedWorkspaces = [...formattedAssignedWorkspaces].sort((a, b) => a.name.localeCompare(b.name))
// Only check workspace availability if we're not in the process of switching organizations
if (!isOrganizationSwitching) {
setTimeout(() => {
if (user && user.activeWorkspaceId && !sortedWorkspaces.find((item) => item.id === user.activeWorkspaceId)) {
setShowWorkspaceUnavailableDialog(true)
}
}, 500)
}
setAssignedWorkspaces(sortedWorkspaces)
if (isOrganizationSwitching && sortedWorkspaces.length > 0) {
// After organization switch, switch to the first workspace in the list
switchWorkspaceApi.request(sortedWorkspaces[0].id)
} else {
setIsOrganizationSwitching(false)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getWorkspacesByUserIdApi.data])
useEffect(() => {
if (getWorkspacesByUserIdApi.error) {
setIsWorkspaceSwitching(false)
}
}, [getWorkspacesByUserIdApi.error])
useEffect(() => {
if (getOrganizationsByUserIdApi.data) {
const formattedAssignedOrgs = getOrganizationsByUserIdApi.data.map((organization) => ({
id: organization.organizationId,
name: `${organization.user.name || organization.user.email}'s Organization`
}))
const sortedOrgs = [...formattedAssignedOrgs].sort((a, b) => a.name.localeCompare(b.name))
// Only check workspace availability after a short delay to allow store updates to complete
setTimeout(() => {
if (user && user.activeOrganizationId && !sortedOrgs.find((item) => item.id === user.activeOrganizationId)) {
setActiveOrganizationId(undefined)
setShowWorkspaceUnavailableDialog(true)
}
}, 500)
setAssignedOrganizations(sortedOrgs)
getWorkspacesByUserIdApi.request(user.id)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getOrganizationsByUserIdApi.data])
useEffect(() => {
if (getOrganizationsByUserIdApi.error) {
setIsOrganizationSwitching(false)
}
}, [getOrganizationsByUserIdApi.error])
useEffect(() => {
if (switchWorkspaceApi.data) {
setIsWorkspaceSwitching(false)
setIsOrganizationSwitching(false)
store.dispatch(workspaceSwitchSuccess(switchWorkspaceApi.data))
// get the current path and navigate to the same after refresh
navigate('/', { replace: true })
navigate(0)
}
}, [switchWorkspaceApi.data, navigate])
useEffect(() => {
if (switchWorkspaceApi.error) {
setIsWorkspaceSwitching(false)
setIsOrganizationSwitching(false)
}
}, [switchWorkspaceApi.error])
useEffect(() => {
setActiveOrganizationId(user.activeOrganizationId)
setActiveWorkspaceId(user.activeWorkspaceId)
}, [user])
return (
<>
{isAuthenticated && user ? (
<>
<StyledMenu anchorEl={orgAnchorEl} open={orgMenuOpen} onClose={handleOrgClose}>
{assignedOrganizations.map((org) => (
<MenuItem key={org.id} onClick={() => handleOrgSwitch(org.id)} selected={org.id === activeOrganizationId}>
<ListItemText>{org.name}</ListItemText>
{org.id === activeOrganizationId && (
<ListItemIcon sx={{ minWidth: 'auto' }}>
<Check />
</ListItemIcon>
)}
</MenuItem>
))}
</StyledMenu>
<StyledMenu anchorEl={workspaceAnchorEl} open={workspaceMenuOpen} onClose={handleWorkspaceClose}>
{assignedWorkspaces.map((workspace) => (
<MenuItem
key={workspace.id}
onClick={() => switchWorkspace(workspace.id)}
selected={workspace.id === activeWorkspaceId}
>
<ListItemText>{workspace.name}</ListItemText>
{workspace.id === activeWorkspaceId && (
<ListItemIcon sx={{ minWidth: 'auto' }}>
<Check />
</ListItemIcon>
)}
</MenuItem>
))}
</StyledMenu>
<Breadcrumbs aria-label='breadcrumb'>
<StyledBreadcrumb
isDarkMode={customization.isDarkMode}
label={assignedOrganizations.find((org) => org.id === activeOrganizationId)?.name || 'Organization'}
deleteIcon={<IconChevronDown size={16} />}
onDelete={handleOrgClick}
onClick={handleOrgClick}
/>
<StyledBreadcrumb
isDarkMode={customization.isDarkMode}
label={assignedWorkspaces.find((ws) => ws.id === activeWorkspaceId)?.name || 'Workspace'}
deleteIcon={<IconChevronDown size={16} />}
onDelete={handleWorkspaceClick}
onClick={handleWorkspaceClick}
/>
</Breadcrumbs>
</>
) : null}
<Dialog open={isOrganizationSwitching} PaperProps={{ style: { backgroundColor: 'transparent', boxShadow: 'none' } }}>
<DialogContent>
<Stack spacing={2} alignItems='center'>
<CircularProgress />
<Typography variant='body1' style={{ color: 'white' }}>
Switching organization...
</Typography>
</Stack>
</DialogContent>
</Dialog>
<Dialog open={isWorkspaceSwitching} PaperProps={{ style: { backgroundColor: 'transparent', boxShadow: 'none' } }}>
<DialogContent>
<Stack spacing={2} alignItems='center'>
<CircularProgress />
<Typography variant='body1' style={{ color: 'white' }}>
Switching workspace...
</Typography>
</Stack>
</DialogContent>
</Dialog>
<Dialog
open={showWorkspaceUnavailableDialog}
disableEscapeKeyDown
disableBackdropClick
PaperProps={{
style: {
padding: '20px',
minWidth: '400px'
}
}}
>
<DialogContent>
<Stack spacing={3}>
<Typography variant='h5'>Workspace Unavailable</Typography>
{assignedWorkspaces.length > 0 && !activeOrganizationId ? (
<>
<Typography variant='body1'>
Your current workspace is no longer available. Please select another workspace to continue.
</Typography>
<Select
fullWidth
value=''
onChange={(event) => {
setShowWorkspaceUnavailableDialog(false)
switchWorkspace(event.target.value)
}}
displayEmpty
>
<MenuItem disabled value=''>
<em>Select Workspace</em>
</MenuItem>
{assignedWorkspaces.map((workspace, index) => (
<MenuItem key={index} value={workspace.id}>
{workspace.name}
</MenuItem>
))}
</Select>
</>
) : (
<>
<Typography variant='body1'>
Workspace is no longer available. Please select a different organization/workspace to continue.
</Typography>
<Select
fullWidth
value={activeOrganizationId || ''}
onChange={(event) => {
handleUnavailableOrgSwitch(event.target.value)
}}
displayEmpty
>
<MenuItem disabled value=''>
<em>Select Organization</em>
</MenuItem>
{assignedOrganizations.map((org, index) => (
<MenuItem key={index} value={org.id}>
{org.name}
</MenuItem>
))}
</Select>
{activeOrganizationId && assignedWorkspaces.length > 0 && (
<Select
fullWidth
value={activeWorkspaceId || ''}
onChange={(event) => {
setShowWorkspaceUnavailableDialog(false)
switchWorkspace(event.target.value)
}}
displayEmpty
sx={{ mt: 2 }}
>
<MenuItem disabled value=''>
<em>Select Workspace</em>
</MenuItem>
{assignedWorkspaces.map((workspace, index) => (
<MenuItem key={index} value={workspace.id}>
{workspace.name}
</MenuItem>
))}
</Select>
)}
</>
)}
</Stack>
</DialogContent>
</Dialog>
</>
)
}
OrgWorkspaceBreadcrumbs.propTypes = {}
export default OrgWorkspaceBreadcrumbs
@@ -1,10 +1,12 @@
import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction, REMOVE_DIRTY } from '@/store/actions'
import { exportData, stringify } from '@/utils/exportImport'
import useNotifier from '@/utils/useNotifier'
import PropTypes from 'prop-types'
import { useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { useDispatch, useSelector } from 'react-redux'
import { useNavigate } from 'react-router-dom'
import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction, REMOVE_DIRTY } from '@/store/actions'
import { exportData, stringify } from '@/utils/exportImport'
import useNotifier from '@/utils/useNotifier'
// material-ui
import {
@@ -35,22 +37,23 @@ import { useTheme } from '@mui/material/styles'
import PerfectScrollbar from 'react-perfect-scrollbar'
// project imports
import { PermissionListItemButton } from '@/ui-component/button/RBACButtons'
import MainCard from '@/ui-component/cards/MainCard'
import AboutDialog from '@/ui-component/dialog/AboutDialog'
import Transitions from '@/ui-component/extended/Transitions'
// assets
import ExportingGIF from '@/assets/images/Exporting.gif'
import { IconFileExport, IconFileUpload, IconInfoCircle, IconLogout, IconSettings, IconX } from '@tabler/icons-react'
import { IconFileExport, IconFileUpload, IconInfoCircle, IconLogout, IconSettings, IconUserEdit, IconX } from '@tabler/icons-react'
import './index.css'
//API
// API
import exportImportApi from '@/api/exportimport'
// Hooks
import useApi from '@/hooks/useApi'
import { useConfig } from '@/store/context/ConfigContext'
import { getErrorMessage } from '@/utils/errorHandler'
import { useNavigate } from 'react-router-dom'
const dataToExport = [
'Agentflows',
@@ -165,21 +168,60 @@ ExportDialog.propTypes = {
onExport: PropTypes.func
}
const ImportDialog = ({ show }) => {
const portalElement = document.getElementById('portal')
const component = show ? (
<Dialog open={show} fullWidth maxWidth='sm' aria-labelledby='import-dialog-title' aria-describedby='import-dialog-description'>
<DialogTitle sx={{ fontSize: '1rem' }} id='import-dialog-title'>
Importing...
</DialogTitle>
<DialogContent>
<Box sx={{ height: 'auto', display: 'flex', justifyContent: 'center', mb: 3 }}>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<img
style={{
objectFit: 'cover',
height: 'auto',
width: 'auto'
}}
src={ExportingGIF}
alt='ImportingGIF'
/>
<span>Importing data might takes a while</span>
</div>
</Box>
</DialogContent>
</Dialog>
) : null
return createPortal(component, portalElement)
}
ImportDialog.propTypes = {
show: PropTypes.bool
}
// ==============================|| PROFILE MENU ||============================== //
const ProfileSection = ({ username, handleLogout }) => {
const ProfileSection = ({ handleLogout }) => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const { isCloud } = useConfig()
const [open, setOpen] = useState(false)
const [aboutDialogOpen, setAboutDialogOpen] = useState(false)
const [exportDialogOpen, setExportDialogOpen] = useState(false)
const [importDialogOpen, setImportDialogOpen] = useState(false)
const anchorRef = useRef(null)
const inputRef = useRef()
const navigate = useNavigate()
const currentUser = useSelector((state) => state.auth.user)
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated)
const importAllApi = useApi(exportImportApi.importData)
const exportAllApi = useApi(exportImportApi.exportData)
@@ -223,6 +265,7 @@ const ProfileSection = ({ username, handleLogout }) => {
if (!e.target.files) return
const file = e.target.files[0]
setImportDialogOpen(true)
const reader = new FileReader()
reader.onload = (evt) => {
@@ -236,6 +279,7 @@ const ProfileSection = ({ username, handleLogout }) => {
}
const importAllSuccess = () => {
setImportDialogOpen(false)
dispatch({ type: REMOVE_DIRTY })
enqueueSnackbar({
message: `Import All successful`,
@@ -284,6 +328,7 @@ const ProfileSection = ({ username, handleLogout }) => {
useEffect(() => {
if (importAllApi.error) {
setImportDialogOpen(false)
let errMsg = 'Invalid Imported File'
let error = importAllApi.error
if (error?.response?.data) {
@@ -331,7 +376,6 @@ const ProfileSection = ({ username, handleLogout }) => {
if (prevOpen.current === true && open === false) {
anchorRef.current.focus()
}
prevOpen.current = open
}, [open])
@@ -380,10 +424,16 @@ const ProfileSection = ({ username, handleLogout }) => {
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MainCard border={false} elevation={16} content={false} boxShadow shadow={theme.shadows[16]}>
{username && (
{isAuthenticated && currentUser ? (
<Box sx={{ p: 2 }}>
<Typography component='span' variant='h4'>
{username}
{currentUser.name}
</Typography>
</Box>
) : (
<Box sx={{ p: 2 }}>
<Typography component='span' variant='h4'>
User
</Typography>
</Box>
)}
@@ -406,7 +456,8 @@ const ProfileSection = ({ username, handleLogout }) => {
}
}}
>
<ListItemButton
<PermissionListItemButton
permissionId='workspace:export'
sx={{ borderRadius: `${customization.borderRadius}px` }}
onClick={() => {
setExportDialogOpen(true)
@@ -416,8 +467,9 @@ const ProfileSection = ({ username, handleLogout }) => {
<IconFileExport stroke={1.5} size='1.3rem' />
</ListItemIcon>
<ListItemText primary={<Typography variant='body2'>Export</Typography>} />
</ListItemButton>
<ListItemButton
</PermissionListItemButton>
<PermissionListItemButton
permissionId='workspace:import'
sx={{ borderRadius: `${customization.borderRadius}px` }}
onClick={() => {
importAll()
@@ -427,7 +479,7 @@ const ProfileSection = ({ username, handleLogout }) => {
<IconFileUpload stroke={1.5} size='1.3rem' />
</ListItemIcon>
<ListItemText primary={<Typography variant='body2'>Import</Typography>} />
</ListItemButton>
</PermissionListItemButton>
<input ref={inputRef} type='file' hidden onChange={fileChange} accept='.json' />
<ListItemButton
sx={{ borderRadius: `${customization.borderRadius}px` }}
@@ -439,19 +491,31 @@ const ProfileSection = ({ username, handleLogout }) => {
<ListItemIcon>
<IconInfoCircle stroke={1.5} size='1.3rem' />
</ListItemIcon>
<ListItemText primary={<Typography variant='body2'>About Flowise</Typography>} />
<ListItemText primary={<Typography variant='body2'>Version</Typography>} />
</ListItemButton>
{localStorage.getItem('username') && localStorage.getItem('password') && (
{isAuthenticated && !currentUser.isSSO && !isCloud && (
<ListItemButton
sx={{ borderRadius: `${customization.borderRadius}px` }}
onClick={handleLogout}
onClick={() => {
setOpen(false)
navigate('/user-profile')
}}
>
<ListItemIcon>
<IconLogout stroke={1.5} size='1.3rem' />
<IconUserEdit stroke={1.5} size='1.3rem' />
</ListItemIcon>
<ListItemText primary={<Typography variant='body2'>Logout</Typography>} />
<ListItemText primary={<Typography variant='body2'>Update Profile</Typography>} />
</ListItemButton>
)}
<ListItemButton
sx={{ borderRadius: `${customization.borderRadius}px` }}
onClick={handleLogout}
>
<ListItemIcon>
<IconLogout stroke={1.5} size='1.3rem' />
</ListItemIcon>
<ListItemText primary={<Typography variant='body2'>Logout</Typography>} />
</ListItemButton>
</List>
</Box>
</PerfectScrollbar>
@@ -463,12 +527,12 @@ const ProfileSection = ({ username, handleLogout }) => {
</Popper>
<AboutDialog show={aboutDialogOpen} onCancel={() => setAboutDialogOpen(false)} />
<ExportDialog show={exportDialogOpen} onCancel={() => setExportDialogOpen(false)} onExport={(data) => onExport(data)} />
<ImportDialog show={importDialogOpen} />
</>
)
}
ProfileSection.propTypes = {
username: PropTypes.string,
handleLogout: PropTypes.func
}
@@ -0,0 +1,386 @@
import { useEffect, useRef, useState } from 'react'
import { useSelector } from 'react-redux'
import { useNavigate } from 'react-router-dom'
// material-ui
import { Check } from '@mui/icons-material'
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'
import {
Dialog,
DialogContent,
CircularProgress,
Button,
Select,
Typography,
Stack,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
DialogActions
} from '@mui/material'
import { alpha, styled } from '@mui/material/styles'
// api
import userApi from '@/api/user'
import workspaceApi from '@/api/workspace'
import accountApi from '@/api/account.api'
// hooks
import useApi from '@/hooks/useApi'
import { useConfig } from '@/store/context/ConfigContext'
// store
import { store } from '@/store'
import { logoutSuccess, workspaceSwitchSuccess } from '@/store/reducers/authSlice'
// ==============================|| WORKSPACE SWITCHER ||============================== //
const StyledMenu = styled((props) => (
<Menu
elevation={0}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right'
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right'
}}
{...props}
/>
))(({ theme }) => ({
'& .MuiPaper-root': {
borderRadius: 6,
marginTop: theme.spacing(1),
minWidth: 180,
boxShadow:
'rgb(255, 255, 255) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 0px 0px 1px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px',
'& .MuiMenu-list': {
padding: '4px 0'
},
'& .MuiMenuItem-root': {
'& .MuiSvgIcon-root': {
fontSize: 18,
color: theme.palette.text.secondary,
marginRight: theme.spacing(1.5)
},
'&:active': {
backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity)
}
}
}
}))
const WorkspaceSwitcher = () => {
const navigate = useNavigate()
const user = useSelector((state) => state.auth.user)
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated)
const features = useSelector((state) => state.auth.features)
const { isEnterpriseLicensed } = useConfig()
const [anchorEl, setAnchorEl] = useState(null)
const open = Boolean(anchorEl)
const prevOpen = useRef(open)
const [assignedWorkspaces, setAssignedWorkspaces] = useState([])
const [activeWorkspace, setActiveWorkspace] = useState(undefined)
const [isSwitching, setIsSwitching] = useState(false)
const [showWorkspaceUnavailableDialog, setShowWorkspaceUnavailableDialog] = useState(false)
const [showErrorDialog, setShowErrorDialog] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
const getWorkspacesByOrganizationIdUserIdApi = useApi(userApi.getWorkspacesByOrganizationIdUserId)
const getWorkspacesByUserIdApi = useApi(userApi.getWorkspacesByUserId)
const switchWorkspaceApi = useApi(workspaceApi.switchWorkspace)
const logoutApi = useApi(accountApi.logout)
const handleClick = (event) => {
setAnchorEl(event.currentTarget)
}
const handleClose = () => {
setAnchorEl(null)
}
const switchWorkspace = async (id) => {
setAnchorEl(null)
if (activeWorkspace !== id) {
setIsSwitching(true)
switchWorkspaceApi.request(id)
}
}
const handleLogout = () => {
logoutApi.request()
}
useEffect(() => {
// Fetch workspaces when component mounts
if (isAuthenticated && user) {
const WORKSPACE_FLAG = 'feat:workspaces'
if (Object.hasOwnProperty.call(features, WORKSPACE_FLAG)) {
const flag = features[WORKSPACE_FLAG] === 'true' || features[WORKSPACE_FLAG] === true
if (flag) {
if (isEnterpriseLicensed) {
getWorkspacesByOrganizationIdUserIdApi.request(user.activeOrganizationId, user.id)
} else {
getWorkspacesByUserIdApi.request(user.id)
}
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isAuthenticated, user, features, isEnterpriseLicensed])
useEffect(() => {
if (getWorkspacesByOrganizationIdUserIdApi.data) {
const formattedAssignedWorkspaces = getWorkspacesByOrganizationIdUserIdApi.data.map((item) => ({
id: item.workspaceId,
name: item.workspace.name
}))
const sortedWorkspaces = [...formattedAssignedWorkspaces].sort((a, b) => a.name.localeCompare(b.name))
// Only check workspace availability after a short delay to allow store updates to complete
setTimeout(() => {
if (user && user.activeWorkspaceId && !sortedWorkspaces.find((item) => item.id === user.activeWorkspaceId)) {
setShowWorkspaceUnavailableDialog(true)
}
}, 500)
setAssignedWorkspaces(sortWorkspaces(sortedWorkspaces))
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getWorkspacesByOrganizationIdUserIdApi.data, user.activeWorkspaceId])
useEffect(() => {
if (getWorkspacesByUserIdApi.data) {
const formattedAssignedWorkspaces = getWorkspacesByUserIdApi.data.map((item) => ({
id: item.workspaceId,
name: item.workspace.name
}))
const sortedWorkspaces = [...formattedAssignedWorkspaces].sort((a, b) => a.name.localeCompare(b.name))
// Only check workspace availability after a short delay to allow store updates to complete
setTimeout(() => {
if (user && user.activeWorkspaceId && !sortedWorkspaces.find((item) => item.id === user.activeWorkspaceId)) {
setShowWorkspaceUnavailableDialog(true)
}
}, 500)
setAssignedWorkspaces(sortWorkspaces(sortedWorkspaces))
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getWorkspacesByUserIdApi.data, user.activeWorkspaceId])
useEffect(() => {
if (switchWorkspaceApi.data) {
setIsSwitching(false)
store.dispatch(workspaceSwitchSuccess(switchWorkspaceApi.data))
// get the current path and navigate to the same after refresh
navigate('/', { replace: true })
navigate(0)
}
}, [switchWorkspaceApi.data, navigate])
useEffect(() => {
if (switchWorkspaceApi.error) {
setIsSwitching(false)
setShowWorkspaceUnavailableDialog(false)
// Set error message and show error dialog
setErrorMessage(switchWorkspaceApi.error.message || 'Failed to switch workspace')
setShowErrorDialog(true)
}
}, [switchWorkspaceApi.error])
useEffect(() => {
try {
if (logoutApi.data && logoutApi.data.message === 'logged_out') {
store.dispatch(logoutSuccess())
window.location.href = logoutApi.data.redirectTo
}
} catch (e) {
console.error(e)
}
}, [logoutApi.data])
useEffect(() => {
setActiveWorkspace(user.activeWorkspace)
prevOpen.current = open
}, [open, user])
const sortWorkspaces = (assignedWorkspaces) => {
// Sort workspaces alphabetically by name, with special characters last
const sortedWorkspaces = assignedWorkspaces
? [...assignedWorkspaces].sort((a, b) => {
const isSpecialA = /^[^a-zA-Z0-9]/.test(a.name)
const isSpecialB = /^[^a-zA-Z0-9]/.test(b.name)
// If one has special char and other doesn't, special char goes last
if (isSpecialA && !isSpecialB) return 1
if (!isSpecialA && isSpecialB) return -1
// If both are special or both are not special, sort alphabetically
return a.name.localeCompare(b.name, undefined, {
numeric: true,
sensitivity: 'base'
})
})
: []
return sortedWorkspaces
}
return (
<>
{isAuthenticated &&
user &&
assignedWorkspaces?.length > 1 &&
!(assignedWorkspaces.length === 1 && user.activeWorkspace === 'Default Workspace') ? (
<>
<Button
sx={{ mr: 4 }}
id='workspace-switcher'
aria-controls={open ? 'workspace-switcher-menu' : undefined}
aria-haspopup='true'
aria-expanded={open ? 'true' : undefined}
disableElevation
onClick={handleClick}
endIcon={<KeyboardArrowDownIcon />}
>
{user.activeWorkspace}
</Button>
<StyledMenu
id='workspace-switcher-menu'
MenuListProps={{
'aria-labelledby': 'workspace-switcher'
}}
anchorEl={anchorEl}
open={open}
onClose={handleClose}
>
{assignedWorkspaces.map((item, index) => (
<MenuItem
onClick={() => {
switchWorkspace(item.id)
}}
key={index}
disableRipple
>
{item.id === user.activeWorkspaceId ? (
<>
<ListItemIcon>
<Check />
</ListItemIcon>
<ListItemText>{item.name}</ListItemText>
</>
) : (
<ListItemText inset>{item.name}</ListItemText>
)}
</MenuItem>
))}
</StyledMenu>
</>
) : null}
<Dialog open={isSwitching} PaperProps={{ style: { backgroundColor: 'transparent', boxShadow: 'none' } }}>
<DialogContent>
<Stack spacing={2} alignItems='center'>
<CircularProgress />
<Typography variant='body1' style={{ color: 'white' }}>
Switching workspace...
</Typography>
</Stack>
</DialogContent>
</Dialog>
<Dialog
open={showWorkspaceUnavailableDialog}
disableEscapeKeyDown
disableBackdropClick
PaperProps={{
style: {
padding: '20px',
minWidth: '400px'
}
}}
>
<DialogContent>
<Stack spacing={3}>
<Typography variant='h5'>Workspace Unavailable</Typography>
<Typography variant='body1'>
Your current workspace is no longer available. Please select another workspace to continue.
</Typography>
<Select
fullWidth
value=''
onChange={(event) => {
setShowWorkspaceUnavailableDialog(false)
switchWorkspace(event.target.value)
}}
displayEmpty
>
<MenuItem disabled value=''>
<em>Select Workspace</em>
</MenuItem>
{assignedWorkspaces.map((workspace, index) => (
<MenuItem key={index} value={workspace.id}>
{workspace.name}
</MenuItem>
))}
</Select>
</Stack>
</DialogContent>
{assignedWorkspaces.length === 0 && (
<DialogActions>
<Button onClick={handleLogout} variant='contained' color='primary'>
Logout
</Button>
</DialogActions>
)}
</Dialog>
{/* Error Dialog */}
<Dialog
open={showErrorDialog}
disableEscapeKeyDown
disableBackdropClick
PaperProps={{
style: {
padding: '20px',
minWidth: '400px'
}
}}
>
<DialogContent>
<Stack spacing={3}>
<Typography variant='h5'>Workspace Switch Error</Typography>
<Typography variant='body1'>{errorMessage}</Typography>
{isEnterpriseLicensed && (
<Typography variant='body2' color='text.secondary'>
Please contact your administrator for assistance.
</Typography>
)}
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={handleLogout} variant='contained' color='primary'>
Logout
</Button>
</DialogActions>
</Dialog>
</>
)
}
WorkspaceSwitcher.propTypes = {}
export default WorkspaceSwitcher
@@ -1,22 +1,35 @@
import PropTypes from 'prop-types'
import { useSelector, useDispatch } from 'react-redux'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
// material-ui
import { useTheme } from '@mui/material/styles'
import { Avatar, Box, ButtonBase, Switch } from '@mui/material'
import { styled } from '@mui/material/styles'
import { Button, Avatar, Box, ButtonBase, Switch, Typography, Link } from '@mui/material'
import { useTheme, styled, darken } from '@mui/material/styles'
// project imports
import LogoSection from '../LogoSection'
import ProfileSection from './ProfileSection'
import WorkspaceSwitcher from '@/layout/MainLayout/Header/WorkspaceSwitcher'
import OrgWorkspaceBreadcrumbs from '@/layout/MainLayout/Header/OrgWorkspaceBreadcrumbs'
import PricingDialog from '@/ui-component/subscription/PricingDialog'
// assets
import { IconMenu2 } from '@tabler/icons-react'
import { IconMenu2, IconX, IconSparkles } from '@tabler/icons-react'
// store
import { store } from '@/store'
import { SET_DARKMODE } from '@/store/actions'
import { useConfig } from '@/store/context/ConfigContext'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'
import { logoutSuccess } from '@/store/reducers/authSlice'
// API
import accountApi from '@/api/account.api'
// Hooks
import useApi from '@/hooks/useApi'
import useNotifier from '@/utils/useNotifier'
// ==============================|| MAIN NAVBAR / HEADER ||============================== //
@@ -67,14 +80,87 @@ const MaterialUISwitch = styled(Switch)(({ theme }) => ({
}
}))
const GitHubStarButton = ({ starCount, isDark }) => {
const theme = useTheme()
const formattedStarCount = starCount.toLocaleString()
return (
<Link href='https://github.com/FlowiseAI/Flowise' target='_blank' underline='none' sx={{ display: 'inline-flex' }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
borderRadius: '3px',
overflow: 'hidden',
border: `1px solid ${isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}`,
fontSize: '12px',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif',
fontWeight: 600,
lineHeight: 1
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
padding: '3px 10px',
backgroundColor: isDark ? darken(theme.palette.background.paper, 0.2) : '#f6f8fa',
color: isDark ? '#c9d1d9' : '#24292e',
borderRight: `1px solid ${isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}`
}}
>
<svg height='16' width='16' viewBox='0 0 16 16' style={{ marginRight: '4px', fill: isDark ? '#c9d1d9' : '#24292e' }}>
<path
fillRule='evenodd'
d='M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z'
></path>
</svg>
<Typography variant='caption' sx={{ fontWeight: 600, color: isDark ? 'white' : theme.palette.text.primary }}>
Star
</Typography>
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
padding: '3px 10px',
backgroundColor: isDark ? theme.palette.background.paper : 'white'
}}
>
<Typography variant='caption' sx={{ fontWeight: 600, color: isDark ? 'white' : theme.palette.text.primary }}>
{formattedStarCount}
</Typography>
</Box>
</Box>
</Link>
)
}
GitHubStarButton.propTypes = {
starCount: PropTypes.number.isRequired,
isDark: PropTypes.bool.isRequired
}
const Header = ({ handleLeftDrawerToggle }) => {
const theme = useTheme()
const navigate = useNavigate()
const customization = useSelector((state) => state.customization)
const logoutApi = useApi(accountApi.logout)
const [isDark, setIsDark] = useState(customization.isDarkMode)
const dispatch = useDispatch()
const { isEnterpriseLicensed, isCloud, isOpenSource } = useConfig()
const currentUser = useSelector((state) => state.auth.user)
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated)
const [isPricingOpen, setIsPricingOpen] = useState(false)
const [starCount, setStarCount] = useState(0)
useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const changeDarkMode = () => {
dispatch({ type: SET_DARKMODE, isDarkMode: !isDark })
@@ -83,15 +169,52 @@ const Header = ({ handleLeftDrawerToggle }) => {
}
const signOutClicked = () => {
localStorage.removeItem('username')
localStorage.removeItem('password')
navigate('/', { replace: true })
navigate(0)
logoutApi.request()
enqueueSnackbar({
message: 'Logging out...',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
useEffect(() => {
try {
if (logoutApi.data && logoutApi.data.message === 'logged_out') {
store.dispatch(logoutSuccess())
window.location.href = logoutApi.data.redirectTo
}
} catch (e) {
console.error(e)
}
}, [logoutApi.data])
useEffect(() => {
if (isCloud || isOpenSource) {
const fetchStarCount = async () => {
try {
const response = await fetch('https://api.github.com/repos/FlowiseAI/Flowise')
const data = await response.json()
if (data.stargazers_count) {
setStarCount(data.stargazers_count)
}
} catch (error) {
setStarCount(0)
}
}
fetchStarCount()
}
}, [isCloud, isOpenSource])
return (
<>
{/* logo & toggler button */}
<Box
sx={{
width: 228,
@@ -104,31 +227,91 @@ const Header = ({ handleLeftDrawerToggle }) => {
<Box component='span' sx={{ display: { xs: 'none', md: 'block' }, flexGrow: 1 }}>
<LogoSection />
</Box>
<ButtonBase sx={{ borderRadius: '12px', overflow: 'hidden' }}>
<Avatar
variant='rounded'
sx={{
...theme.typography.commonAvatar,
...theme.typography.mediumAvatar,
transition: 'all .2s ease-in-out',
background: theme.palette.secondary.light,
color: theme.palette.secondary.dark,
'&:hover': {
background: theme.palette.secondary.dark,
color: theme.palette.secondary.light
}
}}
onClick={handleLeftDrawerToggle}
color='inherit'
>
<IconMenu2 stroke={1.5} size='1.3rem' />
</Avatar>
</ButtonBase>
{isAuthenticated && (
<ButtonBase sx={{ borderRadius: '12px', overflow: 'hidden' }}>
<Avatar
variant='rounded'
sx={{
...theme.typography.commonAvatar,
...theme.typography.mediumAvatar,
transition: 'all .2s ease-in-out',
background: theme.palette.secondary.light,
color: theme.palette.secondary.dark,
'&:hover': {
background: theme.palette.secondary.dark,
color: theme.palette.secondary.light
}
}}
onClick={handleLeftDrawerToggle}
color='inherit'
>
<IconMenu2 stroke={1.5} size='1.3rem' />
</Avatar>
</ButtonBase>
)}
</Box>
<Box sx={{ flexGrow: 1 }} />
{isCloud || isOpenSource ? (
<Box
sx={{
flexGrow: 1,
px: 4,
display: 'flex',
alignItems: 'center',
'& span': {
display: 'flex',
alignItems: 'center'
}
}}
>
<GitHubStarButton starCount={starCount} isDark={isDark} />
</Box>
) : (
<Box sx={{ flexGrow: 1 }} />
)}
{isEnterpriseLicensed && isAuthenticated && <WorkspaceSwitcher />}
{isCloud && isAuthenticated && <OrgWorkspaceBreadcrumbs />}
{isCloud && currentUser?.isOrganizationAdmin && (
<Button
variant='contained'
sx={{
mr: 1,
ml: 2,
borderRadius: 15,
background: (theme) =>
`linear-gradient(90deg, ${theme.palette.primary.main} 10%, ${theme.palette.secondary.main} 100%)`,
color: (theme) => theme.palette.secondary.contrastText,
boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
transition: 'all 0.3s ease',
'&:hover': {
background: (theme) =>
`linear-gradient(90deg, ${darken(theme.palette.primary.main, 0.1)} 10%, ${darken(
theme.palette.secondary.main,
0.1
)} 100%)`,
boxShadow: '0 4px 8px rgba(0,0,0,0.3)'
}
}}
onClick={() => setIsPricingOpen(true)}
startIcon={<IconSparkles size={20} />}
>
Upgrade
</Button>
)}
{isPricingOpen && isCloud && (
<PricingDialog
open={isPricingOpen}
onClose={(planUpdated) => {
setIsPricingOpen(false)
if (planUpdated) {
navigate('/')
navigate(0)
}
}}
/>
)}
<MaterialUISwitch checked={isDark} onChange={changeDarkMode} />
<Box sx={{ ml: 2 }}></Box>
<ProfileSection handleLogout={signOutClicked} username={localStorage.getItem('username') ?? ''} />
<ProfileSection handleLogout={signOutClicked} />
</>
)
}
@@ -0,0 +1,111 @@
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'
import { store } from '@/store'
// material-ui
import { Divider, Box, Button, List, ListItemButton, ListItemIcon, Typography } from '@mui/material'
import { useTheme } from '@mui/material/styles'
// project imports
import useNotifier from '@/utils/useNotifier'
import { useConfig } from '@/store/context/ConfigContext'
// API
import { logoutSuccess } from '@/store/reducers/authSlice'
// Hooks
import useApi from '@/hooks/useApi'
// icons
import { IconFileText, IconLogout, IconX } from '@tabler/icons-react'
import accountApi from '@/api/account.api'
const CloudMenuList = () => {
const customization = useSelector((state) => state.customization)
const dispatch = useDispatch()
useNotifier()
const theme = useTheme()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const logoutApi = useApi(accountApi.logout)
const { isCloud } = useConfig()
const signOutClicked = () => {
logoutApi.request()
enqueueSnackbar({
message: 'Logging out...',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
useEffect(() => {
try {
if (logoutApi.data && logoutApi.data.message === 'logged_out') {
store.dispatch(logoutSuccess())
window.location.href = logoutApi.data.redirectTo
}
} catch (e) {
console.error(e)
}
}, [logoutApi.data])
return (
<>
{isCloud && (
<Box>
<Divider sx={{ height: '1px', borderColor: theme.palette.grey[900] + 25, my: 0 }} />
<List sx={{ p: '16px', py: 2, display: 'flex', flexDirection: 'column', gap: 1 }}>
<a href='https://docs.flowiseai.com' target='_blank' rel='noreferrer' style={{ textDecoration: 'none' }}>
<ListItemButton
sx={{
borderRadius: `${customization.borderRadius}px`,
alignItems: 'flex-start',
backgroundColor: 'inherit',
py: 1.25,
pl: '24px'
}}
>
<ListItemIcon sx={{ my: 'auto', minWidth: 36 }}>
<IconFileText size='1.3rem' strokeWidth='1.5' />
</ListItemIcon>
<Typography variant='body1' color='inherit' sx={{ my: 0.5 }}>
Documentation
</Typography>
</ListItemButton>
</a>
<ListItemButton
onClick={signOutClicked}
sx={{
borderRadius: `${customization.borderRadius}px`,
alignItems: 'flex-start',
backgroundColor: 'inherit',
py: 1.25,
pl: '24px'
}}
>
<ListItemIcon sx={{ my: 'auto', minWidth: 36 }}>
<IconLogout size='1.3rem' strokeWidth='1.5' />
</ListItemIcon>
<Typography variant='body1' color='inherit' sx={{ my: 0.5 }}>
Logout
</Typography>
</ListItemButton>
</List>
</Box>
)}
</>
)
}
export default CloudMenuList
@@ -7,19 +7,25 @@ import { Divider, List, Typography } from '@mui/material'
// project imports
import NavItem from '../NavItem'
import NavCollapse from '../NavCollapse'
import { useAuth } from '@/hooks/useAuth'
import { Available } from '@/ui-component/rbac/available'
// ==============================|| SIDEBAR MENU LIST GROUP ||============================== //
const NavGroup = ({ item }) => {
const theme = useTheme()
const { hasPermission, hasDisplay } = useAuth()
// menu list collapse & items
const items = item.children?.map((menu) => {
const listItems = (menu, level = 1) => {
// Filter based on display and permission
if (!shouldDisplayMenu(menu)) return null
// Handle item and group types
switch (menu.type) {
case 'collapse':
return <NavCollapse key={menu.id} menu={menu} level={1} />
return <NavCollapse key={menu.id} menu={menu} level={level} />
case 'item':
return <NavItem key={menu.id} item={menu} level={1} navType='MENU' />
return <NavItem key={menu.id} item={menu} level={level} navType='MENU' />
default:
return (
<Typography key={menu.id} variant='h6' color='error' align='center'>
@@ -27,7 +33,40 @@ const NavGroup = ({ item }) => {
</Typography>
)
}
})
}
const shouldDisplayMenu = (menu) => {
// Handle permission check
if (menu.permission && !hasPermission(menu.permission)) {
return false // Do not render if permission is lacking
}
// If `display` is defined, check against cloud/enterprise conditions
if (menu.display) {
const shouldsiplay = hasDisplay(menu.display)
return shouldsiplay
}
// If `display` is not defined, display by default
return true
}
const renderPrimaryItems = () => {
const primaryGroup = item.children.find((child) => child.id === 'primary')
return primaryGroup.children
}
const renderNonPrimaryGroups = () => {
let nonprimaryGroups = item.children.filter((child) => child.id !== 'primary')
// Display chilren based on permission and display
nonprimaryGroups = nonprimaryGroups.map((group) => {
const children = group.children.filter((menu) => shouldDisplayMenu(menu))
return { ...group, children }
})
// Get rid of group with empty children
nonprimaryGroups = nonprimaryGroups.filter((group) => group.children.length > 0)
return nonprimaryGroups
}
return (
<>
@@ -44,13 +83,31 @@ const NavGroup = ({ item }) => {
</Typography>
)
}
sx={{ py: '20px' }}
sx={{ p: '16px', py: 2, display: 'flex', flexDirection: 'column', gap: 1 }}
>
{items}
{renderPrimaryItems().map((menu) => listItems(menu))}
</List>
{/* group divider */}
<Divider sx={{ mt: 0.25, mb: 1.25 }} />
{renderNonPrimaryGroups().map((group) => {
const groupPermissions = group.children.map((menu) => menu.permission).join(',')
return (
<Available key={group.id} permission={groupPermissions}>
<>
<Divider sx={{ height: '1px', borderColor: theme.palette.grey[900] + 25, my: 0 }} />
<List
subheader={
<Typography variant='caption' sx={{ ...theme.typography.subMenuCaption }} display='block' gutterBottom>
{group.title}
</Typography>
}
sx={{ p: '16px', py: 2, display: 'flex', flexDirection: 'column', gap: 1 }}
>
{group.children.map((menu) => listItems(menu))}
</List>
</>
</Available>
)
})}
</>
)
}
@@ -101,7 +101,6 @@ const NavItem = ({ item, level, navType, onClick, onUploadFile }) => {
disabled={item.disabled}
sx={{
borderRadius: `${customization.borderRadius}px`,
mb: 0.5,
alignItems: 'flex-start',
backgroundColor: level > 1 ? 'transparent !important' : 'inherit',
py: level > 1 ? 1 : 1.25,
@@ -1,14 +1,14 @@
// material-ui
import { Typography } from '@mui/material'
import { Box, Typography } from '@mui/material'
// project imports
import NavGroup from './NavGroup'
import menuItem from '@/menu-items'
import { menuItems } from '@/menu-items'
// ==============================|| SIDEBAR MENU LIST ||============================== //
const MenuList = () => {
const navItems = menuItem.items.map((item) => {
const navItems = menuItems.items.map((item) => {
switch (item.type) {
case 'group':
return <NavGroup key={item.id} item={item} />
@@ -21,7 +21,7 @@ const MenuList = () => {
}
})
return <>{navItems}</>
return <Box>{navItems}</Box>
}
export default MenuList
@@ -0,0 +1,58 @@
import { Box, Skeleton, Typography } from '@mui/material'
import { useTheme } from '@mui/material/styles'
import PropTypes from 'prop-types'
import { StyledButton } from '@/ui-component/button/StyledButton'
const TrialInfo = ({ billingPortalUrl, isLoading, paymentMethodExists, trialDaysLeft }) => {
const theme = useTheme()
return (
<Box
sx={{
p: '24px',
py: 2,
display: 'flex',
flexDirection: 'column',
alignItems: 'start',
gap: 2,
borderTop: 1,
borderBottom: '1px solid',
borderColor: theme.palette.grey[900] + 25,
width: '100%'
}}
>
{isLoading ? (
<Box display='flex' flexDirection='column' gap={1} sx={{ width: '100%' }}>
<Skeleton width='100%' height={32} />
<Skeleton width='100%' height={32} />
</Box>
) : (
<>
<Typography variant='body1' color='inherit' sx={{ lineHeight: '1.5' }}>
There are{' '}
<Typography variant='' color='error'>
{trialDaysLeft} days left
</Typography>{' '}
in your trial. {!paymentMethodExists ? 'Update your payment method to avoid service interruption.' : ''}
</Typography>
{!paymentMethodExists && (
<a href={billingPortalUrl} target='_blank' rel='noreferrer' style={{ width: '100%' }}>
<StyledButton variant='contained' sx={{ borderRadius: 2, height: 32, width: '100%' }}>
Update Payment Method
</StyledButton>
</a>
)}
</>
)}
</Box>
)
}
TrialInfo.propTypes = {
billingPortalUrl: PropTypes.string,
isLoading: PropTypes.bool,
paymentMethodExists: PropTypes.bool,
trialDaysLeft: PropTypes.number
}
export default TrialInfo
@@ -1,4 +1,5 @@
import PropTypes from 'prop-types'
import { useSelector } from 'react-redux'
// material-ui
import { useTheme } from '@mui/material/styles'
@@ -11,6 +12,9 @@ import { BrowserView, MobileView } from 'react-device-detect'
// project imports
import MenuList from './MenuList'
import LogoSection from '../LogoSection'
import CloudMenuList from '@/layout/MainLayout/Sidebar/CloudMenuList'
// store
import { drawerWidth, headerHeight } from '@/store/constant'
// ==============================|| SIDEBAR DRAWER ||============================== //
@@ -18,6 +22,7 @@ import { drawerWidth, headerHeight } from '@/store/constant'
const Sidebar = ({ drawerOpen, drawerToggle, window }) => {
const theme = useTheme()
const matchUpMd = useMediaQuery(theme.breakpoints.up('md'))
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated)
const drawer = (
<>
@@ -36,16 +41,18 @@ const Sidebar = ({ drawerOpen, drawerToggle, window }) => {
component='div'
style={{
height: !matchUpMd ? 'calc(100vh - 56px)' : `calc(100vh - ${headerHeight}px)`,
paddingLeft: '16px',
paddingRight: '16px'
display: 'flex',
flexDirection: 'column'
}}
>
<MenuList />
<CloudMenuList />
</PerfectScrollbar>
</BrowserView>
<MobileView>
<Box sx={{ px: 2 }}>
<MenuList />
<CloudMenuList />
</Box>
</MobileView>
</>
@@ -62,30 +69,31 @@ const Sidebar = ({ drawerOpen, drawerToggle, window }) => {
}}
aria-label='mailbox folders'
>
<Drawer
container={container}
variant={matchUpMd ? 'persistent' : 'temporary'}
anchor='left'
open={drawerOpen}
onClose={drawerToggle}
sx={{
'& .MuiDrawer-paper': {
width: drawerWidth,
background: theme.palette.background.default,
color: theme.palette.text.primary,
[theme.breakpoints.up('md')]: {
top: `${headerHeight}px`
},
borderRight: drawerOpen ? '1px solid' : 'none',
borderColor: drawerOpen ? theme.palette.primary[200] + 75 : 'transparent',
zIndex: 1000
}
}}
ModalProps={{ keepMounted: true }}
color='inherit'
>
{drawer}
</Drawer>
{isAuthenticated && (
<Drawer
container={container}
variant={matchUpMd ? 'persistent' : 'temporary'}
anchor='left'
open={drawerOpen}
onClose={drawerToggle}
sx={{
'& .MuiDrawer-paper': {
width: drawerWidth,
background: theme.palette.background.default,
color: theme.palette.text.primary,
[theme.breakpoints.up('md')]: {
top: `${headerHeight}px`
},
borderRight: drawerOpen ? '1px solid' : 'none',
borderColor: drawerOpen ? theme.palette.grey[900] + 25 : 'transparent'
}
}}
ModalProps={{ keepMounted: true }}
color='inherit'
>
{drawer}
</Drawer>
)}
</Box>
)
}
+1 -1
View File
@@ -86,7 +86,7 @@ const MainLayout = () => {
transition: leftDrawerOpened ? theme.transitions.create('width') : 'none'
}}
>
<Toolbar sx={{ height: `${headerHeight}px`, borderBottom: '1px solid', borderColor: theme.palette.primary[200] + 75 }}>
<Toolbar sx={{ height: `${headerHeight}px`, borderBottom: '1px solid', borderColor: theme.palette.grey[900] + 25 }}>
<Header handleLeftDrawerToggle={handleLeftDrawerToggle} />
</Toolbar>
</AppBar>