@marimo-team/islands 0.23.16-dev9 → 0.23.17-dev0
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/{chat-ui-JeekqoMH.js → chat-ui-DsZJj75A.js} +3104 -3107
- package/dist/{code-visibility-DveLKNtj.js → common-CotPLtrU.js} +1068 -1036
- package/dist/{formats-CO8cn5_K.js → formats-Dwq1THhr.js} +1 -1
- package/dist/{html-to-image-Bif4utQZ.js → html-to-image-CZ1kLKkq.js} +7336 -6342
- package/dist/main.js +211 -147
- package/dist/{node-sql-parser-DLl_zNw0.js → node-sql-parser-v1Vi-Fft.js} +16867 -16665
- package/dist/{process-output-TAzy-GUk.js → process-output-bVfbcx5_.js} +30 -26
- package/dist/{reveal-component-DxV4Ppa2.js → reveal-component-IoHipy82.js} +634 -626
- package/dist/style.css +1 -1
- package/dist/toDate-CqhBWjvO.js +713 -0
- package/dist/{vega-component-DphdxaJ_.js → vega-component-C7nO5zcf.js} +2 -2
- package/package.json +3 -4
- package/src/__mocks__/requests.ts +56 -4
- package/src/components/data-table/__tests__/__snapshots__/chart-spec-model.test.ts.snap +174 -21
- package/src/components/data-table/__tests__/chart-spec-model.test.ts +102 -1
- package/src/components/data-table/__tests__/columns.test.tsx +9 -1
- package/src/components/data-table/column-summary/chart-spec-model.tsx +163 -103
- package/src/components/data-table/column-summary/legacy-chart-spec.ts +15 -7
- package/src/components/data-table/column-summary/utils.ts +26 -7
- package/src/components/data-table/columns.tsx +11 -1
- package/src/components/data-table/types.ts +1 -1
- package/src/components/editor/__tests__/viewer-banner.test.tsx +14 -0
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +99 -18
- package/src/components/editor/actions/__tests__/pdf-export.test.ts +32 -13
- package/src/components/editor/actions/pair-with-agent-commands.ts +21 -10
- package/src/components/editor/actions/pair-with-agent-modal.tsx +2 -2
- package/src/components/editor/actions/pdf-export.ts +11 -14
- package/src/components/editor/actions/useNotebookActions.tsx +28 -19
- package/src/components/editor/cell/code/__tests__/cell-editor-activity-lifecycle.test.ts +250 -0
- package/src/components/editor/cell/code/cell-editor.tsx +13 -7
- package/src/components/editor/chrome/__tests__/state.test.ts +7 -1
- package/src/components/editor/chrome/panels/__tests__/logs-panel.test.tsx +25 -0
- package/src/components/editor/chrome/panels/__tests__/panel-accordion-state.test.ts +42 -0
- package/src/components/editor/chrome/panels/file-explorer-panel.tsx +6 -17
- package/src/components/editor/chrome/panels/logs-panel.tsx +12 -2
- package/src/components/editor/chrome/panels/panel-accordion-state.ts +51 -0
- package/src/components/editor/chrome/panels/session-panel.tsx +5 -16
- package/src/components/editor/chrome/panels/write-secret-modal.tsx +40 -12
- package/src/components/editor/chrome/state.ts +10 -0
- package/src/components/editor/connections/__tests__/json-credentials.test.ts +114 -0
- package/src/components/editor/connections/__tests__/paths.test.ts +39 -0
- package/src/components/editor/connections/__tests__/quick-add-data-sources.test.tsx +113 -0
- package/src/components/editor/connections/add-connection-dialog.tsx +2 -0
- package/src/components/editor/connections/components.tsx +6 -2
- package/src/components/editor/connections/database/__tests__/__snapshots__/as-code.test.ts.snap +27 -5
- package/src/components/editor/connections/database/__tests__/as-code.test.ts +33 -0
- package/src/components/editor/connections/database/as-code.ts +37 -13
- package/src/components/editor/connections/database/schemas.ts +6 -5
- package/src/components/editor/connections/form-renderers.tsx +99 -1
- package/src/components/editor/connections/json-credentials.ts +57 -0
- package/src/components/editor/connections/paths.ts +21 -0
- package/src/components/editor/connections/quick-add-data-sources.tsx +106 -0
- package/src/components/editor/connections/secret-combobox.tsx +41 -20
- package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +88 -2
- package/src/components/editor/connections/storage/__tests__/as-code.test.ts +128 -8
- package/src/components/editor/connections/storage/as-code.ts +79 -36
- package/src/components/editor/connections/storage/schemas.ts +85 -44
- package/src/components/editor/file-tree/__tests__/renderers.test.ts +31 -0
- package/src/components/editor/file-tree/file-viewer.tsx +11 -7
- package/src/components/editor/file-tree/renderers.tsx +21 -0
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +2 -2
- package/src/components/editor/viewer-banner.tsx +6 -1
- package/src/components/forms/form.tsx +59 -24
- package/src/components/forms/options.ts +2 -1
- package/src/components/slides/__tests__/cell-output-block.test.tsx +40 -0
- package/src/components/slides/__tests__/slide-cell-view.test.tsx +33 -0
- package/src/components/slides/reveal-component.tsx +22 -17
- package/src/components/slides/slide-cell-view.tsx +1 -1
- package/src/components/slides/slides.css +4 -2
- package/src/components/static-html/share-modal.tsx +1 -1
- package/src/core/ai/context/providers/file.ts +8 -29
- package/src/core/cells/__tests__/cells.test.ts +12 -2
- package/src/core/cells/__tests__/logs.test.ts +5 -5
- package/src/core/cells/cells.ts +17 -4
- package/src/core/cells/document-changes.ts +1 -0
- package/src/core/cells/logs.ts +14 -4
- package/src/core/codemirror/cm.ts +3 -0
- package/src/core/codemirror/code-lens/__tests__/actions.test.ts +68 -0
- package/src/core/codemirror/code-lens/__tests__/analyzer.test.ts +224 -0
- package/src/core/codemirror/code-lens/__tests__/entities.test.ts +175 -0
- package/src/core/codemirror/code-lens/__tests__/extension.test.ts +325 -0
- package/src/core/codemirror/code-lens/__tests__/perf.test.ts +121 -0
- package/src/core/codemirror/code-lens/__tests__/popover.test.ts +93 -0
- package/src/core/codemirror/code-lens/actions.ts +33 -0
- package/src/core/codemirror/code-lens/analyzer.ts +284 -0
- package/src/core/codemirror/code-lens/entities.ts +123 -0
- package/src/core/codemirror/code-lens/extension.ts +298 -0
- package/src/core/codemirror/code-lens/icons.ts +36 -0
- package/src/core/codemirror/code-lens/popover.tsx +240 -0
- package/src/core/codemirror/copilot/__tests__/language-server.test.ts +396 -100
- package/src/core/codemirror/copilot/__tests__/transport.test.ts +128 -540
- package/src/core/codemirror/copilot/language-server.ts +182 -70
- package/src/core/codemirror/copilot/transport.ts +15 -207
- package/src/core/codemirror/copilot/types.ts +9 -2
- package/src/core/codemirror/find-replace/__tests__/navigate.test.ts +42 -30
- package/src/core/codemirror/find-replace/navigate.ts +2 -0
- package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +24 -3
- package/src/core/codemirror/go-to-definition/commands.ts +93 -90
- package/src/core/codemirror/language/__tests__/sql.test.ts +627 -61
- package/src/core/codemirror/language/languages/python.ts +23 -11
- package/src/core/codemirror/language/languages/sql/completion-store.ts +5 -1
- package/src/core/codemirror/language/languages/sql/renderers.tsx +3 -3
- package/src/core/codemirror/language/languages/sql/sql.ts +251 -16
- package/src/core/codemirror/lsp/__tests__/federated-lsp.test.ts +161 -0
- package/src/core/codemirror/lsp/__tests__/log-messages.test.ts +53 -0
- package/src/core/codemirror/lsp/__tests__/normalize-markdown-math.test.ts +25 -0
- package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +598 -156
- package/src/core/codemirror/lsp/__tests__/transports.test.ts +112 -0
- package/src/core/codemirror/lsp/federated-lsp.ts +76 -23
- package/src/core/codemirror/lsp/log-messages.ts +37 -0
- package/src/core/codemirror/lsp/normalize-markdown-math.ts +8 -4
- package/src/core/codemirror/lsp/notebook-lsp.ts +220 -56
- package/src/core/codemirror/lsp/transports.ts +8 -1
- package/src/core/codemirror/lsp/types.ts +68 -8
- package/src/core/codemirror/lsp/utils.ts +14 -0
- package/src/core/codemirror/python-node-names.ts +90 -0
- package/src/core/codemirror/reactive-references/analyzer.ts +52 -57
- package/src/core/codemirror/utils.ts +28 -0
- package/src/core/config/feature-flag.tsx +3 -1
- package/src/core/datasets/data-source-discovery.ts +5 -0
- package/src/core/datasets/request-registry.ts +11 -0
- package/src/core/edit-app.tsx +1 -2
- package/src/core/export/__tests__/hooks.test.ts +2 -4
- package/src/core/export/hooks.ts +4 -3
- package/src/core/islands/__tests__/bridge.test.ts +107 -0
- package/src/core/islands/bootstrap.ts +1 -0
- package/src/core/islands/bridge.ts +18 -0
- package/src/core/kernel/messages.ts +2 -0
- package/src/core/lsp/__tests__/transport.test.ts +509 -337
- package/src/core/lsp/transport.ts +304 -132
- package/src/core/mode.ts +10 -0
- package/src/core/network/__tests__/api.test.ts +42 -0
- package/src/core/network/__tests__/connection.test.ts +45 -0
- package/src/core/network/__tests__/requests-lazy.test.ts +47 -1
- package/src/core/network/__tests__/requests-network.test.ts +48 -0
- package/src/core/network/api.ts +27 -0
- package/src/core/network/connection.ts +15 -0
- package/src/core/network/requests-lazy.ts +3 -0
- package/src/core/network/requests-network.ts +31 -8
- package/src/core/network/requests-static.ts +3 -0
- package/src/core/network/requests-toasting.tsx +3 -0
- package/src/core/network/types.ts +27 -6
- package/src/core/rtc/state.ts +9 -0
- package/src/core/static/download-html.ts +5 -14
- package/src/core/wasm/__tests__/bridge.test.ts +99 -5
- package/src/core/wasm/bridge.ts +42 -9
- package/src/core/wasm/worker/types.ts +10 -3
- package/src/core/wasm/worker/worker.ts +7 -8
- package/src/core/websocket/useMarimoKernelConnection.tsx +4 -0
- package/src/css/app/codemirror.css +16 -2
- package/src/hooks/__tests__/useDataSourceDiscovery.test.ts +80 -0
- package/src/hooks/useDataSourceDiscovery.ts +18 -0
- package/src/plugins/impl/DataTablePlugin.tsx +4 -1
- package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +158 -0
- package/src/plugins/impl/anywidget/__tests__/model.test.ts +55 -0
- package/src/plugins/impl/anywidget/model.ts +20 -5
- package/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap +33 -21
- package/src/plugins/impl/data-frames/forms/renderers.tsx +11 -8
- package/src/stories/log-viewer.stories.tsx +12 -6
- package/src/utils/__tests__/blob.test.ts +27 -1
- package/src/utils/__tests__/download.test.tsx +44 -4
- package/src/utils/blob.ts +24 -1
- package/src/utils/download.ts +14 -15
- package/dist/toDate-CmR_xR3P.js +0 -692
|
@@ -12,6 +12,8 @@ import type {
|
|
|
12
12
|
EnvironmentInfo,
|
|
13
13
|
ExportAsHTMLRequest,
|
|
14
14
|
ExportAsMarkdownRequest,
|
|
15
|
+
ExportAsScriptRequest,
|
|
16
|
+
ExportedFile,
|
|
15
17
|
FileCopyRequest,
|
|
16
18
|
FileCopyResponse,
|
|
17
19
|
FileCreateRequest,
|
|
@@ -96,8 +98,11 @@ export interface RawBridge {
|
|
|
96
98
|
load_packages(request: string): Promise<string>;
|
|
97
99
|
read_file(request: string): Promise<string>;
|
|
98
100
|
set_interrupt_buffer(request: Uint8Array): Promise<string>;
|
|
99
|
-
export_html(request: ExportAsHTMLRequest): Promise<string
|
|
100
|
-
export_markdown(
|
|
101
|
+
export_html(request: ExportAsHTMLRequest): Promise<ExportedFile<string>>;
|
|
102
|
+
export_markdown(
|
|
103
|
+
request: ExportAsMarkdownRequest,
|
|
104
|
+
): Promise<ExportedFile<string>>;
|
|
105
|
+
export_script(request: ExportAsScriptRequest): Promise<ExportedFile<string>>;
|
|
101
106
|
}
|
|
102
107
|
|
|
103
108
|
export type BridgePayload<T extends keyof RawBridge> = T extends keyof RawBridge
|
|
@@ -105,9 +110,11 @@ export type BridgePayload<T extends keyof RawBridge> = T extends keyof RawBridge
|
|
|
105
110
|
: undefined;
|
|
106
111
|
|
|
107
112
|
export type SerializedBridge = {
|
|
108
|
-
[P in keyof RawBridge]: RawBridge[P] extends (
|
|
113
|
+
[P in Exclude<keyof RawBridge, "save">]: RawBridge[P] extends (
|
|
109
114
|
payload: string,
|
|
110
115
|
) => Promise<unknown>
|
|
111
116
|
? (payload: string) => Promise<string>
|
|
112
117
|
: RawBridge[P];
|
|
118
|
+
} & {
|
|
119
|
+
save(payload: string): Promise<void>;
|
|
113
120
|
};
|
|
@@ -250,13 +250,9 @@ const requestHandler = createRPCRequestHandler({
|
|
|
250
250
|
* Save the notebook
|
|
251
251
|
*/
|
|
252
252
|
saveNotebook: async (opts: SaveNotebookRequest) => {
|
|
253
|
-
// Partially duplicated from save-worker.ts
|
|
254
253
|
await pyodideReadyPromise; // Make sure loading is done
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
save_file
|
|
258
|
-
`);
|
|
259
|
-
await saveFile(JSON.stringify(opts), WasmFileSystem.NOTEBOOK_FILENAME);
|
|
254
|
+
const bridge = await bridgeReady.promise;
|
|
255
|
+
await bridge.save(JSON.stringify(opts));
|
|
260
256
|
},
|
|
261
257
|
|
|
262
258
|
/**
|
|
@@ -287,8 +283,11 @@ const requestHandler = createRPCRequestHandler({
|
|
|
287
283
|
`);
|
|
288
284
|
}
|
|
289
285
|
|
|
290
|
-
//
|
|
291
|
-
if (
|
|
286
|
+
// Script export also parses Markdown frontmatter when present.
|
|
287
|
+
if (
|
|
288
|
+
functionName === "export_markdown" ||
|
|
289
|
+
functionName === "export_script"
|
|
290
|
+
) {
|
|
292
291
|
await self.pyodide.runPythonAsync(`
|
|
293
292
|
import micropip
|
|
294
293
|
|
|
@@ -42,6 +42,7 @@ import { connectionTransportTypeAtom, useSetAppConfig } from "../config/config";
|
|
|
42
42
|
import { useDataSourceActions } from "../datasets/data-source-connections";
|
|
43
43
|
import type { ConnectionName } from "../datasets/engines";
|
|
44
44
|
import {
|
|
45
|
+
DiscoverDataSources,
|
|
45
46
|
PreviewSQLSchemaList,
|
|
46
47
|
PreviewSQLTable,
|
|
47
48
|
PreviewSQLTableList,
|
|
@@ -394,6 +395,9 @@ export function useMarimoKernelConnection(opts: {
|
|
|
394
395
|
case "validate-sql-result":
|
|
395
396
|
ValidateSQL.resolve(msg.data.request_id as RequestId, msg.data);
|
|
396
397
|
return;
|
|
398
|
+
case "data-source-discovery-result":
|
|
399
|
+
DiscoverDataSources.resolve(msg.data.request_id as RequestId, msg.data);
|
|
400
|
+
return;
|
|
397
401
|
case "secret-keys-result":
|
|
398
402
|
SECRETS_REGISTRY.resolve(msg.data.request_id, msg.data);
|
|
399
403
|
return;
|
|
@@ -90,8 +90,22 @@
|
|
|
90
90
|
@apply bg-muted border border-border rounded-md px-1;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
/* The message, the docs link, and related info are siblings; stack them. */
|
|
94
|
+
.cm-lsp-diagnostic-message {
|
|
95
|
+
@apply flex flex-col items-start gap-1;
|
|
96
|
+
|
|
97
|
+
/* Rendered markdown, so the `pre-wrap` on `.cm-diagnostic` would only turn
|
|
98
|
+
the whitespace between block tags into blank lines. */
|
|
99
|
+
white-space: normal;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.cm-diagnostic-code-link {
|
|
103
|
+
@apply text-xs;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* The docs link already reads `source(code)`; don't repeat it below. */
|
|
107
|
+
.cm-diagnostic:has(.cm-diagnostic-code-link) .cm-diagnosticSource {
|
|
108
|
+
@apply hidden;
|
|
95
109
|
}
|
|
96
110
|
}
|
|
97
111
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { DiscoverDataSources } from "@/core/datasets/request-registry";
|
|
5
|
+
import { loadDataSourceDiscovery } from "../useDataSourceDiscovery";
|
|
6
|
+
|
|
7
|
+
describe("loadDataSourceDiscovery", () => {
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.restoreAllMocks();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("returns the kernel's secret-free discovery model", async () => {
|
|
13
|
+
const sources = [
|
|
14
|
+
{
|
|
15
|
+
id: "pyiceberg-prod",
|
|
16
|
+
integration: "pyiceberg",
|
|
17
|
+
category: "catalog" as const,
|
|
18
|
+
displayName: "PyIceberg (prod)",
|
|
19
|
+
confidence: "high" as const,
|
|
20
|
+
origins: [
|
|
21
|
+
{
|
|
22
|
+
type: "configuration" as const,
|
|
23
|
+
label: "Resolved PyIceberg configuration",
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
configuration: [
|
|
27
|
+
{
|
|
28
|
+
field: "Catalog",
|
|
29
|
+
value: {
|
|
30
|
+
kind: "safe-literal" as const,
|
|
31
|
+
value: "prod",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
code: 'catalog = load_catalog("prod")',
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
const request = vi.spyOn(DiscoverDataSources, "request").mockResolvedValue({
|
|
39
|
+
request_id: "request-id",
|
|
40
|
+
sources,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const detected = await loadDataSourceDiscovery();
|
|
44
|
+
|
|
45
|
+
expect({ detected, requests: request.mock.calls }).toMatchInlineSnapshot(`
|
|
46
|
+
{
|
|
47
|
+
"detected": [
|
|
48
|
+
{
|
|
49
|
+
"category": "catalog",
|
|
50
|
+
"code": "catalog = load_catalog("prod")",
|
|
51
|
+
"confidence": "high",
|
|
52
|
+
"configuration": [
|
|
53
|
+
{
|
|
54
|
+
"field": "Catalog",
|
|
55
|
+
"value": {
|
|
56
|
+
"kind": "safe-literal",
|
|
57
|
+
"value": "prod",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
"displayName": "PyIceberg (prod)",
|
|
62
|
+
"id": "pyiceberg-prod",
|
|
63
|
+
"integration": "pyiceberg",
|
|
64
|
+
"origins": [
|
|
65
|
+
{
|
|
66
|
+
"label": "Resolved PyIceberg configuration",
|
|
67
|
+
"type": "configuration",
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
"requests": [
|
|
73
|
+
[
|
|
74
|
+
{},
|
|
75
|
+
],
|
|
76
|
+
],
|
|
77
|
+
}
|
|
78
|
+
`);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type { DetectedDataSource } from "@/core/datasets/data-source-discovery";
|
|
4
|
+
import { DiscoverDataSources } from "@/core/datasets/request-registry";
|
|
5
|
+
import { useAsyncData } from "./useAsyncData";
|
|
6
|
+
|
|
7
|
+
export async function loadDataSourceDiscovery(): Promise<DetectedDataSource[]> {
|
|
8
|
+
const result = await DiscoverDataSources.request({});
|
|
9
|
+
return result.sources;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Reusable UI-facing hook for kernel-managed datasource discovery.
|
|
14
|
+
* Consumers decide how to render, filter, or act on the detected model.
|
|
15
|
+
*/
|
|
16
|
+
export function useDataSourceDiscovery() {
|
|
17
|
+
return useAsyncData(loadDataSourceDiscovery, []);
|
|
18
|
+
}
|
|
@@ -908,7 +908,10 @@ const DataTableComponent = ({
|
|
|
908
908
|
);
|
|
909
909
|
}, [fieldTypes, columnSummaries]);
|
|
910
910
|
|
|
911
|
-
const
|
|
911
|
+
const rowHeaderNames = new Set(rowHeaders.map(([name]) => name));
|
|
912
|
+
const fieldTypesOrInferred =
|
|
913
|
+
fieldTypes ??
|
|
914
|
+
inferFieldTypes(data).filter(([name]) => !rowHeaderNames.has(name));
|
|
912
915
|
|
|
913
916
|
const memoizedUnclampedFieldTypes =
|
|
914
917
|
useDeepCompareMemoize(fieldTypesOrInferred);
|
|
@@ -30,6 +30,164 @@ beforeAll(() => {
|
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
describe("LoadingDataTableComponent", () => {
|
|
33
|
+
it("keeps data columns when inferred types include a row header", async () => {
|
|
34
|
+
const host = document.createElement("div");
|
|
35
|
+
const data = JSON.stringify([{ index: "first", value: 1 }]);
|
|
36
|
+
|
|
37
|
+
render(
|
|
38
|
+
<Provider store={store}>
|
|
39
|
+
<TooltipProvider>
|
|
40
|
+
<LoadingDataTableComponent
|
|
41
|
+
label={null}
|
|
42
|
+
totalRows={1}
|
|
43
|
+
pagination={false}
|
|
44
|
+
pageSize={10}
|
|
45
|
+
selection={null}
|
|
46
|
+
showDownload={false}
|
|
47
|
+
showFilters={false}
|
|
48
|
+
showColumnSummaries={false}
|
|
49
|
+
showDataTypes={false}
|
|
50
|
+
showPageSizeSelector={false}
|
|
51
|
+
showColumnExplorer={false}
|
|
52
|
+
showRowExplorer={false}
|
|
53
|
+
showChartBuilder={false}
|
|
54
|
+
rowHeaders={[["index", ["string", "object"]]]}
|
|
55
|
+
fieldTypes={undefined}
|
|
56
|
+
totalColumns={1}
|
|
57
|
+
maxColumns={1}
|
|
58
|
+
hasStableRowId={false}
|
|
59
|
+
lazy={false}
|
|
60
|
+
host={host}
|
|
61
|
+
showSearch={false}
|
|
62
|
+
value={[]}
|
|
63
|
+
setValue={vi.fn()}
|
|
64
|
+
data={data}
|
|
65
|
+
search={vi.fn().mockResolvedValue({
|
|
66
|
+
data,
|
|
67
|
+
total_rows: 1,
|
|
68
|
+
cell_styles: null,
|
|
69
|
+
cell_hover_texts: null,
|
|
70
|
+
})}
|
|
71
|
+
download_as={vi.fn() as DownloadAsArgs}
|
|
72
|
+
get_column_summaries={vi.fn()}
|
|
73
|
+
get_data_url={vi.fn() as GetDataUrl}
|
|
74
|
+
get_row_ids={vi.fn() as GetRowIds}
|
|
75
|
+
get_size_bytes={vi.fn().mockResolvedValue({ size_bytes: null })}
|
|
76
|
+
/>
|
|
77
|
+
</TooltipProvider>
|
|
78
|
+
</Provider>,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
await waitFor(() => {
|
|
82
|
+
expect(
|
|
83
|
+
screen.getByRole("columnheader", { name: "value" }),
|
|
84
|
+
).toBeInTheDocument();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("does not log duplicate keys when a pivot becomes empty", async () => {
|
|
89
|
+
const host = document.createElement("div");
|
|
90
|
+
const consoleError = vi
|
|
91
|
+
.spyOn(console, "error")
|
|
92
|
+
.mockImplementation(() => {});
|
|
93
|
+
const populatedData = JSON.stringify([
|
|
94
|
+
{ DSGROUP: "a", no: 1, yes: 1 },
|
|
95
|
+
{ DSGROUP: "b", no: 0, yes: 1 },
|
|
96
|
+
]);
|
|
97
|
+
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
98
|
+
<Provider store={store}>
|
|
99
|
+
<TooltipProvider>{children}</TooltipProvider>
|
|
100
|
+
</Provider>
|
|
101
|
+
);
|
|
102
|
+
const commonProps = {
|
|
103
|
+
label: null,
|
|
104
|
+
pagination: false,
|
|
105
|
+
pageSize: 10,
|
|
106
|
+
selection: null,
|
|
107
|
+
showDownload: false,
|
|
108
|
+
showFilters: false,
|
|
109
|
+
showColumnSummaries: false as const,
|
|
110
|
+
showDataTypes: true,
|
|
111
|
+
showPageSizeSelector: false,
|
|
112
|
+
showColumnExplorer: false,
|
|
113
|
+
showRowExplorer: false,
|
|
114
|
+
showChartBuilder: false,
|
|
115
|
+
rowHeaders: [
|
|
116
|
+
["DSGROUP", ["string", "object"]],
|
|
117
|
+
] as FieldTypesWithExternalType,
|
|
118
|
+
maxColumns: "all" as const,
|
|
119
|
+
hasStableRowId: false,
|
|
120
|
+
lazy: false,
|
|
121
|
+
host,
|
|
122
|
+
showSearch: false,
|
|
123
|
+
value: [] as (number | string | { rowId: string; columnName?: string })[],
|
|
124
|
+
setValue: vi.fn(),
|
|
125
|
+
download_as: vi.fn() as DownloadAsArgs,
|
|
126
|
+
get_column_summaries: vi.fn(),
|
|
127
|
+
get_data_url: vi.fn() as GetDataUrl,
|
|
128
|
+
get_row_ids: vi.fn() as GetRowIds,
|
|
129
|
+
get_size_bytes: vi.fn().mockResolvedValue({ size_bytes: null }),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const { rerender } = render(
|
|
134
|
+
<Wrapper>
|
|
135
|
+
<LoadingDataTableComponent
|
|
136
|
+
{...commonProps}
|
|
137
|
+
totalRows={2}
|
|
138
|
+
totalColumns={2}
|
|
139
|
+
fieldTypes={[
|
|
140
|
+
["no", ["number", "int64"]],
|
|
141
|
+
["yes", ["number", "int64"]],
|
|
142
|
+
]}
|
|
143
|
+
data={populatedData}
|
|
144
|
+
search={vi.fn().mockResolvedValue({
|
|
145
|
+
data: populatedData,
|
|
146
|
+
total_rows: 2,
|
|
147
|
+
cell_styles: null,
|
|
148
|
+
cell_hover_texts: null,
|
|
149
|
+
})}
|
|
150
|
+
/>
|
|
151
|
+
</Wrapper>,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
await waitFor(() => {
|
|
155
|
+
expect(screen.getAllByRole("row")).toHaveLength(3);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await act(async () => {
|
|
159
|
+
rerender(
|
|
160
|
+
<Wrapper>
|
|
161
|
+
<LoadingDataTableComponent
|
|
162
|
+
{...commonProps}
|
|
163
|
+
totalRows={0}
|
|
164
|
+
totalColumns={0}
|
|
165
|
+
fieldTypes={undefined}
|
|
166
|
+
data="[]"
|
|
167
|
+
search={vi.fn().mockResolvedValue({
|
|
168
|
+
data: "[]",
|
|
169
|
+
total_rows: 0,
|
|
170
|
+
cell_styles: null,
|
|
171
|
+
cell_hover_texts: null,
|
|
172
|
+
})}
|
|
173
|
+
/>
|
|
174
|
+
</Wrapper>,
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await waitFor(() => {
|
|
179
|
+
expect(screen.getByText("No results.")).toBeInTheDocument();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const duplicateKeyErrors = consoleError.mock.calls.filter((args) =>
|
|
183
|
+
args.some((arg) => typeof arg === "string" && arg.includes("same key")),
|
|
184
|
+
);
|
|
185
|
+
expect(duplicateKeyErrors).toHaveLength(0);
|
|
186
|
+
} finally {
|
|
187
|
+
consoleError.mockRestore();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
33
191
|
/**
|
|
34
192
|
* Regression test for https://github.com/marimo-team/marimo/issues/8023
|
|
35
193
|
*
|
|
@@ -263,6 +263,61 @@ describe("Model", () => {
|
|
|
263
263
|
await TestUtils.nextTick(); // flush
|
|
264
264
|
expect(callback).toHaveBeenCalledTimes(1);
|
|
265
265
|
});
|
|
266
|
+
|
|
267
|
+
it("should not mark kernel-pushed state dirty", () => {
|
|
268
|
+
getMarimoInternal(model).updateAndEmitDiffs({ foo: "changed", bar: 456 });
|
|
269
|
+
|
|
270
|
+
model.save_changes();
|
|
271
|
+
expect(mockComm.sendUpdate).not.toHaveBeenCalled();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("should not echo kernel-pushed state alongside a local set", () => {
|
|
275
|
+
// Kernel pushes a large trait; widget ESM then sets one field.
|
|
276
|
+
getMarimoInternal(model).updateAndEmitDiffs({
|
|
277
|
+
foo: "from kernel",
|
|
278
|
+
bar: 123,
|
|
279
|
+
});
|
|
280
|
+
model.set("bar", 456);
|
|
281
|
+
|
|
282
|
+
model.save_changes();
|
|
283
|
+
expect(mockComm.sendUpdate).toHaveBeenCalledExactlyOnceWith({
|
|
284
|
+
bar: 456,
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("should drop a pending local write superseded by a kernel push", () => {
|
|
289
|
+
// Local write, not yet saved...
|
|
290
|
+
model.set("foo", "local");
|
|
291
|
+
// ...then the kernel pushes a newer value for the same key.
|
|
292
|
+
getMarimoInternal(model).updateAndEmitDiffs({
|
|
293
|
+
foo: "from kernel",
|
|
294
|
+
bar: 123,
|
|
295
|
+
});
|
|
296
|
+
expect(model.get("foo")).toBe("from kernel");
|
|
297
|
+
|
|
298
|
+
// Saving must not resend the stale local value.
|
|
299
|
+
model.save_changes();
|
|
300
|
+
expect(mockComm.sendUpdate).not.toHaveBeenCalled();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("should drop a pending local write the kernel pushed the same value for", () => {
|
|
304
|
+
const callback = vi.fn();
|
|
305
|
+
model.on("change:foo", callback);
|
|
306
|
+
|
|
307
|
+
// Local write, not yet saved...
|
|
308
|
+
model.set("foo", "agreed");
|
|
309
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
310
|
+
|
|
311
|
+
// ...then the kernel independently pushes the same value.
|
|
312
|
+
getMarimoInternal(model).updateAndEmitDiffs({
|
|
313
|
+
foo: "agreed",
|
|
314
|
+
bar: 123,
|
|
315
|
+
});
|
|
316
|
+
expect(callback).toHaveBeenCalledTimes(1); // no spurious re-emit
|
|
317
|
+
|
|
318
|
+
model.save_changes();
|
|
319
|
+
expect(mockComm.sendUpdate).not.toHaveBeenCalled();
|
|
320
|
+
});
|
|
266
321
|
});
|
|
267
322
|
|
|
268
323
|
describe("emitCustomMessage", () => {
|
|
@@ -53,7 +53,8 @@ export function getMarimoInternal<T extends ModelState>(
|
|
|
53
53
|
|
|
54
54
|
export class Model<T extends ModelState> implements AnyModel<T> {
|
|
55
55
|
#ANY_CHANGE_EVENT = "change";
|
|
56
|
-
|
|
56
|
+
/** Keys last written by the widget that the kernel hasn't seen yet. */
|
|
57
|
+
#dirtyFields: Set<keyof T>;
|
|
57
58
|
#data: T;
|
|
58
59
|
#comm: MarimoComm<T>;
|
|
59
60
|
#listeners: Record<string, Set<EventHandler> | undefined> = {};
|
|
@@ -70,7 +71,7 @@ export class Model<T extends ModelState> implements AnyModel<T> {
|
|
|
70
71
|
constructor(data: T, comm: MarimoComm<T>, signal?: AbortSignal) {
|
|
71
72
|
this.#data = data;
|
|
72
73
|
this.#comm = comm;
|
|
73
|
-
this.#dirtyFields = new
|
|
74
|
+
this.#dirtyFields = new Set();
|
|
74
75
|
if (signal) {
|
|
75
76
|
signal.addEventListener("abort", () => {
|
|
76
77
|
Logger.debug("[Model] Signal aborted, clearing all listeners");
|
|
@@ -140,7 +141,7 @@ export class Model<T extends ModelState> implements AnyModel<T> {
|
|
|
140
141
|
|
|
141
142
|
set<K extends keyof T>(key: K, value: T[K]): void {
|
|
142
143
|
this.#data = { ...this.#data, [key]: value };
|
|
143
|
-
this.#dirtyFields.
|
|
144
|
+
this.#dirtyFields.add(key);
|
|
144
145
|
this.#emit(`change:${key as K & string}`, value);
|
|
145
146
|
this.#emitAnyChange();
|
|
146
147
|
}
|
|
@@ -151,7 +152,7 @@ export class Model<T extends ModelState> implements AnyModel<T> {
|
|
|
151
152
|
}
|
|
152
153
|
// Only send the dirty fields, not the entire state.
|
|
153
154
|
const partialData = Object.fromEntries(
|
|
154
|
-
this.#dirtyFields.
|
|
155
|
+
[...this.#dirtyFields].map((key) => [key, this.#data[key]]),
|
|
155
156
|
) as Partial<T>;
|
|
156
157
|
|
|
157
158
|
// Clear the dirty fields to avoid sending again.
|
|
@@ -204,6 +205,15 @@ export class Model<T extends ModelState> implements AnyModel<T> {
|
|
|
204
205
|
}
|
|
205
206
|
}
|
|
206
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Apply a kernel-originated state update.
|
|
210
|
+
*
|
|
211
|
+
* Unlike `set()`, this never marks fields dirty — the kernel already
|
|
212
|
+
* has these values, so `save_changes()` must not echo them back.
|
|
213
|
+
*
|
|
214
|
+
* The kernel wins conflicts: a pending unsaved widget write to the same
|
|
215
|
+
* key is dropped, not replayed on top.
|
|
216
|
+
*/
|
|
207
217
|
#updateAndEmitDiffs(value: T) {
|
|
208
218
|
if (value == null) {
|
|
209
219
|
return;
|
|
@@ -211,9 +221,14 @@ export class Model<T extends ModelState> implements AnyModel<T> {
|
|
|
211
221
|
|
|
212
222
|
Object.keys(value).forEach((key) => {
|
|
213
223
|
const k = key as keyof T;
|
|
224
|
+
// Unconditional: guarding on inequality would leave a same-valued
|
|
225
|
+
// pending write dirty, and echo it back on save.
|
|
226
|
+
this.#dirtyFields.delete(k);
|
|
214
227
|
// Shallow equal since these can be large objects
|
|
215
228
|
if (this.#data[k] !== value[k]) {
|
|
216
|
-
this
|
|
229
|
+
this.#data = { ...this.#data, [k]: value[k] };
|
|
230
|
+
this.#emit(`change:${k as keyof T & string}`, value[k]);
|
|
231
|
+
this.#emitAnyChange();
|
|
217
232
|
}
|
|
218
233
|
});
|
|
219
234
|
}
|
|
@@ -749,14 +749,8 @@ exports[`renderZodSchema > should render a form group_by 1`] = `
|
|
|
749
749
|
class="flex flex-col gap-1"
|
|
750
750
|
>
|
|
751
751
|
<div
|
|
752
|
-
class="flex flex-row
|
|
752
|
+
class="flex flex-row gap-2 items-center"
|
|
753
753
|
>
|
|
754
|
-
<label
|
|
755
|
-
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
756
|
-
for="_r_1u_-form-item"
|
|
757
|
-
>
|
|
758
|
-
Drop N/A
|
|
759
|
-
</label>
|
|
760
754
|
<button
|
|
761
755
|
aria-checked="false"
|
|
762
756
|
aria-describedby="_r_1u_-form-item-description"
|
|
@@ -769,6 +763,16 @@ exports[`renderZodSchema > should render a form group_by 1`] = `
|
|
|
769
763
|
type="button"
|
|
770
764
|
value="on"
|
|
771
765
|
/>
|
|
766
|
+
<div
|
|
767
|
+
class="flex flex-col gap-1"
|
|
768
|
+
>
|
|
769
|
+
<label
|
|
770
|
+
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
771
|
+
for="_r_1u_-form-item"
|
|
772
|
+
>
|
|
773
|
+
Drop N/A
|
|
774
|
+
</label>
|
|
775
|
+
</div>
|
|
772
776
|
</div>
|
|
773
777
|
</div>
|
|
774
778
|
</div>
|
|
@@ -1252,14 +1256,8 @@ exports[`renderZodSchema > should render a form sample_rows 1`] = `
|
|
|
1252
1256
|
class="flex flex-col gap-1"
|
|
1253
1257
|
>
|
|
1254
1258
|
<div
|
|
1255
|
-
class="flex flex-row
|
|
1259
|
+
class="flex flex-row gap-2 items-center"
|
|
1256
1260
|
>
|
|
1257
|
-
<label
|
|
1258
|
-
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
1259
|
-
for="_r_2n_-form-item"
|
|
1260
|
-
>
|
|
1261
|
-
Sample with replacement
|
|
1262
|
-
</label>
|
|
1263
1261
|
<button
|
|
1264
1262
|
aria-checked="false"
|
|
1265
1263
|
aria-describedby="_r_2n_-form-item-description"
|
|
@@ -1272,6 +1270,16 @@ exports[`renderZodSchema > should render a form sample_rows 1`] = `
|
|
|
1272
1270
|
type="button"
|
|
1273
1271
|
value="on"
|
|
1274
1272
|
/>
|
|
1273
|
+
<div
|
|
1274
|
+
class="flex flex-col gap-1"
|
|
1275
|
+
>
|
|
1276
|
+
<label
|
|
1277
|
+
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
1278
|
+
for="_r_2n_-form-item"
|
|
1279
|
+
>
|
|
1280
|
+
Sample with replacement
|
|
1281
|
+
</label>
|
|
1282
|
+
</div>
|
|
1275
1283
|
</div>
|
|
1276
1284
|
</div>
|
|
1277
1285
|
</div>
|
|
@@ -1468,14 +1476,8 @@ exports[`renderZodSchema > should render a form sort_column 1`] = `
|
|
|
1468
1476
|
class="flex flex-col gap-1"
|
|
1469
1477
|
>
|
|
1470
1478
|
<div
|
|
1471
|
-
class="flex flex-row
|
|
1479
|
+
class="flex flex-row gap-2 items-center"
|
|
1472
1480
|
>
|
|
1473
|
-
<label
|
|
1474
|
-
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
1475
|
-
for="_r_1f_-form-item"
|
|
1476
|
-
>
|
|
1477
|
-
Ascending
|
|
1478
|
-
</label>
|
|
1479
1481
|
<button
|
|
1480
1482
|
aria-checked="false"
|
|
1481
1483
|
aria-describedby="_r_1f_-form-item-description"
|
|
@@ -1488,6 +1490,16 @@ exports[`renderZodSchema > should render a form sort_column 1`] = `
|
|
|
1488
1490
|
type="button"
|
|
1489
1491
|
value="on"
|
|
1490
1492
|
/>
|
|
1493
|
+
<div
|
|
1494
|
+
class="flex flex-col gap-1"
|
|
1495
|
+
>
|
|
1496
|
+
<label
|
|
1497
|
+
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
1498
|
+
for="_r_1f_-form-item"
|
|
1499
|
+
>
|
|
1500
|
+
Ascending
|
|
1501
|
+
</label>
|
|
1502
|
+
</div>
|
|
1491
1503
|
</div>
|
|
1492
1504
|
</div>
|
|
1493
1505
|
</div>
|
|
@@ -411,9 +411,12 @@ const ColumnFilterForm = <T extends FieldValues>({
|
|
|
411
411
|
|
|
412
412
|
const columnRenderer = columnIdRenderer<T>();
|
|
413
413
|
const children = [
|
|
414
|
-
renderZodSchema(
|
|
415
|
-
|
|
416
|
-
|
|
414
|
+
renderZodSchema({
|
|
415
|
+
schema: columnIdSchema,
|
|
416
|
+
form,
|
|
417
|
+
path: `${path}.column_id` as Path<T>,
|
|
418
|
+
renderers: [columnRenderer],
|
|
419
|
+
}),
|
|
417
420
|
];
|
|
418
421
|
|
|
419
422
|
// When column ID changes, get the new dtype and reset the operator
|
|
@@ -486,12 +489,12 @@ const ColumnFilterForm = <T extends FieldValues>({
|
|
|
486
489
|
if (operandSchemas.length === 1) {
|
|
487
490
|
children.push(
|
|
488
491
|
<React.Fragment key="value">
|
|
489
|
-
{renderZodSchema(
|
|
490
|
-
operandSchemas[0],
|
|
492
|
+
{renderZodSchema({
|
|
493
|
+
schema: operandSchemas[0],
|
|
491
494
|
form,
|
|
492
|
-
`${path}.value` as Path<T>,
|
|
493
|
-
[],
|
|
494
|
-
)}
|
|
495
|
+
path: `${path}.value` as Path<T>,
|
|
496
|
+
renderers: [],
|
|
497
|
+
})}
|
|
495
498
|
</React.Fragment>,
|
|
496
499
|
);
|
|
497
500
|
}
|