mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 15:00:57 +03:00
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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user