All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Introduced NavigationTabsContext to manage tab state and navigation within the dashboard. - Added DashboardTabs component for improved tabbed navigation, allowing users to add, close, and reorder tabs. - Enhanced Dashboard layout to conditionally render tab panes based on the active tab, improving user experience. - Updated various components to utilize navigation tab context, ensuring consistent title management and tab interactions. - Implemented drag-and-drop functionality for tabs, enabling users to rearrange their workspace effectively. - Refactored CSS styles for dashboard tabs and panes to ensure proper layout and responsiveness across devices.
509 lines
16 KiB
JavaScript
509 lines
16 KiB
JavaScript
// DashboardNavigation.js
|
|
import { useContext, useEffect, useState, useMemo } from 'react'
|
|
import {
|
|
Menu,
|
|
Flex,
|
|
Tag,
|
|
Space,
|
|
Button,
|
|
Badge,
|
|
Divider,
|
|
Typography,
|
|
Popover
|
|
} from 'antd'
|
|
import { LoadingOutlined } from '@ant-design/icons'
|
|
import { AuthContext } from '../context/AuthContext'
|
|
import { SpotlightContext } from '../context/SpotlightContext'
|
|
import { ApiServerContext } from '../context/ApiServerContext'
|
|
import { NotificationContext } from '../context/NotificationContext'
|
|
import Tooltip from './Tooltip'
|
|
import { useNavigate, useLocation } from 'react-router-dom'
|
|
import { Header } from 'antd/es/layout/layout'
|
|
import { useMediaQuery } from 'react-responsive'
|
|
import KeyboardShortcut from './KeyboardShortcut'
|
|
import UserProfilePopover from './UserProfilePopover'
|
|
|
|
import FarmControlLogo from '../../Logos/FarmControlLogo'
|
|
import FarmControlLogoSmall from '../../Logos/FarmControlLogoSmall'
|
|
import MenuIcon from '../../Icons/MenuIcon'
|
|
import ProductionIcon from '../../Icons/ProductionIcon'
|
|
import InventoryIcon from '../../Icons/InventoryIcon'
|
|
import FinanceIcon from '../../Icons/FinanceIcon'
|
|
import SalesIcon from '../../Icons/SalesIcon'
|
|
import PersonIcon from '../../Icons/PersonIcon'
|
|
import CloudIcon from '../../Icons/CloudIcon'
|
|
import BellIcon from '../../Icons/BellIcon'
|
|
import SearchIcon from '../../Icons/SearchIcon'
|
|
import SettingsIcon from '../../Icons/SettingsIcon'
|
|
import DeveloperIcon from '../../Icons/DeveloperIcon'
|
|
import { ElectronContext } from '../context/ElectronContext'
|
|
import DashboardWindowButtons from './DashboardWindowButtons'
|
|
import WindowAppMenu from './WindowAppMenu'
|
|
import WebAppSwitcher from './WebAppSwitcher'
|
|
import DashboardTabs from './DashboardTabs'
|
|
import {
|
|
filterSidebarItemsByListPermission,
|
|
getSidebarDefaultPath,
|
|
getSidebarItems,
|
|
getSidebarMenuSections,
|
|
getSidebarSelectedKey,
|
|
isSidebarSectionVisible
|
|
} from '../../../database/Sidebars'
|
|
import { getSidebarIconNode } from '../../Icons/sidebarIconMap'
|
|
|
|
import { useAppUpdateContext } from '../context/AppUpdateContext'
|
|
import { useThemeContext } from '../context/ThemeContext'
|
|
import { getEffectiveAppearance } from '../../../database/Settings'
|
|
|
|
const { Text } = Typography
|
|
|
|
const mapSidebarItemsToMenu = (items, navigate) =>
|
|
items.map((item) => {
|
|
if (item?.type === 'divider') {
|
|
return item
|
|
}
|
|
|
|
const mappedItem = {
|
|
key: item.key,
|
|
icon: item.icon || getSidebarIconNode(item.iconKey),
|
|
label: item.label
|
|
}
|
|
|
|
if (item.path) {
|
|
mappedItem.onClick = () => navigate(item.path)
|
|
}
|
|
|
|
if (item?.children && Array.isArray(item.children)) {
|
|
mappedItem.children = mapSidebarItemsToMenu(item.children, navigate)
|
|
}
|
|
|
|
return mappedItem
|
|
})
|
|
|
|
const DashboardNavigation = () => {
|
|
const { userProfile } = useContext(AuthContext)
|
|
const { showSpotlight } = useContext(SpotlightContext)
|
|
const { connecting, connected, userSettings, userSettingsLoaded } =
|
|
useContext(ApiServerContext)
|
|
const { authenticated } = useContext(AuthContext)
|
|
const {
|
|
showNavigationLabels,
|
|
setShowNavigationLabels,
|
|
setThemeMode,
|
|
setDensityMode
|
|
} = useThemeContext()
|
|
const { toggleNotificationCenter, unreadCount } =
|
|
useContext(NotificationContext)
|
|
const [apiServerState, setApiServerState] = useState('disconnected')
|
|
const navigate = useNavigate()
|
|
const location = useLocation()
|
|
const [selectedKey, setSelectedKey] = useState('production')
|
|
const isMobile = useMediaQuery({ maxWidth: 768 })
|
|
const {
|
|
platform,
|
|
isElectron,
|
|
getAppSettings,
|
|
setSidebarViewMenu,
|
|
isFullScreen,
|
|
isMaximized
|
|
} = useContext(ElectronContext)
|
|
const { availableUpdate, checkForUpdates } = useAppUpdateContext()
|
|
|
|
useEffect(() => {
|
|
if (!userSettingsLoaded) return
|
|
|
|
const hydrateAppearance = async () => {
|
|
const electronSettings = isElectron ? await getAppSettings() : {}
|
|
const effective = getEffectiveAppearance({
|
|
isElectron,
|
|
userAppearance: userSettings?.appearance || {},
|
|
electronSettings
|
|
})
|
|
if (effective.theme) setThemeMode(effective.theme)
|
|
if (effective.density) setDensityMode(effective.density)
|
|
if (effective.showNavigationLabels !== undefined) {
|
|
setShowNavigationLabels(effective.showNavigationLabels)
|
|
}
|
|
}
|
|
|
|
void hydrateAppearance()
|
|
}, [
|
|
getAppSettings,
|
|
isElectron,
|
|
setDensityMode,
|
|
setShowNavigationLabels,
|
|
setThemeMode,
|
|
userSettings?.appearance,
|
|
userSettingsLoaded
|
|
])
|
|
|
|
const includeDev = import.meta.env.DEV
|
|
const includeDevItems = import.meta.env.MODE === 'development'
|
|
const mainMenuItems = useMemo(() => {
|
|
const iconStyles = {
|
|
fontSize: !showNavigationLabels ? '16px' : undefined,
|
|
marginLeft: !showNavigationLabels ? '10px' : undefined
|
|
}
|
|
const navigationIcon = (icon, label) =>
|
|
showNavigationLabels ? (
|
|
icon
|
|
) : (
|
|
<Tooltip title={label} listenParents={1}>
|
|
{icon}
|
|
</Tooltip>
|
|
)
|
|
|
|
return [
|
|
{
|
|
key: 'production',
|
|
label: 'Production',
|
|
className: 'electrobun-webkit-app-region-no-drag',
|
|
icon: navigationIcon(
|
|
<ProductionIcon style={iconStyles} />,
|
|
'Production'
|
|
)
|
|
},
|
|
{
|
|
key: 'inventory',
|
|
label: 'Inventory',
|
|
className: 'electrobun-webkit-app-region-no-drag',
|
|
icon: navigationIcon(<InventoryIcon style={iconStyles} />, 'Inventory')
|
|
},
|
|
{
|
|
key: 'sales',
|
|
label: 'Sales',
|
|
className: 'electrobun-webkit-app-region-no-drag',
|
|
icon: navigationIcon(<SalesIcon style={iconStyles} />, 'Sales')
|
|
},
|
|
{
|
|
key: 'finance',
|
|
label: 'Finance',
|
|
className: 'electrobun-webkit-app-region-no-drag',
|
|
icon: navigationIcon(<FinanceIcon style={iconStyles} />, 'Finance')
|
|
},
|
|
{
|
|
key: 'management',
|
|
label: 'Management',
|
|
className: 'electrobun-webkit-app-region-no-drag',
|
|
icon: navigationIcon(<SettingsIcon style={iconStyles} />, 'Management')
|
|
}
|
|
].filter((item) =>
|
|
isSidebarSectionVisible(item.key, { includeDev, userProfile })
|
|
)
|
|
}, [includeDev, showNavigationLabels, userProfile])
|
|
|
|
const sidebarMenuItems = useMemo(() => {
|
|
const items = filterSidebarItemsByListPermission(
|
|
getSidebarItems(selectedKey, { includeDev: includeDevItems }),
|
|
userProfile
|
|
)
|
|
return mapSidebarItemsToMenu(items, navigate)
|
|
}, [includeDevItems, navigate, selectedKey, userProfile])
|
|
|
|
const selectedSidebarItemKey = useMemo(
|
|
() =>
|
|
getSidebarSelectedKey(selectedKey, location.pathname, {
|
|
includeDev: includeDevItems,
|
|
userProfile
|
|
}),
|
|
[includeDevItems, location.pathname, selectedKey, userProfile]
|
|
)
|
|
|
|
const headerMenuItems = isMobile
|
|
? sidebarMenuItems
|
|
: showNavigationLabels
|
|
? mainMenuItems
|
|
: mainMenuItems.map((item) => ({ ...item, label: undefined }))
|
|
|
|
const [userPopoverOpen, setUserPopoverOpen] = useState(false)
|
|
|
|
const userPopoverContent = (
|
|
<UserProfilePopover onClose={() => setUserPopoverOpen(false)} />
|
|
)
|
|
|
|
useEffect(() => {
|
|
const pathParts = location.pathname.split('/').filter(Boolean)
|
|
const sectionKey = pathParts[1]
|
|
if (!sectionKey) return
|
|
const nextItem = mainMenuItems.find((item) => item.key == sectionKey)
|
|
setSelectedKey(nextItem?.key || sectionKey)
|
|
}, [location.pathname, mainMenuItems])
|
|
|
|
useEffect(() => {
|
|
if (connecting == true) {
|
|
setApiServerState('connecting')
|
|
} else if (connected == true) {
|
|
setApiServerState('connected')
|
|
} else {
|
|
setApiServerState('disconnected')
|
|
}
|
|
}, [connecting, connected])
|
|
|
|
const handleMainMenuClick = ({ key }) => {
|
|
navigate(getSidebarDefaultPath(key, { includeDev, userProfile }))
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (!isElectron || !setSidebarViewMenu) return
|
|
const sections = getSidebarMenuSections({ includeDev, userProfile })
|
|
setSidebarViewMenu(sections)
|
|
}, [includeDev, isElectron, setSidebarViewMenu, userProfile])
|
|
|
|
const isMacOSApp = isElectron && platform == 'darwin'
|
|
const isOtherApp = isElectron && platform != 'darwin'
|
|
|
|
const showDesktopLogo = !isElectron && !isMobile
|
|
const showMobileLogo = !isElectron && isMobile
|
|
|
|
const showControls = !isElectron || authenticated
|
|
|
|
const menu = (
|
|
<Menu
|
|
mode='horizontal'
|
|
className={`${isElectron ? 'electron-navigation' : null} ${!showNavigationLabels && !isMobile ? 'no-navigation-labels' : ''}${isMobile ? 'mobile-menu' : ''}`}
|
|
items={headerMenuItems}
|
|
style={{
|
|
flexWrap: 'wrap',
|
|
flexGrow: isElectron && !isMobile ? 0 : 1,
|
|
border: 0,
|
|
minWidth: 0
|
|
}}
|
|
onClick={isMobile ? undefined : handleMainMenuClick}
|
|
selectedKeys={[isMobile ? selectedSidebarItemKey : selectedKey]}
|
|
overflowedIndicator={
|
|
<Button
|
|
type='text'
|
|
icon={<MenuIcon />}
|
|
style={{ marginBottom: '4px' }}
|
|
/>
|
|
}
|
|
/>
|
|
)
|
|
|
|
const navigationContents = (
|
|
<Flex style={{ width: '100%' }} align='center'>
|
|
{isMacOSApp ? <DashboardWindowButtons /> : null}
|
|
{isOtherApp ? (
|
|
<>
|
|
<WindowAppMenu />{' '}
|
|
<Divider
|
|
type='vertical'
|
|
style={{
|
|
height: '14px',
|
|
margin: showNavigationLabels
|
|
? '3px 3px 0 1.5px'
|
|
: '3px 0px 0 1.5px'
|
|
}}
|
|
/>
|
|
</>
|
|
) : null}
|
|
{showControls && isMobile && menu}
|
|
{showDesktopLogo == true ? (
|
|
<FarmControlLogo
|
|
style={{
|
|
fontSize: '200px',
|
|
height: '18px',
|
|
marginRight: '15px'
|
|
}}
|
|
/>
|
|
) : showMobileLogo == true ? (
|
|
<FarmControlLogoSmall
|
|
style={{
|
|
fontSize: '48px',
|
|
marginRight: isElectron ? '20px' : '25px'
|
|
}}
|
|
/>
|
|
) : null}
|
|
<div style={{ flexGrow: 1, minWidth: 0 }}>
|
|
{showControls && !isMobile ? (
|
|
<Flex align='center' style={{ width: '100%', minWidth: 0 }}>
|
|
{menu}
|
|
{isElectron ? (
|
|
<>
|
|
<Divider
|
|
type='vertical'
|
|
style={{
|
|
margin: showNavigationLabels
|
|
? '3px 16px 0 4px'
|
|
: '3px 16px 0 0',
|
|
height: '14px'
|
|
}}
|
|
/>
|
|
<DashboardTabs />
|
|
</>
|
|
) : null}
|
|
</Flex>
|
|
) : null}
|
|
{!showControls && (
|
|
<Text
|
|
type='secondary'
|
|
style={{
|
|
fontSize: '14px',
|
|
marginLeft: '8px',
|
|
userSelect: 'none',
|
|
'--webkit-user-select': 'none'
|
|
}}
|
|
>
|
|
Farm Control
|
|
</Text>
|
|
)}
|
|
</div>
|
|
|
|
<div className='electrobun-webkit-app-region-no-drag'>
|
|
<Flex
|
|
gap={'small'}
|
|
align='center'
|
|
style={{ marginTop: '-2px', marginRight: '6px' }}
|
|
>
|
|
{showControls && (
|
|
<Space style={{ paddingTop: '2px', marginRight: '8px' }}>
|
|
<WebAppSwitcher />
|
|
<KeyboardShortcut
|
|
shortcut='alt+q'
|
|
hint='ALT Q'
|
|
onTrigger={() => showSpotlight()}
|
|
>
|
|
<Button
|
|
icon={<SearchIcon />}
|
|
type='text'
|
|
style={{ marginTop: '4px' }}
|
|
onClick={() => showSpotlight()}
|
|
/>
|
|
</KeyboardShortcut>
|
|
<Badge
|
|
count={unreadCount}
|
|
size='small'
|
|
offset={[-5, 8]}
|
|
style={{ padding: 0, fontWeight: 600 }}
|
|
>
|
|
<KeyboardShortcut
|
|
shortcut='alt+n'
|
|
hint='ALT N'
|
|
onTrigger={() => toggleNotificationCenter()}
|
|
>
|
|
<Button
|
|
icon={<BellIcon />}
|
|
type='text'
|
|
style={{ marginTop: '2px' }}
|
|
onClick={() => toggleNotificationCenter()}
|
|
/>
|
|
</KeyboardShortcut>
|
|
</Badge>
|
|
</Space>
|
|
)}
|
|
{import.meta.env.MODE === 'development' && (
|
|
<Space>
|
|
{apiServerState === 'connected' ? (
|
|
<Tooltip content='Connected to api server'>
|
|
<Tag
|
|
color='success'
|
|
style={{ marginRight: 0 }}
|
|
icon={<CloudIcon />}
|
|
/>
|
|
</Tooltip>
|
|
) : null}
|
|
{apiServerState === 'connecting' ? (
|
|
<Tooltip content='Connecting to api erver...'>
|
|
<Tag
|
|
color='warning'
|
|
style={{ marginRight: 0 }}
|
|
icon={<LoadingOutlined />}
|
|
/>
|
|
</Tooltip>
|
|
) : null}
|
|
{apiServerState === 'disconnected' ? (
|
|
<Tooltip content='Disconnected from api server'>
|
|
<Tag
|
|
color='error'
|
|
style={{ marginRight: 0 }}
|
|
icon={<CloudIcon />}
|
|
/>
|
|
</Tooltip>
|
|
) : null}
|
|
<Tooltip content='Developer'>
|
|
<Tag
|
|
color='yellow'
|
|
style={{ marginRight: 0 }}
|
|
icon={<DeveloperIcon />}
|
|
onClick={() => {
|
|
navigate('/dashboard/developer/sessionstorage')
|
|
}}
|
|
/>
|
|
</Tooltip>
|
|
</Space>
|
|
)}
|
|
{showControls && userProfile ? (
|
|
<Space>
|
|
<Popover
|
|
content={userPopoverContent}
|
|
placement='bottomRight'
|
|
trigger='hover'
|
|
open={userPopoverOpen}
|
|
onOpenChange={setUserPopoverOpen}
|
|
arrow={false}
|
|
>
|
|
<Tag style={{ marginRight: 0 }} icon={<PersonIcon />}>
|
|
{!isMobile && (userProfile?.name || userProfile.username)}
|
|
</Tag>
|
|
</Popover>
|
|
</Space>
|
|
) : null}
|
|
{showControls && isElectron && availableUpdate ? (
|
|
<Tag
|
|
icon={<CloudIcon />}
|
|
style={{ cursor: 'pointer', margin: '2px 0 0 0' }}
|
|
color='cyan'
|
|
onClick={() => checkForUpdates()}
|
|
>
|
|
Update Available
|
|
</Tag>
|
|
) : null}
|
|
</Flex>
|
|
</div>
|
|
{isOtherApp ? <DashboardWindowButtons /> : null}
|
|
</Flex>
|
|
)
|
|
|
|
return (
|
|
<>
|
|
{isElectron ? (
|
|
<Flex
|
|
className={`ant-menu-horizontal electron-navigation-wrapper ant-menu-light ${isFullScreen || (isMaximized && isOtherApp) ? 'electrobun-webkit-app-region-no-drag' : 'electrobun-webkit-app-region-drag'}`}
|
|
style={{ lineHeight: '40px', padding: '0 2px 0 2px' }}
|
|
>
|
|
{navigationContents}
|
|
</Flex>
|
|
) : (
|
|
<Flex vertical>
|
|
<Header
|
|
style={{
|
|
width: '100vw',
|
|
padding: 0,
|
|
marginBottom: '0.1px',
|
|
background: 'unset'
|
|
}}
|
|
theme='light'
|
|
className='ant-menu-horizontal'
|
|
>
|
|
<Flex
|
|
gap={'large'}
|
|
align='center'
|
|
className='ant-menu-light'
|
|
style={{
|
|
padding: isMobile ? '0 12px' : '0 26px',
|
|
height: '100%',
|
|
borderBottom: '1px solid rgba(5, 5, 5, 0.00)'
|
|
}}
|
|
>
|
|
{navigationContents}
|
|
</Flex>
|
|
<Divider style={{ margin: 0 }} />
|
|
</Header>
|
|
</Flex>
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default DashboardNavigation
|