LCOV - code coverage report
Current view: top level - dashql-app/src/session/shell - session_shells_page.tsx Coverage Total Hit
Test: DashQL 915828b Lines: 100.0 % 79 79
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as React from 'react';
       2              : import * as styles from './session_shells_page.module.css';
       3              : import symbols from '@ankoh/dashql-svg-symbols';
       4              : 
       5              : import { useConnectionRegistry } from '../../connections/connection_registry.js';
       6              : import { connectionLabelParts } from '../../connections/connection_label.js';
       7              : import { AnchorAlignment, AnchorSide } from '../../ui/foundations/anchored_position.js';
       8              : import { AnchoredOverlay } from '../../ui/foundations/anchored_overlay.js';
       9              : import { ChevronDownIcon } from '../../ui/foundations/symbol_icon.js';
      10              : import { ConnectionMenu, HealthDot } from '../scripts/script_connection_picker.js';
      11              : import { useConnectionOrdinals } from '../connection_labels.js';
      12              : import { SessionShellPage } from './session_shell_page.js';
      13              : 
      14              : export interface SessionShellsPageDependencies {
      15              :     useConnectionRegistry: typeof useConnectionRegistry;
      16              :     useConnectionOrdinals: typeof useConnectionOrdinals;
      17              :     ConnectionMenu: typeof ConnectionMenu;
      18              :     SessionShellPage: typeof SessionShellPage;
      19              : }
      20              : 
      21            1 : const DEFAULT_DEPENDENCIES: SessionShellsPageDependencies = {
      22            1 :     useConnectionRegistry,
      23            1 :     useConnectionOrdinals,
      24            1 :     ConnectionMenu,
      25            1 :     SessionShellPage,
      26            1 : };
      27              : 
      28              : interface Props {
      29              :     sessionId: string;
      30              :     sessionName?: string | null;
      31              :     /// The connection that the visible shell runs on, null while the session has none
      32              :     connectionId: string | null;
      33              :     /// Show the shell of another connection
      34              :     onPickConnection: (connectionId: string) => void;
      35              :     dependencies?: SessionShellsPageDependencies;
      36              : }
      37              : 
      38              : /// The shells of a session, one per connection.
      39              : /// The header names the connection of the visible shell and switches to the shells of other connections.
      40              : /// A shell lives until the page closes, the shells of other connections are hidden and keep their scrollback.
      41            1 : export const SessionShellsPage: React.FC<Props> = (props: Props) => {
      42            1 :     const dependencies = props.dependencies ?? DEFAULT_DEPENDENCIES;
      43            1 :     const [registry] = dependencies.useConnectionRegistry();
      44            1 :     const ordinals = dependencies.useConnectionOrdinals();
      45            1 :     const [open, setOpen] = React.useState(false);
      46            1 :     const triggerRef = React.useRef<HTMLButtonElement>(null);
      47              : 
      48              :     // The connections that got a shell, in the order they were opened
      49            1 :     const [opened, setOpened] = React.useState<string[]>(() => props.connectionId == null ? [] : [props.connectionId]);
      50            1 :     if (props.connectionId != null && !opened.includes(props.connectionId)) {
      51            1 :         setOpened([...opened, props.connectionId]);
      52              :     }
      53              :     // The shell of a deleted connection closes
      54            1 :     const shells = opened.filter(connectionId => registry.connections.has(connectionId));
      55              : 
      56            1 :     const connection = props.connectionId == null ? null : registry.connections.get(props.connectionId) ?? null;
      57            1 :     const labelParts = connection == null ? null : connectionLabelParts(connection, ordinals.get(connection.connectionId) ?? null);
      58            1 :     const label = labelParts == null ? 'No connection' : `${labelParts.name} ${labelParts.detail}`;
      59              : 
      60            1 :     return (
      61            1 :         <div className={styles.page}>
      62            1 :             <header className={styles.header} data-electron-drag-region>
      63            1 :                 <AnchoredOverlay
      64            1 :                     open={open}
      65            1 :                     onOpen={() => setOpen(true)}
      66            1 :                     onClose={() => setOpen(false)}
      67            1 :                     side={AnchorSide.OutsideBottom}
      68            1 :                     align={AnchorAlignment.Center}
      69            1 :                     anchorOffset={4}
      70            1 :                     anchorRef={triggerRef}
      71            1 :                     returnFocusRef={triggerRef}
      72            1 :                     renderAnchor={anchorProps => (
      73            1 :                         <button
      74            1 :                             {...anchorProps}
      75            1 :                             ref={triggerRef}
      76            1 :                             type="button"
      77            1 :                             className={styles.trigger}
      78            1 :                             aria-label={`Connection of the shell: ${label}`}
      79            1 :                             data-testid="shell-connection-picker"
      80            1 :                             title={label}
      81              :                         >
      82              :                             {/* The monochrome icon takes the white of the dark header */}
      83            1 :                             {connection != null && (
      84            1 :                                 <svg className={styles.trigger_icon} width="16px" height="16px" aria-hidden="true">
      85            1 :                                     <use xlinkHref={`${symbols}#${connection.connectorInfo.icons.uncolored}`} />
      86            1 :                                 </svg>
      87              :                             )}
      88            1 :                             <span className={styles.trigger_label}>{labelParts?.name ?? label}</span>
      89            1 :                             {labelParts != null && <span className={styles.trigger_detail}>{labelParts.detail}</span>}
      90            1 :                             {connection != null && <span className={styles.trigger_status}><HealthDot health={connection.connectionHealth} /></span>}
      91            1 :                             <ChevronDownIcon size={16} />
      92            1 :                         </button>
      93              :                     )}
      94              :                 >
      95            1 :                     <dependencies.ConnectionMenu
      96            1 :                         sessionId={props.sessionId}
      97            1 :                         label="the shell"
      98            1 :                         connectionId={props.connectionId}
      99            1 :                         onPick={connectionId => {
     100            1 :                             setOpen(false);
     101            1 :                             if (connectionId !== props.connectionId) props.onPickConnection(connectionId);
     102            1 :                         }}
     103            1 :                     />
     104            1 :                 </AnchoredOverlay>
     105            1 :             </header>
     106            1 :             <div className={styles.shells}>
     107            1 :                 {shells.map(connectionId => (
     108            1 :                     <div
     109            1 :                         key={connectionId}
     110            1 :                         className={connectionId === props.connectionId ? styles.shell : styles.shell_hidden}
     111            1 :                         data-testid="session-shell-layer"
     112            1 :                         data-connection-id={connectionId}
     113              :                     >
     114            1 :                         <dependencies.SessionShellPage
     115            1 :                             sessionId={props.sessionId}
     116            1 :                             sessionName={props.sessionName}
     117            1 :                             connectionId={connectionId}
     118            1 :                             ordinal={ordinals.get(connectionId) ?? null}
     119            1 :                             active={connectionId === props.connectionId}
     120            1 :                         />
     121            1 :                     </div>
     122            1 :                 ))}
     123            1 :             </div>
     124            1 :         </div>
     125              :     );
     126            1 : };
     127              : 
     128              : export default SessionShellsPage;
        

Generated by: LCOV version 2.0-1