@marimo-team/frontend 0.23.4-dev0 → 0.23.4-dev10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{CellStatus-BTL_sgUY.js → CellStatus-PIeZtHEl.js} +1 -1
- package/dist/assets/JsonOutput-D5TiQGJ4.js +49 -0
- package/dist/assets/{MarimoErrorOutput-BBD405a6.js → MarimoErrorOutput-B9_RO5eX.js} +2 -2
- package/dist/assets/{RenderHTML-BT9obKLc.js → RenderHTML-DR6lqJGS.js} +1 -1
- package/dist/assets/{add-cell-with-ai-qEfAzC8R.js → add-cell-with-ai-DjfY8mF1.js} +9 -9
- package/dist/assets/{add-connection-dialog-HCShSlr3.js → add-connection-dialog-Dg6K--Uw.js} +2 -2
- package/dist/assets/{agent-panel-CoBHxHpJ.js → agent-panel-oPuvVsjs.js} +3 -3
- package/dist/assets/{ai-model-dropdown-CYx9ZfdS.js → ai-model-dropdown-By4Ohb0R.js} +3 -3
- package/dist/assets/{app-config-button-DC5UlXZA.js → app-config-button-YS8lpoUa.js} +1 -1
- package/dist/assets/{cell-editor-BMjfXh0J.js → cell-editor-CdyZleud.js} +11 -11
- package/dist/assets/{cell-link-BA7Demf0.js → cell-link-d38hFEon.js} +1 -1
- package/dist/assets/{cells-CJlo_hG2.js → cells-A-ljPpuj.js} +55 -55
- package/dist/assets/{chat-display-DbztpRD4.js → chat-display-Be0U92hE.js} +1 -1
- package/dist/assets/{chat-panel-H-xFzATI.js → chat-panel-BEz970S3.js} +1 -1
- package/dist/assets/{chat-ui-CFqAy2ck.js → chat-ui-CdW7awFa.js} +1 -1
- package/dist/assets/{column-preview-DzN2QumC.js → column-preview-B1KZjh-X.js} +1 -1
- package/dist/assets/command-CXDOCJwp.js +1 -0
- package/dist/assets/{command-palette-DSSR50KF.js → command-palette-C7AbtCWz.js} +1 -1
- package/dist/assets/{common-CquNY-ok.js → common-DC9LEIym.js} +1 -1
- package/dist/assets/{components-BXKuiPiF.js → components-BDnRtsA5.js} +1 -1
- package/dist/assets/{components-BiBOcq1x.js → components-FepcpFGL.js} +1 -1
- package/dist/assets/{datasource-BKDU-4D5.js → datasource-BCnGIjqn.js} +2 -2
- package/dist/assets/{dependency-graph-panel-ayo-Mm2j.js → dependency-graph-panel-k1WQZ4m-.js} +1 -1
- package/dist/assets/{documentation-panel-ph6755dz.js → documentation-panel-BMrGuByB.js} +1 -1
- package/dist/assets/{download-mzsKQgiy.js → download-Iyng0xCz.js} +4 -4
- package/dist/assets/{edit-page-Bxa5DWoE.js → edit-page-CbGDoOay.js} +6 -6
- package/dist/assets/{error-panel-BfG_JeMA.js → error-panel-CHSsnRFF.js} +1 -1
- package/dist/assets/{file-explorer-panel-D7-d0Puf.js → file-explorer-panel-Cjlf3QNa.js} +3 -3
- package/dist/assets/{file-icons-BNrh8MRG.js → file-icons-DTNZv05h.js} +1 -1
- package/dist/assets/{floating-outline-BMB4_phA.js → floating-outline-CSNGDMAm.js} +1 -1
- package/dist/assets/{focus-BX3gXJxx.js → focus-Baz7rlo0.js} +1 -1
- package/dist/assets/form-D1abghOq.js +2 -0
- package/dist/assets/{home-page-B1ELDhn8.js → home-page-QvO22-4u.js} +1 -1
- package/dist/assets/{hooks-b3J1eFE-.js → hooks-4gSLiehn.js} +1 -1
- package/dist/assets/{html-to-image-Cz8lDF-Y.js → html-to-image-tbPEMBJg.js} +1 -1
- package/dist/assets/index-DH5UUBE4.css +2 -0
- package/dist/assets/{index-BjiE1T38.js → index-rjcv10Dv.js} +8 -8
- package/dist/assets/{kiosk-mode-Dxbs5i5g.js → kiosk-mode-cWTXpegb.js} +1 -1
- package/dist/assets/{layout-lxwMDUgI.js → layout-C4u-1iTo.js} +5 -5
- package/dist/assets/{logs-panel-BW6S5AI1.js → logs-panel-Dgkr6V7q.js} +1 -1
- package/dist/assets/loro_wasm_bg-Bu4Ofw1K.js +2 -0
- package/dist/assets/{loro_wasm_bg-DXuHQ2hl.js → loro_wasm_bg-DWJCS9pK.js} +1 -1
- package/dist/assets/{markdown-renderer-CeHY2KoQ.js → markdown-renderer-CFrgiLqu.js} +1 -1
- package/dist/assets/{name-cell-input-B1BCybeo.js → name-cell-input-C5LXTI3T.js} +1 -1
- package/dist/assets/{outline-panel-DgMz83Mx.js → outline-panel-0lyts6z4.js} +1 -1
- package/dist/assets/{packages-panel-CbjavFh2.js → packages-panel-BhI1lbfw.js} +1 -1
- package/dist/assets/{panels-BPzk3EbR.js → panels-CpvbcuTn.js} +1 -1
- package/dist/assets/{process-output-CggRIZ6s.js → process-output-BwZ5VxXO.js} +1 -1
- package/dist/assets/{readonly-python-code-Ccd8HM-7.js → readonly-python-code-B0nrQE3U.js} +1 -1
- package/dist/assets/{reveal-component-BamXsTkr.js → reveal-component-C2X5QJAC.js} +1 -1
- package/dist/assets/{run-page-C5cZvltI.js → run-page-BYdI3kiE.js} +1 -1
- package/dist/assets/{scratchpad-panel-PMFH4Ktp.js → scratchpad-panel-BP6-rZV6.js} +1 -1
- package/dist/assets/{session-panel-CUTSr9rt.js → session-panel-YGK2D21A.js} +1 -1
- package/dist/assets/{slide-form-Bi8ZYBHP.js → slide-form-BE8M6oJo.js} +1 -1
- package/dist/assets/{snippets-panel-CdhDsAII.js → snippets-panel-CnN632Ki.js} +1 -1
- package/dist/assets/{state-C5AUgyZT.js → state-Bsz477is.js} +1 -1
- package/dist/assets/{state-DHlRrwyY.js → state-CoUA-8Ay.js} +2 -2
- package/dist/assets/{textarea-BXPC1-kb.js → textarea-Dd0InTQJ.js} +1 -1
- package/dist/assets/{tracing-wSufGcF9.js → tracing-DjZGgWTo.js} +1 -1
- package/dist/assets/{tracing-panel-BO6MMAXa.js → tracing-panel-8eFzEjG0.js} +2 -2
- package/dist/assets/{useAddCell-CqBbGhrY.js → useAddCell-De9xD63_.js} +1 -1
- package/dist/assets/{useCellActionButton-DYwx4pGE.js → useCellActionButton-Bjxun3wK.js} +1 -1
- package/dist/assets/{useDeleteCell-BJlGbsbV.js → useDeleteCell-C2Stbb6Q.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-rkAIQ6zL.js → useDependencyPanelTab-8WaMRWbo.js} +1 -1
- package/dist/assets/useNotebookActions-bCZiRDS-.js +1 -0
- package/dist/assets/{useRunCells-B531RIUE.js → useRunCells-DU1tawR-.js} +1 -1
- package/dist/assets/{useSplitCell-BnuJW5uo.js → useSplitCell-BZPuhd8z.js} +1 -1
- package/dist/index.html +25 -25
- package/package.json +4 -4
- package/src/components/data-table/__tests__/column-header.test.ts +63 -0
- package/src/components/data-table/column-header.tsx +50 -159
- package/src/components/data-table/data-table.tsx +11 -2
- package/src/components/data-table/filter-by-values-picker.tsx +241 -0
- package/src/components/data-table/filter-pill-editor.tsx +470 -0
- package/src/components/data-table/filter-pills.tsx +177 -41
- package/src/components/editor/actions/pair-with-agent-modal.tsx +1 -1
- package/src/components/editor/actions/useNotebookActions.tsx +1 -0
- package/src/components/ui/checkbox.tsx +8 -4
- package/src/components/ui/combobox.tsx +3 -0
- package/src/css/app/Cell.css +4 -0
- package/src/plugins/impl/DataTablePlugin.tsx +1 -0
- package/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap +15 -15
- package/src/utils/sets.ts +13 -0
- package/dist/assets/JsonOutput-DXnOS_Hk.js +0 -49
- package/dist/assets/command-DbT_zkRP.js +0 -1
- package/dist/assets/form-CPDlIjdV.js +0 -2
- package/dist/assets/index-BYLYJcAY.css +0 -2
- package/dist/assets/loro_wasm_bg-Ds40eH8K.js +0 -2
- package/dist/assets/useNotebookActions-Dga3qEHF.js +0 -1
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
TextIcon,
|
|
10
10
|
XIcon,
|
|
11
11
|
} from "lucide-react";
|
|
12
|
-
import {
|
|
12
|
+
import { useRef, useState } from "react";
|
|
13
13
|
import { useLocale } from "react-aria";
|
|
14
14
|
import {
|
|
15
15
|
DropdownMenu,
|
|
@@ -22,24 +22,12 @@ import {
|
|
|
22
22
|
DropdownMenuSubTrigger,
|
|
23
23
|
DropdownMenuTrigger,
|
|
24
24
|
} from "@/components/ui/dropdown-menu";
|
|
25
|
-
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
26
|
-
import { ErrorBanner } from "@/plugins/impl/common/error-banner";
|
|
27
25
|
import type { CalculateTopKRows } from "@/plugins/impl/DataTablePlugin";
|
|
28
26
|
import type { OperatorType } from "@/plugins/impl/data-frames/utils/operators";
|
|
29
27
|
import { logNever } from "@/utils/assertNever";
|
|
30
28
|
import { cn } from "@/utils/cn";
|
|
31
|
-
import { Logger } from "@/utils/Logger";
|
|
32
29
|
import { capitalize } from "@/utils/strings";
|
|
33
|
-
import { Spinner } from "../icons/spinner";
|
|
34
30
|
import { Button } from "../ui/button";
|
|
35
|
-
import { Checkbox } from "../ui/checkbox";
|
|
36
|
-
import {
|
|
37
|
-
Command,
|
|
38
|
-
CommandEmpty,
|
|
39
|
-
CommandInput,
|
|
40
|
-
CommandItem,
|
|
41
|
-
CommandList,
|
|
42
|
-
} from "../ui/command";
|
|
43
31
|
import { DraggablePopover } from "../ui/draggable-popover";
|
|
44
32
|
import { Input } from "../ui/input";
|
|
45
33
|
import { NumberField } from "../ui/number-field";
|
|
@@ -52,8 +40,12 @@ import {
|
|
|
52
40
|
SelectTrigger,
|
|
53
41
|
SelectValue,
|
|
54
42
|
} from "../ui/select";
|
|
55
|
-
import {
|
|
56
|
-
import {
|
|
43
|
+
import { FilterByValuesList } from "./filter-by-values-picker";
|
|
44
|
+
import {
|
|
45
|
+
type ColumnFilterForType,
|
|
46
|
+
type ColumnFilterValue,
|
|
47
|
+
Filter,
|
|
48
|
+
} from "./filters";
|
|
57
49
|
import {
|
|
58
50
|
ClearFilterMenuItem,
|
|
59
51
|
FilterButtons,
|
|
@@ -66,9 +58,6 @@ import {
|
|
|
66
58
|
renderSortFilterIcon,
|
|
67
59
|
renderSorts,
|
|
68
60
|
} from "./header-items";
|
|
69
|
-
import { detectSentinel, stringifyUnknownValue } from "./utils";
|
|
70
|
-
|
|
71
|
-
const TOP_K_ROWS = 30;
|
|
72
61
|
|
|
73
62
|
interface DataTableColumnHeaderProps<
|
|
74
63
|
TData,
|
|
@@ -534,6 +523,26 @@ const TextFilter = <TData, TValue>({
|
|
|
534
523
|
);
|
|
535
524
|
};
|
|
536
525
|
|
|
526
|
+
/**
|
|
527
|
+
* Seed the filter-by-values picker from a column's existing filter value.
|
|
528
|
+
*
|
|
529
|
+
* Reopening the picker should reflect what's already applied. Only `select`
|
|
530
|
+
* filters carry checkbox-style values; other filter shapes (number, text,
|
|
531
|
+
* etc.) seed an empty list.
|
|
532
|
+
*/
|
|
533
|
+
export function seedFromFilter(value: ColumnFilterValue | undefined): {
|
|
534
|
+
values: unknown[];
|
|
535
|
+
operator: Extract<OperatorType, "in" | "not_in">;
|
|
536
|
+
} {
|
|
537
|
+
if (value && "type" in value && value.type === "select") {
|
|
538
|
+
return {
|
|
539
|
+
values: [...value.options],
|
|
540
|
+
operator: value.operator === "not_in" ? "not_in" : "in",
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
return { values: [], operator: "in" };
|
|
544
|
+
}
|
|
545
|
+
|
|
537
546
|
const PopoverFilterByValues = <TData, TValue>({
|
|
538
547
|
setIsFilterValueOpen,
|
|
539
548
|
calculateTopKRows,
|
|
@@ -543,69 +552,13 @@ const PopoverFilterByValues = <TData, TValue>({
|
|
|
543
552
|
calculateTopKRows?: CalculateTopKRows;
|
|
544
553
|
column: Column<TData, TValue>;
|
|
545
554
|
}) => {
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
const { data, isPending, error } = useAsyncData(async () => {
|
|
550
|
-
if (!calculateTopKRows) {
|
|
551
|
-
return null;
|
|
552
|
-
}
|
|
553
|
-
const res = await calculateTopKRows({ column: column.id, k: TOP_K_ROWS });
|
|
554
|
-
return res.data;
|
|
555
|
-
}, []);
|
|
556
|
-
|
|
557
|
-
const filteredData = useMemo(() => {
|
|
558
|
-
if (!data) {
|
|
559
|
-
return [];
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
try {
|
|
563
|
-
return data.filter(([value, _count]) => {
|
|
564
|
-
// Check if value exists and can be converted to string
|
|
565
|
-
// Keep null values for filtering
|
|
566
|
-
return value === undefined
|
|
567
|
-
? false
|
|
568
|
-
: String(value).toLowerCase().includes(query.toLowerCase());
|
|
569
|
-
});
|
|
570
|
-
} catch (error_) {
|
|
571
|
-
Logger.error("Error filtering data", error_);
|
|
572
|
-
return [];
|
|
573
|
-
}
|
|
574
|
-
}, [data, query]);
|
|
575
|
-
|
|
576
|
-
let dataTable: React.ReactNode;
|
|
577
|
-
|
|
578
|
-
if (isPending) {
|
|
579
|
-
dataTable = <Spinner size="medium" className="mx-auto mt-12 mb-10" />;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
if (error) {
|
|
583
|
-
dataTable = <ErrorBanner error={error} className="my-10 mx-4" />;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
const handleToggle = (value: unknown) => {
|
|
587
|
-
setChosenValues((prev) => {
|
|
588
|
-
const checked = prev.has(value);
|
|
589
|
-
const newSet = new Set(prev);
|
|
590
|
-
if (checked) {
|
|
591
|
-
newSet.delete(value);
|
|
592
|
-
} else {
|
|
593
|
-
newSet.add(value);
|
|
594
|
-
}
|
|
595
|
-
return newSet;
|
|
596
|
-
});
|
|
597
|
-
};
|
|
555
|
+
const seed = seedFromFilter(
|
|
556
|
+
column.getFilterValue() as ColumnFilterValue | undefined,
|
|
557
|
+
);
|
|
598
558
|
|
|
599
|
-
const
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
}
|
|
603
|
-
if (checked) {
|
|
604
|
-
setChosenValues(new Set(filteredData.map(([value]) => value)));
|
|
605
|
-
} else {
|
|
606
|
-
setChosenValues(new Set());
|
|
607
|
-
}
|
|
608
|
-
};
|
|
559
|
+
const [chosenValues, setChosenValues] = useState<Set<unknown>>(
|
|
560
|
+
() => new Set(seed.values),
|
|
561
|
+
);
|
|
609
562
|
|
|
610
563
|
const handleApply = () => {
|
|
611
564
|
if (chosenValues.size === 0) {
|
|
@@ -613,87 +566,13 @@ const PopoverFilterByValues = <TData, TValue>({
|
|
|
613
566
|
return;
|
|
614
567
|
}
|
|
615
568
|
column.setFilterValue(
|
|
616
|
-
Filter.select({
|
|
569
|
+
Filter.select({
|
|
570
|
+
options: [...chosenValues],
|
|
571
|
+
operator: seed.operator,
|
|
572
|
+
}),
|
|
617
573
|
);
|
|
618
574
|
};
|
|
619
575
|
|
|
620
|
-
if (data) {
|
|
621
|
-
const allChecked = chosenValues.size === filteredData.length;
|
|
622
|
-
|
|
623
|
-
dataTable = (
|
|
624
|
-
<>
|
|
625
|
-
<Command className="text-sm outline-hidden" shouldFilter={false}>
|
|
626
|
-
<CommandInput
|
|
627
|
-
placeholder={`Search among the top ${data.length} values`}
|
|
628
|
-
autoFocus={true}
|
|
629
|
-
onValueChange={(value) => setQuery(value.trim())}
|
|
630
|
-
/>
|
|
631
|
-
<CommandEmpty>No results found.</CommandEmpty>
|
|
632
|
-
<CommandList className="border-b">
|
|
633
|
-
{filteredData.length > 0 && (
|
|
634
|
-
<CommandItem
|
|
635
|
-
value="__select-all__"
|
|
636
|
-
className="border-b rounded-none px-3"
|
|
637
|
-
onSelect={() => handleToggleAll(!allChecked)}
|
|
638
|
-
>
|
|
639
|
-
<Checkbox
|
|
640
|
-
checked={chosenValues.size === filteredData.length}
|
|
641
|
-
aria-label="Select all"
|
|
642
|
-
className="mr-3 h-3.5 w-3.5"
|
|
643
|
-
/>
|
|
644
|
-
<span className="font-bold flex-1">{column.id}</span>
|
|
645
|
-
<span className="font-bold">Count</span>
|
|
646
|
-
</CommandItem>
|
|
647
|
-
)}
|
|
648
|
-
{filteredData.map(([value, count], rowIndex) => {
|
|
649
|
-
const isSelected = chosenValues.has(value);
|
|
650
|
-
const valueString = stringifyUnknownValue({ value });
|
|
651
|
-
const sentinel = detectSentinel(
|
|
652
|
-
value,
|
|
653
|
-
column.columnDef.meta?.dataType,
|
|
654
|
-
);
|
|
655
|
-
|
|
656
|
-
return (
|
|
657
|
-
<CommandItem
|
|
658
|
-
key={rowIndex}
|
|
659
|
-
value={valueString}
|
|
660
|
-
className="not-last:border-b rounded-none px-3"
|
|
661
|
-
onSelect={() => handleToggle(value)}
|
|
662
|
-
>
|
|
663
|
-
<Checkbox
|
|
664
|
-
checked={isSelected}
|
|
665
|
-
aria-label="Select row"
|
|
666
|
-
className="mr-3 h-3.5 w-3.5"
|
|
667
|
-
/>
|
|
668
|
-
<span className="flex-1 overflow-hidden max-h-20 line-clamp-3">
|
|
669
|
-
{sentinel ? (
|
|
670
|
-
<SentinelCell sentinel={sentinel} />
|
|
671
|
-
) : (
|
|
672
|
-
valueString
|
|
673
|
-
)}
|
|
674
|
-
</span>
|
|
675
|
-
<span className="ml-3">{count}</span>
|
|
676
|
-
</CommandItem>
|
|
677
|
-
);
|
|
678
|
-
})}
|
|
679
|
-
</CommandList>
|
|
680
|
-
{filteredData.length === TOP_K_ROWS && (
|
|
681
|
-
<span className="text-xs text-muted-foreground mt-1.5 text-center">
|
|
682
|
-
Only showing the top {TOP_K_ROWS} values
|
|
683
|
-
</span>
|
|
684
|
-
)}
|
|
685
|
-
</Command>
|
|
686
|
-
<FilterButtons
|
|
687
|
-
onApply={handleApply}
|
|
688
|
-
onClear={() => {
|
|
689
|
-
setChosenValues(new Set());
|
|
690
|
-
}}
|
|
691
|
-
clearButtonDisabled={chosenValues.size === 0}
|
|
692
|
-
/>
|
|
693
|
-
</>
|
|
694
|
-
);
|
|
695
|
-
}
|
|
696
|
-
|
|
697
576
|
return (
|
|
698
577
|
<DraggablePopover
|
|
699
578
|
open={true}
|
|
@@ -709,7 +588,19 @@ const PopoverFilterByValues = <TData, TValue>({
|
|
|
709
588
|
<XIcon className="h-4 w-4" />
|
|
710
589
|
</Button>
|
|
711
590
|
</PopoverClose>
|
|
712
|
-
<div className="flex flex-col gap-1.5 py-2">
|
|
591
|
+
<div className="flex flex-col gap-1.5 py-2">
|
|
592
|
+
<FilterByValuesList
|
|
593
|
+
column={column}
|
|
594
|
+
calculateTopKRows={calculateTopKRows}
|
|
595
|
+
chosenValues={chosenValues}
|
|
596
|
+
onChange={(values) => setChosenValues(new Set(values))}
|
|
597
|
+
/>
|
|
598
|
+
<FilterButtons
|
|
599
|
+
onApply={handleApply}
|
|
600
|
+
onClear={() => setChosenValues(new Set())}
|
|
601
|
+
clearButtonDisabled={chosenValues.size === 0}
|
|
602
|
+
/>
|
|
603
|
+
</div>
|
|
713
604
|
</DraggablePopover>
|
|
714
605
|
);
|
|
715
606
|
};
|
|
@@ -22,7 +22,10 @@ import React, { memo } from "react";
|
|
|
22
22
|
import { useLocale } from "react-aria";
|
|
23
23
|
|
|
24
24
|
import { Table } from "@/components/ui/table";
|
|
25
|
-
import type {
|
|
25
|
+
import type {
|
|
26
|
+
CalculateTopKRows,
|
|
27
|
+
GetRowIds,
|
|
28
|
+
} from "@/plugins/impl/DataTablePlugin";
|
|
26
29
|
import { cn } from "@/utils/cn";
|
|
27
30
|
import {
|
|
28
31
|
PANEL_TYPES,
|
|
@@ -89,6 +92,7 @@ interface DataTableProps<TData> extends Partial<ExportActionProps> {
|
|
|
89
92
|
showFilters?: boolean;
|
|
90
93
|
filters?: ColumnFiltersState;
|
|
91
94
|
onFiltersChange?: OnChangeFn<ColumnFiltersState>;
|
|
95
|
+
calculateTopKRows?: CalculateTopKRows;
|
|
92
96
|
reloading?: boolean;
|
|
93
97
|
// Columns
|
|
94
98
|
freezeColumnsLeft?: string[];
|
|
@@ -139,6 +143,7 @@ const DataTableInternal = <TData,>({
|
|
|
139
143
|
showFilters = false,
|
|
140
144
|
filters,
|
|
141
145
|
onFiltersChange,
|
|
146
|
+
calculateTopKRows,
|
|
142
147
|
reloading,
|
|
143
148
|
freezeColumnsLeft,
|
|
144
149
|
freezeColumnsRight,
|
|
@@ -282,7 +287,11 @@ const DataTableInternal = <TData,>({
|
|
|
282
287
|
|
|
283
288
|
return (
|
|
284
289
|
<div className={cn(wrapperClassName, "flex flex-col space-y-1")}>
|
|
285
|
-
<FilterPills
|
|
290
|
+
<FilterPills
|
|
291
|
+
filters={filters}
|
|
292
|
+
table={table}
|
|
293
|
+
calculateTopKRows={calculateTopKRows}
|
|
294
|
+
/>
|
|
286
295
|
<CellSelectionProvider>
|
|
287
296
|
<div
|
|
288
297
|
part="table-wrapper"
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
"use no memo";
|
|
3
|
+
|
|
4
|
+
import type { Column } from "@tanstack/react-table";
|
|
5
|
+
import { ChevronDownIcon } from "lucide-react";
|
|
6
|
+
import { useMemo, useState } from "react";
|
|
7
|
+
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
8
|
+
import { ErrorBanner } from "@/plugins/impl/common/error-banner";
|
|
9
|
+
import type { CalculateTopKRows } from "@/plugins/impl/DataTablePlugin";
|
|
10
|
+
import { cn } from "@/utils/cn";
|
|
11
|
+
import { Logger } from "@/utils/Logger";
|
|
12
|
+
import { Sets } from "@/utils/sets";
|
|
13
|
+
import { smartMatch } from "@/utils/smartMatch";
|
|
14
|
+
import { Spinner } from "../icons/spinner";
|
|
15
|
+
import { Button } from "../ui/button";
|
|
16
|
+
import { Checkbox } from "../ui/checkbox";
|
|
17
|
+
import {
|
|
18
|
+
Command,
|
|
19
|
+
CommandEmpty,
|
|
20
|
+
CommandInput,
|
|
21
|
+
CommandItem,
|
|
22
|
+
CommandList,
|
|
23
|
+
} from "../ui/command";
|
|
24
|
+
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
|
25
|
+
import { SentinelCell } from "./sentinel-cell";
|
|
26
|
+
import { detectSentinel, stringifyUnknownValue } from "./utils";
|
|
27
|
+
|
|
28
|
+
const TOP_K_ROWS = 30;
|
|
29
|
+
|
|
30
|
+
interface Props<TData, TValue> {
|
|
31
|
+
column: Column<TData, TValue>;
|
|
32
|
+
calculateTopKRows?: CalculateTopKRows;
|
|
33
|
+
chosenValues: unknown[];
|
|
34
|
+
onChange: (values: unknown[]) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const FilterByValuesPicker = <TData, TValue>({
|
|
38
|
+
column,
|
|
39
|
+
calculateTopKRows,
|
|
40
|
+
chosenValues,
|
|
41
|
+
onChange,
|
|
42
|
+
}: Props<TData, TValue>) => {
|
|
43
|
+
const [open, setOpen] = useState(false);
|
|
44
|
+
|
|
45
|
+
const chosenValuesSet = useMemo(() => new Set(chosenValues), [chosenValues]);
|
|
46
|
+
|
|
47
|
+
const selectedValuesStr = useMemo(() => {
|
|
48
|
+
if (chosenValuesSet.size === 0) {
|
|
49
|
+
return "Select values…";
|
|
50
|
+
}
|
|
51
|
+
const items = [...chosenValuesSet].map((v) =>
|
|
52
|
+
stringifyUnknownValue({ value: v }),
|
|
53
|
+
);
|
|
54
|
+
return `[${items.join(", ")}]`;
|
|
55
|
+
}, [chosenValuesSet]);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
59
|
+
<PopoverTrigger asChild={true}>
|
|
60
|
+
<Button
|
|
61
|
+
variant="outline"
|
|
62
|
+
size="xs"
|
|
63
|
+
className="h-6 mb-1 w-full justify-between font-normal"
|
|
64
|
+
>
|
|
65
|
+
<span
|
|
66
|
+
className={cn(
|
|
67
|
+
"truncate",
|
|
68
|
+
chosenValuesSet.size === 0 && "text-muted-foreground",
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
{selectedValuesStr}
|
|
72
|
+
</span>
|
|
73
|
+
<ChevronDownIcon className="h-4 w-4 opacity-50 shrink-0" />
|
|
74
|
+
</Button>
|
|
75
|
+
</PopoverTrigger>
|
|
76
|
+
<PopoverContent className="w-80 p-0">
|
|
77
|
+
<FilterByValuesList
|
|
78
|
+
column={column}
|
|
79
|
+
calculateTopKRows={calculateTopKRows}
|
|
80
|
+
chosenValues={chosenValuesSet}
|
|
81
|
+
onChange={onChange}
|
|
82
|
+
/>
|
|
83
|
+
</PopoverContent>
|
|
84
|
+
</Popover>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
interface FilterByValuesListProps<TData, TValue> {
|
|
89
|
+
column: Column<TData, TValue>;
|
|
90
|
+
calculateTopKRows?: CalculateTopKRows;
|
|
91
|
+
chosenValues: Set<unknown>;
|
|
92
|
+
onChange: (values: unknown[]) => void;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Search + checkbox list that powers the "filter by values" picker.
|
|
97
|
+
*/
|
|
98
|
+
export const FilterByValuesList = <TData, TValue>({
|
|
99
|
+
column,
|
|
100
|
+
calculateTopKRows,
|
|
101
|
+
chosenValues,
|
|
102
|
+
onChange,
|
|
103
|
+
}: FilterByValuesListProps<TData, TValue>) => {
|
|
104
|
+
const [query, setQuery] = useState<string>("");
|
|
105
|
+
|
|
106
|
+
const { data, isPending, error } = useAsyncData(async () => {
|
|
107
|
+
if (!calculateTopKRows) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const res = await calculateTopKRows({ column: column.id, k: TOP_K_ROWS });
|
|
111
|
+
return res.data;
|
|
112
|
+
}, [calculateTopKRows, column.id]);
|
|
113
|
+
|
|
114
|
+
const filteredData = useMemo(() => {
|
|
115
|
+
if (!data) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
// try to do includes and also smart match for prefixes
|
|
120
|
+
return data.filter(([value, _count]) => {
|
|
121
|
+
if (value === undefined) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
const str = String(value);
|
|
125
|
+
return (
|
|
126
|
+
smartMatch(query, str) ||
|
|
127
|
+
str.toLowerCase().includes(query.toLowerCase())
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
} catch (error_) {
|
|
131
|
+
Logger.error("Error filtering data", error_);
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
}, [data, query]);
|
|
135
|
+
|
|
136
|
+
const handleToggle = (value: unknown) => {
|
|
137
|
+
onChange([...Sets.toggle(chosenValues, value)]);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const allVisibleChecked =
|
|
141
|
+
filteredData.length > 0 &&
|
|
142
|
+
filteredData.every(([value]) => chosenValues.has(value));
|
|
143
|
+
|
|
144
|
+
const selectAllState: boolean | "indeterminate" = allVisibleChecked
|
|
145
|
+
? true
|
|
146
|
+
: chosenValues.size > 0
|
|
147
|
+
? "indeterminate"
|
|
148
|
+
: false;
|
|
149
|
+
|
|
150
|
+
const handleToggleAll = () => {
|
|
151
|
+
if (!data) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const next = new Set(chosenValues);
|
|
155
|
+
if (allVisibleChecked) {
|
|
156
|
+
for (const [value] of filteredData) {
|
|
157
|
+
next.delete(value);
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
for (const [value] of filteredData) {
|
|
161
|
+
next.add(value);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
onChange([...next]);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
if (isPending) {
|
|
168
|
+
return <Spinner size="medium" className="mx-auto mt-12 mb-10" />;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (error) {
|
|
172
|
+
return <ErrorBanner error={error} className="my-10 mx-4" />;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!data) {
|
|
176
|
+
return (
|
|
177
|
+
<div className="py-6 px-4 text-sm text-muted-foreground text-center">
|
|
178
|
+
No values available
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<Command className="text-sm outline-hidden" shouldFilter={false}>
|
|
185
|
+
<CommandInput
|
|
186
|
+
placeholder={`Search among the top ${data.length} values`}
|
|
187
|
+
autoFocus={true}
|
|
188
|
+
onValueChange={(value) => setQuery(value.trim())}
|
|
189
|
+
/>
|
|
190
|
+
<CommandEmpty>No results found.</CommandEmpty>
|
|
191
|
+
<CommandList>
|
|
192
|
+
{filteredData.length > 0 && (
|
|
193
|
+
<CommandItem
|
|
194
|
+
value="__select-all__"
|
|
195
|
+
className="border-b rounded-none px-3"
|
|
196
|
+
onSelect={handleToggleAll}
|
|
197
|
+
>
|
|
198
|
+
<Checkbox
|
|
199
|
+
checked={selectAllState}
|
|
200
|
+
aria-label="Select all"
|
|
201
|
+
className="mr-3 h-3.5 w-3.5"
|
|
202
|
+
/>
|
|
203
|
+
<span className="font-bold flex-1">{column.id}</span>
|
|
204
|
+
<span className="font-bold">Count</span>
|
|
205
|
+
</CommandItem>
|
|
206
|
+
)}
|
|
207
|
+
{filteredData.map(([value, count]) => {
|
|
208
|
+
const isSelected = chosenValues.has(value);
|
|
209
|
+
const valueString = stringifyUnknownValue({ value });
|
|
210
|
+
const sentinel = detectSentinel(
|
|
211
|
+
value,
|
|
212
|
+
column.columnDef.meta?.dataType,
|
|
213
|
+
);
|
|
214
|
+
return (
|
|
215
|
+
<CommandItem
|
|
216
|
+
key={valueString}
|
|
217
|
+
value={valueString}
|
|
218
|
+
className="not-last:border-b rounded-none px-3"
|
|
219
|
+
onSelect={() => handleToggle(value)}
|
|
220
|
+
>
|
|
221
|
+
<Checkbox
|
|
222
|
+
checked={isSelected}
|
|
223
|
+
aria-label="Select row"
|
|
224
|
+
className="mr-3 h-3.5 w-3.5"
|
|
225
|
+
/>
|
|
226
|
+
<span className="flex-1 overflow-hidden max-h-20 line-clamp-3">
|
|
227
|
+
{sentinel ? <SentinelCell sentinel={sentinel} /> : valueString}
|
|
228
|
+
</span>
|
|
229
|
+
<span className="ml-3">{count}</span>
|
|
230
|
+
</CommandItem>
|
|
231
|
+
);
|
|
232
|
+
})}
|
|
233
|
+
</CommandList>
|
|
234
|
+
{data.length === TOP_K_ROWS && (
|
|
235
|
+
<span className="text-xs text-muted-foreground py-1.5 text-center">
|
|
236
|
+
Only showing the top {TOP_K_ROWS} values
|
|
237
|
+
</span>
|
|
238
|
+
)}
|
|
239
|
+
</Command>
|
|
240
|
+
);
|
|
241
|
+
};
|