UNPKG

@flanksource/clicky-ui

Version:

Flanksource Clicky UI — React component library built on shadcn/ui with light/dark and density theming.

1,212 lines 59.2 kB
import { jsxs, Fragment, jsx } from "react/jsx-runtime"; import { useMemo, useState, useEffect, Fragment as Fragment$1 } from "react"; import { useSort } from "../hooks/use-sort.js"; import { cn } from "../lib/utils.js"; import { FilterBar, FilterBarFilterPanel, FilterBarRangePanel } from "../components/FilterBar.js"; import { Icon } from "./Icon.js"; import { SortableHeader } from "./SortableHeader.js"; import { parseTimestamp, chooseTimestampFormat, modeToFormat, resolveDateMath, Timestamp } from "./cells/Timestamp.js"; import { tagActionsFromRecord, TagActionsProvider, tagFilterTokens, TagList, normalizeTags, splitTagToken } from "./cells/TagList.js"; import { StatusDot } from "./cells/StatusDot.js"; import { normalizeStatus } from "./cells/status-mapping.js"; const DEFAULT_COLUMN_MIN_WIDTH = 64; const DEFAULT_COLUMN_WIDTH = 160; const DEFAULT_GROW_COLUMN_WIDTH = 224; const DEFAULT_SHRINK_COLUMN_WIDTH = 96; const COLUMN_WIDTH_STORAGE_PREFIX = "clicky-ui-data-table-column-widths"; const COLUMN_VISIBILITY_STORAGE_PREFIX = "clicky-ui-data-table-column-visibility"; const DENSITY_STORAGE_PREFIX = "clicky-ui-data-table-density"; const DENSITY_OPTIONS = [ { value: "compact", icon: "ph:rows", label: "Compact" }, { value: "comfortable", icon: "ph:list", label: "Comfortable" }, { value: "spacious", icon: "ph:list-dashes", label: "Spacious" } ]; const DATA_TABLE_HEADER_DENSITY_CLASS = "px-2.5 py-1.5 density-compact:px-2 density-compact:py-1 density-comfortable:px-2.5 density-comfortable:py-1.5 density-spacious:px-4 density-spacious:py-3"; const DATA_TABLE_CELL_DENSITY_CLASS = "px-2.5 py-1.5 density-compact:px-2 density-compact:py-0.5 density-comfortable:px-2.5 density-comfortable:py-1.5 density-spacious:px-4 density-spacious:py-3"; const TIMESTAMP_RANGE_PRESETS = [ { label: "Last 5 minutes", from: "now-5m", to: "now" }, { label: "Last 15 minutes", from: "now-15m", to: "now" }, { label: "Last 1 hour", from: "now-1h", to: "now" }, { label: "Last 6 hours", from: "now-6h", to: "now" }, { label: "Last 24 hours", from: "now-24h", to: "now" }, { label: "Last 7 days", from: "now-7d", to: "now" }, { label: "Last 30 days", from: "now-30d", to: "now" } ]; function DataTable({ data, columns: columnsInput, emptyMessage: _emptyMessage = "No data", className, autoFilter = false, showGlobalFilter = true, globalFilter, onGlobalFilterChange, globalFilterPlaceholder = "Search all columns…", defaultSort, filterBarProps, getRowId, onRowClick, isRowClickable, getRowHref, renderExpandedRow, resizableColumns = true, persistColumnWidths = true, columnResizeStorageKey, hideableColumns = true, persistColumnVisibility = true, columnVisibilityStorageKey, density, defaultDensity, onDensityChange, persistDensity = true, densityStorageKey, showDensityControl, showHeaderFilters = true }) { const columns = useMemo( () => columnsInput.map( (column) => typeof column === "string" ? { key: column, label: column } : column ), [columnsInput] ); const columnKeysSignature = useMemo( () => columns.map((column) => column.key).join("|"), [columns] ); const resolvedColumnResizeStorageKey = columnResizeStorageKey ?? `${COLUMN_WIDTH_STORAGE_PREFIX}:${columnKeysSignature}`; const resolvedColumnVisibilityStorageKey = columnVisibilityStorageKey ?? `${COLUMN_VISIBILITY_STORAGE_PREFIX}:${columnKeysSignature}`; const resolvedDensityStorageKey = densityStorageKey ?? `${DENSITY_STORAGE_PREFIX}:${columnKeysSignature}`; const [columnWidths, setColumnWidths] = useState( () => persistColumnWidths ? readStoredColumnWidths(resolvedColumnResizeStorageKey, columns) : {} ); const [hiddenColumns, setHiddenColumns] = useState( () => persistColumnVisibility ? readStoredHiddenColumns(resolvedColumnVisibilityStorageKey, columns) : {} ); const densityControlled = density !== void 0; const [localDensityOverride, setLocalDensityOverride] = useState(() => { if (densityControlled) return density; if (persistDensity) return readStoredDensityOverride(resolvedDensityStorageKey) ?? defaultDensity; return defaultDensity; }); const [columnMenu, setColumnMenu] = useState(null); const [headerFilterMenu, setHeaderFilterMenu] = useState(null); const [textFilters, setTextFilters] = useState({}); const [multiFilters, setMultiFilters] = useState({}); const [numberFilters, setNumberFilters] = useState({}); const [expandedRows, setExpandedRows] = useState({}); const [localGlobalFilter, setLocalGlobalFilter] = useState(""); const [timeRangeFilter, setTimeRangeFilter] = useState(() => { var _a, _b, _c, _d; const seed = columns.find( (column) => { var _a2; return column.kind === "timestamp" && ((_a2 = column.timestamp) == null ? void 0 : _a2.defaultRange); } ); return { from: ((_b = (_a = seed == null ? void 0 : seed.timestamp) == null ? void 0 : _a.defaultRange) == null ? void 0 : _b.from) ?? "", to: ((_d = (_c = seed == null ? void 0 : seed.timestamp) == null ? void 0 : _c.defaultRange) == null ? void 0 : _d.to) ?? "" }; }); useEffect(() => { setColumnWidths((current) => { const stored = persistColumnWidths ? readStoredColumnWidths(resolvedColumnResizeStorageKey, columns) : {}; const next = pruneColumnWidths({ ...stored, ...current }, columns); return sameColumnWidths(current, next) ? current : next; }); }, [columnKeysSignature, persistColumnWidths, resolvedColumnResizeStorageKey]); useEffect(() => { if (!persistColumnWidths) return; writeStoredColumnWidths(resolvedColumnResizeStorageKey, columnWidths); }, [columnWidths, persistColumnWidths, resolvedColumnResizeStorageKey]); useEffect(() => { setHiddenColumns((current) => { const stored = persistColumnVisibility ? readStoredHiddenColumns(resolvedColumnVisibilityStorageKey, columns) : {}; const next = pruneHiddenColumns({ ...stored, ...current }, columns); return sameHiddenColumns(current, next) ? current : next; }); }, [columnKeysSignature, persistColumnVisibility, resolvedColumnVisibilityStorageKey]); useEffect(() => { if (!persistColumnVisibility) return; writeStoredHiddenColumns(resolvedColumnVisibilityStorageKey, hiddenColumns); }, [hiddenColumns, persistColumnVisibility, resolvedColumnVisibilityStorageKey]); useEffect(() => { if (densityControlled) return; setLocalDensityOverride( persistDensity ? readStoredDensityOverride(resolvedDensityStorageKey) ?? defaultDensity : defaultDensity ); }, [defaultDensity, densityControlled, persistDensity, resolvedDensityStorageKey]); useEffect(() => { if (densityControlled || !persistDensity) return; writeStoredDensityOverride(resolvedDensityStorageKey, localDensityOverride); }, [densityControlled, localDensityOverride, persistDensity, resolvedDensityStorageKey]); useEffect(() => { if (!columnMenu && !headerFilterMenu) return; const close = () => { setColumnMenu(null); setHeaderFilterMenu(null); }; const closeOnEscape = (event) => { if (event.key === "Escape") close(); }; document.addEventListener("click", close); document.addEventListener("keydown", closeOnEscape); return () => { document.removeEventListener("click", close); document.removeEventListener("keydown", closeOnEscape); }; }, [columnMenu, headerFilterMenu]); const rows = useMemo( () => data.map((row, index) => ({ id: (getRowId == null ? void 0 : getRowId(row, index)) ?? String(index), row })), [data, getRowId] ); const timestampFormats = useMemo(() => { var _a; const out = {}; for (const column of columns) { if (column.kind !== "timestamp") continue; const dates = []; for (const row of data) { const raw = resolvePath(row, column.key); const parsed = parseTimestamp(raw); if (parsed) dates.push(parsed); } const dataFormat = chooseTimestampFormat(dates); out[column.key] = modeToFormat(((_a = column.timestamp) == null ? void 0 : _a.mode) ?? "auto", dataFormat); } return out; }, [columns, data]); const effectiveColumns = useMemo( () => columns.map((column) => applyKindDefaults(column, timestampFormats[column.key])), [columns, timestampFormats] ); const visibleColumns = useMemo(() => { if (!hideableColumns) return effectiveColumns; const next = effectiveColumns.filter((column) => !hiddenColumns[column.key]); return next.length > 0 ? next : effectiveColumns; }, [effectiveColumns, hiddenColumns, hideableColumns]); const hideableColumnCount = useMemo( () => effectiveColumns.filter(isColumnHideable).length, [effectiveColumns] ); const visibleHideableColumnCount = useMemo( () => visibleColumns.filter(isColumnHideable).length, [visibleColumns] ); const showColumnVisibilityControl = hideableColumns && hideableColumnCount > 1; const resolvedShowDensityControl = showDensityControl ?? showColumnVisibilityControl; const showTablePreferencesControl = showColumnVisibilityControl || resolvedShowDensityControl; const densityOverride = densityControlled ? density : localDensityOverride; const tagActionsByColumn = useMemo(() => { const out = {}; for (const column of visibleColumns) { if (column.kind !== "tags") continue; const columnKey = column.key; const current = multiFilters[columnKey] ?? {}; const handler = (next) => setMultiFilters((state) => updateFilterRecord(state, columnKey, next)); out[columnKey] = tagActionsFromRecord(current, handler); } return out; }, [multiFilters, visibleColumns]); const timeRangeColumn = useMemo(() => { if (filterBarProps == null ? void 0 : filterBarProps.timeRange) return null; return visibleColumns.find( (column) => { var _a; return column.kind === "timestamp" && column.filterable !== false && ((_a = column.timestamp) == null ? void 0 : _a.autoRangeFilter) !== false; } ) ?? null; }, [filterBarProps == null ? void 0 : filterBarProps.timeRange, visibleColumns]); const filterableColumns = useMemo( () => visibleColumns.filter((column) => { if (column.filterable === false) return false; if (column.kind === "timestamp") return false; return true; }), [visibleColumns] ); const generatedFilters = useMemo(() => { if (!autoFilter) return []; return filterableColumns.map((column) => { var _a; const numberBounds = getNumericFilterBounds(rows, column); if (numberBounds) { return { column, kind: "number", options: [], numberBounds }; } const values = /* @__PURE__ */ new Set(); for (const record of rows) { for (const token of getFilterTokens(record.row, column)) { if (token) values.add(token); } } const options = Array.from(values).sort((left, right) => left.localeCompare(right, void 0, { numeric: true })).map((value) => ({ value, label: value })); const multiCap = column.kind === "tags" ? 50 : 20; const fitsMulti = options.length >= 2 && options.length <= multiCap; if (column.kind === "tags" && fitsMulti) { const separator = ((_a = column.tags) == null ? void 0 : _a.separator) ?? "="; const groups = groupTagOptions(options, separator); if (groups.length >= 2) { return { column, kind: "nested-multi", options, groups }; } } return { column, kind: fitsMulti ? "multi" : "text", options }; }); }, [autoFilter, filterableColumns, rows]); useEffect(() => { setTextFilters((current) => pruneTextFilterState(current, generatedFilters)); setMultiFilters((current) => pruneMultiFilterState(current, generatedFilters)); setNumberFilters((current) => pruneNumberFilterState(current, generatedFilters)); }, [generatedFilters]); const globalFilterControlled = globalFilter !== void 0; const effectiveGlobalFilter = globalFilterControlled ? globalFilter : localGlobalFilter; const setEffectiveGlobalFilter = globalFilterControlled ? onGlobalFilterChange ?? (() => { }) : setLocalGlobalFilter; const nativeFilters = useMemo( () => generatedFilters.map((filter) => { var _a, _b, _c; const columnKey = filter.column.key; if (filter.kind === "multi") { return { key: columnKey, kind: "multi", label: labelText(filter.column), value: multiFilters[columnKey] ?? {}, onChange: (next) => setMultiFilters((current) => updateFilterRecord(current, columnKey, next)), options: filter.options }; } if (filter.kind === "nested-multi") { return { key: columnKey, kind: "nested-multi", label: labelText(filter.column), value: multiFilters[columnKey] ?? {}, onChange: (next) => setMultiFilters((current) => updateFilterRecord(current, columnKey, next)), groups: filter.groups ?? [] }; } if (filter.kind === "number") { const numberFilter = { key: columnKey, kind: "number", label: labelText(filter.column), value: numberFilters[columnKey] ?? {}, onChange: (next) => setNumberFilters((current) => updateNumberFilterValue(current, columnKey, next)) }; if (((_a = filter.numberBounds) == null ? void 0 : _a.min) !== void 0) { numberFilter.domainMin = filter.numberBounds.min; } if (((_b = filter.numberBounds) == null ? void 0 : _b.max) !== void 0) { numberFilter.domainMax = filter.numberBounds.max; } if (((_c = filter.numberBounds) == null ? void 0 : _c.step) !== void 0) { numberFilter.step = filter.numberBounds.step; } return numberFilter; } return { key: columnKey, kind: "text", label: labelText(filter.column), value: textFilters[columnKey] ?? "", onChange: (next) => setTextFilters((current) => updateFilterValue(current, columnKey, next)) }; }), [generatedFilters, multiFilters, numberFilters, textFilters] ); const nativeFilterByColumn = useMemo( () => new Map(nativeFilters.map((filter) => [filter.key, filter])), [nativeFilters] ); const hasCustomFilterBarContent = Boolean( (filterBarProps == null ? void 0 : filterBarProps.leading) || (filterBarProps == null ? void 0 : filterBarProps.children) || (filterBarProps == null ? void 0 : filterBarProps.trailing) || (filterBarProps == null ? void 0 : filterBarProps.timeRange) || (filterBarProps == null ? void 0 : filterBarProps.dateRange) ); const autoTimeRange = useMemo(() => { var _a; if (!autoFilter || !timeRangeColumn) return null; return { from: timeRangeFilter.from, to: timeRangeFilter.to, onApply: (from, to) => setTimeRangeFilter({ from, to }), presets: ((_a = timeRangeColumn.timestamp) == null ? void 0 : _a.rangePresets) ?? TIMESTAMP_RANGE_PRESETS, fromPlaceholder: "now-24h", toPlaceholder: "now", emptyLabel: "Any time" }; }, [autoFilter, timeRangeColumn, timeRangeFilter.from, timeRangeFilter.to]); const showFilterBar = autoFilter && (showGlobalFilter || nativeFilters.length > 0 || !!autoTimeRange) || hasCustomFilterBarContent || showTablePreferencesControl; const setDensityOverride = (next) => { if (!densityControlled) setLocalDensityOverride(next); onDensityChange == null ? void 0 : onDensityChange(next); }; const filterBarTrailing = showTablePreferencesControl ? /* @__PURE__ */ jsxs(Fragment, { children: [ filterBarProps == null ? void 0 : filterBarProps.trailing, /* @__PURE__ */ jsx(ColumnVisibilityTrigger, { onOpen: (event) => setColumnMenu(menuStateFromTrigger(event)) }) ] }) : filterBarProps == null ? void 0 : filterBarProps.trailing; const showHeaderFilterControls = autoFilter && showHeaderFilters; const filteredRows = useMemo(() => { const globalNeedle = effectiveGlobalFilter.trim().toLowerCase(); const now = /* @__PURE__ */ new Date(); const rangeFromDate = timeRangeColumn ? resolveDateMath(timeRangeFilter.from, now) : null; const rangeToDate = timeRangeColumn ? resolveDateMath(timeRangeFilter.to, now) : null; const rangeColumnKey = timeRangeColumn == null ? void 0 : timeRangeColumn.key; return rows.filter(({ row }) => { if (rangeColumnKey && (rangeFromDate || rangeToDate)) { const raw = resolvePath(row, rangeColumnKey); const ts = parseTimestamp(raw); if (!ts) return false; if (rangeFromDate && ts.getTime() < rangeFromDate.getTime()) return false; if (rangeToDate && ts.getTime() > rangeToDate.getTime()) return false; } if (globalNeedle) { const haystack = filterableColumns.flatMap((column) => getFilterTokens(row, column)).join(" ").toLowerCase(); if (!haystack.includes(globalNeedle)) { return false; } } for (const filter of generatedFilters) { const tokens = getFilterTokens(row, filter.column); if (filter.kind === "text") { const needle = (textFilters[filter.column.key] ?? "").trim().toLowerCase(); if (needle && !tokens.some((token) => token.toLowerCase().includes(needle))) { return false; } } else if (filter.kind === "number") { const range = numberFilters[filter.column.key] ?? {}; const min = parseNumberInput(range.min); const max = parseNumberInput(range.max); const hasMin = String(range.min ?? "").trim() !== ""; const hasMax = String(range.max ?? "").trim() !== ""; if (hasMin || hasMax) { const values = getFilterNumbers(row, filter.column); const matches = values.some( (value) => (min == null || value >= min) && (max == null || value <= max) ); if (!matches) { return false; } } } else { const selected = multiFilters[filter.column.key] ?? {}; const include = Object.entries(selected).filter(([, mode]) => mode === "include").map(([value]) => value); const exclude = Object.entries(selected).filter(([, mode]) => mode === "exclude").map(([value]) => value); if (include.length > 0 && !tokens.some((token) => include.includes(token))) { return false; } if (exclude.length > 0 && tokens.some((token) => exclude.includes(token))) { return false; } } } return true; }); }, [ effectiveGlobalFilter, filterableColumns, generatedFilters, multiFilters, numberFilters, rows, textFilters, timeRangeColumn, timeRangeFilter.from, timeRangeFilter.to ]); const sortResolvers = useMemo( () => Object.fromEntries( effectiveColumns.map((column) => [ column.key, (record) => getSortValue(record.row, column) ]) ), [effectiveColumns] ); const { sorted, sort, toggle } = useSort(filteredRows, { defaultDir: (defaultSort == null ? void 0 : defaultSort.dir) ?? "asc", resolvers: sortResolvers, ...(defaultSort == null ? void 0 : defaultSort.key) ? { defaultKey: defaultSort.key } : {} }); const startColumnResize = (event, column) => { event.preventDefault(); event.stopPropagation(); const startX = event.clientX; const header = event.currentTarget.closest("th"); const measuredWidth = (header == null ? void 0 : header.getBoundingClientRect().width) ?? 0; const startWidth = measuredWidth || columnWidths[column.key] || defaultColumnWidth(column); const onMove = (moveEvent) => { const nextWidth = clampColumnWidth(column, startWidth + moveEvent.clientX - startX); setColumnWidths((current) => ({ ...current, [column.key]: nextWidth })); }; const onUp = () => { document.removeEventListener("mousemove", onMove); document.removeEventListener("mouseup", onUp); document.body.style.cursor = ""; document.body.style.userSelect = ""; }; document.addEventListener("mousemove", onMove); document.addEventListener("mouseup", onUp); document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none"; }; const autoFitColumn = (event, column) => { var _a; event.preventDefault(); event.stopPropagation(); const header = event.currentTarget.closest("th"); const table = event.currentTarget.closest("table"); if (!header || !table) return; const columnIndex = Array.from(((_a = header.parentElement) == null ? void 0 : _a.children) ?? []).indexOf(header); if (columnIndex < 0) return; const width = measureColumnContentWidth(table, columnIndex, column); setColumnWidths((current) => ({ ...current, [column.key]: width })); }; const toggleColumnVisibility = (column) => { if (!isColumnHideable(column)) return; setHiddenColumns((current) => { const hidden = current[column.key] === true; if (!hidden && visibleHideableColumnCount <= 1) return current; const next = { ...current }; if (hidden) { delete next[column.key]; } else { next[column.key] = true; } return pruneHiddenColumns(next, columns); }); }; const showAllColumns = () => setHiddenColumns({}); const openHeaderColumnMenu = (event, column) => { if (!showColumnVisibilityControl) return; event.preventDefault(); setColumnMenu(menuStateFromPointer(event, column.key)); }; const openHeaderFilterMenu = (event, columnKey) => { event.preventDefault(); event.stopPropagation(); setHeaderFilterMenu(menuStateFromTrigger(event, columnKey)); }; return /* @__PURE__ */ jsxs("div", { className: cn("flex min-h-0 flex-col gap-3", className), "data-density": densityOverride, children: [ showFilterBar && /* @__PURE__ */ jsx( FilterBar, { ...filterBarProps, ...autoFilter && showGlobalFilter ? { search: { value: effectiveGlobalFilter, onChange: setEffectiveGlobalFilter, placeholder: globalFilterPlaceholder } } : {}, ...autoTimeRange ? { timeRange: autoTimeRange } : {}, trailing: filterBarTrailing, filters: nativeFilters } ), /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-auto rounded-md border border-border", children: /* @__PURE__ */ jsxs("table", { className: "w-max table-auto text-left text-sm", children: [ /* @__PURE__ */ jsx("colgroup", { children: visibleColumns.map((column) => /* @__PURE__ */ jsx( "col", { style: columnStyle(column, columnWidths), className: column.shrink && !column.grow ? "w-px" : void 0 }, column.key )) }), /* @__PURE__ */ jsx("thead", { className: "sticky top-0 z-10 bg-muted shadow-[0_1px_0_0_var(--tw-shadow-color)] shadow-border", children: /* @__PURE__ */ jsx("tr", { className: "border-b border-border text-xs text-muted-foreground", children: visibleColumns.map((column) => /* @__PURE__ */ jsxs( "th", { className: cn( "group/header relative whitespace-nowrap font-medium", DATA_TABLE_HEADER_DENSITY_CLASS, alignmentClass(column.align), resizableColumns && column.resizable !== false && "select-none", column.headerClassName ), onContextMenu: (event) => openHeaderColumnMenu(event, column), children: [ /* @__PURE__ */ jsxs( "div", { className: cn( "flex min-w-0 items-center gap-1", headerAlignmentClass(column.align), resizableColumns && column.resizable !== false && "pr-2" ), children: [ /* @__PURE__ */ jsx("span", { className: "min-w-0", children: column.sortable === false ? /* @__PURE__ */ jsx("span", { children: column.label }) : /* @__PURE__ */ jsx( SortableHeader, { active: (sort == null ? void 0 : sort.key) === column.key, ...(sort == null ? void 0 : sort.key) === column.key ? { dir: sort.dir } : {}, ...column.align ? { align: column.align } : {}, onClick: () => toggle(column.key), children: column.label } ) }), showHeaderFilterControls && (nativeFilterByColumn.has(column.key) || autoTimeRange && (timeRangeColumn == null ? void 0 : timeRangeColumn.key) === column.key) && /* @__PURE__ */ jsx( HeaderFilterButton, { column, active: nativeFilterByColumn.has(column.key) ? isFilterBarFilterActive(nativeFilterByColumn.get(column.key)) : Boolean(timeRangeFilter.from || timeRangeFilter.to), onOpen: (event) => openHeaderFilterMenu(event, column.key) } ) ] } ), resizableColumns && column.resizable !== false && /* @__PURE__ */ jsx( "span", { role: "separator", "aria-label": `Resize ${labelText(column)} column`, "aria-orientation": "vertical", className: "absolute right-0 top-0 flex h-full w-3 cursor-col-resize touch-none items-center justify-center border-r border-border/70 bg-gradient-to-l from-border/30 to-transparent transition-colors hover:border-primary hover:from-primary/20", onClick: (event) => { event.preventDefault(); event.stopPropagation(); }, onDoubleClick: (event) => autoFitColumn(event, column), onMouseDown: (event) => startColumnResize(event, column), children: /* @__PURE__ */ jsx( "span", { "aria-hidden": true, className: "h-4 w-0.5 rounded-full bg-border transition-colors group-hover/header:bg-primary/70" } ) } ) ] }, column.key )) }) }), /* @__PURE__ */ jsx("tbody", { children: sorted.map((record) => { const href = getRowHref == null ? void 0 : getRowHref(record.row); const expanded = expandedRows[record.id] ?? false; const expandedContent = (renderExpandedRow == null ? void 0 : renderExpandedRow(record.row, { columns: effectiveColumns, visibleColumns, tagActionsByColumn })) ?? null; const expandable = expandedContent !== null; const rowClickEnabled = (isRowClickable == null ? void 0 : isRowClickable(record.row)) ?? !!onRowClick; const clickable = !!href || rowClickEnabled || expandable; return /* @__PURE__ */ jsxs(Fragment$1, { children: [ /* @__PURE__ */ jsx( "tr", { className: cn( "border-b border-border/60 align-top", clickable && "cursor-pointer hover:bg-accent/40" ), onClick: () => { if (expandable) { setExpandedRows((current) => ({ ...current, [record.id]: !current[record.id] })); } if (rowClickEnabled) { onRowClick == null ? void 0 : onRowClick(record.row); } }, children: visibleColumns.map((column, index) => { const rawValue = resolvePath(record.row, column.key); let content = column.render ? column.render(rawValue, record.row) : formatCell(rawValue); if (column.kind === "tags" && tagActionsByColumn[column.key]) { content = /* @__PURE__ */ jsx(TagActionsProvider, { value: tagActionsByColumn[column.key], children: content }); } return /* @__PURE__ */ jsx( "td", { className: cn( DATA_TABLE_CELL_DENSITY_CLASS, alignmentClass(column.align), column.cellClassName ), children: /* @__PURE__ */ jsx( CellContent, { column, ...columnWidths[column.key] !== void 0 ? { width: columnWidths[column.key] } : {}, children: href && index === 0 ? /* @__PURE__ */ jsx("a", { href, className: "hover:underline", children: content }) : content } ) }, column.key ); }) } ), expanded && expandedContent && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", { colSpan: visibleColumns.length, className: "bg-muted/40 p-density-3", children: /* @__PURE__ */ jsx("div", { className: "rounded-md border border-border bg-background p-density-3", children: expandedContent }) }) }) ] }, record.id); }) }) ] }) }), /* @__PURE__ */ jsxs("div", { className: "px-1 text-xs text-muted-foreground", children: [ sorted.length, " of ", data.length, " row", data.length === 1 ? "" : "s" ] }), columnMenu && showTablePreferencesControl && /* @__PURE__ */ jsx( ColumnVisibilityMenu, { columns: effectiveColumns, hiddenColumns, anchor: columnMenu, showColumnVisibilityControl, showDensityControl: resolvedShowDensityControl, densityOverride, visibleHideableColumnCount, onToggle: toggleColumnVisibility, onShowAll: showAllColumns, onDensityChange: setDensityOverride, onClose: () => setColumnMenu(null) } ), headerFilterMenu && /* @__PURE__ */ jsx( HeaderFilterMenu, { filter: nativeFilterByColumn.get(headerFilterMenu.columnKey ?? ""), ...autoTimeRange && (timeRangeColumn == null ? void 0 : timeRangeColumn.key) === headerFilterMenu.columnKey ? { timeRange: autoTimeRange } : {}, anchor: headerFilterMenu, onClose: () => setHeaderFilterMenu(null) } ) ] }); } function HeaderFilterButton({ column, active, onOpen }) { return /* @__PURE__ */ jsx( "button", { type: "button", "aria-label": `Open ${labelText(column)} column filter`, "aria-haspopup": "dialog", "aria-pressed": active, className: cn( "inline-flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", active && "bg-accent text-foreground" ), onClick: onOpen, children: /* @__PURE__ */ jsx(Icon, { name: "codicon:filter", className: "text-xs" }) } ); } function HeaderFilterMenu({ filter, timeRange, anchor, onClose }) { if (!filter && !timeRange) return null; const label = (filter == null ? void 0 : filter.label) ?? "Time range"; const timeRangeActive = Boolean((timeRange == null ? void 0 : timeRange.from) || (timeRange == null ? void 0 : timeRange.to)); return /* @__PURE__ */ jsxs( "div", { role: "dialog", "aria-label": `${label} column filter`, className: "fixed z-50 rounded-md border border-border bg-popover p-2 text-popover-foreground shadow-lg shadow-black/5", style: { left: anchor.x, top: anchor.y }, onClick: (event) => event.stopPropagation(), onContextMenu: (event) => event.preventDefault(), children: [ /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between gap-3", children: [ /* @__PURE__ */ jsx("div", { className: "truncate text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: label }), /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [ filter && /* @__PURE__ */ jsx( "button", { type: "button", className: "rounded px-1.5 py-0.5 text-xs text-primary transition-colors hover:bg-accent focus:bg-accent focus:outline-none disabled:text-muted-foreground", onClick: () => clearFilterBarFilter(filter), disabled: !isFilterBarFilterActive(filter), children: "Clear all" } ), timeRange && /* @__PURE__ */ jsx( "button", { type: "button", className: "rounded px-1.5 py-0.5 text-xs text-primary transition-colors hover:bg-accent focus:bg-accent focus:outline-none disabled:text-muted-foreground", onClick: () => timeRange.onApply("", ""), disabled: !timeRangeActive, children: "Clear all" } ), /* @__PURE__ */ jsx( "button", { type: "button", "aria-label": "Close column filter", title: "Close", className: "inline-flex h-6 w-6 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:outline-none", onClick: onClose, children: /* @__PURE__ */ jsx(Icon, { name: "codicon:close", className: "text-sm" }) } ) ] }) ] }), /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [ filter && /* @__PURE__ */ jsx(FilterBarFilterPanel, { filter, chrome: "embedded" }), timeRange && /* @__PURE__ */ jsx(FilterBarRangePanel, { kind: "time", label: "Time range", ...timeRange }) ] }) ] } ); } function clearFilterBarFilter(filter) { if (filter.kind === "text" || filter.kind === "lookup" || filter.kind === "enum") { filter.onChange(""); return; } if (filter.kind === "lookup-multi" || filter.kind === "select-multi") { filter.onChange([]); return; } if (filter.kind === "number") { filter.onChange({}); return; } if (filter.kind === "boolean") { filter.onChange(false); return; } filter.onChange({}); } function ColumnVisibilityTrigger({ onOpen }) { return /* @__PURE__ */ jsx( "button", { type: "button", "aria-label": "Open column menu", "aria-haspopup": "menu", className: "inline-flex h-[34px] w-[34px] items-center justify-center rounded-md border border-input bg-background text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", onClick: (event) => { event.stopPropagation(); onOpen(event); }, children: /* @__PURE__ */ jsx(Icon, { name: "codicon:ellipsis", className: "text-sm" }) } ); } function ColumnVisibilityMenu({ columns, hiddenColumns, anchor, showColumnVisibilityControl, showDensityControl, densityOverride, visibleHideableColumnCount, onToggle, onShowAll, onDensityChange, onClose }) { const activeColumn = anchor.columnKey ? columns.find((column) => column.key === anchor.columnKey) : void 0; const canHideActiveColumn = activeColumn && isColumnHideable(activeColumn) && visibleHideableColumnCount > 1; return /* @__PURE__ */ jsxs( "div", { role: "menu", "aria-label": "Column menu", className: "fixed z-50 min-w-[16rem] rounded-md border border-border bg-popover p-1.5 text-popover-foreground shadow-lg shadow-black/5", style: { left: anchor.x, top: anchor.y }, onClick: (event) => event.stopPropagation(), onContextMenu: (event) => event.preventDefault(), children: [ showColumnVisibilityControl && /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 px-2 py-1.5 text-xs font-medium text-muted-foreground", children: [ /* @__PURE__ */ jsx("span", { children: "Columns" }), /* @__PURE__ */ jsx( "button", { type: "button", className: "rounded px-1.5 py-0.5 text-xs text-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:outline-none", onClick: onShowAll, children: "Show all" } ) ] }), activeColumn && /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsxs( "button", { type: "button", role: "menuitem", disabled: !canHideActiveColumn, className: cn( "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:outline-none", !canHideActiveColumn && "cursor-not-allowed opacity-50" ), onClick: () => { if (!canHideActiveColumn) return; onToggle(activeColumn); onClose(); }, children: [ /* @__PURE__ */ jsx(Icon, { name: "codicon:eye-closed", className: "text-sm text-muted-foreground" }), /* @__PURE__ */ jsxs("span", { children: [ "Hide ", labelText(activeColumn) ] }) ] } ), /* @__PURE__ */ jsx("div", { className: "my-1 h-px bg-border" }) ] }), /* @__PURE__ */ jsx("div", { className: "max-h-72 overflow-auto", children: columns.map((column) => { const hideable = isColumnHideable(column); const visible = hiddenColumns[column.key] !== true; const disabled = !hideable || visible && visibleHideableColumnCount <= 1; return /* @__PURE__ */ jsxs( "label", { className: cn( "flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors hover:bg-accent hover:text-accent-foreground", disabled && "cursor-not-allowed opacity-50" ), children: [ /* @__PURE__ */ jsx( "input", { type: "checkbox", className: "h-4 w-4 rounded border-border", checked: visible, disabled, onChange: () => onToggle(column) } ), /* @__PURE__ */ jsx("span", { className: "truncate", children: labelText(column) }) ] }, column.key ); }) }) ] }), showDensityControl && /* @__PURE__ */ jsx( DensityMenuSection, { densityOverride, separated: showColumnVisibilityControl, onDensityChange } ) ] } ); } function DensityMenuSection({ densityOverride, separated, onDensityChange }) { const current = densityOverride ?? "inherit"; return /* @__PURE__ */ jsxs("div", { className: cn(separated && "mt-1 border-t border-border pt-1"), children: [ /* @__PURE__ */ jsx("div", { className: "px-2 py-1.5 text-xs font-medium text-muted-foreground", children: "Density" }), /* @__PURE__ */ jsxs( "button", { type: "button", role: "menuitemradio", "aria-checked": current === "inherit", className: densityMenuItemClassName(current === "inherit"), onClick: () => onDensityChange(void 0), children: [ /* @__PURE__ */ jsx(Icon, { name: "ph:arrows-in-line-vertical", className: "text-sm text-muted-foreground" }), /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: "Use page density" }), current === "inherit" ? /* @__PURE__ */ jsx(Icon, { name: "ph:check", className: "text-sm text-foreground" }) : /* @__PURE__ */ jsx("span", { className: "inline-block h-4 w-4", "aria-hidden": true }) ] } ), DENSITY_OPTIONS.map((option) => { const active = current === option.value; return /* @__PURE__ */ jsxs( "button", { type: "button", role: "menuitemradio", "aria-checked": active, className: densityMenuItemClassName(active), onClick: () => onDensityChange(option.value), children: [ /* @__PURE__ */ jsx(Icon, { name: option.icon, className: "text-sm text-muted-foreground" }), /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: option.label }), active ? /* @__PURE__ */ jsx(Icon, { name: "ph:check", className: "text-sm text-foreground" }) : /* @__PURE__ */ jsx("span", { className: "inline-block h-4 w-4", "aria-hidden": true }) ] }, option.value ); }) ] }); } function densityMenuItemClassName(active) { return cn( "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:outline-none", active && "text-foreground" ); } function CellContent({ column, width, children }) { return /* @__PURE__ */ jsx( "div", { className: cn(cellContentClassName(column, width), alignmentClass(column.align)), style: cellContentStyle(width), children } ); } function getSortValue(row, column) { const rawValue = resolvePath(row, column.key); return column.sortValue ? column.sortValue(rawValue, row) : rawValue; } function getFilterCandidate(row, column) { const rawValue = resolvePath(row, column.key); return column.filterValue ? column.filterValue(rawValue, row) : rawValue; } function getFilterTokens(row, column) { return normalizeTokens(getFilterCandidate(row, column)); } function getFilterNumbers(row, column) { return normalizeNumbers(getFilterCandidate(row, column)); } function normalizeTokens(value) { if (Array.isArray(value)) { return value.flatMap((item) => normalizeTokens(item)).filter(Boolean); } if (value == null) return []; if (typeof value === "object") return [JSON.stringify(value)]; const token = String(value).trim(); return token ? [token] : []; } function normalizeNumbers(value) { if (Array.isArray(value)) { return value.flatMap((item) => normalizeNumbers(item)); } const parsed = parseNumberInput(value); return parsed == null ? [] : [parsed]; } function updateFilterValue(state, key, nextValue) { const next = { ...state }; if (nextValue.trim()) { next[key] = nextValue; } else { delete next[key]; } return next; } function updateFilterRecord(state, key, nextValue) { const next = { ...state }; if (Object.keys(nextValue).length > 0) { next[key] = nextValue; } else { delete next[key]; } return next; } function updateNumberFilterValue(state, key, nextValue) { const next = { ...state }; const hasMin = String(nextValue.min ?? "").trim() !== ""; const hasMax = String(nextValue.max ?? "").trim() !== ""; if (hasMin || hasMax) { next[key] = nextValue; } else { delete next[key]; } return next; } function pruneTextFilterState(state, filters) { return pruneFilterState(state, filters, "text", (value) => !String(value ?? "").trim()); } function pruneMultiFilterState(state, filters) { const allowed = new Set( filters.filter((filter) => filter.kind === "multi" || filter.kind === "nested-multi").map((filter) => filter.column.key) ); return Object.fromEntries( Object.entries(state).filter( ([key, value]) => allowed.has(key) && Object.keys(value).length > 0 ) ); } function pruneNumberFilterState(state, filters) { return pruneFilterState( state, filters, "number", (value) => !String(value.min ?? "").trim() && !String(value.max ?? "").trim() ); } function pruneFilterState(state, filters, kind, isEmpty) { const allowed = new Set( filters.filter((filter) => filter.kind === kind).map((filter) => filter.column.key) ); return Object.fromEntries( Object.entries(state).filter(([key, value]) => allowed.has(key) && !isEmpty(value)) ); } function resolvePath(obj, path) { return path.split(".").reduce((current, key) => { if (current && typeof current === "object") { return current[key]; } return void 0; }, obj); } function formatCell(value) { if (value == null || value === "") { return /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "—" }); } if (typeof value === "boolean") return value ? "True" : "False"; if (typeof value === "object") return JSON.stringify(value); return String(value); } function groupTagOptions(options, separator) { const byKey = /* @__PURE__ */ new Map(); for (const option of options) { const { key } = splitTagToken(option.value, separator); const list = byKey.get(key) ?? []; list.push(option); byKey.set(key, list); } return Array.from(byKey.entries()).sort(([a], [b]) => { if (a === "" && b !== "") return 1; if (b === "" && a !== "") return -1; return a.localeCompare(b); }).map(([groupKey, groupOptions]) => ({ groupKey, label: groupKey === "" ? "Other" : groupKey, options: groupOptions })); } function applyKindDefaults(column, timestampFormat) { if (!column.kind) return column; if (column.kind === "timestamp") { const format = timestampFormat ?? "iso"; return { ...column, render: column.render ?? ((value) => { var _a; return /* @__PURE__ */ jsx( Timestamp, { value, format, showTitleOnHover: ((_a = column.timestamp) == null ? void 0 : _a.alwaysShowFullOnHover) !== false } ); }), sortValue: column.sortValue ?? ((value) => { var _a; return ((_a = parseTimestamp(value)) == null ? void 0 : _a.getTime()) ?? null; }), filterValue: column.filterValue ?? ((value) => { const parsed = parseTimestamp(value); return parsed ? parsed.toISOString() : ""; }) }; } if (column.kind === "tags") { const opts = column.tags; const separator = (opts == null ? void 0 : opts.separator) ?? "="; return { ...column, render: column.render ?? ((value) => /* @__PURE__ */ jsx( TagList, { tags: normalizeTags(value, separator), maxVisible: (opts == null ? void 0 : opts.maxVisible) ?? 3 } )), filterValue: column.filterValue ?? ((value) => tagFilterTokens(value, separator)), sortValue: column.sortValue ?? ((value) => tagFilterTokens(value, separator).length) }; } if (column.kind === "status") { const opts = column.status; const map = (opts == null ? void 0 : opts.map) ?? ((raw) => normalizeStatus(raw)); return { ...column, render: column.render ?? ((value, row) => { var _a; const status = map(value, row); if (!status) return /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "—" }); const label = (opts == null ? void 0 : opts.showLabel) ? typeof value === "string" ? value : status : void 0; const title = ((_a = opts == null ? void 0 : opts.title) == null ? void 0 : _a.call(opts, value, row)) ?? (typeof value === "string" ? value : void 0); return /* @__PURE__ */ jsx( StatusDot, { status, ...label ? { label } : {}, ...title ? { title } : {} } ); }), filterValue: column.filterValue ?? ((value, row) => { const status = map(value, row); return status ?? ""; }), sortValue: column.sortValue ?? ((value, row) => map(value, row) ?? "") }; } return column; } function prettifyKey(key) { return key.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]/g, " ").replace(/\b\w/g, (value) => value.toUpperCase()).trim(); } function labelText(column) { if (typeof column.label === "string") return column.label; return prettifyKey(column.key.split(".").at(-1) ?? column.key); } function isColumnHideable(column) { return column.hideable !== false; } function isFilterBarFilterActive(filter) { if (filter.kind === "text" || filter.kind === "lookup" || filter.kind === "enum") { return String(filter.value ?? "").trim() !== ""; } if (filter.kind === "lookup-multi" || filter.kind === "select-multi") { return filter.value.length > 0; } if (filter.kind === "number") { return String(filter.value.min ?? "").trim() !== "" || String(filter.value.max ?? "").trim() !== ""; } if (filter.kind === "boolean") return filter.value; return Object.keys(filter.value).length > 0; } function menuStateFromPointer(event, columnKey) { const padding = 8; const width