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

            Line data    Source code
       1              : import * as React from 'react';
       2              : import * as styles from './nav_rail.module.css';
       3              : import symbols from '@ankoh/dashql-svg-symbols';
       4              : 
       5              : import { useLocation } from 'react-router-dom';
       6              : import { Tooltip } from '../ui/foundations/tooltip.js';
       7              : import { SESSION_PATH, SELECT_SESSION, useRouteContext, useRouterNavigate } from './router/router.js';
       8              : import { SessionViewMode, useSessionViewMode } from '../session/session_view_mode.js';
       9              : import { classNames } from '../utils/classnames.js';
      10              : import { useAppConfig, useAppReconfigure } from '../config/app_config.js';
      11              : import { useAppFlavor } from '../config/flavor_provider.js';
      12              : import type { AppFlavorRailPage } from '../config/flavors.js';
      13              : 
      14              : /// Is the pathname a page of the workspace, the sessions and their connections?
      15            1 : export function isWorkspacePath(pathname: string): boolean {
      16            1 :     return pathname === '/' || pathname === '/session' || pathname.startsWith('/sessions/');
      17            1 : }
      18              : 
      19              : /// Open the workbench of the current session, leaving the shell
      20            1 : export function useOpenWorkbench(): () => void {
      21            1 :     const route = useRouteContext();
      22            1 :     const navigate = useRouterNavigate();
      23            1 :     const { setMode } = useSessionViewMode();
      24            1 :     return React.useCallback(() => {
      25            0 :         setMode(SessionViewMode.Session);
      26            0 :         navigate(route.sessionId == null
      27            0 :             ? { type: SESSION_PATH, value: null }
      28            0 :             : { type: SELECT_SESSION, value: route.sessionId });
      29            1 :     }, [navigate, route.sessionId, setMode]);
      30            1 : }
      31              : 
      32              : /// Open the shell of the current session
      33            1 : function useOpenShell(): (() => void) | null {
      34            1 :     const route = useRouteContext();
      35            1 :     const navigate = useRouterNavigate();
      36            1 :     const { setMode } = useSessionViewMode();
      37            1 :     const sessionId = route.sessionId;
      38            1 :     const openShell = React.useCallback(() => {
      39            0 :         if (sessionId == null) return;
      40            0 :         setMode(SessionViewMode.Shell);
      41            0 :         navigate({ type: SELECT_SESSION, value: sessionId });
      42            1 :     }, [navigate, sessionId, setMode]);
      43            1 :     return sessionId == null ? null : openShell;
      44            1 : }
      45              : 
      46              : /// Is the rail wide, showing the labels of its buttons? Persisted in the app settings.
      47            1 : export function useNavRailWide(): [boolean, () => void] {
      48            1 :     const config = useAppConfig();
      49            1 :     const reconfigure = useAppReconfigure();
      50            1 :     const wide = config?.settings?.navRailWide ?? false;
      51            1 :     const toggle = React.useCallback(() => {
      52            0 :         reconfigure(value => value == null ? null : {
      53            0 :             ...value,
      54            0 :             settings: { ...value.settings, navRailWide: !value.settings?.navRailWide },
      55            0 :         });
      56            1 :     }, [reconfigure]);
      57            1 :     return [wide, toggle];
      58            1 : }
      59              : 
      60              : type NavRailButtonProps = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> & {
      61              :     label: string;
      62              :     active?: boolean;
      63              :     /// Show the label next to the icon instead of in a tooltip
      64              :     wide: boolean;
      65              :     ref?: React.Ref<HTMLButtonElement>;
      66              :     children: React.ReactNode;
      67              : };
      68              : 
      69              : /// An icon button of the rail, labeled by a tooltip or, in the wide rail, by its label
      70            1 : export const NavRailButton = ({ label, active, wide, className, children, ...props }: NavRailButtonProps) => {
      71            1 :     const button = (
      72            1 :         <button
      73            1 :             {...props}
      74            1 :             type="button"
      75            1 :             className={classNames(styles.button, className, { [styles.active]: active, [styles.button_wide]: wide })}
      76            1 :             aria-label={props['aria-label'] ?? label}
      77              :         >
      78            1 :             {children}
      79            1 :             {wide && <span className={styles.button_label}>{label}</span>}
      80            1 :         </button>
      81              :     );
      82            1 :     return wide ? button : <Tooltip type="label" direction="e" text={label}>{button}</Tooltip>;
      83            1 : };
      84              : 
      85              : /// An icon of the rail from the svg sprite
      86            1 : const NavRailIcon = (props: { icon: string }) => (
      87            1 :     <svg width="16px" height="16px" aria-hidden="true">
      88            1 :         <use xlinkHref={`${symbols}#${props.icon}`} />
      89            1 :     </svg>
      90              : );
      91              : 
      92              : /// The pages of the rail in the groups of the flavor, with a separator between the groups.
      93              : /// The pages that are not there yet are buttons that do nothing.
      94            1 : export const NavRailPages = (props: { wide: boolean }): React.ReactElement => {
      95            1 :     const flavor = useAppFlavor();
      96            1 :     const location = useLocation();
      97            1 :     const { mode } = useSessionViewMode();
      98            1 :     const openWorkbench = useOpenWorkbench();
      99            1 :     const openShell = useOpenShell();
     100            1 :     const onSessionPath = isWorkspacePath(location.pathname);
     101            1 :     const shellActive = onSessionPath && mode === SessionViewMode.Shell;
     102            1 :     const workspaceActive = onSessionPath && !shellActive;
     103            1 :     const renderPage = (page: AppFlavorRailPage) => {
     104            1 :         switch (page.type) {
     105            1 :             case 'workspace':
     106            1 :                 return (
     107            1 :                     <NavRailButton key="workspace" label="Workspace" wide={props.wide} active={workspaceActive} aria-current={workspaceActive ? 'page' : undefined} onClick={openWorkbench}>
     108            1 :                         <NavRailIcon icon="compose_24" />
     109            1 :                     </NavRailButton>
     110              :                 );
     111            1 :             case 'shell':
     112            1 :                 return openShell == null ? null : (
     113            1 :                     <NavRailButton key="shell" label="Shell" wide={props.wide} active={shellActive} aria-current={shellActive ? 'page' : undefined} onClick={openShell}>
     114            1 :                         <NavRailIcon icon="command_palette_24" />
     115            1 :                     </NavRailButton>
     116              :                 );
     117            1 :             case 'upcoming':
     118            1 :                 return (
     119              :                     // Not disabled but aria-disabled, so that the tooltip still names the page
     120            1 :                     <NavRailButton key={page.label} label={page.label} wide={props.wide} className={styles.button_upcoming} aria-disabled="true">
     121            1 :                         <NavRailIcon icon={page.icon} />
     122            1 :                     </NavRailButton>
     123              :                 );
     124            1 :         }
     125            1 :     };
     126            1 :     return (
     127            1 :         <>
     128            1 :             {flavor.railPages.map((group, i) => (
     129            1 :                 <React.Fragment key={i}>
     130            1 :                     {i > 0 && <div className={styles.separator} role="separator" />}
     131            1 :                     {group.map(renderPage)}
     132            1 :                 </React.Fragment>
     133            1 :             ))}
     134            1 :         </>
     135              :     );
     136            1 : };
     137              : 
     138              : /// The rail left of the page card that switches between the pages of the app.
     139              : /// The shell is a page of its own although it shows the current session.
     140              : /// The button at the bottom widens the rail to show the labels of the buttons.
     141            1 : export const NavRail = (): React.ReactElement => {
     142            1 :     const [wide, toggleWide] = useNavRailWide();
     143            1 :     return (
     144              :         // Electron excludes the buttons via the global no-drag rules
     145            1 :         <nav className={classNames(styles.rail, { [styles.rail_wide]: wide })} aria-label="Areas" data-electron-drag-region>
     146            1 :             <div className={styles.group}>
     147            1 :                 <NavRailPages wide={wide} />
     148            1 :             </div>
     149            1 :             <div className={styles.group}>
     150              :                 {/* The octicons show the sidebar on the right, flipped it sits on the left like the rail */}
     151            1 :                 <NavRailButton label={wide ? 'Collapse' : 'Expand'} wide={wide} aria-expanded={wide} onClick={toggleWide}>
     152            1 :                     <svg className={styles.icon_flipped} width="16px" height="16px" aria-hidden="true">
     153            1 :                         <use xlinkHref={`${symbols}#${wide ? 'sidebar_collapse_24' : 'sidebar_expand_24'}`} />
     154            1 :                     </svg>
     155            1 :                 </NavRailButton>
     156            1 :             </div>
     157            1 :         </nav>
     158              :     );
     159            1 : };
        

Generated by: LCOV version 2.0-1