LCOV - code coverage report
Current view: top level - dashql-app/src/app - navbar.tsx Coverage Total Hit
Test: DashQL 915828b Lines: 97.1 % 173 168
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as React from 'react';
       2              : import * as styles from './navbar.module.css';
       3              : import symbols from '@ankoh/dashql-svg-symbols';
       4              : 
       5              : import { AnchorAlignment, AnchorSide } from '../ui/foundations/anchored_position.js';
       6              : import { HoverMode, NavBarButtonWithRef } from './navbar_button.js';
       7              : import { InternalsViewerOverlay } from './internals/internals_overlay.js';
       8              : import { AgentViewOverlay } from '../agent/agent_view.js';
       9              : import { useAgentBridge } from '../agent/agent_bridge.js';
      10              : import { PlatformType, isMacDesktopHost, usePlatformType } from '../platform/platform_type.js';
      11              : import { DASHQL_VERSION } from '../globals.js';
      12              : import { VersionCheckIndicator } from '../ui/version/version_viewer.js';
      13              : import { VersionInfoOverlay } from '../ui/version/version_viewer.js';
      14              : import { useLogger } from '../platform/logger/logger_provider.js';
      15              : import { useRouteContext } from './router/router.js';
      16              : import { useVersionCheck } from '../platform/version/version_check.js';
      17              : import { useLocation } from 'react-router-dom';
      18              : import { FlavorLogo, useAppFlavor } from '../config/flavor_provider.js';
      19              : import { NavRail, useNavRailWide } from './nav_rail.js';
      20              : import { useMacWindowLayout } from './mac_window_layout.js';
      21              : import { useColorSchemeToggle } from '../config/color_scheme.js';
      22              : import { useOpenGreeter } from '../session/session_creation.js';
      23              : 
      24            1 : const LOG_CTX = "navbar";
      25              : 
      26              : interface NavBarItemProps {
      27              :     label: string;
      28              :     icon: React.ReactNode;
      29              :     /// The props of an anchored overlay
      30              :     anchor: object;
      31              :     onClick: () => void;
      32              :     'data-testid'?: string;
      33              : }
      34              : 
      35            1 : const NavBarItem = ({ label, icon, anchor, ...props }: NavBarItemProps) => (
      36            1 :     <NavBarButtonWithRef {...anchor} {...props} className={styles.tab_button} hover={HoverMode.Darken}>
      37            1 :         <>
      38            1 :             {icon}
      39            1 :             <span className={styles.tab_button_text}>{label}</span>
      40            1 :         </>
      41            1 :     </NavBarButtonWithRef>
      42              : );
      43              : 
      44              : /// Overlays open below the bar at the top
      45            1 : const OVERLAY_POSITION = { side: AnchorSide.OutsideBottom, align: AnchorAlignment.End, anchorOffset: 16 };
      46              : 
      47            1 : const InternalsButton = (props: { sessionId: string | null }) => {
      48            1 :     const [showInternalsViewerOverlay, setInternalsViewerOverlay] = React.useState<boolean>(false);
      49              : 
      50            1 :     return (
      51            1 :         <div className={styles.tab}>
      52            1 :             <InternalsViewerOverlay
      53            1 :                 sessionId={props.sessionId}
      54            1 :                 isOpen={showInternalsViewerOverlay}
      55            1 :                 onClose={() => setInternalsViewerOverlay(false)}
      56            1 :                 renderAnchor={(p: object) => (
      57            1 :                     <NavBarItem
      58            1 :                         anchor={p}
      59            1 :                         label="Client"
      60            1 :                         icon={(
      61            1 :                             <svg width="14px" height="14px" aria-hidden="true">
      62            1 :                                 <use xlinkHref={`${symbols}#device_desktop_24`} />
      63            1 :                             </svg>
      64              :                         )}
      65            1 :                         onClick={() => setInternalsViewerOverlay(true)}
      66            1 :                     />
      67              :                 )}
      68            1 :                 {...OVERLAY_POSITION}
      69            1 :             />
      70            1 :         </div>
      71              :     );
      72            1 : };
      73              : 
      74            1 : const AgentButton = () => {
      75            1 :     const bridge = useAgentBridge();
      76            1 :     const [showAgentOverlay, setAgentOverlay] = React.useState<boolean>(false);
      77            1 :     if (bridge == null) return null;
      78              : 
      79            1 :     return (
      80            1 :         <div className={styles.tab}>
      81            1 :             <AgentViewOverlay
      82            1 :                 isOpen={showAgentOverlay}
      83            1 :                 onClose={() => setAgentOverlay(false)}
      84            1 :                 renderAnchor={(p: object) => (
      85            1 :                     <NavBarItem
      86            1 :                         anchor={p}
      87            1 :                         label="Agent"
      88            1 :                         icon={(
      89            1 :                             <svg width="14px" height="14px" aria-hidden="true">
      90            1 :                                 <use xlinkHref={`${symbols}#sparkles_fill_16`} />
      91            1 :                             </svg>
      92              :                         )}
      93            1 :                         onClick={() => setAgentOverlay(true)}
      94            1 :                         data-testid="agent-settings"
      95            1 :                     />
      96              :                 )}
      97            1 :                 {...OVERLAY_POSITION}
      98            1 :             />
      99            1 :         </div>
     100              :     );
     101            1 : };
     102              : 
     103            1 : const VersionButton = (props: { iconSize?: number }) => {
     104            1 :     const [showVersionOverlay, setShowVersionOverlay] = React.useState<boolean>(false);
     105            1 :     const versionCheck = useVersionCheck();
     106              : 
     107            1 :     return (
     108            1 :         <div className={styles.tab}>
     109            1 :             <VersionInfoOverlay
     110            1 :                 isOpen={showVersionOverlay}
     111            1 :                 onClose={() => setShowVersionOverlay(false)}
     112            1 :                 renderAnchor={(p: object) => (
     113            1 :                     <NavBarItem
     114            1 :                         anchor={p}
     115            1 :                         label={`Version ${DASHQL_VERSION}`}
     116            1 :                         icon={<VersionCheckIndicator status={versionCheck} size={props.iconSize} />}
     117            1 :                         onClick={() => setShowVersionOverlay(true)}
     118            1 :                     />
     119              :                 )}
     120            1 :                 {...OVERLAY_POSITION}
     121            1 :             />
     122            1 :         </div>
     123              :     );
     124            1 : };
     125              : 
     126              : /// Switches between the light and the dark color scheme, the icon shows the scheme that a click switches to
     127            1 : const ColorSchemeButton = () => {
     128            1 :     const [scheme, toggle] = useColorSchemeToggle();
     129            1 :     const label = scheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode';
     130            1 :     return (
     131            1 :         <div className={styles.tab}>
     132            1 :             <NavBarButtonWithRef
     133            1 :                 className={styles.tab_button}
     134            1 :                 hover={HoverMode.Darken}
     135            1 :                 onClick={toggle}
     136            1 :                 aria-label={label}
     137            1 :                 title={label}
     138            1 :                 data-testid="color-scheme-toggle"
     139              :             >
     140            1 :                 <svg width="14px" height="14px" aria-hidden="true">
     141            1 :                     <use xlinkHref={`${symbols}#${scheme === 'dark' ? 'sun_16' : 'moon_16'}`} />
     142            1 :                 </svg>
     143            1 :             </NavBarButtonWithRef>
     144            1 :         </div>
     145              :     );
     146            1 : };
     147              : 
     148              : /// The height of a wordmark in the navbar, a bit smaller than square logos to match the cap height of text
     149            1 : const WORDMARK_HEIGHT = 16;
     150              : 
     151            1 : const BrandIdentity = () => {
     152            1 :     const flavor = useAppFlavor();
     153            1 :     return (
     154            1 :         <div className={styles.brand_identity} data-electron-drag-region>
     155            1 :             {flavor.logo.wordmark
     156            1 :                 ? <FlavorLogo height={WORDMARK_HEIGHT} ariaHidden />
     157            1 :                 : <>
     158            1 :                     <FlavorLogo height={24} ariaHidden />
     159            1 :                     <span>{flavor.displayName}</span>
     160            1 :                 </>}
     161            1 :         </div>
     162              :     );
     163            1 : };
     164              : 
     165              : /// The height of a wordmark in the stripe at the top, a bit smaller than in the compact navbar
     166            1 : const TOP_BAR_WORDMARK_HEIGHT = 14;
     167              : 
     168              : /// The brand in the stripe at the top, the logo and, next to the wide rail, the name without the tagline.
     169              : /// A square logo is a bit larger without the traffic lights of macOS next to it.
     170              : /// A click opens the greeter.
     171            1 : const TopBarBrand = (props: { mac: boolean, showName: boolean }) => {
     172            1 :     const openGreeter = useOpenGreeter();
     173            1 :     const flavor = useAppFlavor();
     174            1 :     return (
     175            1 :         <button
     176            1 :             type="button"
     177            1 :             className={`${styles.brand_identity} ${styles.brand_button}`}
     178            1 :             aria-label={`${flavor.displayName}, new session`}
     179            1 :             data-testid="brand-home"
     180            1 :             data-electron-drag-region="false"
     181            1 :             onClick={openGreeter}
     182              :         >
     183            1 :             {flavor.logo.wordmark
     184            1 :                 ? <FlavorLogo height={TOP_BAR_WORDMARK_HEIGHT} ariaHidden />
     185            1 :                 : <>
     186            1 :                     <FlavorLogo height={props.mac ? 22 : 24} ariaHidden />
     187            1 :                     {props.showName && <span>{flavor.displayName}</span>}
     188            1 :                 </>}
     189            1 :         </button>
     190              :     );
     191            1 : };
     192              : 
     193            1 : export const CompactNavBar = (props: { showInternals?: boolean }): React.ReactElement => {
     194            1 :     const route = useRouteContext();
     195            1 :     const platform = usePlatformType();
     196            1 :     const navbarClass = platform === PlatformType.DESKTOP ? styles.navbar_mac : styles.navbar_default;
     197              : 
     198            1 :     return (
     199            1 :         <header className={`${navbarClass} ${styles.navbar_overlay}`} data-electron-drag-region>
     200            1 :             <BrandIdentity />
     201            1 :             <div className={styles.navbar_actions}>
     202            1 :                 {(props.showInternals ?? true) && <InternalsButton sessionId={route.sessionId} />}
     203            1 :                 <VersionButton />
     204            1 :                 <ColorSchemeButton />
     205            1 :             </div>
     206            1 :         </header>
     207              :     );
     208            1 : };
     209              : 
     210              : /// Stand-ins for the traffic lights of the macOS window when the browser shows the layout of the desktop app on macOS
     211            1 : const TrafficLightPlaceholders = () => (
     212            0 :     <div className={styles.traffic_lights} aria-hidden="true">
     213            0 :         <span className={styles.traffic_light_close} />
     214            0 :         <span className={styles.traffic_light_minimize} />
     215            0 :         <span className={styles.traffic_light_zoom} />
     216            0 :     </div>
     217              : );
     218              : 
     219              : /// The navbar is a rail of pages left of the page card below a dark stripe that holds the brand on the left and the overlays on the right.
     220              : /// On the desktop app on macOS, the brand follows the traffic lights.
     221              : /// The Mac Window Layout setting shows them in the browser.
     222            1 : export function NavBarContainer(props: { children: React.ReactElement }) {
     223            1 :     const logger = useLogger();
     224            1 :     const location = useLocation();
     225            1 :     const route = useRouteContext();
     226            1 :     const macHost = isMacDesktopHost();
     227            1 :     const isMac = useMacWindowLayout();
     228            1 :     const [railWide] = useNavRailWide();
     229              : 
     230            1 :     React.useEffect(() => {
     231            1 :         logger.debug("Navigated to path", { "path": location.pathname }, LOG_CTX);
     232            1 :     }, [location.pathname]);
     233              : 
     234            1 :     return (
     235            1 :         <div className={railWide ? `${styles.container} ${styles.container_wide}` : styles.container}>
     236            1 :             <header key={2} className={isMac ? `${styles.top_bar} ${styles.top_bar_mac}` : styles.top_bar} data-electron-drag-region>
     237            1 :                 <TopBarBrand mac={isMac} showName={railWide} />
     238            1 :                 <div className={styles.top_bar_actions}>
     239            1 :                     <AgentButton />
     240            1 :                     <InternalsButton sessionId={route.sessionId} />
     241            1 :                     <VersionButton iconSize={14} />
     242            1 :                     <ColorSchemeButton />
     243            1 :                 </div>
     244            1 :             </header>
     245            1 :             <NavRail key={0} />
     246            1 :             <div key={1} className={styles.page_container}>
     247            1 :                 {props.children}
     248            1 :             </div>
     249            1 :             {isMac && !macHost && <TrafficLightPlaceholders />}
     250            1 :         </div>
     251              :     );
     252            1 : }
        

Generated by: LCOV version 2.0-1