diff --git a/assets/stylesheets/App.css b/assets/stylesheets/App.css
index 576d4c6f..d32f01ed 100644
--- a/assets/stylesheets/App.css
+++ b/assets/stylesheets/App.css
@@ -125,14 +125,17 @@
opacity: 1;
visibility: visible;
transition:
- opacity 0.2s ease,
- visibility 0.2s ease;
+ opacity 0.2s ease 0.2s,
+ visibility 0s linear 0.2s;
}
.dashboard-navigation-fade-hidden {
opacity: 0;
visibility: hidden;
pointer-events: none;
+ transition:
+ opacity 0.2s ease 0s,
+ visibility 0s linear 0.2s;
}
.dashboard-navigation-window-title {
@@ -140,12 +143,46 @@
inset: 0;
display: flex;
align-items: center;
- padding-left: 14px;
+ justify-content: space-between;
+ padding-left: 16px;
+ padding-right: 16px;
font-size: 14px;
pointer-events: none;
user-select: none;
-webkit-user-select: none;
+}
+
+.dashboard-navigation-center-border {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ height: 1px;
+ background: var(--color-header-border);
+ pointer-events: none;
+ opacity: 1;
+ /* Wait for outgoing chrome to finish fading before this line appears. */
+ transition: opacity 0.2s ease 0.2s;
+}
+
+.dashboard-navigation-center-border-hidden {
+ opacity: 0;
+ /* Stay through the gap, then fade out as incoming chrome fades in. */
+ transition: opacity 0.2s ease 0.2s;
+}
+
+.dashboard-menu-container {
+ position: relative;
+ height: 42px;
+ padding-top: 2px;
+}
+.dashboard-menu-container:after {
border-bottom: 1px solid var(--color-header-border);
+ content: '';
+ position: absolute;
+ left: 0;
+ right: -2px;
+ bottom: 0;
}
.electron-navigation-leading,
@@ -3460,7 +3497,7 @@ body.objectKanbanColumnResizing * {
display: flex;
align-items: flex-start;
gap: 4px;
- padding-left: 12px;
+ padding-left: 14px;
height: 100%;
max-height: 42px;
}
@@ -3486,7 +3523,7 @@ body.objectKanbanColumnResizing * {
}
.dashboard-tab-item-container:first-child:before {
- left: -12px;
+ left: -14px;
}
.dashboard-tab-item-container:last-child:before {
diff --git a/src/components/Dashboard/common/DashboardDevControls.jsx b/src/components/Dashboard/common/DashboardDevControls.jsx
new file mode 100644
index 00000000..b0590300
--- /dev/null
+++ b/src/components/Dashboard/common/DashboardDevControls.jsx
@@ -0,0 +1,70 @@
+import { useContext, useEffect, useState } from 'react'
+import { Space, Tag } from 'antd'
+import { LoadingOutlined } from '@ant-design/icons'
+import { useNavigate } from 'react-router-dom'
+import { ApiServerContext } from '../context/ApiServerContext'
+import Tooltip from './Tooltip'
+import CloudIcon from '../../Icons/CloudIcon'
+import DeveloperIcon from '../../Icons/DeveloperIcon'
+
+const DashboardDevControls = () => {
+ const { connecting, connected } = useContext(ApiServerContext)
+ const navigate = useNavigate()
+ const [apiServerState, setApiServerState] = useState('disconnected')
+
+ useEffect(() => {
+ if (connecting == true) {
+ setApiServerState('connecting')
+ } else if (connected == true) {
+ setApiServerState('connected')
+ } else {
+ setApiServerState('disconnected')
+ }
+ }, [connecting, connected])
+
+ if (import.meta.env.MODE !== 'development') return null
+
+ return (
+
+ {apiServerState === 'connected' ? (
+
+ }
+ />
+
+ ) : null}
+ {apiServerState === 'connecting' ? (
+
+ }
+ />
+
+ ) : null}
+ {apiServerState === 'disconnected' ? (
+
+ }
+ />
+
+ ) : null}
+
+ }
+ onClick={() => {
+ navigate('/dashboard/developer/sessionstorage')
+ }}
+ />
+
+
+ )
+}
+
+export default DashboardDevControls
diff --git a/src/components/Dashboard/common/DashboardNavigation.jsx b/src/components/Dashboard/common/DashboardNavigation.jsx
index 6168ca28..93e4d1a1 100644
--- a/src/components/Dashboard/common/DashboardNavigation.jsx
+++ b/src/components/Dashboard/common/DashboardNavigation.jsx
@@ -1,5 +1,6 @@
// DashboardNavigation.js
-import { useContext, useEffect, useState, useMemo } from 'react'
+import { useContext, useEffect, useState, useMemo, useCallback, memo } from 'react'
+import PropTypes from 'prop-types'
import {
Menu,
Flex,
@@ -11,7 +12,6 @@ import {
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'
@@ -35,9 +35,9 @@ 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 DashboardDevControls from './DashboardDevControls'
import WindowAppMenu from './WindowAppMenu'
import WebAppSwitcher from './WebAppSwitcher'
import DashboardTabs from './DashboardTabs'
@@ -57,6 +57,225 @@ import { getEffectiveAppearance } from '../../../database/Settings'
const { Text } = Typography
+const NAV_CENTER_FADE_STYLE = {
+ width: '100%',
+ minWidth: 0,
+ display: 'flex',
+ alignItems: 'center'
+}
+const NAV_ELECTRON_CENTER_FADE_STYLE = {
+ flex: 1,
+ minWidth: 0,
+ display: 'flex',
+ alignItems: 'center'
+}
+const NAV_ELECTRON_TABS_STYLE = { flex: 1, minWidth: 0, display: 'flex' }
+const NAV_ELECTRON_DIVIDER_STYLE_WITH_LABELS = {
+ margin: '3px 1px 0 4px',
+ height: '14px'
+}
+const NAV_ELECTRON_DIVIDER_STYLE = {
+ margin: '3px 1px 0 0',
+ height: '14px'
+}
+const NAV_TRAILING_FLEX_STYLE = { marginTop: '-2px', marginRight: '6px' }
+const NAV_TRAILING_SPACE_STYLE = { paddingTop: '2px', marginRight: '8px' }
+const NAV_SEARCH_BUTTON_STYLE = { marginTop: '4px' }
+const NAV_BELL_BUTTON_STYLE = { marginTop: '2px' }
+const NAV_BADGE_STYLE = { padding: 0, fontWeight: 600 }
+const NAV_UPDATE_TAG_STYLE = { cursor: 'pointer', margin: '2px 0 0 0' }
+const NAV_OVERFLOW_BUTTON_STYLE = { marginBottom: '4px' }
+
+const navigationFadeClass = (visible) =>
+ `dashboard-navigation-fade${visible ? '' : ' dashboard-navigation-fade-hidden'}`
+
+const NavigationFade = memo(function NavigationFade({ visible, children, style }) {
+ return (
+
+ {children}
+
+ )
+})
+
+NavigationFade.propTypes = {
+ visible: PropTypes.bool,
+ children: PropTypes.node,
+ style: PropTypes.object
+}
+
+const DashboardNavigationTrailingItems = memo(
+ function DashboardNavigationTrailingItems({
+ controlsReady,
+ isMobile,
+ isElectron,
+ unreadCount,
+ userProfile,
+ availableUpdate,
+ userPopoverOpen,
+ onUserPopoverOpenChange,
+ onSearch,
+ onNotifications,
+ onCheckUpdates
+ }) {
+ return (
+
+ {controlsReady ? (
+
+
+
+ }
+ type='text'
+ style={NAV_SEARCH_BUTTON_STYLE}
+ onClick={onSearch}
+ />
+
+
+
+ }
+ type='text'
+ style={NAV_BELL_BUTTON_STYLE}
+ onClick={onNotifications}
+ />
+
+
+
+ ) : null}
+
+ {controlsReady && userProfile ? (
+
+ onUserPopoverOpenChange(false)}
+ />
+ }
+ placement='bottomRight'
+ trigger='hover'
+ open={userPopoverOpen}
+ onOpenChange={onUserPopoverOpenChange}
+ arrow={false}
+ >
+ }>
+ {!isMobile && (userProfile?.name || userProfile.username)}
+
+
+
+ ) : null}
+ {controlsReady && isElectron && availableUpdate ? (
+ }
+ style={NAV_UPDATE_TAG_STYLE}
+ color='cyan'
+ onClick={onCheckUpdates}
+ >
+ Update Available
+
+ ) : null}
+
+ )
+ }
+)
+
+DashboardNavigationTrailingItems.propTypes = {
+ controlsReady: PropTypes.bool,
+ isMobile: PropTypes.bool,
+ isElectron: PropTypes.bool,
+ unreadCount: PropTypes.number,
+ userProfile: PropTypes.shape({
+ name: PropTypes.string,
+ username: PropTypes.string
+ }),
+ availableUpdate: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
+ userPopoverOpen: PropTypes.bool,
+ onUserPopoverOpenChange: PropTypes.func,
+ onSearch: PropTypes.func,
+ onNotifications: PropTypes.func,
+ onCheckUpdates: PropTypes.func
+}
+
+const DashboardElectronCenterItems = memo(function DashboardElectronCenterItems({
+ menu,
+ showNavigationLabels
+}) {
+ return (
+ <>
+ {menu}
+
+
+
+
+ >
+ )
+})
+
+DashboardElectronCenterItems.propTypes = {
+ menu: PropTypes.node,
+ showNavigationLabels: PropTypes.bool
+}
+
+const DashboardNavigationTrailing = memo(function DashboardNavigationTrailing({
+ showControls,
+ isOtherApp,
+ ...itemProps
+}) {
+ return (
+ <>
+
+
+
+
+
+ {isOtherApp ? : null}
+ >
+ )
+})
+
+DashboardNavigationTrailing.propTypes = {
+ showControls: PropTypes.bool,
+ isOtherApp: PropTypes.bool,
+ controlsReady: PropTypes.bool,
+ isMobile: PropTypes.bool,
+ isElectron: PropTypes.bool,
+ unreadCount: PropTypes.number,
+ userProfile: PropTypes.shape({
+ name: PropTypes.string,
+ username: PropTypes.string
+ }),
+ availableUpdate: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
+ userPopoverOpen: PropTypes.bool,
+ onUserPopoverOpenChange: PropTypes.func,
+ onSearch: PropTypes.func,
+ onNotifications: PropTypes.func,
+ onCheckUpdates: PropTypes.func
+}
+
const mapSidebarItemsToMenu = (items, navigate) =>
items.map((item) => {
if (item?.type === 'divider') {
@@ -83,8 +302,7 @@ const mapSidebarItemsToMenu = (items, navigate) =>
const DashboardNavigation = () => {
const { userProfile } = useContext(AuthContext)
const { showSpotlight } = useContext(SpotlightContext)
- const { connecting, connected, userSettings, userSettingsLoaded } =
- useContext(ApiServerContext)
+ const { userSettings, userSettingsLoaded } = useContext(ApiServerContext)
const { authenticated } = useContext(AuthContext)
const {
showNavigationLabels,
@@ -94,7 +312,6 @@ const DashboardNavigation = () => {
} = useThemeContext()
const { toggleNotificationCenter, unreadCount } =
useContext(NotificationContext)
- const [apiServerState, setApiServerState] = useState('disconnected')
const navigate = useNavigate()
const location = useLocation()
const [selectedKey, setSelectedKey] = useState('production')
@@ -209,19 +426,19 @@ const DashboardNavigation = () => {
[includeDevItems, location.pathname, selectedKey, userProfile]
)
- const headerMenuItems = isMobile
- ? sidebarMenuItems
- : showNavigationLabels
- ? mainMenuItems
- : mainMenuItems.map((item) => ({ ...item, label: undefined }))
+ const headerMenuItems = useMemo(
+ () =>
+ isMobile
+ ? sidebarMenuItems
+ : showNavigationLabels
+ ? mainMenuItems
+ : mainMenuItems.map((item) => ({ ...item, label: undefined })),
+ [isMobile, mainMenuItems, showNavigationLabels, sidebarMenuItems]
+ )
const [userPopoverOpen, setUserPopoverOpen] = useState(false)
const [anyModalVisible, setAnyModalVisible] = useState(false)
- const userPopoverContent = (
- setUserPopoverOpen(false)} />
- )
-
useEffect(() => {
const pathParts = location.pathname.split('/').filter(Boolean)
const sectionKey = pathParts[1]
@@ -230,19 +447,12 @@ const DashboardNavigation = () => {
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 }))
- }
+ const handleMainMenuClick = useCallback(
+ ({ key }) => {
+ navigate(getSidebarDefaultPath(key, { includeDev, userProfile }))
+ },
+ [includeDev, navigate, userProfile]
+ )
useEffect(() => {
if (!isElectron || !setSidebarViewMenu) return
@@ -297,146 +507,69 @@ const DashboardNavigation = () => {
}
}, [])
- const showControls = (!isElectron || authenticated) && !anyModalVisible
+ const controlsReady = !isElectron || authenticated
+ const showControls = controlsReady && !anyModalVisible
+ const handleSearch = useCallback(() => {
+ showSpotlight()
+ }, [showSpotlight])
+ const handleNotifications = useCallback(() => {
+ toggleNotificationCenter()
+ }, [toggleNotificationCenter])
+ const handleCheckUpdates = useCallback(() => {
+ checkForUpdates()
+ }, [checkForUpdates])
+ const overflowedIndicator = useMemo(
+ () => (
+ } style={NAV_OVERFLOW_BUTTON_STYLE} />
+ ),
+ []
+ )
- const menu = (
- }
- style={{ marginBottom: '4px' }}
- />
- }
- />
+ const menu = useMemo(
+ () => (
+
+ ),
+ [
+ handleMainMenuClick,
+ headerMenuItems,
+ isElectron,
+ isMobile,
+ overflowedIndicator,
+ selectedKey,
+ selectedSidebarItemKey,
+ showNavigationLabels
+ ]
)
const navigationTrailing = (
- <>
-
-
- {showControls && (
-
-
- showSpotlight()}
- >
- }
- type='text'
- style={{ marginTop: '4px' }}
- onClick={() => showSpotlight()}
- />
-
-
- toggleNotificationCenter()}
- >
- }
- type='text'
- style={{ marginTop: '2px' }}
- onClick={() => toggleNotificationCenter()}
- />
-
-
-
- )}
- {import.meta.env.MODE === 'development' && (
-
- {apiServerState === 'connected' ? (
-
- }
- />
-
- ) : null}
- {apiServerState === 'connecting' ? (
-
- }
- />
-
- ) : null}
- {apiServerState === 'disconnected' ? (
-
- }
- />
-
- ) : null}
-
- }
- onClick={() => {
- navigate('/dashboard/developer/sessionstorage')
- }}
- />
-
-
- )}
- {showControls && userProfile ? (
-
-
- }>
- {!isMobile && (userProfile?.name || userProfile.username)}
-
-
-
- ) : null}
- {showControls && isElectron && availableUpdate ? (
- }
- style={{ cursor: 'pointer', margin: '2px 0 0 0' }}
- color='cyan'
- onClick={() => checkForUpdates()}
- >
- Update Available
-
- ) : null}
-
-
- {isOtherApp ? : null}
- >
+
)
const navigationLeading = (
@@ -456,7 +589,9 @@ const DashboardNavigation = () => {
/>
>
) : null}
- {showControls && isMobile && menu}
+ {isMobile ? (
+ {menu}
+ ) : null}
{showDesktopLogo == true ? (
{
)
const windowTitle = (
-
- Farm Control
-
+ Farm Control
+
+ )
+
+ const navigationCenterBorder = (
+
)
const navigationContents = (
@@ -493,18 +637,12 @@ const DashboardNavigation = () => {
{navigationLeading}
{!isMobile ? (
-
+
{menu}
-
+
) : null}
{windowTitle}
+ {navigationCenterBorder}
{navigationTrailing}
@@ -524,32 +662,21 @@ const DashboardNavigation = () => {
>
{navigationLeading}
- {showControls && !isMobile ? menu : null}
- {showControls && !isMobile ? (
-
- ) : null}
{!isMobile ? (
-
-
-
+
+
) : null}
{windowTitle}
+ {navigationCenterBorder}
{navigationTrailing}