@marimo-team/islands 0.23.11-dev8 → 0.23.11

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 (82) hide show
  1. package/dist/{ConnectedDataExplorerComponent-Z3RP7Vmm.js → ConnectedDataExplorerComponent-WqG-xX4l.js} +13 -13
  2. package/dist/{ErrorBoundary-DpbaKVv7.js → ErrorBoundary-BNx_OSVo.js} +2 -2
  3. package/dist/{any-language-editor-CAgFD4Kd.js → any-language-editor-rPSlOll9.js} +5 -5
  4. package/dist/assets/__vite-browser-external-DAm_YW43.js +1 -0
  5. package/dist/assets/{worker-CC0Oul9k.js → worker-DEDLIQQV.js} +2 -2
  6. package/dist/{button-C5K9fIPF.js → button-vQhauTmO.js} +20 -0
  7. package/dist/{capabilities-BceAxrAW.js → capabilities-BEHzIS99.js} +1 -1
  8. package/dist/{chat-ui-c1FdlK-C.js → chat-ui-k2kqhCv5.js} +135 -106
  9. package/dist/{check-BCaJeT-J.js → check-nrzHDi45.js} +1 -1
  10. package/dist/{code-visibility-C-06v5KM.js → code-visibility-CFUW5L3i.js} +81 -68
  11. package/dist/{copy-UqRYxiOg.js → copy-UhDed7D4.js} +2 -2
  12. package/dist/{dist-Dk6PV_d3.js → dist-DYGLrbYQ.js} +2 -2
  13. package/dist/{error-banner-CJXYJ6Sb.js → error-banner-BHAkVFc2.js} +2 -2
  14. package/dist/{esm-BaH2eg5-.js → esm-Bqu9AE2K.js} +1 -1
  15. package/dist/{extends-D_hDsj6R.js → extends-9Yl5BEcg.js} +4 -4
  16. package/dist/{formats-C4wO47tk.js → formats-BV4bOfMI.js} +3 -3
  17. package/dist/{glide-data-editor-BPkCPs7L.js → glide-data-editor-BDTq6YUb.js} +9 -9
  18. package/dist/{html-to-image-D5-EpALB.js → html-to-image-C86pQALH.js} +16 -16
  19. package/dist/{input-OdWHkobi.js → input-AKkGXdyV.js} +6 -6
  20. package/dist/{label-CC4ytI1X.js → label-E3ZJXHu8.js} +2 -2
  21. package/dist/{loader-BWLPpjKK.js → loader-YPuQvn1Y.js} +1 -1
  22. package/dist/main.js +981 -978
  23. package/dist/{mermaid-BotvIKg9.js → mermaid-QFAR9YgY.js} +5 -5
  24. package/dist/{process-output-WDZE0cyS.js → process-output-nNw4OpSj.js} +3 -3
  25. package/dist/{reveal-component-yjK700z1.js → reveal-component-0u6v5UUq.js} +10 -10
  26. package/dist/{spec-X7FwLJni.js → spec-B45_YCNI.js} +4 -4
  27. package/dist/{strings-J57tzLr3.js → strings-Cq2s9_EQ.js} +4 -4
  28. package/dist/style.css +1 -1
  29. package/dist/{swiper-component-B2t5sN1q.js → swiper-component-BNa_4kh2.js} +2 -2
  30. package/dist/{toDate-d8RCRrRd.js → toDate-Do1xRzAo.js} +3 -3
  31. package/dist/{tooltip-DpcyNkQ2.js → tooltip-Bz3OAwrU.js} +3 -3
  32. package/dist/{types-ChtMFmZ2.js → types-D8gEGs4R.js} +1 -1
  33. package/dist/{useAsyncData-PonK__yh.js → useAsyncData-CL3o2p4i.js} +1 -1
  34. package/dist/{useDateFormatter-QB-3MpYr.js → useDateFormatter-BC6iSz9g.js} +2 -2
  35. package/dist/{useDeepCompareMemoize-D3NGWke6.js → useDeepCompareMemoize-BPx2MuOK.js} +1 -1
  36. package/dist/{useIframeCapabilities-C4JTXTIh.js → useIframeCapabilities-C6Ta3EyP.js} +1 -1
  37. package/dist/{useLifecycle-00mO3OSS.js → useLifecycle-C3Ec71q0.js} +3 -3
  38. package/dist/{useTheme-DEhDzATN.js → useTheme-ZhT6uIu3.js} +4 -3
  39. package/dist/{vega-component-DGPUhbDs.js → vega-component-C3AWYGAL.js} +10 -10
  40. package/dist/{zod-aLSua2NL.js → zod-DXqkaI_w.js} +1 -1
  41. package/package.json +1 -1
  42. package/src/components/ai/ai-model-dropdown.tsx +6 -0
  43. package/src/components/app-config/ai-config.tsx +73 -55
  44. package/src/components/app-config/data-form.tsx +33 -43
  45. package/src/components/app-config/is-overridden.tsx +125 -58
  46. package/src/components/app-config/user-config-form.tsx +178 -227
  47. package/src/components/chat/chat-display.tsx +5 -0
  48. package/src/components/chat/chat-panel.tsx +21 -8
  49. package/src/components/chat/tool-call/tool-call-view.tsx +4 -0
  50. package/src/components/chat/tool-call/tool-history-row.tsx +26 -5
  51. package/src/components/data-table/TableTopBar.tsx +4 -4
  52. package/src/components/data-table/__tests__/columns.test.tsx +20 -0
  53. package/src/components/data-table/__tests__/data-table.test.tsx +28 -0
  54. package/src/components/data-table/columns.tsx +16 -1
  55. package/src/components/data-table/data-table.tsx +3 -3
  56. package/src/components/data-table/filters/types.ts +1 -0
  57. package/src/components/data-table/renderers.tsx +5 -1
  58. package/src/components/datasources/__tests__/install-package-button.test.tsx +11 -0
  59. package/src/components/datasources/__tests__/utils.test.ts +127 -2
  60. package/src/components/datasources/datasources.tsx +44 -17
  61. package/src/components/datasources/install-package-button.tsx +3 -1
  62. package/src/components/datasources/utils.ts +45 -0
  63. package/src/components/editor/actions/useNotebookActions.tsx +62 -21
  64. package/src/components/editor/actions/useSetCodeVisibility.ts +57 -0
  65. package/src/components/editor/header/__tests__/status.test.tsx +0 -4
  66. package/src/components/editor/header/status.tsx +2 -3
  67. package/src/core/cells/__tests__/cells.test.ts +33 -0
  68. package/src/core/cells/cells.ts +1 -1
  69. package/src/core/config/config-schema.ts +6 -1
  70. package/src/core/edit-app.tsx +14 -0
  71. package/src/core/hotkeys/hotkeys.ts +20 -0
  72. package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +0 -13
  73. package/src/core/websocket/types.ts +0 -1
  74. package/src/core/websocket/useMarimoKernelConnection.tsx +0 -14
  75. package/src/css/app/print.css +8 -0
  76. package/src/css/md.css +12 -0
  77. package/src/plugins/impl/DataTablePlugin.tsx +12 -5
  78. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +1 -1
  79. package/src/plugins/impl/chat/chat-ui.tsx +1 -0
  80. package/src/plugins/impl/data-frames/DataFramePlugin.tsx +1 -1
  81. package/dist/assets/__vite-browser-external-eshhtsgZ.js +0 -1
  82. package/src/components/editor/actions/useHideAllMarkdownCode.ts +0 -53
