Line data Source code
1 : import * as React from 'react';
2 : import * as styles from './tab_header.module.css';
3 :
4 : import { QueryExecutionState, QueryExecutionStatus } from '../query/query_execution_state.js';
5 : import { ButtonSize, ButtonVariant, IconButton } from '../ui/foundations/button.js';
6 : import { ScreenFullIcon } from '../ui/foundations/symbol_icon.js';
7 : import { classNames } from '../utils/classnames.js';
8 : import { useComputationRegistry } from '../compute/computation_registry.js';
9 :
10 : interface TabHeaderProps {
11 : title: string;
12 : detail?: string | null;
13 : /// When set, the header becomes clickable (the feed footer uses this to open the full result table)
14 : /// and ends with an expand button doing the same. Omitted elsewhere, where the header is a plain,
15 : /// non-clickable label + count.
16 : onClick?: () => void;
17 : /// Render the count as a link, for previews that only show part of the rows (clickable variant).
18 : detailIsLink?: boolean;
19 : /// Optional right-aligned content (e.g. a "delete cached" button). Rendered on both variants; on
20 : /// the clickable variant the title/count sit in an inner button while the actions sit beside it,
21 : /// since nesting interactive controls inside a button is invalid.
22 : actions?: React.ReactNode;
23 : }
24 :
25 : /// The header bar shown atop a session tab body: a title with an optional greyish "N of M rows"
26 : /// count. Shared by the feed footer (clickable, opens the full result table) and the full views (inert),
27 : /// so the surfaces read identically.
28 1 : export const TabHeader: React.FC<TabHeaderProps> = ({ title, detail, onClick, detailIsLink, actions }) => {
29 1 : if (onClick != null) {
30 1 : return (
31 1 : <div className={styles.tab_header}>
32 1 : <button type="button" className={styles.tab_header_clickable_inner} onClick={onClick}>
33 1 : <span className={styles.tab_header_title}>{title}</span>
34 1 : {detail && (
35 1 : <span className={classNames(styles.tab_header_detail, { [styles.tab_header_detail_link]: detailIsLink })}>
36 1 : {detail}
37 1 : </span>
38 : )}
39 1 : </button>
40 1 : <div className={styles.tab_header_actions}>
41 1 : {actions}
42 1 : <IconButton
43 1 : variant={ButtonVariant.Invisible}
44 1 : size={ButtonSize.Small}
45 1 : aria-label={`Expand ${title}`}
46 1 : data-testid="tab-header-expand"
47 1 : onClick={onClick}
48 : >
49 1 : <ScreenFullIcon size={16} />
50 1 : </IconButton>
51 1 : </div>
52 1 : </div>
53 : );
54 : }
55 1 : return (
56 1 : <div className={styles.tab_header}>
57 1 : <span className={styles.tab_header_title}>{title}</span>
58 1 : {detail && <span className={styles.tab_header_detail}>{detail}</span>}
59 1 : {actions && <div className={styles.tab_header_actions}>{actions}</div>}
60 1 : </div>
61 : );
62 1 : };
63 :
64 : /// Whether the query produced a materialized result, and its total row count. Shared so the footer's
65 : /// preview and the Details tabs derive the "N rows" detail identically.
66 1 : export function useResultRowCount(queryState: QueryExecutionState | null): { hasResult: boolean; totalRows: number | null } {
67 1 : const computationState = useComputationRegistry()?.[0];
68 1 : const hasResult = queryState != null
69 1 : && queryState.status === QueryExecutionStatus.SUCCEEDED
70 1 : && computationState?.tableComputations[queryState.queryId] != null;
71 1 : const totalRows = hasResult
72 1 : ? (computationState?.tableComputations[queryState!.queryId]?.dataTable.numRows ?? null)
73 1 : : null;
74 1 : return { hasResult, totalRows };
75 1 : }
76 :
77 : /// Format a plain "N rows" / "N row" detail string from a total (null when the total is unknown).
78 1 : export function formatRowCountDetail(totalRows: number | null): string | null {
79 1 : if (totalRows == null) return null;
80 1 : return `${totalRows} ${totalRows === 1 ? 'row' : 'rows'}`;
81 1 : }
|