/** * A data grid with IDEA's result-view extras, shared by console results and * the table editor: the header's local filter (values of the loaded rows), * client-side sorting when the caller does not sort on the server, and the * Record / Value / Aggregates side panel following the selection. */ import { useMemo, useState } from "react"; import clsx from "clsx"; import { Button, Checkbox, Icon, IconButton, Input, Popup, Select, Tabs, T, type MenuItem } from "@/ui"; import { EXPORT_FORMATS, exportData, type ExportData, type ExportFormat } from "./export"; import { Grid, type GridColumn, type GridProps, type GridRange } from "./Grid"; import type { Cell } from "./model"; import { aggregates, distinctValues, formatNumber, toggleFilterValue, visibleRows, type LocalFilter, type LocalSort, } from "./resultView"; import { openDialog } from "./store"; import styles from "./Database.module.css"; import { tx } from "@/platform/i18n"; export type DataPanel = "record" | "value" | "aggregates"; /** The Export button's menu: the Export Data dialog (with the toolbar's extractor), then IDEA's quick Copy as… extractors. */ export function exportMenu(data: ExportData, source: string, format?: ExportFormat): MenuItem[] { return [ { label: "Export Data…", icon: "export", run: () => openDialog({ kind: "export", data, source, format }) }, { kind: "separator" }, ...EXPORT_FORMATS.map((f): MenuItem => ({ label: tx`Copy as ${f.label}`, icon: "copy", run: () => void navigator.clipboard?.writeText(exportData(f.id, data)), })), ]; } /** IDEA's extractor drop-down at the right of a data toolbar ("CSV ▾"). */ export function ExtractorSelect({ value, onChange }: { value: ExportFormat; onChange: (f: ExportFormat) => void }) { return ( setQuery(e.target.value)} />
{list.map(({ value: v, count }) => (
props.onToggle(all, v, on)} label={v === null ? <null> : v} /> {count}
))} {!list.length && (
No matching values
)}
); } const AGGREGATE_ROWS: ReadonlyArray<[label: string, key: keyof ReturnType]> = [ ["Count", "count"], ["Distinct", "distinct"], ["Nulls", "nulls"], ["Sum", "sum"], ["Avg", "avg"], ["Median", "median"], ["Min", "min"], ["Max", "max"], ]; function SidePanel(props: { panel: DataPanel; onPanelChange: (p: DataPanel | null) => void; columns: readonly GridColumn[]; range: GridRange | null; value: (row: number, col: number) => Cell; editable: boolean; onEdit: (row: number, col: number, v: Cell) => void; }) { const { range, value, columns } = props; return ( ); } /** One field per column of the lead row; edits apply on Enter / leaving the field. */ function RecordView(props: { columns: readonly GridColumn[]; row: number; value: (row: number, col: number) => Cell; editable: boolean; onEdit: (row: number, col: number, v: Cell) => void; }) { return (
{props.columns.map((c, i) => ( props.onEdit(props.row, i, v)} /> ))}
); } function RecordField(props: { column: GridColumn; cell: Cell; editable: boolean; onCommit: (v: Cell) => void }) { const [text, setText] = useState(props.cell ?? ""); const commit = () => { if (text !== (props.cell ?? "") || (props.cell === null && text !== "")) props.onCommit(text); }; const c = props.column; return ( ); } function ValueView(props: { column: string; cell: Cell; editable: boolean; onCommit: (v: Cell) => void }) { const [text, setText] = useState(props.cell ?? ""); const dirty = text !== (props.cell ?? "") || (props.cell === null && text !== ""); return (