LCOV - code coverage report
Current view: top level - dashql-app/src/session/persistence - storage_writer_view.tsx Coverage Total Hit
Test: DashQL 915828b Lines: 99.5 % 217 216
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as React from 'react';
       2              : import * as styles from './storage_writer_view.module.css';
       3              : 
       4              : import { List, useListCallbackRef } from 'react-window';
       5              : import type { RowComponentProps } from 'react-window';
       6              : import { XIcon, ChevronUpIcon, ChevronDownIcon } from '../../ui/foundations/symbol_icon.js';
       7              : 
       8              : import { ButtonVariant, IconButton } from '../../ui/foundations/button.js';
       9              : import { AnchoredOverlay } from '../../ui/foundations/anchored_overlay.js';
      10              : import { OverlaySize } from '../../ui/foundations/overlay.js';
      11              : import { AnchorAlignment, AnchorSide } from '../../ui/foundations/anchored_position.js';
      12              : import { JsonView } from '../../ui/json/json_view.js';
      13              : import { useStorageWriter } from '../../storage/storage_provider.js';
      14              : import { StorageWriteKey, StorageWriterStatistics, StorageWriteStatisticsMap, storageWriteKeyBelongsToSession, storageWriteKeyWithinSession } from '../../storage/storage_writer.js';
      15              : import { formatBytes, formatMilliseconds } from '../../utils/format.js';
      16              : import { observeSize } from '../../ui/foundations/size_observer.js';
      17              : import { useKeyEvents } from '../../utils/key_events.js';
      18              : import { VerticalScrollShadows } from '../../ui/foundations/vertical_scroll_shadows.js';
      19              : 
      20              : export const ROW_HEIGHT = 32;
      21              : 
      22              : export interface StorageWriterEntry {
      23              :     key: StorageWriteKey;
      24              :     /// The key with the active session's id prefix stripped, shown in the Key column (the full key
      25              :     /// stays in the row tooltip and the detail modal).
      26              :     label: string;
      27              :     stats: StorageWriterStatistics;
      28              : }
      29              : 
      30              : export interface StorageWriterRowProps {
      31              :     entries: StorageWriterEntry[];
      32              :     showDetail: (rowIndex: number) => void;
      33              :     selectedIndex: number;
      34              : }
      35              : 
      36            1 : export const StorageWriterRow = (props: RowComponentProps<StorageWriterRowProps>) => {
      37            1 :     const { entries, showDetail, selectedIndex } = props;
      38            1 :     const rowIndex = props.index;
      39            1 :     const entry = entries[rowIndex];
      40              : 
      41            1 :     if (!entry) {
      42            0 :         return <div style={props.style} />;
      43              :     }
      44              : 
      45            1 :     const isSelected = rowIndex === selectedIndex;
      46            1 :     return (
      47            1 :         <div
      48            1 :             className={`${styles.stat_row} ${isSelected ? styles.stat_row_selected : ''}`}
      49            1 :             style={props.style}
      50            1 :             onClick={() => showDetail(rowIndex)}
      51              :         >
      52            1 :             <div className={styles.stat_row_main}>
      53            1 :                 <div className={styles.stat_cell_key} title={entry.key}>
      54            1 :                     {entry.label}
      55            1 :                 </div>
      56            1 :                 <div className={styles.stat_cell_tasks}>
      57            1 :                     {entry.stats.totalScheduledWrites}
      58            1 :                 </div>
      59            1 :                 <div className={styles.stat_cell_writes}>
      60            1 :                     {entry.stats.totalWrites}
      61            1 :                 </div>
      62            1 :                 <div className={styles.stat_cell_time}>
      63            1 :                     {formatMilliseconds(entry.stats.totalWriteTime)}
      64            1 :                 </div>
      65            1 :                 <div className={styles.stat_cell_bytes}>
      66            1 :                     {formatBytes(entry.stats.totalWrittenBytes)}
      67            1 :                 </div>
      68            1 :             </div>
      69            1 :         </div>
      70              :     );
      71            1 : };
      72              : 
      73            1 : function buildEntries(statsMap: StorageWriteStatisticsMap, sessionId: string): StorageWriterEntry[] {
      74            1 :     const entries: StorageWriterEntry[] = [...statsMap.entries()]
      75            1 :         .filter(([key]) => storageWriteKeyBelongsToSession(key, sessionId))
      76            1 :         .map(([key, stats]) => ({ key, label: storageWriteKeyWithinSession(key, sessionId), stats }));
      77              :     // Sort by last write ascending, the list scrolls to its end where the most recent write is.
      78              :     // Entries with no write come first.
      79            1 :     entries.sort((a, b) => {
      80            1 :         const at = a.stats.lastWrite?.getTime() ?? -1;
      81            1 :         const bt = b.stats.lastWrite?.getTime() ?? -1;
      82            1 :         return at - bt;
      83            1 :     });
      84            1 :     return entries;
      85            1 : }
      86              : 
      87            1 : function entryToObject(entry: StorageWriterEntry): object {
      88            1 :     return {
      89            1 :         key: entry.key,
      90            1 :         totalScheduledWrites: entry.stats.totalScheduledWrites,
      91            1 :         totalWrites: entry.stats.totalWrites,
      92            1 :         totalWrittenBytes: entry.stats.totalWrittenBytes,
      93            1 :         totalWriteTime: entry.stats.totalWriteTime,
      94            1 :         lastWrite: entry.stats.lastWrite?.toISOString() ?? null,
      95            1 :     };
      96            1 : }
      97              : 
      98            1 : export function StorageWriterView(props: { sessionId: string | null; onClose: () => void; }) {
      99            1 :     const { sessionId } = props;
     100              : 
     101              :     // Subscribe for storage write statistics
     102            1 :     const storageWriter = useStorageWriter();
     103            1 :     const [statsMap, setStatsMap] = React.useState<StorageWriteStatisticsMap | null>(storageWriter.getStatistics());
     104            1 :     React.useEffect(() => {
     105            1 :         const listener = setStatsMap;
     106            1 :         storageWriter.subscribeStatisticsListener(listener);
     107            1 :         return () => storageWriter.unsubscribeStatisticsListener(listener);
     108            1 :     }, []);
     109              : 
     110              :     // Build sorted entries from the stats map, scoped to the active session.
     111            1 :     const entries = React.useMemo<StorageWriterEntry[]>(() => {
     112            1 :         if (!statsMap || sessionId == null) return [];
     113            1 :         return buildEntries(statsMap, sessionId);
     114            1 :     }, [statsMap, sessionId]);
     115              : 
     116              :     // Container size for the virtual list
     117            1 :     const containerRef = React.useRef<HTMLDivElement>(null);
     118            1 :     const containerSize = observeSize(containerRef);
     119            1 :     const containerWidth = containerSize?.width ?? 200;
     120            1 :     const containerHeight = containerSize?.height ?? 100;
     121              : 
     122              :     // Detail modal state: [entry | null, index]
     123            1 :     const [modalState, setModalState] = React.useState<[StorageWriterEntry | null, number]>([null, -1]);
     124            1 :     const [modalEntry, modalIndex] = modalState;
     125              : 
     126            1 :     const closeRef = React.useRef<HTMLButtonElement>(null);
     127            1 :     const closeModal = React.useCallback(() => setModalState([null, -1]), []);
     128              : 
     129            1 :     const showDetail = React.useCallback((rowIndex: number) => {
     130            1 :         const entry = entries[rowIndex] ?? null;
     131            1 :         if (entry) {
     132            1 :             setModalState([entry, rowIndex]);
     133              :         }
     134            1 :     }, [entries]);
     135              : 
     136            1 :     const showPrevious = React.useCallback(() => {
     137            1 :         if (modalIndex <= 0) return;
     138            1 :         showDetail(modalIndex - 1);
     139            1 :     }, [modalIndex, showDetail]);
     140              : 
     141            1 :     const showNext = React.useCallback(() => {
     142            1 :         if (modalIndex >= entries.length - 1) return;
     143            1 :         showDetail(modalIndex + 1);
     144            1 :     }, [modalIndex, entries.length, showDetail]);
     145              : 
     146              :     // Keyboard navigation when modal is open
     147            1 :     useKeyEvents(
     148            1 :         modalEntry
     149            1 :             ? [
     150            1 :                 {
     151            1 :                     key: 'ArrowUp',
     152            1 :                     callback: (e: KeyboardEvent) => {
     153            1 :                         e.preventDefault();
     154            1 :                         e.stopPropagation();
     155            1 :                         showPrevious();
     156            1 :                     },
     157            1 :                     capture: true,
     158            1 :                 },
     159            1 :                 {
     160            1 :                     key: 'ArrowDown',
     161            1 :                     callback: (e: KeyboardEvent) => {
     162            1 :                         e.preventDefault();
     163            1 :                         e.stopPropagation();
     164            1 :                         showNext();
     165            1 :                     },
     166            1 :                     capture: true,
     167            1 :                 },
     168            1 :             ]
     169            1 :             : []
     170            1 :     );
     171              : 
     172              :     // Auto-scroll to bottom when entries change; scroll to selected row when modal opens
     173            1 :     const [listApi, setListApi] = useListCallbackRef(null);
     174            1 :     const getScrollElement = React.useCallback(() => listApi?.element ?? null, [listApi]);
     175            1 :     React.useEffect(() => {
     176            1 :         if (modalIndex >= 0 && listApi) {
     177            1 :             listApi.scrollToRow({ index: modalIndex, align: 'center' });
     178            1 :         } else if (listApi && entries.length > 0) {
     179            1 :             listApi.scrollToRow({ index: entries.length - 1, align: 'end' });
     180              :         }
     181            1 :     }, [entries, modalIndex, listApi]);
     182              : 
     183            1 :     const rowProps = React.useMemo<StorageWriterRowProps>(() => ({
     184            1 :         entries,
     185            1 :         showDetail,
     186            1 :         selectedIndex: modalIndex,
     187            1 :     }), [entries, showDetail, modalIndex]);
     188              : 
     189            1 :     const modalObject = React.useMemo<object | null>(
     190            1 :         () => modalEntry ? entryToObject(modalEntry) : null,
     191            1 :         [modalEntry]
     192            1 :     );
     193              : 
     194            1 :     return (
     195            1 :         <>
     196            1 :             <div className={styles.overlay}>
     197            1 :                 <div className={styles.header_container}>
     198            1 :                     <div className={styles.header_left_container}>
     199            1 :                         <div className={styles.title}>Storage Writer</div>
     200            1 :                     </div>
     201            1 :                     <div className={styles.header_right_container}>
     202            1 :                         <IconButton
     203            1 :                             ref={closeRef}
     204            1 :                             variant={ButtonVariant.Invisible}
     205            1 :                             aria-label="close-overlay"
     206            1 :                             onClick={props.onClose}
     207              :                         >
     208            1 :                             <XIcon />
     209            1 :                         </IconButton>
     210            1 :                     </div>
     211            1 :                 </div>
     212            1 :                 <div className={styles.stat_header_row}>
     213            1 :                     <div className={styles.stat_header_main}>
     214            1 :                         <div className={styles.stat_cell_key}>Key</div>
     215            1 :                         <div className={styles.stat_cell_tasks}>Tasks</div>
     216            1 :                         <div className={styles.stat_cell_writes}>Writes</div>
     217            1 :                         <div className={styles.stat_cell_time}>Time</div>
     218            1 :                         <div className={styles.stat_cell_bytes}>Bytes</div>
     219            1 :                     </div>
     220            1 :                 </div>
     221            1 :                 <div className={styles.stat_grid_container} ref={containerRef}>
     222            1 :                     {entries.length === 0 ? (
     223            1 :                         <div className={styles.empty_state}>
     224            1 :                             {sessionId == null ? 'Select a session to see its storage writes' : 'Nothing to see here'}
     225            1 :                         </div>
     226              :                     ) : (
     227            1 :                         <List
     228            1 :                             listRef={setListApi}
     229            1 :                             style={{ width: containerWidth, height: containerHeight }}
     230            1 :                             rowCount={entries.length}
     231            1 :                             rowHeight={ROW_HEIGHT}
     232            1 :                             rowComponent={StorageWriterRow}
     233            1 :                             rowProps={rowProps}
     234            1 :                         />
     235              :                     )}
     236            1 :                     <VerticalScrollShadows
     237            1 :                         getScrollElement={getScrollElement}
     238            1 :                         refreshKey={entries.length}
     239            1 :                     />
     240            1 :                 </div>
     241            1 :             </div>
     242            1 :             <AnchoredOverlay
     243            1 :                 renderAnchor={null}
     244            1 :                 anchorRef={containerRef}
     245            1 :                 returnFocusRef={closeRef}
     246            1 :                 open={modalEntry !== null}
     247            1 :                 onClose={closeModal}
     248            1 :                 width={OverlaySize.L}
     249            1 :                 align={AnchorAlignment.Start}
     250            1 :                 side={AnchorSide.OutsideLeft}
     251              :             >
     252            1 :                 <div className={styles.detail_modal}>
     253            1 :                     <div className={styles.detail_modal_main}>
     254            1 :                         <div className={styles.detail_modal_content}>
     255            1 :                             {modalObject && <JsonView value={modalObject} />}
     256            1 :                         </div>
     257            1 :                     </div>
     258            1 :                     <div className={styles.detail_modal_sidebar}>
     259            1 :                         <div className={styles.detail_modal_sidebar_top}>
     260            1 :                             <IconButton
     261            1 :                                 variant={ButtonVariant.Invisible}
     262            1 :                                 aria-label="Close"
     263            1 :                                 onClick={closeModal}
     264              :                             >
     265            1 :                                 <XIcon />
     266            1 :                             </IconButton>
     267            1 :                         </div>
     268            1 :                         <div className={styles.detail_modal_sidebar_bottom}>
     269            1 :                             <IconButton
     270            1 :                                 variant={ButtonVariant.Invisible}
     271            1 :                                 aria-label="Previous entry"
     272            1 :                                 onClick={showPrevious}
     273            1 :                                 disabled={modalIndex <= 0}
     274              :                             >
     275            1 :                                 <ChevronUpIcon />
     276            1 :                             </IconButton>
     277            1 :                             <IconButton
     278            1 :                                 variant={ButtonVariant.Invisible}
     279            1 :                                 aria-label="Next entry"
     280            1 :                                 onClick={showNext}
     281            1 :                                 disabled={modalIndex >= entries.length - 1}
     282              :                             >
     283            1 :                                 <ChevronDownIcon />
     284            1 :                             </IconButton>
     285            1 :                         </div>
     286            1 :                     </div>
     287            1 :                 </div>
     288            1 :             </AnchoredOverlay>
     289            1 :         </>
     290              :     );
     291            1 : }
        

Generated by: LCOV version 2.0-1