@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.
Files changed (89) hide show
  1. package/dist/assets/{CellStatus-BTL_sgUY.js → CellStatus-PIeZtHEl.js} +1 -1
  2. package/dist/assets/JsonOutput-D5TiQGJ4.js +49 -0
  3. package/dist/assets/{MarimoErrorOutput-BBD405a6.js → MarimoErrorOutput-B9_RO5eX.js} +2 -2
  4. package/dist/assets/{RenderHTML-BT9obKLc.js → RenderHTML-DR6lqJGS.js} +1 -1
  5. package/dist/assets/{add-cell-with-ai-qEfAzC8R.js → add-cell-with-ai-DjfY8mF1.js} +9 -9
  6. package/dist/assets/{add-connection-dialog-HCShSlr3.js → add-connection-dialog-Dg6K--Uw.js} +2 -2
  7. package/dist/assets/{agent-panel-CoBHxHpJ.js → agent-panel-oPuvVsjs.js} +3 -3
  8. package/dist/assets/{ai-model-dropdown-CYx9ZfdS.js → ai-model-dropdown-By4Ohb0R.js} +3 -3
  9. package/dist/assets/{app-config-button-DC5UlXZA.js → app-config-button-YS8lpoUa.js} +1 -1
  10. package/dist/assets/{cell-editor-BMjfXh0J.js → cell-editor-CdyZleud.js} +11 -11
  11. package/dist/assets/{cell-link-BA7Demf0.js → cell-link-d38hFEon.js} +1 -1
  12. package/dist/assets/{cells-CJlo_hG2.js → cells-A-ljPpuj.js} +55 -55
  13. package/dist/assets/{chat-display-DbztpRD4.js → chat-display-Be0U92hE.js} +1 -1
  14. package/dist/assets/{chat-panel-H-xFzATI.js → chat-panel-BEz970S3.js} +1 -1
  15. package/dist/assets/{chat-ui-CFqAy2ck.js → chat-ui-CdW7awFa.js} +1 -1
  16. package/dist/assets/{column-preview-DzN2QumC.js → column-preview-B1KZjh-X.js} +1 -1
  17. package/dist/assets/command-CXDOCJwp.js +1 -0
  18. package/dist/assets/{command-palette-DSSR50KF.js → command-palette-C7AbtCWz.js} +1 -1
  19. package/dist/assets/{common-CquNY-ok.js → common-DC9LEIym.js} +1 -1
  20. package/dist/assets/{components-BXKuiPiF.js → components-BDnRtsA5.js} +1 -1
  21. package/dist/assets/{components-BiBOcq1x.js → components-FepcpFGL.js} +1 -1
  22. package/dist/assets/{datasource-BKDU-4D5.js → datasource-BCnGIjqn.js} +2 -2
  23. package/dist/assets/{dependency-graph-panel-ayo-Mm2j.js → dependency-graph-panel-k1WQZ4m-.js} +1 -1
  24. package/dist/assets/{documentation-panel-ph6755dz.js → documentation-panel-BMrGuByB.js} +1 -1
  25. package/dist/assets/{download-mzsKQgiy.js → download-Iyng0xCz.js} +4 -4
  26. package/dist/assets/{edit-page-Bxa5DWoE.js → edit-page-CbGDoOay.js} +6 -6
  27. package/dist/assets/{error-panel-BfG_JeMA.js → error-panel-CHSsnRFF.js} +1 -1
  28. package/dist/assets/{file-explorer-panel-D7-d0Puf.js → file-explorer-panel-Cjlf3QNa.js} +3 -3
  29. package/dist/assets/{file-icons-BNrh8MRG.js → file-icons-DTNZv05h.js} +1 -1
  30. package/dist/assets/{floating-outline-BMB4_phA.js → floating-outline-CSNGDMAm.js} +1 -1
  31. package/dist/assets/{focus-BX3gXJxx.js → focus-Baz7rlo0.js} +1 -1
  32. package/dist/assets/form-D1abghOq.js +2 -0
  33. package/dist/assets/{home-page-B1ELDhn8.js → home-page-QvO22-4u.js} +1 -1
  34. package/dist/assets/{hooks-b3J1eFE-.js → hooks-4gSLiehn.js} +1 -1
  35. package/dist/assets/{html-to-image-Cz8lDF-Y.js → html-to-image-tbPEMBJg.js} +1 -1
  36. package/dist/assets/index-DH5UUBE4.css +2 -0
  37. package/dist/assets/{index-BjiE1T38.js → index-rjcv10Dv.js} +8 -8
  38. package/dist/assets/{kiosk-mode-Dxbs5i5g.js → kiosk-mode-cWTXpegb.js} +1 -1
  39. package/dist/assets/{layout-lxwMDUgI.js → layout-C4u-1iTo.js} +5 -5
  40. package/dist/assets/{logs-panel-BW6S5AI1.js → logs-panel-Dgkr6V7q.js} +1 -1
  41. package/dist/assets/loro_wasm_bg-Bu4Ofw1K.js +2 -0
  42. package/dist/assets/{loro_wasm_bg-DXuHQ2hl.js → loro_wasm_bg-DWJCS9pK.js} +1 -1
  43. package/dist/assets/{markdown-renderer-CeHY2KoQ.js → markdown-renderer-CFrgiLqu.js} +1 -1
  44. package/dist/assets/{name-cell-input-B1BCybeo.js → name-cell-input-C5LXTI3T.js} +1 -1
  45. package/dist/assets/{outline-panel-DgMz83Mx.js → outline-panel-0lyts6z4.js} +1 -1
  46. package/dist/assets/{packages-panel-CbjavFh2.js → packages-panel-BhI1lbfw.js} +1 -1
  47. package/dist/assets/{panels-BPzk3EbR.js → panels-CpvbcuTn.js} +1 -1
  48. package/dist/assets/{process-output-CggRIZ6s.js → process-output-BwZ5VxXO.js} +1 -1
  49. package/dist/assets/{readonly-python-code-Ccd8HM-7.js → readonly-python-code-B0nrQE3U.js} +1 -1
  50. package/dist/assets/{reveal-component-BamXsTkr.js → reveal-component-C2X5QJAC.js} +1 -1
  51. package/dist/assets/{run-page-C5cZvltI.js → run-page-BYdI3kiE.js} +1 -1
  52. package/dist/assets/{scratchpad-panel-PMFH4Ktp.js → scratchpad-panel-BP6-rZV6.js} +1 -1
  53. package/dist/assets/{session-panel-CUTSr9rt.js → session-panel-YGK2D21A.js} +1 -1
  54. package/dist/assets/{slide-form-Bi8ZYBHP.js → slide-form-BE8M6oJo.js} +1 -1
  55. package/dist/assets/{snippets-panel-CdhDsAII.js → snippets-panel-CnN632Ki.js} +1 -1
  56. package/dist/assets/{state-C5AUgyZT.js → state-Bsz477is.js} +1 -1
  57. package/dist/assets/{state-DHlRrwyY.js → state-CoUA-8Ay.js} +2 -2
  58. package/dist/assets/{textarea-BXPC1-kb.js → textarea-Dd0InTQJ.js} +1 -1
  59. package/dist/assets/{tracing-wSufGcF9.js → tracing-DjZGgWTo.js} +1 -1
  60. package/dist/assets/{tracing-panel-BO6MMAXa.js → tracing-panel-8eFzEjG0.js} +2 -2
  61. package/dist/assets/{useAddCell-CqBbGhrY.js → useAddCell-De9xD63_.js} +1 -1
  62. package/dist/assets/{useCellActionButton-DYwx4pGE.js → useCellActionButton-Bjxun3wK.js} +1 -1
  63. package/dist/assets/{useDeleteCell-BJlGbsbV.js → useDeleteCell-C2Stbb6Q.js} +1 -1
  64. package/dist/assets/{useDependencyPanelTab-rkAIQ6zL.js → useDependencyPanelTab-8WaMRWbo.js} +1 -1
  65. package/dist/assets/useNotebookActions-bCZiRDS-.js +1 -0
  66. package/dist/assets/{useRunCells-B531RIUE.js → useRunCells-DU1tawR-.js} +1 -1
  67. package/dist/assets/{useSplitCell-BnuJW5uo.js → useSplitCell-BZPuhd8z.js} +1 -1
  68. package/dist/index.html +25 -25
  69. package/package.json +4 -4
  70. package/src/components/data-table/__tests__/column-header.test.ts +63 -0
  71. package/src/components/data-table/column-header.tsx +50 -159
  72. package/src/components/data-table/data-table.tsx +11 -2
  73. package/src/components/data-table/filter-by-values-picker.tsx +241 -0
  74. package/src/components/data-table/filter-pill-editor.tsx +470 -0
  75. package/src/components/data-table/filter-pills.tsx +177 -41
  76. package/src/components/editor/actions/pair-with-agent-modal.tsx +1 -1
  77. package/src/components/editor/actions/useNotebookActions.tsx +1 -0
  78. package/src/components/ui/checkbox.tsx +8 -4
  79. package/src/components/ui/combobox.tsx +3 -0
  80. package/src/css/app/Cell.css +4 -0
  81. package/src/plugins/impl/DataTablePlugin.tsx +1 -0
  82. package/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap +15 -15
  83. package/src/utils/sets.ts +13 -0
  84. package/dist/assets/JsonOutput-DXnOS_Hk.js +0 -49
  85. package/dist/assets/command-DbT_zkRP.js +0 -1
  86. package/dist/assets/form-CPDlIjdV.js +0 -2
  87. package/dist/assets/index-BYLYJcAY.css +0 -2
  88. package/dist/assets/loro_wasm_bg-Ds40eH8K.js +0 -2
  89. 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 { useMemo, useRef, useState } from "react";
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 { type ColumnFilterForType, Filter } from "./filters";
56
- import { SentinelCell } from "./sentinel-cell";
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 [chosenValues, setChosenValues] = useState<Set<unknown>>(new Set());
547
- const [query, setQuery] = useState<string>("");
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 handleToggleAll = (checked: boolean) => {
600
- if (!data) {
601
- return;
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({ options: [...chosenValues], operator: "in" }),
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">{dataTable}</div>
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 { GetRowIds } from "@/plugins/impl/DataTablePlugin";
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 filters={filters} table={table} />
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
+ };