LCOV - code coverage report
Current view: top level - dashql-app/src/session/feed - session_feed_page.tsx Coverage Total Hit
Test: DashQL 915828b Lines: 98.5 % 260 256
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as React from 'react';
       2              : import * as styles from './session_feed_page.module.css';
       3              : 
       4              : import { LinkIcon, PaperAirplaneIcon, PlugIcon, SquareFillIcon, SymbolIcon, ThreeBarsIcon } from '../../ui/foundations/symbol_icon.js';
       5              : 
       6              : import { ConnectionHealth, type ConnectionState } from '../../connections/connection_state.js';
       7              : import {
       8              :     isEphemeralSession,
       9              :     scriptConnectionIds,
      10              :     SELECT_SCRIPT,
      11              :     sessionConnectionIds,
      12              :     type SessionScripts,
      13              : } from '../scripts/session_scripts.js';
      14              : import type { ModifySessionScripts } from '../scripts/session_scripts_registry.js';
      15              : import { Button, ButtonSize, ButtonVariant, IconButton } from '../../ui/foundations/button.js';
      16              : import { SessionNavigationDrawer } from '../session_navigation_drawer.js';
      17              : import { SessionWorkbenchSidebar } from '../session_workbench_sidebar.js';
      18              : import { SessionURLShareOverlay } from '../persistence/session_url_share_overlay.js';
      19              : import { SessionFileSaveOverlay } from '../persistence/session_file_save_overlay.js';
      20              : import { ScriptEditorDialog } from '../scripts/script_editor_dialog.js';
      21              : import { SessionFeed, type SessionFeedRevealRequest } from './session_feed.js';
      22              : import { Dashboard } from '../dashboard/dashboard.js';
      23              : import { DataflowCanvas } from '../dataflow/dataflow_canvas.js';
      24              : import { SessionViewMode, useSessionViewMode } from '../session_view_mode.js';
      25              : import { SegmentedControl, SegmentedControlSize } from '../../ui/foundations/segmented_control.js';
      26              : import { useSessionExecution } from '../scripts/session_execution_provider.js';
      27              : import { IndicatorStatus, StatusIndicator } from '../../ui/foundations/status_indicator.js';
      28              : import { AnchoredOverlay } from '../../ui/foundations/anchored_overlay.js';
      29              : import { AnchorAlignment, AnchorSide } from '../../ui/foundations/anchored_position.js';
      30              : import { connectionHealthKey, connectionsToConnect, ExecuteConnectionGuard, SessionConnectionAttentionContext, useSessionConnectionAttention } from '../session_connection_attention.js';
      31              : import { SessionConnectionButtons } from './session_connection_buttons.js';
      32              : import { useAppFlavor } from '../../config/flavor_provider.js';
      33              : import { ConnectionOverviewPage } from '../../connections/connection_overview_page.js';
      34              : import { AgentSessionMenu } from '../../agent/agent_session_menu.js';
      35              : import type { RenameDraftReferences } from '../scripts/script_rename.js';
      36              : 
      37            1 : const FileZipIcon = SymbolIcon('file_zip_16');
      38            1 : const CloseIcon = SymbolIcon('x_16');
      39              : // The views of the session view switch, octicons
      40            1 : const FeedViewIcon = SymbolIcon('rows_16');
      41            1 : const DashboardViewIcon = SymbolIcon('dashboard_16');
      42            1 : const DataflowViewIcon = SymbolIcon('workflow_16');
      43              : /// The views of the switch, the session first
      44            1 : const SESSION_VIEWS: [SessionViewMode, string, typeof FeedViewIcon][] = [
      45            1 :     [SessionViewMode.Session, 'Feed', FeedViewIcon],
      46            1 :     [SessionViewMode.Dashboard, 'Dashboard', DashboardViewIcon],
      47            1 :     [SessionViewMode.Dataflow, 'Dataflow', DataflowViewIcon],
      48            1 : ];
      49              : 
      50              : export interface SessionFeedPageDependencies {
      51              :     SessionFeed: typeof SessionFeed;
      52              :     ScriptEditorDialog: typeof ScriptEditorDialog;
      53              :     SessionWorkbenchSidebar: typeof SessionWorkbenchSidebar;
      54              :     ConnectionOverviewPage: typeof ConnectionOverviewPage;
      55              :     SessionNavigationDrawer: typeof SessionNavigationDrawer;
      56              :     ThreeBarsIcon: typeof ThreeBarsIcon;
      57              :     IconButton: typeof IconButton;
      58              :     Button: typeof Button;
      59              :     Dashboard: typeof Dashboard;
      60              :     DataflowCanvas: typeof DataflowCanvas;
      61              :     SessionURLShareOverlay: typeof SessionURLShareOverlay;
      62              :     SessionFileSaveOverlay: typeof SessionFileSaveOverlay;
      63              :     SessionConnectionButtons: typeof SessionConnectionButtons;
      64              :     useSessionViewMode: typeof useSessionViewMode;
      65              :     SegmentedControl: typeof SegmentedControl;
      66              :     useSessionExecution: typeof useSessionExecution;
      67              : }
      68              : 
      69            1 : const DEFAULT_DEPENDENCIES: SessionFeedPageDependencies = {
      70            1 :     SessionFeed,
      71            1 :     ScriptEditorDialog,
      72            1 :     SessionWorkbenchSidebar,
      73            1 :     ConnectionOverviewPage,
      74            1 :     SessionNavigationDrawer,
      75            1 :     ThreeBarsIcon,
      76            1 :     IconButton,
      77            1 :     Button,
      78            1 :     Dashboard,
      79            1 :     DataflowCanvas,
      80            1 :     SessionURLShareOverlay,
      81            1 :     SessionFileSaveOverlay,
      82            1 :     SessionConnectionButtons,
      83            1 :     useSessionViewMode,
      84            1 :     SegmentedControl,
      85            1 :     useSessionExecution,
      86            1 : };
      87              : 
      88              : interface Props {
      89              :     sessionScripts: SessionScripts;
      90              :     modifySessionScripts: ModifySessionScripts;
      91              :     /// The connection that the next script of the session runs on
      92              :     connection: ConnectionState | null;
      93              :     /// The connections that the scripts of the session run on
      94              :     scriptConnections?: ReadonlyMap<string, ConnectionState>;
      95              :     active: boolean;
      96              :     dependencies?: SessionFeedPageDependencies;
      97              : }
      98              : 
      99            1 : export const SessionFeedPage: React.FC<Props> = (props) => {
     100            1 :     const dependencies = props.dependencies ?? DEFAULT_DEPENDENCIES;
     101              :     // The composer of the feed renames the references in its draft, also for renames in the dataflow
     102            1 :     const draftRenameRef = React.useRef<RenameDraftReferences | null>(null);
     103              :     // The script that the editor dialog of the dataflow shows. It belongs to the session it was opened in and closes
     104              :     // when another session is selected or the script is gone.
     105            1 :     const [details, setDetails] = React.useState<{ sessionId: string; scriptId: number } | null>(null);
     106            1 :     const showDetails = details != null && details.sessionId === props.sessionScripts.sessionId
     107            1 :         && props.sessionScripts.scripts[details.scriptId] != null;
     108            1 :     React.useEffect(() => setDetails(null), [props.sessionScripts.sessionId]);
     109              :     // The dialog selects its script, the commands of the session (Ctrl+E) run the selected script
     110            1 :     const inspectScript = React.useCallback((scriptKey: number) => {
     111            1 :         const target = props.sessionScripts.scripts[scriptKey];
     112            1 :         if (target?.fileName == null) return;
     113            1 :         props.modifySessionScripts({ type: SELECT_SCRIPT, value: target.fileName });
     114            1 :         setDetails({ sessionId: props.sessionScripts.sessionId, scriptId: scriptKey });
     115            1 :     }, [props.sessionScripts.scripts, props.sessionScripts.sessionId, props.modifySessionScripts]);
     116            1 :     const closeDetails = React.useCallback(() => setDetails(null), []);
     117            1 :     const [navigationDrawerOpen, setNavigationDrawerOpen] = React.useState(false);
     118            1 :     const [overviewConnectionId, setOverviewConnectionId] = React.useState<string | null>(null);
     119            1 :     const closeOverview = React.useCallback(() => setOverviewConnectionId(null), []);
     120            1 :     const navigationDrawerTriggerRef = React.useRef<HTMLButtonElement>(null);
     121            1 :     const shareTriggerRef = React.useRef<HTMLButtonElement>(null);
     122            1 :     const exportTriggerRef = React.useRef<HTMLButtonElement>(null);
     123            1 :     const [shareOpen, setShareOpen] = React.useState(false);
     124            1 :     const [exportOpen, setExportOpen] = React.useState(false);
     125            1 :     const archiveExport = useAppFlavor().features.sessionArchiveExport;
     126              :     // A session run takes every connection that a script runs on, they have to be connected first
     127            1 :     const scriptConnections = props.scriptConnections ?? new Map(props.connection == null ? [] : [[props.connection.connectionId, props.connection]]);
     128            1 :     const runConnections = scriptConnectionIds(props.sessionScripts).flatMap(connectionId => scriptConnections.get(connectionId) ?? []);
     129              :     // The bar also shows the connection that the composer drafts the next script on, e.g. the one picked in an empty session
     130            1 :     const barConnections = sessionConnectionIds(props.sessionScripts).flatMap(connectionId => scriptConnections.get(connectionId) ?? []);
     131            1 :     const toConnect = connectionsToConnect(runConnections);
     132            1 :     const disconnected = toConnect.length > 0;
     133            1 :     const connectionAttention = useSessionConnectionAttention(props.sessionScripts.sessionId, connectionHealthKey(barConnections));
     134            1 :     const scriptConnectionsIdle = runConnections.length > 0 && runConnections.every(connection =>
     135            1 :         connection.connectionHealth === ConnectionHealth.ONLINE && (connection.queriesActive?.size ?? 0) === 0);
     136            1 :     const execution = dependencies.useSessionExecution(props.sessionScripts.sessionId);
     137            1 :     const run = execution.state;
     138            1 :     const [executionDetailsOpen, setExecutionDetailsOpen] = React.useState(false);
     139            1 :     React.useEffect(() => setExecutionDetailsOpen(false), [props.sessionScripts.sessionId]);
     140            1 :     React.useEffect(() => setOverviewConnectionId(null), [props.sessionScripts.sessionId]);
     141            1 :     const executeRef = React.useRef<HTMLButtonElement>(null);
     142            1 :     const progress = run ? `${run.completedScripts}/${run.totalScripts}` : '';
     143            1 :     const statusText = !run ? '' : run.phase === 'planning' ? 'Planning...'
     144            1 :         : run.phase === 'stopping' ? `Stopping · ${progress} completed`
     145            1 :         : run.phase === 'failed' ? `${progress} completed`
     146            1 :         : run.phase === 'cancelled' ? `Stopped · ${progress} completed`
     147            1 :         : run.phase === 'succeeded' ? `${progress} succeeded` : `${progress} completed`;
     148            1 :     const { mode, setMode } = dependencies.useSessionViewMode();
     149              :     // The greeter shows the feed, its session has nothing to run, show or share until a script promotes it
     150            1 :     const ephemeral = isEphemeralSession(props.sessionScripts);
     151            1 :     React.useEffect(() => {
     152            1 :         if (ephemeral && (mode === SessionViewMode.Dashboard || mode === SessionViewMode.Dataflow)) setMode(SessionViewMode.Session);
     153            1 :     }, [ephemeral, mode, setMode]);
     154            1 :     const dashboardActive = mode === SessionViewMode.Dashboard;
     155            1 :     const dataflowActive = mode === SessionViewMode.Dataflow;
     156            1 :     const activeView = dashboardActive ? SessionViewMode.Dashboard : dataflowActive ? SessionViewMode.Dataflow : SessionViewMode.Session;
     157            1 :     const overviewActive = overviewConnectionId != null;
     158            1 :     const feedActive = props.active && !dashboardActive && !dataflowActive && !overviewActive;
     159              :     // A script opened from the sidebar shows in the feed
     160            1 :     const [revealRequest, setRevealRequest] = React.useState<SessionFeedRevealRequest | null>(null);
     161            1 :     const openScript = React.useCallback((sessionId: string, fileName: string) => {
     162            0 :         setOverviewConnectionId(null);
     163            0 :         setDetails(null);
     164            0 :         if (mode !== SessionViewMode.Session) setMode(SessionViewMode.Session);
     165            0 :         setRevealRequest({ sessionId, fileName });
     166            1 :     }, [mode, setMode]);
     167            1 :     const workbench = (closeAfterSelection: boolean) => (
     168            1 :         <dependencies.SessionWorkbenchSidebar
     169            1 :             sessionScripts={props.sessionScripts}
     170            1 :             closeAfterSelection={closeAfterSelection ? () => setNavigationDrawerOpen(false) : undefined}
     171            1 :             openOverviewId={overviewConnectionId}
     172            1 :             onOpenOverview={setOverviewConnectionId}
     173            1 :             onOpenScript={openScript}
     174            1 :         />
     175              :     );
     176              : 
     177            1 :     return (
     178            1 :         <SessionConnectionAttentionContext.Provider value={connectionAttention}>
     179            1 :         <div className={styles.page}>
     180            1 :             <header className={styles.view_bar} data-electron-drag-region>
     181            1 :                 <dependencies.IconButton className={styles.mobile_menu_button} ref={navigationDrawerTriggerRef} variant={ButtonVariant.Default} aria-label="Open session workbench" data-testid="open-workbench" onClick={() => setNavigationDrawerOpen(true)}>
     182            1 :                     <dependencies.ThreeBarsIcon />
     183            1 :                 </dependencies.IconButton>
     184              :                 {/* A connection overview is not part of the session, its controls are hidden */}
     185            1 :                 {!overviewActive && <>
     186            1 :                     {!ephemeral && <dependencies.SegmentedControl
     187            1 :                         aria-label="Session view"
     188            1 :                         data-testid="session-view-switch"
     189            1 :                         size={SegmentedControlSize.Small}
     190            1 :                         onChange={(selectedIndex) => {
     191            1 :                             setOverviewConnectionId(null);
     192              :                             // The editor dialog belongs to the dataflow view
     193            1 :                             setDetails(null);
     194            1 :                             setMode(SESSION_VIEWS[selectedIndex]?.[0] ?? SessionViewMode.Session);
     195            1 :                         }}
     196              :                     >
     197            1 :                         {SESSION_VIEWS.map(([view, label, Icon]) => (
     198            1 :                             <dependencies.SegmentedControl.IconButton
     199            1 :                                 key={view}
     200            1 :                                 className={styles.view_switch_button}
     201            1 :                                 aria-label={label}
     202            1 :                                 icon={<Icon size={14} />}
     203            1 :                                 selected={activeView === view}
     204            1 :                             />
     205            1 :                         ))}
     206            1 :                     </dependencies.SegmentedControl>}
     207            1 :                     {!ephemeral && <div className={styles.execution_controls}>
     208            1 :                         <ExecuteConnectionGuard sessionId={props.sessionScripts.sessionId} blocked={disconnected && !execution.busy}>
     209            1 :                         <dependencies.Button ref={executeRef} size={ButtonSize.Small} variant={ButtonVariant.Invisible}
     210            1 :                             leadingVisual={execution.busy ? SquareFillIcon : disconnected ? PlugIcon : PaperAirplaneIcon}
     211            1 :                             aria-label={execution.busy ? 'Stop session execution' : disconnected ? 'Connect to run' : 'Execute All'}
     212            1 :                             data-testid={execution.busy ? 'stop-execution' : 'execute-all'}
     213            1 :                             data-connect={!execution.busy && disconnected ? '' : undefined}
     214            1 :                             disabled={execution.busy ? run?.phase === 'stopping' : !disconnected && (!scriptConnectionsIdle
     215            1 :                                 || execution.runActive || props.sessionScripts.scriptOrder.length === 0)}
     216            1 :                             onClick={() => execution.busy ? execution.stop()
     217              :                                 // The connections that are not connected yet pulse in the bar, nothing opens by itself
     218            1 :                                 : disconnected ? connectionAttention.request(props.sessionScripts.sessionId)
     219            1 :                                 : execution.start(props.modifySessionScripts)}>
     220            1 :                             <span className={styles.action_label}>{execution.busy ? run?.phase === 'stopping' ? 'Stopping' : 'Stop' : disconnected ? 'Connect to run' : 'Execute All'}</span>
     221            1 :                         </dependencies.Button>
     222            1 :                         </ExecuteConnectionGuard>
     223            1 :                         <div className={styles.execution_status} role="status" aria-live="polite" aria-atomic="true" data-testid="execution-status" data-phase={run?.phase}
     224            1 :                             title={run?.error ?? undefined} aria-label={run?.error ? `${statusText}: ${run.error}` : undefined}>
     225            1 :                             {run && <>
     226            1 :                                 <AnchoredOverlay
     227            1 :                                     open={executionDetailsOpen} onOpen={() => setExecutionDetailsOpen(true)} onClose={() => setExecutionDetailsOpen(false)}
     228            1 :                                     side={AnchorSide.OutsideBottom} align={AnchorAlignment.Start} anchorOffset={4}
     229            1 :                                     renderAnchor={anchorProps => <dependencies.Button {...anchorProps}
     230            1 :                                         size={ButtonSize.Small} variant={ButtonVariant.Invisible} aria-label="Show session execution details" data-testid="execution-details-toggle"
     231            1 :                                         leadingVisual={<span className={styles.execution_indicator} aria-hidden="true" data-testid="execution-indicator"><StatusIndicator width="16px" height="16px" status={execution.busy
     232            1 :                                             ? IndicatorStatus.Running : run.phase === 'failed' ? IndicatorStatus.Failed
     233            1 :                                                 : run.phase === 'succeeded' ? IndicatorStatus.Succeeded : IndicatorStatus.Skip} /></span>}>
     234            1 :                                         <span className={styles.execution_progress}>{statusText}</span>
     235            1 :                                     </dependencies.Button>}>
     236            1 :                                     <section className={styles.execution_error_card} role="dialog" aria-label="Session execution details" data-testid="execution-details">
     237            1 :                                         <header className={styles.execution_error_header}>
     238            1 :                                             <h2 className={styles.execution_details_title}>{run.phase === 'failed' ? 'Execution failed' : 'Session execution'}</h2>
     239            1 :                                             <dependencies.IconButton size={ButtonSize.Small} variant={ButtonVariant.Invisible}
     240            1 :                                                 aria-label="Close session execution details" data-testid="execution-details-close" onClick={() => setExecutionDetailsOpen(false)}>
     241            1 :                                                 <CloseIcon size={16} />
     242            1 :                                             </dependencies.IconButton>
     243            1 :                                         </header>
     244            1 :                                         <div className={styles.execution_details_body}>
     245            1 :                                             <div>{statusText}</div>
     246            1 :                                             {run.activeFileName && <div>{run.activeFileName} · statement {run.activeStatementIndex}/{run.activeStatementCount}</div>}
     247            1 :                                             <div>{run.completedStatements}/{run.totalStatements} statements completed</div>
     248            1 :                                             {run.error && <div className={styles.execution_error_message}>{run.error}</div>}
     249            1 :                                         </div>
     250            1 :                                     </section>
     251            1 :                                 </AnchoredOverlay>
     252            1 :                             </>}
     253            1 :                         </div>
     254            1 :                     </div>}
     255            1 :                     <div className={styles.header_actions}>
     256              :                         {/* The connections of the session, the user sees which one is not connected */}
     257            1 :                         <dependencies.SessionConnectionButtons connections={barConnections} attention={connectionAttention.active} />
     258            1 :                         <AgentSessionMenu sessionId={props.sessionScripts.sessionId} labelClassName={styles.action_label} />
     259            1 :                         {!ephemeral && <dependencies.Button ref={shareTriggerRef} size={ButtonSize.Small} variant={ButtonVariant.Invisible} leadingVisual={LinkIcon} aria-label="Share as URL" data-testid="share-url" aria-expanded={shareOpen} onClick={() => {
     260            1 :                             setExportOpen(false);
     261            1 :                             setShareOpen(value => !value);
     262            1 :                         }}>
     263            1 :                             <span className={styles.action_label}>Share as URL</span>
     264            1 :                         </dependencies.Button>}
     265            1 :                         <dependencies.SessionURLShareOverlay sessionId={props.sessionScripts.sessionId} isOpen={shareOpen} setIsOpen={setShareOpen} anchorRef={shareTriggerRef} />
     266            1 :                         {archiveExport && !ephemeral && (
     267            1 :                             <>
     268            1 :                                 <dependencies.Button ref={exportTriggerRef} size={ButtonSize.Small} variant={ButtonVariant.Invisible} leadingVisual={FileZipIcon} aria-label="Export .dashql" data-testid="export-session" aria-expanded={exportOpen} onClick={() => {
     269            1 :                                     setShareOpen(false);
     270            1 :                                     setExportOpen(value => !value);
     271            1 :                                 }}>
     272            1 :                                     <span className={styles.action_label}>Export .dashql</span>
     273            1 :                                 </dependencies.Button>
     274            1 :                                 <dependencies.SessionFileSaveOverlay
     275            1 :                                     isOpen={exportOpen}
     276            1 :                                     setIsOpen={setExportOpen}
     277            1 :                                     anchorRef={exportTriggerRef}
     278            1 :                                     sessionScripts={props.sessionScripts}
     279            1 :                                     fileName={props.sessionScripts.name ?? props.sessionScripts.sessionMetadata.originalFileName ?? 'session'}
     280            1 :                                 />
     281            1 :                             </>
     282              :                         )}
     283            1 :                     </div>
     284            1 :                 </>}
     285            1 :             </header>
     286            1 :             <aside className={styles.navigation_sidebar}>
     287            1 :                 {workbench(false)}
     288            1 :             </aside>
     289            1 :             <main className={styles.body_container} id="session-body">
     290            1 :                 <div className={styles.content_container}>
     291            1 :                     <div className={feedActive ? styles.feed_layer : styles.feed_layer_hidden}>
     292            1 :                         <dependencies.SessionFeed
     293            1 :                             sessionScripts={props.sessionScripts}
     294            1 :                             modifySessionScripts={props.modifySessionScripts}
     295            1 :                             draftRenameRef={draftRenameRef}
     296            1 :                             active={feedActive}
     297            1 :                             revealRequest={revealRequest}
     298            1 :                             conn={props.connection}
     299            1 :                             scriptConnections={props.scriptConnections}
     300            1 :                         />
     301            1 :                     </div>
     302            1 :                     {overviewConnectionId != null && (
     303            1 :                         <dependencies.ConnectionOverviewPage
     304            1 :                             connectionId={overviewConnectionId}
     305            1 :                             onClose={closeOverview}
     306            1 :                         />
     307              :                     )}
     308            1 :                     {dashboardActive && !overviewActive && (
     309            1 :                         <dependencies.Dashboard
     310            1 :                             sessionScripts={props.sessionScripts}
     311            1 :                             modifySessionScripts={props.modifySessionScripts}
     312            1 :                             scriptConnections={scriptConnections}
     313            1 :                             draftRenameRef={draftRenameRef}
     314            1 :                         />
     315              :                     )}
     316            1 :                     {dataflowActive && !overviewActive && (
     317            1 :                         <div className={styles.dataflow_layout}>
     318            1 :                             <div className={styles.dataflow_canvas}>
     319            1 :                                 <dependencies.DataflowCanvas
     320            1 :                                     key={props.sessionScripts.sessionId}
     321            1 :                                     sessionScripts={props.sessionScripts}
     322            1 :                                     modifySessionScripts={props.modifySessionScripts}
     323            1 :                                     scriptConnections={scriptConnections}
     324            1 :                                     draftRenameRef={draftRenameRef}
     325            1 :                                     onInspect={inspectScript}
     326            1 :                                 />
     327            1 :                             </div>
     328            1 :                             {showDetails && (
     329            1 :                                 <dependencies.ScriptEditorDialog
     330            1 :                                     key={details.scriptId}
     331            1 :                                     sessionScripts={props.sessionScripts}
     332            1 :                                     modifySessionScripts={props.modifySessionScripts}
     333            1 :                                     scriptKey={details.scriptId}
     334            1 :                                     scriptConnections={scriptConnections}
     335            1 :                                     draftRenameRef={draftRenameRef}
     336            1 :                                     onNavigateToScript={inspectScript}
     337            1 :                                     onClose={closeDetails}
     338            1 :                                 />
     339              :                             )}
     340            1 :                         </div>
     341              :                     )}
     342            1 :                 </div>
     343            1 :             </main>
     344            1 :             {navigationDrawerOpen && (
     345            1 :                 <dependencies.SessionNavigationDrawer open onClose={() => setNavigationDrawerOpen(false)} returnFocusRef={navigationDrawerTriggerRef}>
     346            1 :                     {workbench(true)}
     347            1 :                 </dependencies.SessionNavigationDrawer>
     348              :             )}
     349            1 :         </div>
     350            1 :         </SessionConnectionAttentionContext.Provider>
     351              :     );
     352            1 : };
        

Generated by: LCOV version 2.0-1