@marimo-team/frontend 0.23.7-dev55 → 0.23.7-dev57

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 (116) hide show
  1. package/dist/assets/{CellStatus-DXNmZJpi.js → CellStatus-DGBvmSvq.js} +1 -1
  2. package/dist/assets/JsonOutput-BEbyS3oG.js +53 -0
  3. package/dist/assets/{LazyAnyLanguageCodeMirror-B2pl_WA3.js → LazyAnyLanguageCodeMirror-GdhQ07zA.js} +2 -2
  4. package/dist/assets/{MarimoErrorOutput-YGhIA85d.js → MarimoErrorOutput-XWqnhvJ6.js} +1 -1
  5. package/dist/assets/{RenderHTML-Dz1OIbOh.js → RenderHTML-B5r25cP5.js} +1 -1
  6. package/dist/assets/{RunButton-D1IZ1Yr0.js → RunButton-Dbak5hfa.js} +1 -1
  7. package/dist/assets/{add-cell-with-ai-hIPYl46r.js → add-cell-with-ai-BbZkMqv2.js} +1 -1
  8. package/dist/assets/{add-connection-dialog-CMR-c9XE.js → add-connection-dialog-Cw6_iYno.js} +1 -1
  9. package/dist/assets/{agent-panel-DnBAoLsM.js → agent-panel-StLA6gDR.js} +1 -1
  10. package/dist/assets/{ai-model-dropdown-zvokTxf_.js → ai-model-dropdown-CjhUqXgj.js} +1 -1
  11. package/dist/assets/{any-language-editor-BgxVFHQ8.js → any-language-editor-Bdhmwznp.js} +1 -1
  12. package/dist/assets/{app-config-button-BjFAqaTN.js → app-config-button-CCs8Jepz.js} +1 -1
  13. package/dist/assets/{cell-editor-Dak_jwhB.js → cell-editor-Cgyoqdi5.js} +1 -1
  14. package/dist/assets/{cell-link-pRI-YfIp.js → cell-link-PQYiMZw1.js} +1 -1
  15. package/dist/assets/{cells-DbE28H1u.js → cells-Dnu4nDoy.js} +1 -1
  16. package/dist/assets/{chat-display-D_nDPZek.js → chat-display-DetTBnqK.js} +1 -1
  17. package/dist/assets/{chat-panel-BktSpl2P.js → chat-panel-CEgw_vg0.js} +1 -1
  18. package/dist/assets/{chat-ui-C0_KcXrv.js → chat-ui-D-Y7p_cT.js} +1 -1
  19. package/dist/assets/{chunk-5FQGJX7Z-DILIU9Rm.js → chunk-5FQGJX7Z-BSzccEgu.js} +3 -3
  20. package/dist/assets/{code-block-37QAKDTI-Bgm-HPiB.js → code-block-37QAKDTI-U2R1jyOo.js} +1 -1
  21. package/dist/assets/{column-preview-CNeXQtKn.js → column-preview-DA6nf5_Q.js} +1 -1
  22. package/dist/assets/{command-palette-CcjZs_TG.js → command-palette-n_e11WBA.js} +1 -1
  23. package/dist/assets/{common-CWRr25jC.js → common-BaBE_ygg.js} +1 -1
  24. package/dist/assets/{components-DUd0ki0p.js → components-CvGaLA5d.js} +1 -1
  25. package/dist/assets/{components-Cj3Al1Y6.js → components-zB5yT_R8.js} +1 -1
  26. package/dist/assets/config-C2lTvbuU.js +1 -0
  27. package/dist/assets/{datasource-Prn_GWOB.js → datasource-I-LOgxeP.js} +1 -1
  28. package/dist/assets/{dependency-graph-panel-DUUCij85.js → dependency-graph-panel-BjOeXp74.js} +1 -1
  29. package/dist/assets/{dist-DxnNQmQo.js → dist-CW3rweKM.js} +1 -1
  30. package/dist/assets/{documentation-panel-CB8xalFX.js → documentation-panel-DMdFXuBf.js} +1 -1
  31. package/dist/assets/{download-DEJbA1IY.js → download-BO6T2USS.js} +1 -1
  32. package/dist/assets/{edit-page-BkHYt2if.js → edit-page-Dos8zz_9.js} +6 -6
  33. package/dist/assets/{error-panel-DG6AtqLR.js → error-panel-iXznkJZ1.js} +1 -1
  34. package/dist/assets/{file-explorer-panel-Co6MlwYD.js → file-explorer-panel-_77UepGi.js} +3 -3
  35. package/dist/assets/{file-icons-BjTIuMQg.js → file-icons-B6DaZdP0.js} +1 -1
  36. package/dist/assets/{file-name-input-CQVbWhL8.js → file-name-input-g2H2sY2h.js} +1 -1
  37. package/dist/assets/{floating-outline-uy6dAsIe.js → floating-outline-DbOtUfo-.js} +1 -1
  38. package/dist/assets/{focus-0RBjdtZw.js → focus-BaOnnMs-.js} +1 -1
  39. package/dist/assets/{form-DNa2VnwU.js → form-CM8vYbSt.js} +1 -1
  40. package/dist/assets/{globals-DI5QlXvl.js → globals-CpVAcN9Z.js} +1 -1
  41. package/dist/assets/{home-page-BSuXANlw.js → home-page-De1W6q6f.js} +1 -1
  42. package/dist/assets/{hooks-B7pYZHjF.js → hooks-7OHHugrQ.js} +1 -1
  43. package/dist/assets/{html-to-image-CIu-0LbU.js → html-to-image-D6SgvARi.js} +1 -1
  44. package/dist/assets/index-1EIgCVR_.js +38 -0
  45. package/dist/assets/{kiosk-mode-Ch75k65P.js → kiosk-mode-Czvj3vmL.js} +1 -1
  46. package/dist/assets/{layout-DFhJt7oJ.js → layout-DQGNHEpb.js} +5 -5
  47. package/dist/assets/{logs-panel-DR-1BC0S.js → logs-panel-BMAfoMJg.js} +1 -1
  48. package/dist/assets/{markdown-renderer-DGqYztXR.js → markdown-renderer-BQ-BQLiJ.js} +3 -3
  49. package/dist/assets/mermaid-4DMBBIKO-C0OyyVdo.js +1 -0
  50. package/dist/assets/{name-cell-input-DwfyLq31.js → name-cell-input-bwfAyC0i.js} +1 -1
  51. package/dist/assets/{outline-panel-CWunrooQ.js → outline-panel-CkZUQcZ1.js} +1 -1
  52. package/dist/assets/{packages-panel-BdcXUFQJ.js → packages-panel-B3dRYuRM.js} +1 -1
  53. package/dist/assets/panels-DvIOAb34.js +1 -0
  54. package/dist/assets/{process-output-CS4QGJvL.js → process-output-9W-JyYdE.js} +1 -1
  55. package/dist/assets/{radio-group-BS2PIEzV.js → radio-group-rsi1ibXY.js} +1 -1
  56. package/dist/assets/{readonly-python-code-C5JNX2fu.js → readonly-python-code-BKYj8PNf.js} +1 -1
  57. package/dist/assets/{reveal-component-PiSHIrbA.js → reveal-component-ClM8W-TD.js} +1 -1
  58. package/dist/assets/{run-page-BovrPK0f.js → run-page-DJOwAe2z.js} +1 -1
  59. package/dist/assets/{scratchpad-panel-CAWFveBD.js → scratchpad-panel-CpM3jVv7.js} +1 -1
  60. package/dist/assets/{secrets-panel-CEh4Wjfn.js → secrets-panel-DqHGq3V8.js} +1 -1
  61. package/dist/assets/{session-panel-BR9h5w96.js → session-panel-jVcSUURP.js} +1 -1
  62. package/dist/assets/{snippets-panel-Y2etH9Qg.js → snippets-panel-DFJd1ui5.js} +1 -1
  63. package/dist/assets/{state-Fa6RzVTL.js → state-BXNNuw9g.js} +1 -1
  64. package/dist/assets/{state-1SbOXCLX.js → state-D9EoHCkz.js} +1 -1
  65. package/dist/assets/{switch-CTn-kJzM.js → switch-BtkQp293.js} +1 -1
  66. package/dist/assets/{terminal-DI2XRUUH.js → terminal-BSE1Vg5d.js} +1 -1
  67. package/dist/assets/{textarea-wgoQLrBS.js → textarea-BBTcSr-i.js} +1 -1
  68. package/dist/assets/{tracing-C9PZ0Pr1.js → tracing-BQU8fBDM.js} +1 -1
  69. package/dist/assets/{tracing-panel-C20Rk6hU.js → tracing-panel-DEVpyGX3.js} +2 -2
  70. package/dist/assets/useBoolean-B8LMGUHl.js +1 -0
  71. package/dist/assets/{useCellActionButton-D_-iAhme.js → useCellActionButton-CpNJthj4.js} +1 -1
  72. package/dist/assets/{useDeleteCell-41mvwiyA.js → useDeleteCell-DFahVcdW.js} +1 -1
  73. package/dist/assets/{useDependencyPanelTab-ELdrL73c.js → useDependencyPanelTab-BNbEyT1o.js} +1 -1
  74. package/dist/assets/{useNotebookActions-CgN-58GN.js → useNotebookActions-DqlAe4Ea.js} +1 -1
  75. package/dist/assets/{useRunCells-CfHlqXY6.js → useRunCells-C0BPo9m1.js} +1 -1
  76. package/dist/assets/{useSplitCell-gHtyz873.js → useSplitCell-BN53wD86.js} +1 -1
  77. package/dist/assets/{vega-component-BPU1T-x7.js → vega-component-C9fDGx86.js} +1 -1
  78. package/dist/assets/{write-secret-modal-DjVzKit_.js → write-secret-modal-Liv_9MXS.js} +1 -1
  79. package/dist/index.html +40 -40
  80. package/package.json +1 -1
  81. package/src/components/data-table/__tests__/column-header.test.tsx +106 -1
  82. package/src/components/data-table/__tests__/filter-pill-editor.test.tsx +88 -2
  83. package/src/components/data-table/__tests__/filters.test.ts +84 -13
  84. package/src/components/data-table/column-header.tsx +152 -26
  85. package/src/components/data-table/date-filter-inputs.tsx +325 -0
  86. package/src/components/data-table/filter-pill-editor.tsx +139 -30
  87. package/src/components/data-table/filter-pills.tsx +31 -57
  88. package/src/components/data-table/filters.ts +88 -66
  89. package/src/components/editor/chrome/wrapper/footer-items/backend-status.tsx +1 -1
  90. package/src/core/runtime/__tests__/runtime.test.ts +38 -17
  91. package/src/core/runtime/runtime.ts +57 -34
  92. package/src/core/websocket/__tests__/useMarimoKernelConnection.hook.test.tsx +5 -4
  93. package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +18 -54
  94. package/src/core/websocket/transports/__tests__/ws.test.ts +125 -0
  95. package/src/core/websocket/transports/basic.ts +1 -3
  96. package/src/core/websocket/transports/transport.ts +0 -1
  97. package/src/core/websocket/transports/ws.ts +96 -0
  98. package/src/core/websocket/useMarimoKernelConnection.tsx +30 -26
  99. package/src/core/websocket/useWebSocket.tsx +3 -18
  100. package/dist/assets/JsonOutput-Dxol3ZtH.js +0 -49
  101. package/dist/assets/config-DGudsRYK.js +0 -1
  102. package/dist/assets/index-DySiGerD.js +0 -42
  103. package/dist/assets/mermaid-4DMBBIKO-B-uFGNnk.js +0 -1
  104. package/dist/assets/panels-CJ1t18_z.js +0 -1
  105. package/dist/assets/useBoolean-DP3412N2.js +0 -1
  106. /package/dist/assets/{bundle.esm-DjhGJy4I.js → bundle.esm-BXIlAZ6T.js} +0 -0
  107. /package/dist/assets/{esm-DLYpPRvw.js → esm-Cb2bnV6o.js} +0 -0
  108. /package/dist/assets/{field-CQGpbXj3.js → field-DNlzfMKW.js} +0 -0
  109. /package/dist/assets/{formats-CJQ67TPE.js → formats-BRq458WH.js} +0 -0
  110. /package/dist/assets/{icons-Ol38nIbL.js → icons-8tfAri2V.js} +0 -0
  111. /package/dist/assets/{micromark-factory-space-P--XWZhg.js → micromark-factory-space-BUQpMdx2.js} +0 -0
  112. /package/dist/assets/{react-resizable-panels.browser.esm-CgWOEYeG.js → react-resizable-panels.browser.esm-Ce2ksurd.js} +0 -0
  113. /package/dist/assets/{renderShortcut-D7FYCtYQ.js → renderShortcut-DK-VjfaX.js} +0 -0
  114. /package/dist/assets/{table-DUSsaCYD.js → table-DQE9hQzM.js} +0 -0
  115. /package/dist/assets/{tree-actions-D9i3o3Zk.js → tree-actions-DY8FUp3V.js} +0 -0
  116. /package/dist/assets/{useDeepCompareMemoize-zUHU--0D.js → useDeepCompareMemoize-CWcgQCbT.js} +0 -0
