@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
JavaScript
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