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

            Line data    Source code
       1              : import * as React from 'react';
       2              : import * as styles from './entry_status_bar.module.css';
       3              : 
       4              : import { EntryStatus } from './entry_status_model.js';
       5              : import { StatusIndicator } from '../ui/foundations/status_indicator.js';
       6              : import { AnchoredOverlay } from '../ui/foundations/anchored_overlay.js';
       7              : import { AnchorAlignment, AnchorSide } from '../ui/foundations/anchored_position.js';
       8              : import { ButtonSize, ButtonVariant, IconButton } from '../ui/foundations/button.js';
       9              : import { JsonView } from '../ui/json/json_view.js';
      10              : import { SymbolIcon } from '../ui/foundations/symbol_icon.js';
      11              : import { ChevronDownIcon, ChevronRightIcon } from '../ui/foundations/symbol_icon.js';
      12              : import { classNames } from '../utils/classnames.js';
      13              : 
      14              : export interface EntryStatusBarDependencies {
      15              :     AnchoredOverlay: typeof AnchoredOverlay;
      16              : }
      17              : 
      18            1 : const DEFAULT_DEPENDENCIES: EntryStatusBarDependencies = {
      19            1 :     AnchoredOverlay,
      20            1 : };
      21              : 
      22              : interface EntryStatusBarProps {
      23              :     /// The derived status to show. When null the caller shouldn't render the bar at all.
      24              :     status: EntryStatus;
      25              :     /// Toggle the result content below this status header.
      26              :     onToggleExpanded?: () => void;
      27              :     expanded?: boolean;
      28              :     controls?: string;
      29              :     /// Cancel the in-flight work represented by this status. When set, a visible Cancel button is
      30              :     /// rendered at the right edge without making the whole bar a nested button.
      31              :     onCancel?: () => void;
      32              :     cancelLabel?: string;
      33              :     /// Result-level actions such as execution age and Refresh. Kept outside the clickable log strip
      34              :     /// so the bar never nests interactive controls.
      35              :     actions?: React.ReactNode;
      36              :     /// Result tabs shown right of the expand caret. With tabs, the caret toggles on its own and the
      37              :     /// status message moves to the right, next to the actions.
      38              :     tabs?: React.ReactNode;
      39              :     compact?: boolean;
      40              :     dependencies?: EntryStatusBarDependencies;
      41              : }
      42              : 
      43            1 : const ErrorDetailCard: React.FC<{
      44              :     detail: Record<string, unknown>;
      45              :     onClose: () => void;
      46            1 : }> = ({ detail, onClose }) => {
      47            1 :     const CloseIcon = SymbolIcon('x_16');
      48            1 :     return (
      49            1 :         <section className={styles.error_detail_card} role="dialog" aria-label="Query error details" data-testid="error-details">
      50            1 :             <header className={styles.error_detail_header}>
      51            1 :                 <h2 className={styles.error_detail_title}>Query error details</h2>
      52            1 :                 <IconButton
      53            1 :                     variant={ButtonVariant.Invisible}
      54            1 :                     size={ButtonSize.Small}
      55            1 :                     onClick={onClose}
      56            1 :                     aria-label="Close query error details"
      57            1 :                     data-testid="error-details-close"
      58              :                 >
      59            1 :                     <CloseIcon size={16} />
      60            1 :                 </IconButton>
      61            1 :             </header>
      62            1 :             <JsonView
      63            1 :                 className={styles.error_detail_json}
      64            1 :                 value={detail}
      65            1 :                 collapsed={2}
      66            1 :                 shortenTextAfterLength={100}
      67            1 :             />
      68            1 :         </section>
      69              :     );
      70            1 : };
      71              : 
      72              : /// The result-card header: it renders a spinner (or check/cross) plus a one-line status message and
      73              : /// optionally toggles the result content below it. Failed-query details are available from a
      74              : /// separate control so the toggle never contains nested interactive elements.
      75            1 : export const EntryStatusBar: React.FC<EntryStatusBarProps> = ({ status, onToggleExpanded, expanded, controls, onCancel, cancelLabel = 'Cancel operation', actions, tabs, compact, dependencies = DEFAULT_DEPENDENCIES }) => {
      76            1 :     const [showDetail, setShowDetail] = React.useState(false);
      77            1 :     const hasErrorDetail = status.errorDetail != null;
      78            1 :     const CancelIcon = SymbolIcon('x_16');
      79            1 :     const InfoIcon = SymbolIcon('info_circle_16');
      80            1 :     const AnchoredOverlayComponent = dependencies.AnchoredOverlay;
      81              : 
      82            1 :     const message = (
      83            1 :         <span className={styles.status_bar_message}>
      84            1 :             {status.message}
      85            1 :         </span>
      86              :     );
      87              :     // Keep error details separate from the log button. Nesting the overlay's focusable anchor inside
      88              :     // that button would create two interactive controls in one another.
      89            1 :     const errorDetail = hasErrorDetail ? (
      90            1 :         <AnchoredOverlayComponent
      91            1 :             open={showDetail}
      92            1 :             onOpen={() => setShowDetail(true)}
      93            1 :             onClose={() => setShowDetail(false)}
      94            1 :             side={AnchorSide.OutsideBottom}
      95            1 :             align={AnchorAlignment.End}
      96            1 :             anchorOffset={4}
      97            1 :             renderAnchor={(p: object) => {
      98              :                 const anchorProps = p as React.ButtonHTMLAttributes<HTMLButtonElement>;
      99            1 :                 return (
     100            1 :                     <IconButton
     101            1 :                         {...anchorProps}
     102            1 :                         variant={ButtonVariant.Invisible}
     103            1 :                         className={styles.error_detail_trigger}
     104            1 :                         aria-label="Show error details"
     105            1 :                         data-testid="error-details-toggle"
     106              :                     >
     107            1 :                         <InfoIcon size={16} />
     108            1 :                     </IconButton>
     109              :                 );
     110            1 :             }}
     111              :         >
     112            1 :             <ErrorDetailCard detail={status.errorDetail!} onClose={() => setShowDetail(false)} />
     113            1 :         </AnchoredOverlayComponent>
     114            1 :     ) : null;
     115            1 :     const indicator = (
     116            1 :         <StatusIndicator
     117            1 :             className={styles.status_bar_spinner}
     118            1 :             status={status.indicator}
     119            1 :             width="14px"
     120            1 :             height="14px"
     121            1 :         />
     122              :     );
     123              : 
     124            1 :     return (
     125            1 :         <div className={classNames(styles.status_bar, {
     126            1 :             [styles.status_bar_compact]: compact,
     127            1 :             [styles.status_bar_collapsed]: expanded === false,
     128            1 :         })}>
     129            1 :             {tabs != null ? (
     130            1 :                 <>
     131            1 :                     {onToggleExpanded != null && (
     132            1 :                         <button
     133            1 :                             type="button"
     134            1 :                             className={styles.status_bar_caret}
     135            1 :                             onClick={onToggleExpanded}
     136            1 :                             aria-label={`${expanded ? 'Collapse' : 'Expand'} result: ${status.message}`}
     137            1 :                             aria-expanded={expanded}
     138            1 :                             aria-controls={controls}
     139              :                         >
     140            1 :                             {expanded ? <ChevronDownIcon size={14} /> : <ChevronRightIcon size={14} />}
     141            1 :                         </button>
     142              :                     )}
     143            1 :                     {tabs}
     144            1 :                     <div className={classNames(styles.status_bar_content, styles.status_bar_content_trailing)}>
     145            1 :                         {indicator}
     146            1 :                         {message}
     147            1 :                     </div>
     148            1 :                 </>
     149            1 :             ) : onToggleExpanded != null ? (
     150            0 :                 <button
     151            0 :                     type="button"
     152            0 :                     className={styles.status_bar_log_button}
     153            0 :                     onClick={onToggleExpanded}
     154            0 :                     aria-label={`${expanded ? 'Collapse' : 'Expand'} result: ${status.message}`}
     155            0 :                     aria-expanded={expanded}
     156            0 :                     aria-controls={controls}
     157              :                 >
     158            0 :                     {expanded ? <ChevronDownIcon size={14} /> : <ChevronRightIcon size={14} />}
     159            0 :                     {indicator}
     160            0 :                     {message}
     161            0 :                 </button>
     162              :             ) : (
     163            1 :                 <div className={styles.status_bar_content}>
     164            1 :                     {indicator}
     165            1 :                     {message}
     166            1 :                 </div>
     167              :             )}
     168            1 :             {onCancel != null && (
     169            1 :                 <IconButton
     170            1 :                     variant={ButtonVariant.Invisible}
     171            1 :                     className={styles.status_bar_cancel}
     172            1 :                     onClick={onCancel}
     173            1 :                     aria-label={cancelLabel}
     174              :                 >
     175            1 :                     <CancelIcon size={16} />
     176            1 :                 </IconButton>
     177              :             )}
     178            1 :             {errorDetail}
     179            1 :             {actions != null && (
     180            1 :                 <div className={styles.status_bar_actions}>{actions}</div>
     181              :             )}
     182            1 :         </div>
     183              :     );
     184            1 : };
        

Generated by: LCOV version 2.0-1