@@ -17,23 +17,46 @@ import {
17
17
  SelectValue,
18
18
  } from "../ui/select";
19
19
  import { Button } from "../ui/button";
20
+ import { DateLikeInput, DateLikeRangeInput } from "./date-filter-inputs";
20
21
  import { FilterByValuesPicker } from "./filter-by-values-picker";
21
22
  import { RegexInput } from "./regex-input";
22
23
  import {
23
24
  type ColumnFilterValue,
25
+ DATETIME_OPS,
24
26
  Filter,
27
+ isDatetimeComparisonOp,
28
+ isNumberComparisonOp,
29
+ isTextScalarOp,
25
30
  MEMBERSHIP_OPS,
26
- NUMBER_COMPARISON_OPS,
27
- type NumberComparisonOp,
28
31
  NUMBER_OPS,
29
32
  TEXT_OPS,
30
- TEXT_SCALAR_OPS,
31
- type TextScalarOp,
32
33
  } from "./filters";
33
34
  import { OPERATOR_LABELS } from "./operator-labels";
34
35
  import { Tooltip } from "../ui/tooltip";
35
36
 
36
- type EditableFilterType = "number" | "text" | "boolean" | "select";
37
+ type EditableFilterType =
38
+ | "number"
39
+ | "text"
40
+ | "boolean"
41
+ | "select"
42
+ | "date"
43
+ | "datetime"
44
+ | "time";
45
+
46
+ type DateLikeEditableFilterType = Extract<
47
+ EditableFilterType,
48
+ "date" | "datetime" | "time"
49
+ >;
50
+
51
+ const DATE_LIKE_TYPES: ReadonlySet<EditableFilterType> = new Set([
52
+ "date",
53
+ "datetime",
54
+ "time",
55
+ ]);
56
+
57
+ const isDateLikeType = (
58
+ type: EditableFilterType,
59
+ ): type is DateLikeEditableFilterType => DATE_LIKE_TYPES.has(type);
37
60
 
