@marimo-team/islands 0.23.11-dev9 → 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.
- package/dist/{ConnectedDataExplorerComponent-Z3RP7Vmm.js → ConnectedDataExplorerComponent-WqG-xX4l.js} +13 -13
- package/dist/{ErrorBoundary-DpbaKVv7.js → ErrorBoundary-BNx_OSVo.js} +2 -2
- package/dist/{any-language-editor-CAgFD4Kd.js → any-language-editor-rPSlOll9.js} +5 -5
- package/dist/assets/__vite-browser-external-DAm_YW43.js +1 -0
- package/dist/assets/{worker-CC0Oul9k.js → worker-DEDLIQQV.js} +2 -2
- package/dist/{button-C5K9fIPF.js → button-vQhauTmO.js} +20 -0
- package/dist/{capabilities-BceAxrAW.js → capabilities-BEHzIS99.js} +1 -1
- package/dist/{chat-ui-c1FdlK-C.js → chat-ui-k2kqhCv5.js} +135 -106
- package/dist/{check-BCaJeT-J.js → check-nrzHDi45.js} +1 -1
- package/dist/{code-visibility-0vPSjYnM.js → code-visibility-CFUW5L3i.js} +81 -68
- package/dist/{copy-UqRYxiOg.js → copy-UhDed7D4.js} +2 -2
- package/dist/{dist-Dk6PV_d3.js → dist-DYGLrbYQ.js} +2 -2
- package/dist/{error-banner-CJXYJ6Sb.js → error-banner-BHAkVFc2.js} +2 -2
- package/dist/{esm-BaH2eg5-.js → esm-Bqu9AE2K.js} +1 -1
- package/dist/{extends-D_hDsj6R.js → extends-9Yl5BEcg.js} +4 -4
- package/dist/{formats-C4wO47tk.js → formats-BV4bOfMI.js} +3 -3
- package/dist/{glide-data-editor-BPkCPs7L.js → glide-data-editor-BDTq6YUb.js} +9 -9
- package/dist/{html-to-image-D5-EpALB.js → html-to-image-C86pQALH.js} +16 -16
- package/dist/{input-OdWHkobi.js → input-AKkGXdyV.js} +6 -6
- package/dist/{label-CC4ytI1X.js → label-E3ZJXHu8.js} +2 -2
- package/dist/{loader-BWLPpjKK.js → loader-YPuQvn1Y.js} +1 -1
- package/dist/main.js +981 -978
- package/dist/{mermaid-BotvIKg9.js → mermaid-QFAR9YgY.js} +5 -5
- package/dist/{process-output-WDZE0cyS.js → process-output-nNw4OpSj.js} +3 -3
- package/dist/{reveal-component-BZpDHGsk.js → reveal-component-0u6v5UUq.js} +10 -10
- package/dist/{spec-X7FwLJni.js → spec-B45_YCNI.js} +4 -4
- package/dist/{strings-J57tzLr3.js → strings-Cq2s9_EQ.js} +4 -4
- package/dist/style.css +1 -1
- package/dist/{swiper-component-B2t5sN1q.js → swiper-component-BNa_4kh2.js} +2 -2
- package/dist/{toDate-d8RCRrRd.js → toDate-Do1xRzAo.js} +3 -3
- package/dist/{tooltip-DpcyNkQ2.js → tooltip-Bz3OAwrU.js} +3 -3
- package/dist/{types-ChtMFmZ2.js → types-D8gEGs4R.js} +1 -1
- package/dist/{useAsyncData-PonK__yh.js → useAsyncData-CL3o2p4i.js} +1 -1
- package/dist/{useDateFormatter-QB-3MpYr.js → useDateFormatter-BC6iSz9g.js} +2 -2
- package/dist/{useDeepCompareMemoize-D3NGWke6.js → useDeepCompareMemoize-BPx2MuOK.js} +1 -1
- package/dist/{useIframeCapabilities-C4JTXTIh.js → useIframeCapabilities-C6Ta3EyP.js} +1 -1
- package/dist/{useLifecycle-00mO3OSS.js → useLifecycle-C3Ec71q0.js} +3 -3
- package/dist/{useTheme-DEhDzATN.js → useTheme-ZhT6uIu3.js} +4 -3
- package/dist/{vega-component-DGPUhbDs.js → vega-component-C3AWYGAL.js} +10 -10
- package/dist/{zod-aLSua2NL.js → zod-DXqkaI_w.js} +1 -1
- package/package.json +1 -1
- package/src/components/ai/ai-model-dropdown.tsx +6 -0
- package/src/components/app-config/ai-config.tsx +73 -55
- package/src/components/app-config/data-form.tsx +33 -43
- package/src/components/app-config/is-overridden.tsx +125 -58
- package/src/components/app-config/user-config-form.tsx +178 -227
- package/src/components/chat/chat-display.tsx +5 -0
- package/src/components/chat/chat-panel.tsx +21 -8
- package/src/components/chat/tool-call/tool-call-view.tsx +4 -0
- package/src/components/chat/tool-call/tool-history-row.tsx +26 -5
- package/src/components/data-table/TableTopBar.tsx +4 -4
- package/src/components/data-table/__tests__/columns.test.tsx +20 -0
- package/src/components/data-table/__tests__/data-table.test.tsx +28 -0
- package/src/components/data-table/columns.tsx +16 -1
- package/src/components/data-table/data-table.tsx +3 -3
- package/src/components/data-table/filters/types.ts +1 -0
- package/src/components/data-table/renderers.tsx +5 -1
- package/src/components/datasources/__tests__/install-package-button.test.tsx +11 -0
- package/src/components/datasources/__tests__/utils.test.ts +127 -2
- package/src/components/datasources/datasources.tsx +44 -17
- package/src/components/datasources/install-package-button.tsx +3 -1
- package/src/components/datasources/utils.ts +45 -0
- package/src/components/editor/actions/useNotebookActions.tsx +62 -21
- package/src/components/editor/actions/useSetCodeVisibility.ts +57 -0
- package/src/components/editor/header/__tests__/status.test.tsx +0 -4
- package/src/components/editor/header/status.tsx +2 -3
- package/src/core/cells/__tests__/cells.test.ts +33 -0
- package/src/core/cells/cells.ts +1 -1
- package/src/core/config/config-schema.ts +6 -1
- package/src/core/edit-app.tsx +14 -0
- package/src/core/hotkeys/hotkeys.ts +20 -0
- package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +0 -13
- package/src/core/websocket/types.ts +0 -1
- package/src/core/websocket/useMarimoKernelConnection.tsx +0 -14
- package/src/css/app/print.css +8 -0
- package/src/css/md.css +12 -0
- package/src/plugins/impl/DataTablePlugin.tsx +12 -5
- package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +1 -1
- package/src/plugins/impl/chat/chat-ui.tsx +1 -0
- package/src/plugins/impl/data-frames/DataFramePlugin.tsx +1 -1
- package/dist/assets/__vite-browser-external-eshhtsgZ.js +0 -1
- 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
|
|
278
|
-
subtitle: "
|
|
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
|
|
287
|
-
|
|
288
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
360
|
+
showSearch={showSearch}
|
|
361
361
|
searchQuery={searchQuery}
|
|
362
362
|
onSearchQueryChange={onSearchQueryChange}
|
|
363
363
|
reloading={reloading}
|
|
@@ -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
|
|
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 {
|
|
7
|
-
|
|
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
|
+
});
|