farmcontrol-ui/src/components/Dashboard/common/DashboardNavigation.jsx
Tom Butcher d16aa373be
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
Implement Navigation Tabs and Dashboard Enhancements
- 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.
2026-09-17 20:39:50 +01:00

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