38
61
  const BOOLEAN_OPS = ["is_true", "is_false", "is_null", "is_not_null"] as const;
39
62
  const SELECT_OPS = MEMBERSHIP_OPS;
@@ -46,6 +69,9 @@ const OPERATORS_BY_TYPE: Record<
46
69
  text: TEXT_OPS,
47
70
  boolean: BOOLEAN_OPS,
48
71
  select: SELECT_OPS,
72
+ date: DATETIME_OPS,
73
+ datetime: DATETIME_OPS,
74
+ time: DATETIME_OPS,
49
75
  };
50
76
 
51
77
  const DEFAULT_OPERATOR: Record<EditableFilterType, OperatorType> = {
@@ -53,6 +79,9 @@ const DEFAULT_OPERATOR: Record<EditableFilterType, OperatorType> = {
53
79
  text: "contains",
54
80
  boolean: "is_true",
55
81
  select: "in",
82
+ date: "between",
83
+ datetime: "between",
84
+ time: "between",
56
85
  };
57
86
 
58
87
  const OPERATORS_WITHOUT_VALUE = new Set<OperatorType>([
@@ -63,22 +92,14 @@ const OPERATORS_WITHOUT_VALUE = new Set<OperatorType>([
63
92
  "is_empty",
64
93
  ]);
65
94
 
66
- const NUMBER_COMPARISON_SET: ReadonlySet<OperatorType> = new Set(
67
- NUMBER_COMPARISON_OPS,
68
- );
69
- const TEXT_SCALAR_SET: ReadonlySet<OperatorType> = new Set(TEXT_SCALAR_OPS);
70
-
71
- const isNumberComparisonOp = (op: OperatorType): op is NumberComparisonOp =>
72
- NUMBER_COMPARISON_SET.has(op);
73
- const isTextScalarOp = (op: OperatorType): op is TextScalarOp =>
74
- TEXT_SCALAR_SET.has(op);
75
-
76
95
  type DraftValue =
77
96
  | { kind: "between"; min?: number; max?: number }
78
97
  | { kind: "single-number"; value?: number }
79
98
  | { kind: "single-text"; text?: string }
80
99
  | { kind: "multi-text"; values?: string[] }
81
100
  | { kind: "options"; options?: unknown[] }
101
+ | { kind: "date-between"; min?: Date; max?: Date }
102
+ | { kind: "date-single"; value?: Date }
82
103
  | { kind: "none" };
83
104
 
84
105
  interface Snapshot {
@@ -116,7 +137,13 @@ export const FilterPillEditor = <TData,>({
116
137
  const editableColumns = table.getAllColumns().filter((c) => {
117
138
  const ft = c.columnDef.meta?.filterType;
118
139
  return (
119
- ft === "number" || ft === "text" || ft === "boolean" || ft === "select"
140
+ ft === "number" ||
141
+ ft === "text" ||
142
+ ft === "boolean" ||
143
+ ft === "select" ||
144
+ ft === "date" ||
145
+ ft === "datetime" ||
146
+ ft === "time"
120
147
  );
121
148
  });
122
149
 
@@ -200,7 +227,7 @@ export const FilterPillEditor = <TData,>({
200
227
  const operatorTriggerRef = useRef<HTMLButtonElement>(null);
201
228
  useEffect(() => {
202
229
  const firstInput = valueSlotRef.current?.querySelector<HTMLElement>(
203
- 'input, [role="combobox"], button',
230
+ 'input, [role="spinbutton"], [role="combobox"], button',
204
231
  );
205
232
  if (firstInput) {
206
233
  firstInput.focus();
@@ -216,6 +243,11 @@ export const FilterPillEditor = <TData,>({
216
243
  e.preventDefault();
217
244
  handleApply();
218
245
  }}
246
+ onKeyDownCapture={(e) => {
247
+ if (e.key === "Tab") {
248
+ e.stopPropagation();
249
+ }
250
+ }}
219
251
  >
220
252
  <div className="flex flex-col gap-1">
221
253
  <label className="text-xs text-muted-foreground" htmlFor={columnId}>
@@ -426,6 +458,36 @@ const ValueSlot = <TData, TValue>({
426
458
  />
427
459
  );
428
460
  }
461
+ if (isDateLikeType(type) && operator === "between") {
462
+ const v =
463
+ value.kind === "date-between" ? value : { kind: "date-between" as const };
464
+ return (
465
+ <DateLikeRangeInput
466
+ key={`${column?.id ?? "_"}-${operator}`}
467
+ filterType={type}
468
+ min={v.min}
469
+ max={v.max}
470
+ onRangeChange={(min, max) =>
471
+ onChange({ kind: "date-between", min, max })
472
+ }
473
+ className="border-input"
474
+ />
475
+ );
476
+ }
477
+ if (isDateLikeType(type) && isDatetimeComparisonOp(operator)) {
478
+ const v =
479
+ value.kind === "date-single" ? value : { kind: "date-single" as const };
480
+ return (
481
+ <DateLikeInput
482
+ key={`${column?.id ?? "_"}-${operator}`}
483
+ filterType={type}
484
+ value={v.value}
485
+ onChange={(next) => onChange({ kind: "date-single", value: next })}
486
+ aria-label="value"
487
+ className="border-input"
488
+ />
489
+ );
490
+ }
429
491
  if (type === "select" && column) {
430
492
  const v = value.kind === "options" ? value : { kind: "options" as const };
431
493
  return (
@@ -443,19 +505,18 @@ const ValueSlot = <TData, TValue>({
443
505
  };
444
506
 
445
507
  function getEditableType(value: ColumnFilterValue): EditableFilterType {
446
- if (value.type === "number") {
447
- return "number";
448
- }
449
- if (value.type === "text") {
450
- return "text";
451
- }
452
- if (value.type === "boolean") {
453
- return "boolean";
454
- }
455
- if (value.type === "select") {
456
- return "select";
508
+ switch (value.type) {
509
+ case "number":
510
+ case "text":
511
+ case "boolean":
512
+ case "select":
513
+ case "date":
514
+ case "datetime":
515
+ case "time":
516
+ return value.type;
517
+ default:
518
+ return "text";
457
519
  }
458
- return "text";
459
520
  }
460
521
 
461
522
  function toDraftValue(value: ColumnFilterValue): DraftValue {
@@ -486,6 +547,21 @@ function toDraftValue(value: ColumnFilterValue): DraftValue {
486
547
  if (value.type === "select") {
487
548
  return { kind: "options", options: [...value.options] };
488
549
  }
550
+ if (
551
+ value.type === "date" ||
552
+ value.type === "datetime" ||
553
+ value.type === "time"
554
+ ) {
555
+ switch (value.operator) {
556
+ case "between":
557
+ return { kind: "date-between", min: value.min, max: value.max };
558
+ case "is_null":
559
+ case "is_not_null":
560
+ return { kind: "none" };
561
+ default:
562
+ return { kind: "date-single", value: value.value };
563
+ }
564
+ }
489
565
  return { kind: "none" };
490
566
  }
491
567
 
@@ -509,6 +585,11 @@ function emptyDraftFor(
509
585
  if (type === "select") {
510
586
  return { kind: "options", options: [] };
511
587
  }
588
+ if (isDateLikeType(type)) {
589
+ return operator === "between"
590
+ ? { kind: "date-between" }
591
+ : { kind: "date-single" };
592
+ }
512
593
  return { kind: "none" };
513
594
  }
514
595
 
@@ -516,7 +597,7 @@ function getMissingValueMessage(
516
597
  type: EditableFilterType,
517
598
  operator: OperatorType,
518
599
  ): string {
519
- if (type === "number" && operator === "between") {
600
+ if (operator === "between") {
520
601
  return "Min and max are required";
521
602
  }
522
603
  if (type === "text" && (operator === "in" || operator === "not_in")) {
@@ -611,5 +692,33 @@ function buildFilterValue({
611
692
  operator: operator === "not_in" ? "not_in" : "in",
612
693
  });
613
694
  }
695
+ if (isDateLikeType(type)) {
696
+ const factory =
697
+ type === "date"
698
+ ? Filter.date
699
+ : type === "datetime"
700
+ ? Filter.datetime
701
+ : Filter.time;
702
+ if (operator === "is_null" || operator === "is_not_null") {
703
+ return factory({ operator });
704
+ }
705
+ if (operator === "between") {
706
+ if (
707
+ draft.kind !== "date-between" ||
708
+ draft.min === undefined ||
709
+ draft.max === undefined
710
+ ) {
711
+ return undefined;
712
+ }
713
+ return factory({ operator: "between", min: draft.min, max: draft.max });
714
+ }
715
+ if (!isDatetimeComparisonOp(operator)) {
716
+ return undefined;
717
+ }
718
+ if (draft.kind !== "date-single" || draft.value === undefined) {
719
+ return undefined;
720
+ }
721
+ return factory({ operator, value: draft.value });
722
+ }
614
723
  return undefined;
615
724
  }
@@ -12,7 +12,11 @@ import { Badge } from "../ui/badge";
12
12
  import { Button } from "../ui/button";
13
13
  import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
14
14
  import { FilterPillEditor } from "./filter-pill-editor";
15
- import type { ColumnFilterValue } from "./filters";
15
+ import {
16
+ type ColumnFilterValue,
17
+ dateToISODate,
18
+ dateToISODateTime,
19
+ } from "./filters";
16
20
  import { OPERATOR_LABELS } from "./operator-labels";
17
21
  import { stringifyUnknownValue } from "./utils";
18
22
 
@@ -82,13 +86,6 @@ const FilterPill = <TData,>({
82
86
  return null;
83
87
  }
84
88
 
85
- // this is temporary, with more operator & datatype support this goes away
86
- const isReadOnly =
87
- "type" in value &&
88
- (value.type === "date" ||
89
- value.type === "datetime" ||
90
- value.type === "time");
91
-
92
89
  const twoSegment = formatted.value === undefined;
93
90
 
94
91
  const handleRemove = (e: React.MouseEvent) => {
@@ -130,20 +127,8 @@ const FilterPill = <TData,>({
130
127
  </Button>
131
128
  );
132
129
 
133
- if (isReadOnly) {
134
- return (
135
- <Badge
136
- variant="outline"
137
- className="bg-background border-border text-foreground"
138
- >
139
- {segments}
140
- {removeButton}
141
- </Badge>
142
- );
143
- }
144
-
145
130
  return (
146
- <Popover open={open} onOpenChange={setOpen} modal={true}>
131
+ <Popover open={open} onOpenChange={setOpen} modal={false}>
147
132
  <Badge
148
133
  variant="outline"
149
134
  className={cn(
@@ -249,23 +234,31 @@ function formatValue(
249
234
  };
250
235
  }
251
236
  }
252
- if (value.type === "date") {
253
- return formatMinMaxLegacy(
254
- value.min?.toISOString(),
255
- value.max?.toISOString(),
256
- );
257
- }
258
- if (value.type === "time") {
259
- return formatMinMaxLegacy(
260
- value.min ? timeFormatter.format(value.min) : undefined,
261
- value.max ? timeFormatter.format(value.max) : undefined,
262
- );
263
- }
264
- if (value.type === "datetime") {
265
- return formatMinMaxLegacy(
266
- value.min?.toISOString(),
267
- value.max?.toISOString(),
268
- );
237
+ if (
238
+ value.type === "date" ||
239
+ value.type === "datetime" ||
240
+ value.type === "time"
241
+ ) {
242
+ const format =
243
+ value.type === "time"
244
+ ? (d: Date) => timeFormatter.format(d)
245
+ : value.type === "date"
246
+ ? dateToISODate
247
+ : dateToISODateTime;
248
+ switch (value.operator) {
249
+ case "between":
250
+ return {
251
+ operator: OPERATOR_LABELS.between.toLowerCase(),
252
+ value: `${format(value.min)} - ${format(value.max)}`,
253
+ };
254
+ case "==":
255
+ case "!=":
256
+ case ">":
257
+ case ">=":
258
+ case "<":
259
+ case "<=":
260
+ return { operator: value.operator, value: format(value.value) };
261
+ }
269
262
  }
270
263
  if (value.type === "boolean") {
271
264
  return { operator: `is ${value.value ? "True" : "False"}` };
@@ -282,22 +275,3 @@ function formatValue(
282
275
  logNever(value);
283
276
  return undefined;
284
277
  }
285
-
286
- function formatMinMaxLegacy(
287
- min: string | number | undefined,
288
- max: string | number | undefined,
289
- ): FormattedFilter | undefined {
290
- if (min === undefined && max === undefined) {
291
- return;
292
- }
293
- if (min === max) {
294
- return { operator: "==", value: String(min) };
295
- }
296
- if (min === undefined) {
297
- return { operator: "<=", value: String(max) };
298
- }
299
- if (max === undefined) {
300
- return { operator: ">=", value: String(min) };
301
- }
302
- return { operator: "between", value: `${min} - ${max}` };
303
- }
@@ -51,6 +51,15 @@ export const TEXT_SCALAR_OPS = [
51
51
  "ends_with",
52
52
  ] as const;
53
53
 
54
+ export const DATETIME_COMPARISON_OPS = [
55
+ "==",
56
+ "!=",
57
+ ">",
58
+ ">=",
59
+ "<",
60
+ "<=",
61
+ ] as const;
62
+
54
63
  export const NUMBER_OPS = [
55
64
  "between",
56
65
  ...NUMBER_COMPARISON_OPS,
@@ -62,11 +71,26 @@ export const TEXT_OPS = [
62
71
  "is_empty",
63
72
  ...NULLISH_OPS,
64
73
  ] as const;
74
+ export const DATETIME_OPS = [
75
+ "between",
76
+ ...DATETIME_COMPARISON_OPS,
77
+ ...NULLISH_OPS,
78
+ ] as const;
65
79
 
66
80
  export type NullishOp = (typeof NULLISH_OPS)[number];
67
81
  export type MembershipOp = (typeof MEMBERSHIP_OPS)[number];
68
82
  export type NumberComparisonOp = (typeof NUMBER_COMPARISON_OPS)[number];
69
83
  export type TextScalarOp = (typeof TEXT_SCALAR_OPS)[number];
84
+ export type DatetimeComparisonOp = (typeof DATETIME_COMPARISON_OPS)[number];
85
+
86
+ const makeOpGuard = <T extends OperatorType>(ops: readonly T[]) => {
87
+ const set = new Set<OperatorType>(ops);
88
+ return (op: OperatorType): op is T => set.has(op);
89
+ };
90
+
91
+ export const isNumberComparisonOp = makeOpGuard(NUMBER_COMPARISON_OPS);
92
+ export const isTextScalarOp = makeOpGuard(TEXT_SCALAR_OPS);
93
+ export const isDatetimeComparisonOp = makeOpGuard(DATETIME_COMPARISON_OPS);
70
94
 
71
95
  interface NullishOpts {
72
96
  operator: NullishOp;
@@ -83,6 +107,11 @@ type TextFilterOpts =
83
107
  | { operator: "is_empty" }
84
108
  | NullishOpts;
85
109
 
110
+ type DateLikeFilterOpts =
111
+ | { operator: "between"; min: Date; max: Date }
112
+ | { operator: DatetimeComparisonOp; value: Date }
113
+ | NullishOpts;
114
+
86
115
  // Filter is a factory function that creates a filter object
87
116
  export const Filter = {
88
117
  number(opts: NumberFilterOpts) {
@@ -97,19 +126,19 @@ export const Filter = {
97
126
  ...opts,
98
127
  } as const;
99
128
  },
100
- date(opts: { min?: Date; max?: Date; operator?: OperatorType }) {
129
+ date(opts: DateLikeFilterOpts) {
101
130
  return {
102
131
  type: "date",
103
132
  ...opts,
104
133
  } as const;
105
134
  },
106
- datetime(opts: { min?: Date; max?: Date; operator?: OperatorType }) {
135
+ datetime(opts: DateLikeFilterOpts) {
107
136
  return {
108
137
  type: "datetime",
109
138
  ...opts,
110
139
  } as const;
111
140
  },
112
- time(opts: { min?: Date; max?: Date; operator?: OperatorType }) {
141
+ time(opts: DateLikeFilterOpts) {
113
142
  return {
114
143
  type: "time",
115
144
  ...opts,
@@ -135,6 +164,26 @@ export type ColumnFilterForType<T extends FilterType> = T extends FilterType
135
164
  ? Extract<ColumnFilterValue, { type: T }>
136
165
  : never;
137
166
 
167
+ function pad2(n: number): string {
168
+ return n.toString().padStart(2, "0");
169
+ }
170
+
171
+ function pad4(n: number): string {
172
+ return n.toString().padStart(4, "0");
173
+ }
174
+
175
+ export function dateToISODate(d: Date): string {
176
+ return `${pad4(d.getFullYear())}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
177
+ }
178
+
179
+ export function dateToISOTime(d: Date): string {
180
+ return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
181
+ }
182
+
183
+ export function dateToISODateTime(d: Date): string {
184
+ return `${dateToISODate(d)}T${dateToISOTime(d)}`;
185
+ }
186
+
138
187
  function isNullishFilter(
139
188
  filter: ColumnFilterValue,
140
189
  ): filter is Extract<
@@ -235,71 +284,44 @@ export function filterToFilterCondition(
235
284
  default:
236
285
  assertNever(filter);
237
286
  }
238
- case "datetime": {
239
- const conditions: FilterConditionType[] = [];
240
- if (filter.min !== undefined) {
241
- conditions.push({
242
- column_id: columnId,
243
- operator: ">=",
244
- value: filter.min.toISOString(),
245
- type: "condition",
246
- negate: false,
247
- });
248
- }
249
- if (filter.max !== undefined) {
250
- conditions.push({
251
- column_id: columnId,
252
- operator: "<=",
253
- value: filter.max.toISOString(),
254
- type: "condition",
255
- negate: false,
256
- });
257
- }
258
- return conditions;
259
- }
260
- case "date": {
261
- const conditions: FilterConditionType[] = [];
262
- if (filter.min !== undefined) {
263
- conditions.push({
264
- column_id: columnId,
265
- operator: ">=",
266
- value: filter.min.toISOString(),
267
- type: "condition",
268
- negate: false,
269
- });
270
- }
271
- if (filter.max !== undefined) {
272
- conditions.push({
273
- column_id: columnId,
274
- operator: "<=",
275
- value: filter.max.toISOString(),
276
- type: "condition",
277
- negate: false,
278
- });
279
- }
280
- return conditions;
281
- }
287
+ case "date":
288
+ case "datetime":
282
289
  case "time": {
283
- const conditions: FilterConditionType[] = [];
284
- if (filter.min !== undefined) {
285
- conditions.push({
286
- column_id: columnId,
287
- operator: ">=",
288
- value: filter.min.toISOString(),
289
- type: "condition",
290
- negate: false,
291
- });
292
- }
293
- if (filter.max !== undefined) {
294
- conditions.push({
295
- column_id: columnId,
296
- operator: "<=",
297
- value: filter.max.toISOString(),
298
- type: "condition",
299
- negate: false,
300
- });
290
+ const encode =
291
+ filter.type === "date"
292
+ ? dateToISODate
293
+ : filter.type === "time"
294
+ ? dateToISOTime
295
+ : dateToISODateTime;
296
+ switch (filter.operator) {
297
+ case "between":
298
+ return [
299
+ {
300
+ column_id: columnId,
301
+ operator: "between",
302
+ value: { min: encode(filter.min), max: encode(filter.max) },
303
+ type: "condition",
304
+ negate: false,
305
+ },
306
+ ];
307
+ case "==":
308
+ case "!=":
309
+ case ">":
310
+ case ">=":
311
+ case "<":
312
+ case "<=":
313
+ return [
314
+ {
315
+ column_id: columnId,
316
+ operator: filter.operator,
317
+ value: encode(filter.value),
318
+ type: "condition",
319
+ negate: false,
320
+ },
321
+ ];
322
+ default:
323
+ assertNever(filter);
301
324
  }
302
- return conditions;
303
325
  }
304
326
  case "boolean":
305
327
  if (filter.value) {
@@ -57,7 +57,7 @@ export const BackendConnectionStatus: React.FC = () => {
57
57
  }
58
58
 
59
59
  try {
60
- const isHealthy = await runtime.isHealthy();
60
+ const isHealthy = await runtime.probeHealth();
61
61
  setConnectionStatus(isHealthy ? "healthy" : "unhealthy");
62
62
  return {
63
63
  isHealthy,