@@ -20,11 +20,14 @@ export const renderUIMessage = ({
20
20
  message,
21
21
  isStreamingReasoning,
22
22
  isLast,
23
+ isActive,
23
24
  addToolApprovalResponse,
24
25
  }: {
25
26
  message: UIMessage;
26
27
  isStreamingReasoning: boolean;
27
28
  isLast: boolean;
29
+ /** Whether the chat is currently streaming/submitting a response. */
30
+ isActive: boolean;
28
31
  addToolApprovalResponse?: ChatAddToolApproveResponseFunction;
29
32
  }) => {
30
33
  return (
@@ -49,6 +52,7 @@ export const renderUIMessage = ({
49
52
  approval={part.approval}
50
53
  onApprove={addToolApprovalResponse}
51
54
  isLive={isLast}
55
+ isActive={isActive}
52
56
  />
53
57
  );
54
58
  }
@@ -102,6 +106,7 @@ export const renderUIMessage = ({
102
106
  approval={part.approval}
103
107
  onApprove={addToolApprovalResponse}
104
108
  isLive={isLast}
109
+ isActive={isActive}
105
110
  />
106
111
  );
107
112
  case "source-document":
@@ -23,6 +23,7 @@ import {
23
23
  PlusIcon,
24
24
  SparklesIcon,
25
25
  SettingsIcon,
26
+ CodeIcon,
26
27
  } from "lucide-react";
27
28
  import { memo, useEffect, useRef, useState } from "react";
28
29
  import useEvent from "react-use-event-hook";
@@ -142,6 +143,7 @@ interface ChatMessageProps {
142
143
  onEdit: (index: number, newValue: string) => void;
143
144
  isStreamingReasoning: boolean;
144
145
  isLast: boolean;
146
+ isActive: boolean;
145
147
  addToolApprovalResponse?: ChatAddToolApproveResponseFunction;
146
148
  }
147
149
 
@@ -152,6 +154,7 @@ const ChatMessageDisplay: React.FC<ChatMessageProps> = memo(
152
154
  onEdit,
153
155
  isStreamingReasoning,
154
156
  isLast,
157
+ isActive,
155
158
  addToolApprovalResponse,
156
159
  }) => {
157
160
  const renderUserMessage = (message: UIMessage) => {
@@ -204,6 +207,7 @@ const ChatMessageDisplay: React.FC<ChatMessageProps> = memo(
204
207
  message,
205
208
  isStreamingReasoning,
206
209
  isLast,
210
+ isActive,
207
211
  addToolApprovalResponse,
208
212
  })}
209
213
  </div>
@@ -268,24 +272,32 @@ const ChatInputFooter: React.FC<ChatInputFooterProps> = memo(
268
272
  {
269
273
  value: "ask",
270
274
  label: "Ask",
271
- subtitle:
272
- "Use AI with access to read-only tools like documentation search",
275
+ subtitle: "AI with access to read-only tools like documentation search",
273
276
  Icon: NotebookText,
274
277
  },
275
278
  {
276
279
  value: "agent",
277
- label: "Agent (beta)",
278
- subtitle: "Use AI with access to read and write tools",
280
+ label: "Agent",
281
+ subtitle: "AI with access to read and write tools",
279
282
  Icon: HatGlasses,
280
283
  },
281
284
  ];
282
285
 
286
+ if (import.meta.env.DEV) {
287
+ modeOptions.push({
288
+ value: "code_mode",
289
+ label: "Code Mode (experimental)",
290
+ subtitle: "AI with access to the notebook's kernel. Use with caution.",
291
+ Icon: CodeIcon,
292
+ });
293
+ }
294
+
283
295
  const isAttachmentSupported =
284
296
  PROVIDERS_THAT_SUPPORT_ATTACHMENTS.has(currentProvider);
285
297
 
286
- const CurrentModeIcon = modeOptions.find(
287
- (o) => o.value === currentMode,
288
- )?.Icon;
298
+ const currentModeOption = modeOptions.find((o) => o.value === currentMode);
299
+ const CurrentModeIcon = currentModeOption?.Icon;
300
+ const CurrentModeLabel = currentModeOption?.label;
289
301
 
290
302
  return (
291
303
  <TooltipProvider>
@@ -294,7 +306,7 @@ const ChatInputFooter: React.FC<ChatInputFooterProps> = memo(
294
306
  <Select value={currentMode} onValueChange={saveModeChange}>
295
307
  <SelectTrigger className="h-6 text-xs border-border shadow-none! ring-0! bg-muted hover:bg-muted/30 py-0 px-2 gap-1.5">
296
308
  {CurrentModeIcon && <CurrentModeIcon className="h-3 w-3" />}
297
- <span className="capitalize">{currentMode}</span>
309
+ <span>{CurrentModeLabel}</span>
298
310
  </SelectTrigger>
299
311
  <SelectContent>
300
312
  <SelectGroup>
@@ -771,6 +783,7 @@ const ChatPanelBody = () => {
771
783
  onEdit={handleMessageEdit}
772
784
  isStreamingReasoning={isStreamingReasoning}
773
785
  isLast={idx === messages.length - 1}
786
+ isActive={isLoading}
774
787
  addToolApprovalResponse={addToolApprovalResponse}
775
788
  />
776
789
  ))}
@@ -24,6 +24,7 @@ interface ToolCallViewProps {
24
24
  * (the user has moved on).
25
25
  */
26
26
  isLive?: boolean;
27
+ isActive?: boolean;
27
28
  }
28
29
 
29
30
  export const ToolCallView: React.FC<ToolCallViewProps> = ({
@@ -37,6 +38,7 @@ export const ToolCallView: React.FC<ToolCallViewProps> = ({
37
38
  index,
38
39
  className,
39
40
  isLive = true,
41
+ isActive = true,
40
42
  }) => {
41
43
  switch (state) {
42
44
  case "approval-requested":
@@ -61,6 +63,7 @@ export const ToolCallView: React.FC<ToolCallViewProps> = ({
61
63
  input={input}
62
64
  index={index}
63
65
  className={className}
66
+ isActive={isActive}
64
67
  />
65
68
  );
66
69
 
@@ -89,6 +92,7 @@ export const ToolCallView: React.FC<ToolCallViewProps> = ({
89
92
  approval={approval}
90
93
  index={index}
91
94
  className={className}
95
+ isActive={isActive}
92
96
  />
93
97
  );
94
98
 
@@ -1,6 +1,12 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { BanIcon, CheckCircleIcon, Loader2, WrenchIcon } from "lucide-react";
3
+ import {
4
+ BanIcon,
5
+ CheckCircleIcon,
6
+ CircleSlashIcon,
7
+ Loader2,
8
+ WrenchIcon,
9
+ } from "lucide-react";
4
10
  import React from "react";
5
11
  import {
6
12
  Accordion,
@@ -29,7 +35,19 @@ const STATUS_LABEL: Record<HistoryState, string> = {
29
35
  "output-denied": "Denied",
30
36
  };
31
37
 
32
- const StatusIcon: React.FC<{ state: HistoryState }> = ({ state }) => {
38
+ const PENDING_STATES = new Set<HistoryState>([
39
+ "input-streaming",
40
+ "input-available",
41
+ "approval-responded",
42
+ ]);
43
+
44
+ const StatusIcon: React.FC<{ state: HistoryState; interrupted: boolean }> = ({
45
+ state,
46
+ interrupted,
47
+ }) => {
48
+ if (interrupted) {
49
+ return <CircleSlashIcon className="h-3 w-3 text-muted-foreground" />;
50
+ }
33
51
  switch (state) {
34
52
  case "input-streaming":
35
53
  case "input-available":
@@ -69,6 +87,7 @@ interface ToolHistoryRowProps {
69
87
  approval?: ToolApproval;
70
88
  index?: number;
71
89
  className?: string;
90
+ isActive?: boolean;
72
91
  }
73
92
 
74
93
  export const ToolHistoryRow: React.FC<ToolHistoryRowProps> = ({
@@ -79,7 +98,9 @@ export const ToolHistoryRow: React.FC<ToolHistoryRowProps> = ({
79
98
  approval,
80
99
  index = 0,
81
100
  className,
101
+ isActive = true,
82
102
  }) => {
103
+ const interrupted = !isActive && PENDING_STATES.has(state);
83
104
  return (
84
105
  <Accordion
85
106
  key={`tool-${index}`}
@@ -91,12 +112,12 @@ export const ToolHistoryRow: React.FC<ToolHistoryRowProps> = ({
91
112
  <AccordionTrigger
92
113
  className={cn(
93
114
  "h-6 text-xs border-border shadow-none! ring-0! bg-muted/60 hover:bg-muted py-0 px-2 gap-1 rounded-sm [&[data-state=open]>svg]:rotate-180 hover:no-underline",
94
- getTriggerToneClass(state),
115
+ interrupted ? "text-muted-foreground" : getTriggerToneClass(state),
95
116
  )}
96
117
  >
97
118
  <span className="flex items-center gap-1">
98
- <StatusIcon state={state} />
99
- {STATUS_LABEL[state]}:
119
+ <StatusIcon state={state} interrupted={interrupted} />
120
+ {interrupted ? "Interrupted" : STATUS_LABEL[state]}:
100
121
  <code className="font-mono text-xs">
101
122
  {formatToolName(toolName)}
102
123
  </code>
@@ -27,7 +27,7 @@ const NOOP_ON_SEARCH = () => {
27
27
 
28
28
  interface TableTopBarProps<TData> extends Partial<ExportActionProps> {
29
29
  table: Table<TData>;
30
- enableSearch: boolean;
30
+ showSearch: boolean;
31
31
  searchQuery?: string;
32
32
  onSearchQueryChange?: (query: string) => void;
33
33
  reloading?: boolean;
@@ -43,7 +43,7 @@ interface TableTopBarProps<TData> extends Partial<ExportActionProps> {
43
43
 
44
44
  export const TableTopBar = <TData,>({
45
45
  table,
46
- enableSearch,
46
+ showSearch,
47
47
  searchQuery,
48
48
  onSearchQueryChange,
49
49
  reloading,
@@ -68,7 +68,7 @@ export const TableTopBar = <TData,>({
68
68
 
69
69
  return (
70
70
  <div className="flex items-center h-10 px-2 border-b gap-2">
71
- {onSearchQueryChange && enableSearch && (
71
+ {onSearchQueryChange && showSearch && (
72
72
  <div className="flex flex-1 items-center gap-1 px-2 rounded-sm focus-within:ring-1 focus-within:ring-border transition-shadow">
73
73
  <SearchIcon className="w-4 h-4 text-muted-foreground shrink-0" />
74
74
  <input
@@ -99,7 +99,7 @@ export const TableTopBar = <TData,>({
99
99
  </div>
100
100
  )}
101
101
 
102
- <div className="flex items-center shrink-0">
102
+ <div className="flex items-center shrink-0 ml-auto">
103
103
  <ColumnVisibilityDropdown table={table} />
104
104
  {showChartBuilder && (
105
105
  <Button
@@ -1082,3 +1082,23 @@ describe("renderCellValue with datetime values", () => {
1082
1082
  });
1083
1083
  });
1084
1084
  });
1085
+
1086
+ test("column_widths sets fixed size and meta.width", () => {
1087
+ const cols = generateColumns({
1088
+ rowHeaders: [],
1089
+ selection: null,
1090
+ fieldTypes: [
1091
+ ["a", ["number", "int64"]],
1092
+ ["b", ["string", "str"]],
1093
+ ],
1094
+ columnWidths: { a: 120 },
1095
+ });
1096
+ const a = cols.find((c) => c.id === "a");
1097
+ const b = cols.find((c) => c.id === "b");
1098
+ expect(a?.size).toBe(120);
1099
+ expect(a?.minSize).toBe(120);
1100
+ expect(a?.maxSize).toBe(120);
1101
+ expect(a?.meta?.width).toBe(120);
1102
+ expect(b?.size).toBeUndefined();
1103
+ expect(b?.meta?.width).toBeUndefined();
1104
+ });
@@ -69,6 +69,34 @@ describe("DataTable", () => {
69
69
  expect(commonProps.rowSelection).toEqual(initialRowSelection);
70
70
  });
71
71
 
72
+ it("hides the search bar when showSearch is false", () => {
73
+ const columns: ColumnDef<TestData>[] = [
74
+ { accessorKey: "name", header: "Name" },
75
+ ];
76
+ const commonProps = {
77
+ data: [{ id: 1, name: "Test 1" }] as TestData[],
78
+ columns,
79
+ totalRows: 1,
80
+ totalColumns: 1,
81
+ pagination: false,
82
+ onSearchQueryChange: () => {},
83
+ };
84
+
85
+ const { rerender } = render(
86
+ <TooltipProvider>
87
+ <DataTable {...commonProps} showSearch={true} />
88
+ </TooltipProvider>,
89
+ );
90
+ expect(screen.queryByPlaceholderText("Search...")).not.toBeNull();
91
+
92
+ rerender(
93
+ <TooltipProvider>
94
+ <DataTable {...commonProps} showSearch={false} />
95
+ </TooltipProvider>,
96
+ );
97
+ expect(screen.queryByPlaceholderText("Search...")).toBeNull();
98
+ });
99
+
72
100
  it("shows the hoverTemplate text as a styled tooltip on hover", async () => {
73
101
  vi.useFakeTimers();
74
102
  interface RowData {
@@ -118,6 +118,7 @@ export function generateColumns<T>({
118
118
  showDataTypes,
119
119
  calculateTopKRows,
120
120
  fractionDigitsByColumn,
121
+ columnWidths,
121
122
  }: {
122
123
  rowHeaders: FieldTypesWithExternalType;
123
124
  selection: DataTableSelection;
@@ -129,6 +130,7 @@ export function generateColumns<T>({
129
130
  showDataTypes?: boolean;
130
131
  calculateTopKRows?: CalculateTopKRows;
131
132
  fractionDigitsByColumn?: Record<string, number>;
133
+ columnWidths?: Record<string, number>;
132
134
  }): ColumnDef<T>[] {
133
135
  // Row-headers are typically index columns
134
136
  const rowHeadersSet = new Set(rowHeaders.map(([columnName]) => columnName));
@@ -311,7 +313,20 @@ export function generateColumns<T>({
311
313
  // Can only sort if key is defined
312
314
  // For example, unnamed index columns, won't be sortable
313
315
  enableSorting: !!key,
314
- meta: getMeta(key),
316
+ meta: {
317
+ ...getMeta(key),
318
+ width: columnWidths?.[key],
319
+ },
320
+ // size seeds the width before measurement; minSize/maxSize pin
321
+ // getSize() to the fixed width so the per-paint measurement writeback
322
+ // cannot drift the header width or sticky-pin offsets.
323
+ ...(columnWidths?.[key] !== undefined
324
+ ? {
325
+ size: columnWidths[key],
326
+ minSize: columnWidths[key],
327
+ maxSize: columnWidths[key],
328
+ }
329
+ : {}),
315
330
  }),
316
331
  );
317
332
 
@@ -104,7 +104,7 @@ interface DataTableProps<TData> extends Partial<ExportActionProps> {
104
104
  onCellSelectionChange?: OnChangeFn<CellSelectionState>;
105
105
  getRowIds?: GetRowIds;
106
106
  // Search
107
- enableSearch?: boolean;
107
+ showSearch?: boolean;
108
108
  searchQuery?: string;
109
109
  onSearchQueryChange?: (query: string) => void;
110
110
  showFilters?: boolean;
@@ -159,7 +159,7 @@ const DataTableInternal = <TData,>({
159
159
  onRowSelectionChange,
160
160
  onCellSelectionChange,
161
161
  getRowIds,
162
- enableSearch = false,
162
+ showSearch = false,
163
163
  searchQuery,
164
164
  onSearchQueryChange,
165
165
  showFilters = false,
@@ -357,7 +357,7 @@ const DataTableInternal = <TData,>({
357
357
  >
358
358
  <TableTopBar
359
359
  table={table}
360
- enableSearch={enableSearch}
360
+ showSearch={showSearch}
361
361
  searchQuery={searchQuery}
362
362
  onSearchQueryChange={onSearchQueryChange}
363
363
  reloading={reloading}
@@ -11,6 +11,7 @@ declare module "@tanstack/react-table" {
11
11
  dataType?: DataType;
12
12
  filterType?: FilterType;
13
13
  minFractionDigits?: number;
14
+ width?: number;
14
15
  }
15
16
  }
16
17
 
@@ -141,6 +141,7 @@ export const DataTableBody = <TData,>({
141
141
  const renderCells = (cells: Cell<TData, unknown>[]) => {
142
142
  return cells.map((cell) => {
143
143
  const { className, style: pinningstyle } = getPinningStyles(cell.column);
144
+ const fixedWidth = cell.column.columnDef.meta?.width;
144
145
  const style = Object.assign(
145
146
  {},
146
147
  cell.getUserStyling?.() || {},
@@ -153,7 +154,8 @@ export const DataTableBody = <TData,>({
153
154
  {...getCellDomProps(cell.id)}
154
155
  key={cell.id}
155
156
  className={cn(
156
- "whitespace-pre truncate max-w-[300px] outline-hidden border-r border-r-border/75",
157
+ "whitespace-pre truncate outline-hidden border-r border-r-border/75",
158
+ !fixedWidth && "max-w-[300px]",
157
159
  cell.column.getColumnWrapping &&
158
160
  cell.column.getColumnWrapping?.() === "wrap" &&
159
161
  COLUMN_WRAPPING_STYLES,
@@ -297,6 +299,7 @@ function getPinningStyles<TData>(
297
299
  isPinned === "left" && column.getIsLastColumn("left");
298
300
  const isFirstRightPinnedColumn =
299
301
  isPinned === "right" && column.getIsFirstColumn("right");
302
+ const fixedWidth = column.columnDef.meta?.width;
300
303
 
301
304
  return {
302
305
  className: cn(isPinned && "bg-inherit", "shadow-r z-10"),
@@ -313,6 +316,7 @@ function getPinningStyles<TData>(
313
316
  position: isPinned ? "sticky" : "relative",
314
317
  zIndex: isPinned ? 1 : 0,
315
318
  width: column.getSize(),
319
+ ...(fixedWidth ? { minWidth: fixedWidth, maxWidth: fixedWidth } : {}),
316
320
  },
317
321
  };
318
322
  }
@@ -5,6 +5,7 @@ import { Provider } from "jotai";
5
5
  import React from "react";
6
6
  import { beforeEach, describe, expect, it, vi } from "vitest";
7
7
  import { MockRequestClient } from "@/__mocks__/requests";
8
+ import { viewStateAtom } from "@/core/mode";
8
9
  import { requestClientAtom } from "@/core/network/requests";
9
10
  import { store } from "@/core/state/jotai";
10
11
  import { InstallPackageButton } from "../install-package-button";
@@ -48,6 +49,7 @@ describe("InstallPackageButton", () => {
48
49
  beforeEach(() => {
49
50
  vi.clearAllMocks();
50
51
  store.set(requestClientAtom, MockRequestClient.create());
52
+ store.set(viewStateAtom, { mode: "edit", cellAnchor: null });
51
53
  });
52
54
 
53
55
  it("should not render when packages is undefined", () => {
@@ -82,4 +84,13 @@ describe("InstallPackageButton", () => {
82
84
  );
83
85
  expect(screen.getByText("Install altair")).toBeInTheDocument();
84
86
  });
87
+
88
+ it("should not render in read mode, where installs are not allowed", () => {
89
+ store.set(viewStateAtom, { mode: "read", cellAnchor: null });
90
+ const { container } = render(
91
+ <InstallPackageButton packages={["altair"]} />,
92
+ { wrapper },
93
+ );
94
+ expect(container.firstChild).toBeNull();
95
+ });
85
96
  });
@@ -3,8 +3,18 @@
3
3
  import { describe, expect, it } from "vitest";
4
4
  import type { SQLTableContext } from "@/core/datasets/data-source-connections";
5
5
  import { DUCKDB_ENGINE } from "@/core/datasets/engines";
6
- import type { DataTable, DataTableColumn } from "@/core/kernel/messages";
7
- import { sqlCode, tableUniqueId } from "../utils";
6
+ import type {
7
+ Database,
8
+ DatabaseSchema,
9
+ DataTable,
10
+ DataTableColumn,
11
+ } from "@/core/kernel/messages";
12
+ import {
13
+ schemaSubtreeMatchesSearch,
14
+ shouldExpandDatabaseForSearch,
15
+ sqlCode,
16
+ tableUniqueId,
17
+ } from "../utils";
8
18
 
9
19
  describe("sqlCode", () => {
10
20
  const mockTable: DataTable = {
@@ -519,3 +529,118 @@ describe("tableUniqueId", () => {
519
529
  );
520
530
  });
521
531
  });
532
+
533
+ function makeTable(name: string): DataTable {
534
+ return {
535
+ name,
536
+ columns: [],
537
+ source: "memory",
538
+ source_type: "local",
539
+ type: "table",
540
+ engine: null,
541
+ indexes: null,
542
+ num_columns: null,
543
+ num_rows: null,
544
+ variable_name: null,
545
+ primary_keys: null,
546
+ };
547
+ }
548
+
549
+ function makeSchema(opts: {
550
+ name: string;
551
+ tables: DataTable[];
552
+ tables_resolved?: boolean;
553
+ child_schemas?: DatabaseSchema[];
554
+ child_schemas_resolved?: boolean;
555
+ }): DatabaseSchema {
556
+ return {
557
+ name: opts.name,
558
+ tables: opts.tables,
559
+ tables_resolved: opts.tables_resolved ?? true,
560
+ child_schemas: opts.child_schemas ?? [],
561
+ child_schemas_resolved: opts.child_schemas_resolved ?? true,
562
+ };
563
+ }
564
+
565
+ function makeDatabase(
566
+ name: string,
567
+ schemas: DatabaseSchema[],
568
+ schemas_resolved = true,
569
+ ): Database {
570
+ return { name, dialect: "duckdb", schemas, schemas_resolved, engine: null };
571
+ }
572
+
573
+ describe("schemaSubtreeMatchesSearch", () => {
574
+ it("returns false for an empty query", () => {
575
+ const schema = makeSchema({ name: "main", tables: [makeTable("users")] });
576
+ expect(schemaSubtreeMatchesSearch(schema, "")).toBe(false);
577
+ expect(schemaSubtreeMatchesSearch(schema, " ")).toBe(false);
578
+ expect(schemaSubtreeMatchesSearch(schema, undefined)).toBe(false);
579
+ });
580
+
581
+ it("matches a table name case-insensitively", () => {
582
+ const schema = makeSchema({ name: "main", tables: [makeTable("Users")] });
583
+ expect(schemaSubtreeMatchesSearch(schema, "user")).toBe(true);
584
+ expect(schemaSubtreeMatchesSearch(schema, "orders")).toBe(false);
585
+ });
586
+
587
+ it("matches a table in a resolved child schema", () => {
588
+ const schema = makeSchema({
589
+ name: "parent",
590
+ tables: [],
591
+ child_schemas: [
592
+ makeSchema({ name: "child", tables: [makeTable("orders")] }),
593
+ ],
594
+ });
595
+ expect(schemaSubtreeMatchesSearch(schema, "orders")).toBe(true);
596
+ });
597
+
598
+ it("ignores deferred tables so search never triggers a fetch", () => {
599
+ const schema = makeSchema({
600
+ name: "main",
601
+ tables: [makeTable("users")],
602
+ tables_resolved: false,
603
+ });
604
+ expect(schemaSubtreeMatchesSearch(schema, "users")).toBe(false);
605
+ });
606
+
607
+ it("ignores deferred child schemas", () => {
608
+ const schema = makeSchema({
609
+ name: "parent",
610
+ tables: [],
611
+ child_schemas: [
612
+ makeSchema({ name: "child", tables: [makeTable("orders")] }),
613
+ ],
614
+ child_schemas_resolved: false,
615
+ });
616
+ expect(schemaSubtreeMatchesSearch(schema, "orders")).toBe(false);
617
+ });
618
+ });
619
+
620
+ describe("shouldExpandDatabaseForSearch", () => {
621
+ it("returns false for an empty query", () => {
622
+ const db = makeDatabase("memory", [
623
+ makeSchema({ name: "main", tables: [makeTable("users")] }),
624
+ ]);
625
+ expect(shouldExpandDatabaseForSearch(db, "")).toBe(false);
626
+ expect(shouldExpandDatabaseForSearch(db, undefined)).toBe(false);
627
+ });
628
+
629
+ it("expands when a loaded schema contains a matching table", () => {
630
+ const db = makeDatabase("memory", [
631
+ makeSchema({ name: "main", tables: [makeTable("users")] }),
632
+ makeSchema({ name: "other", tables: [makeTable("orders")] }),
633
+ ]);
634
+ expect(shouldExpandDatabaseForSearch(db, "user")).toBe(true);
635
+ expect(shouldExpandDatabaseForSearch(db, "nomatch")).toBe(false);
636
+ });
637
+
638
+ it("does not expand when the schema list itself is deferred", () => {
639
+ const db = makeDatabase(
640
+ "memory",
641
+ [makeSchema({ name: "main", tables: [makeTable("users")] })],
642
+ /* schemas_resolved */ false,
643
+ );
644
+ expect(shouldExpandDatabaseForSearch(db, "users")).toBe(false);
645
+ });
646
+ });