@marimo-team/islands 0.23.10-dev9 → 0.23.10
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-CyV83R2m.js → ConnectedDataExplorerComponent-Z3RP7Vmm.js} +31 -31
- package/dist/{ErrorBoundary-rULOrC_p.js → ErrorBoundary-DpbaKVv7.js} +1 -1
- package/dist/{any-language-editor-CiES2a2h.js → any-language-editor-CAgFD4Kd.js} +20 -20
- package/dist/assets/__vite-browser-external-eshhtsgZ.js +1 -0
- package/dist/assets/worker-CC0Oul9k.js +73 -0
- package/dist/{chat-ui-K84W5ecY.js → chat-ui-C4-GMW9y.js} +77 -77
- package/dist/{check-DTbrK0zt.js → check-BCaJeT-J.js} +1 -1
- package/dist/{chunk-5FQGJX7Z-BNjes6Yx.js → chunk-5FQGJX7Z-BbqSm5gU.js} +1227 -1222
- package/dist/{code-block-37QAKDTI-m92Yc8pv.js → code-block-37QAKDTI-O6IH6wAi.js} +1 -1
- package/dist/{code-visibility-v6ucEVUg.js → code-visibility-DrFozVlm.js} +1943 -1223
- package/dist/{copy-5jQ_kGE1.js → copy-UqRYxiOg.js} +1 -1
- package/dist/dist-7QfXoMdB.js +5 -0
- package/dist/{dist-DgnE8F-r.js → dist-A2846XWO.js} +1 -1
- package/dist/dist-BEXXyZig.js +5 -0
- package/dist/{dist-B3pZ0Ab6.js → dist-BR_gyG9L.js} +3 -3
- package/dist/{dist-CcXxepx6.js → dist-BSAt6RhH.js} +27 -27
- package/dist/{dist-Bde4a2kU.js → dist-BY018Paw.js} +8 -8
- package/dist/dist-BYj57OV4.js +5 -0
- package/dist/{dist-CUCNs1ja.js → dist-BaoDKvdy.js} +2 -2
- package/dist/{dist-Cy1WxgBD.js → dist-Bf7SHuNp.js} +5 -5
- package/dist/{dist-Bz_sYWbr.js → dist-Bk75fBZA.js} +2 -2
- package/dist/dist-BlSvQzNr.js +5 -0
- package/dist/{dist-C5VC_yzu.js → dist-BzEzfugY.js} +1 -1
- package/dist/dist-CCBlxAgS.js +8 -0
- package/dist/dist-CIDTVIUf.js +5 -0
- package/dist/{dist-CLUtPrdy.js → dist-CIYBwstr.js} +1 -1
- package/dist/{dist-BotSqB48.js → dist-C_Y3oV3C.js} +12 -12
- package/dist/{dist-BTfv03uy.js → dist-CcWX6tmx.js} +2 -2
- package/dist/{dist-BhM8gdSO.js → dist-CoXAujgg.js} +4 -4
- package/dist/{dist-4j4c7bjm.js → dist-CpxNdDkw.js} +3 -3
- package/dist/dist-CqQyhAM8.js +8 -0
- package/dist/dist-CwRu2Xzh.js +5 -0
- package/dist/{dist-BcuoonNH.js → dist-CxJDU6Bh.js} +9 -9
- package/dist/{dist-DxvORzUR.js → dist-D-W5ny5a.js} +8 -8
- package/dist/dist-D8CDTVgf.js +6 -0
- package/dist/dist-D8DNB0nO.js +8 -0
- package/dist/dist-DL6N_q-A.js +5 -0
- package/dist/{dist-BbbIBDiQ.js → dist-DMjWuVs8.js} +1 -1
- package/dist/dist-DOFbNV_b.js +8 -0
- package/dist/dist-DPrYzMY0.js +6 -0
- package/dist/{dist-h2c8sZvT.js → dist-DZORgqKY.js} +1 -1
- package/dist/{dist-B3P2fFpz.js → dist-DZo4nSS0.js} +14 -14
- package/dist/{dist-D4CewLk6.js → dist-Dax--nl9.js} +1 -1
- package/dist/{dist-DRfcqpxJ.js → dist-DgGbNavJ.js} +2 -2
- package/dist/{dist-C1BYNeCR.js → dist-Dk6PV_d3.js} +10 -10
- package/dist/{dist-fQ0ViXGs.js → dist-Dv_Y15yk.js} +107 -107
- package/dist/{dist-Bfwsv11D.js → dist-DyyjKEYf.js} +2 -2
- package/dist/{dist-p2qyWijU.js → dist-GZXUmt0b.js} +2 -2
- package/dist/{dist-CLJWPTX2.js → dist-LTU8Hdvn.js} +3 -3
- package/dist/{dist-DqAWR3CS.js → dist-M9Vag9Y0.js} +20 -20
- package/dist/{dist-DNdhYsgW.js → dist-U4F-tbMs.js} +79 -62
- package/dist/{dist-RqXTaiir.js → dist-abid3KgM.js} +11 -11
- package/dist/dist-cdmMjgsn.js +5 -0
- package/dist/dist-hT4QzYX-.js +1247 -0
- package/dist/{dist-luvabDEB.js → dist-t9Kf7xqC.js} +2 -2
- package/dist/{error-banner-5bz0L9hS.js → error-banner-CJXYJ6Sb.js} +11 -6
- package/dist/esm-BaH2eg5-.js +1171 -0
- package/dist/{esm-Duie8iU-.js → esm-ga2Bf3O2.js} +43 -43
- package/dist/{extends-CkydH1Q5.js → extends-D_hDsj6R.js} +4 -4
- package/dist/files/wasm-intro.py +11 -8
- package/dist/{formats-DHxc-FdY.js → formats-C4wO47tk.js} +1 -1
- package/dist/{glide-data-editor-CgIxTzAP.js → glide-data-editor-BPkCPs7L.js} +255 -255
- package/dist/{html-to-image-B93KtAVY.js → html-to-image-BwfcJJxA.js} +2412 -2282
- package/dist/{input-_2sjvfne.js → input-OdWHkobi.js} +263 -245
- package/dist/{label-LWtdw5i8.js → label-CC4ytI1X.js} +1 -1
- package/dist/main.js +7630 -7558
- package/dist/{mermaid-4DMBBIKO-DIdL224_.js → mermaid-4DMBBIKO-Eg3D4FVz.js} +1 -1
- package/dist/{mermaid-lXOw5Py9.js → mermaid-BotvIKg9.js} +4 -4
- package/dist/{process-output-BOXi9fnS.js → process-output-DsloEDna.js} +3 -3
- package/dist/{reveal-component-gRcjQbNL.js → reveal-component-3YQBHWuD.js} +698 -620
- package/dist/{spec-B96zNUEA.js → spec-X7FwLJni.js} +4 -4
- package/dist/{strings-Bu3vlb6W.js → strings-J57tzLr3.js} +47 -46
- package/dist/style.css +1 -1
- package/dist/{toDate-x-WRDCH7.js → toDate-d8RCRrRd.js} +2 -2
- package/dist/{tooltip-C5FYOpQc.js → tooltip-DpcyNkQ2.js} +2 -2
- package/dist/{types-CVvp1fKr.js → types-ChtMFmZ2.js} +1 -1
- package/dist/{useAsyncData-iRgKDT5s.js → useAsyncData-PonK__yh.js} +1 -1
- package/dist/{useDateFormatter-BA4FCquG.js → useDateFormatter-QB-3MpYr.js} +2 -2
- package/dist/{useDeepCompareMemoize-CkQ57VS2.js → useDeepCompareMemoize-D3NGWke6.js} +1 -1
- package/dist/{useLifecycle-BBO9PIph.js → useLifecycle-00mO3OSS.js} +2 -2
- package/dist/{useTheme-DHIrRQOe.js → useTheme-DEhDzATN.js} +1 -1
- package/dist/{vega-component-Dq-SH463.js → vega-component-DGPUhbDs.js} +8 -8
- package/dist/{zod-CoBiJ5v4.js → zod-aLSua2NL.js} +24 -23
- package/package.json +4 -3
- package/src/__tests__/setup.ts +16 -0
- package/src/components/chat/chat-display.tsx +2 -2
- package/src/components/data-table/TableBottomBar.tsx +1 -15
- package/src/components/data-table/TableTopBar.tsx +8 -13
- package/src/components/data-table/__tests__/TableBottomBar.test.tsx +6 -12
- package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +227 -0
- package/src/components/data-table/column-visibility-dropdown.tsx +204 -0
- package/src/components/data-table/data-table.tsx +1 -1
- package/src/components/data-table/filter-by-values-picker.tsx +39 -17
- package/src/components/data-table/filter-pills.tsx +1 -1
- package/src/components/datasources/__tests__/filter-empty.test.ts +183 -0
- package/src/components/datasources/datasources.tsx +92 -3
- package/src/components/editor/cell/code/cell-editor.tsx +70 -32
- package/src/components/editor/cell/code/code-placeholder.css +18 -0
- package/src/components/editor/cell/code/code-placeholder.tsx +47 -0
- package/src/components/editor/cell/code/language-toggle.tsx +7 -1
- package/src/components/editor/chrome/wrapper/app-chrome.tsx +97 -52
- package/src/components/editor/chrome/wrapper/lazy-panels.ts +91 -0
- package/src/components/editor/chrome/wrapper/sidebar.tsx +2 -0
- package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +9 -2
- package/src/components/editor/connections/storage/__tests__/as-code.test.ts +62 -27
- package/src/components/editor/connections/storage/add-storage-form.tsx +5 -1
- package/src/components/editor/connections/storage/as-code.ts +20 -6
- package/src/components/editor/documentation.css +35 -0
- package/src/components/editor/file-tree/file-explorer.tsx +8 -18
- package/src/components/editor/file-tree/tree-actions.tsx +46 -1
- package/src/components/editor/notebook-cell.tsx +3 -3
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +20 -0
- package/src/components/editor/renderers/slides-layout/types.ts +1 -0
- package/src/components/markdown/__tests__/markdown-renderer.test.tsx +43 -0
- package/src/components/markdown/markdown-renderer.tsx +24 -1
- package/src/components/slides/__tests__/minimap-actions.test.tsx +166 -0
- package/src/components/slides/__tests__/reveal-component.test.ts +425 -0
- package/src/components/slides/minimap.tsx +127 -10
- package/src/components/slides/reveal-component.tsx +287 -61
- package/src/components/slides/slide-cell-view.tsx +26 -2
- package/src/components/slides/slide-form.tsx +26 -4
- package/src/components/storage/__tests__/storage-inspector.test.ts +180 -0
- package/src/components/storage/storage-inspector.tsx +535 -81
- package/src/components/ui/combobox.tsx +51 -32
- package/src/components/ui/number-field.tsx +5 -1
- package/src/components/ui/reorderable-list.tsx +13 -0
- package/src/components/ui/select-core/__tests__/use-select-list.test.ts +294 -0
- package/src/components/ui/select-core/__tests__/utils.test.ts +222 -0
- package/src/components/ui/select-core/index.ts +16 -0
- package/src/components/ui/select-core/option-row.tsx +33 -0
- package/src/components/ui/select-core/render-slot.ts +20 -0
- package/src/components/ui/select-core/select-list.tsx +248 -0
- package/src/components/ui/select-core/types.ts +44 -0
- package/src/components/ui/select-core/use-select-list.ts +347 -0
- package/src/components/ui/select-core/utils.ts +121 -0
- package/src/components/ui/toast.tsx +1 -0
- package/src/core/cells/__tests__/document-changes.test.ts +2 -2
- package/src/core/cells/__tests__/focus.test.ts +45 -2
- package/src/core/cells/focus.ts +1 -1
- package/src/core/cells/scrollCellIntoView.ts +4 -0
- package/src/core/cells/utils.ts +1 -1
- package/src/core/codemirror/__tests__/editor-mount-scheduler.test.ts +143 -0
- package/src/core/codemirror/copilot/getCodes.ts +3 -1
- package/src/core/codemirror/editor-mount-scheduler.ts +101 -0
- package/src/core/codemirror/language/languages/python.ts +2 -0
- package/src/core/codemirror/language/languages/sql/utils.ts +3 -1
- package/src/core/codemirror/lsp/__tests__/markdown-renderer.test.ts +41 -0
- package/src/core/codemirror/lsp/markdown-renderer.ts +59 -0
- package/src/core/datasets/data-source-connections.ts +2 -0
- package/src/core/errors/errors.ts +1 -1
- package/src/core/storage/__tests__/state.test.ts +49 -0
- package/src/core/storage/__tests__/useStorageEntries.test.tsx +199 -3
- package/src/core/storage/state.ts +124 -13
- package/src/core/storage/types.ts +6 -0
- package/src/core/wasm/worker/bootstrap.ts +12 -4
- package/src/plugins/core/__test__/registerReactComponent.test.ts +96 -0
- package/src/plugins/core/registerReactComponent.tsx +42 -10
- package/src/plugins/impl/MultiselectPlugin.tsx +19 -142
- package/src/plugins/impl/SearchableSelect.tsx +16 -97
- package/src/plugins/impl/SliderPlugin.tsx +132 -18
- package/src/plugins/impl/__tests__/DropdownPlugin.test.tsx +5 -2
- package/src/plugins/impl/__tests__/MultiSelectPlugin.test.ts +1 -1
- package/src/plugins/impl/__tests__/SliderPlugin.test.tsx +278 -8
- package/src/utils/__tests__/numbers.test.ts +20 -0
- package/src/utils/errors.ts +9 -0
- package/src/utils/lazy.ts +6 -1
- package/src/utils/numbers.ts +27 -0
- package/src/utils/schedule-task.ts +71 -0
- package/dist/assets/__vite-browser-external-Ci2ZQfXU.js +0 -1
- package/dist/assets/worker-ip3AI_sN.js +0 -73
- package/dist/dist-0Fif7jnk.js +0 -5
- package/dist/dist-B5h_9sHB.js +0 -6
- package/dist/dist-B9M6R5ye.js +0 -5
- package/dist/dist-BCt3tnck.js +0 -8
- package/dist/dist-BUIJwMwn.js +0 -8
- package/dist/dist-BpquMd3k.js +0 -5
- package/dist/dist-BzJsqYfz.js +0 -5
- package/dist/dist-CA5ELXAf.js +0 -6
- package/dist/dist-CLBRs6Uv.js +0 -5
- package/dist/dist-CStVCMbq.js +0 -5
- package/dist/dist-CZRIEY3Y.js +0 -8
- package/dist/dist-CuUHbFD0.js +0 -5
- package/dist/dist-DV7Iabxb.js +0 -8
- package/dist/dist-DhHh0jLg.js +0 -1247
- package/dist/dist-DuEeHMvL.js +0 -5
- package/dist/esm-CCuYCd3R.js +0 -1171
- package/src/plugins/impl/multiselectFilterFn.tsx +0 -22
- /package/src/components/{data-table → ui}/value-chips.tsx +0 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import type {
|
|
5
|
+
Database,
|
|
6
|
+
DatabaseSchema,
|
|
7
|
+
DataTable,
|
|
8
|
+
} from "@/core/kernel/messages";
|
|
9
|
+
import { filterEmptyDatabases } from "../datasources";
|
|
10
|
+
|
|
11
|
+
function makeTable(name: string): DataTable {
|
|
12
|
+
return {
|
|
13
|
+
name,
|
|
14
|
+
columns: [],
|
|
15
|
+
source: "memory",
|
|
16
|
+
source_type: "local",
|
|
17
|
+
type: "table",
|
|
18
|
+
engine: null,
|
|
19
|
+
indexes: null,
|
|
20
|
+
num_columns: null,
|
|
21
|
+
num_rows: null,
|
|
22
|
+
variable_name: null,
|
|
23
|
+
primary_keys: null,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function makeSchema(opts: {
|
|
28
|
+
name: string;
|
|
29
|
+
tables: DataTable[];
|
|
30
|
+
tables_resolved?: boolean;
|
|
31
|
+
}): DatabaseSchema {
|
|
32
|
+
return {
|
|
33
|
+
name: opts.name,
|
|
34
|
+
tables: opts.tables,
|
|
35
|
+
tables_resolved: opts.tables_resolved ?? true,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makeDatabase(
|
|
40
|
+
name: string,
|
|
41
|
+
schemas: DatabaseSchema[],
|
|
42
|
+
schemas_resolved = true,
|
|
43
|
+
): Database {
|
|
44
|
+
return {
|
|
45
|
+
name,
|
|
46
|
+
dialect: "duckdb",
|
|
47
|
+
schemas,
|
|
48
|
+
schemas_resolved,
|
|
49
|
+
engine: null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe("filterEmptyDatabases", () => {
|
|
54
|
+
it("hides schemas whose tables are resolved and empty", () => {
|
|
55
|
+
const databases = [
|
|
56
|
+
makeDatabase("memory", [
|
|
57
|
+
makeSchema({ name: "main", tables: [makeTable("t1")] }),
|
|
58
|
+
makeSchema({ name: "empty_schema", tables: [] }),
|
|
59
|
+
]),
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
expect(filterEmptyDatabases(databases)).toEqual([
|
|
63
|
+
makeDatabase("memory", [
|
|
64
|
+
makeSchema({ name: "main", tables: [makeTable("t1")] }),
|
|
65
|
+
]),
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("preserves databases whose schemas have not been resolved yet (lazy state)", () => {
|
|
70
|
+
const databases = [
|
|
71
|
+
makeDatabase("not_loaded_yet", [], /* schemas_resolved */ false),
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
expect(filterEmptyDatabases(databases)).toEqual([
|
|
75
|
+
makeDatabase("not_loaded_yet", [], false),
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("hides databases that have been resolved as empty", () => {
|
|
80
|
+
const databases = [
|
|
81
|
+
makeDatabase("really_empty", [], /* schemas_resolved */ true),
|
|
82
|
+
makeDatabase("has_tables", [
|
|
83
|
+
makeSchema({ name: "main", tables: [makeTable("t1")] }),
|
|
84
|
+
]),
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
expect(filterEmptyDatabases(databases)).toEqual([
|
|
88
|
+
makeDatabase("has_tables", [
|
|
89
|
+
makeSchema({ name: "main", tables: [makeTable("t1")] }),
|
|
90
|
+
]),
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("hides databases whose schemas all filtered to empty", () => {
|
|
95
|
+
const databases = [
|
|
96
|
+
makeDatabase("only_empty", [
|
|
97
|
+
makeSchema({ name: "a", tables: [] }),
|
|
98
|
+
makeSchema({ name: "b", tables: [] }),
|
|
99
|
+
]),
|
|
100
|
+
makeDatabase("has_tables", [
|
|
101
|
+
makeSchema({ name: "main", tables: [makeTable("t1")] }),
|
|
102
|
+
]),
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
expect(filterEmptyDatabases(databases)).toEqual([
|
|
106
|
+
makeDatabase("has_tables", [
|
|
107
|
+
makeSchema({ name: "main", tables: [makeTable("t1")] }),
|
|
108
|
+
]),
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("treats missing schemas_resolved as resolved (backward compatible)", () => {
|
|
113
|
+
const databases = [
|
|
114
|
+
{ name: "memory", dialect: "duckdb", schemas: [], engine: null },
|
|
115
|
+
] as Database[];
|
|
116
|
+
|
|
117
|
+
expect(filterEmptyDatabases(databases)).toEqual([]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("preserves schemas whose tables have not been resolved yet", () => {
|
|
121
|
+
const databases = [
|
|
122
|
+
makeDatabase("snowflake_db", [
|
|
123
|
+
// include_tables=False was used; the schema is not actually empty,
|
|
124
|
+
// tables will be fetched lazily on expand.
|
|
125
|
+
makeSchema({ name: "public", tables: [], tables_resolved: false }),
|
|
126
|
+
makeSchema({ name: "audit", tables: [], tables_resolved: false }),
|
|
127
|
+
makeSchema({
|
|
128
|
+
name: "really_empty",
|
|
129
|
+
tables: [],
|
|
130
|
+
tables_resolved: true,
|
|
131
|
+
}),
|
|
132
|
+
]),
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
expect(filterEmptyDatabases(databases)).toEqual([
|
|
136
|
+
makeDatabase("snowflake_db", [
|
|
137
|
+
makeSchema({ name: "public", tables: [], tables_resolved: false }),
|
|
138
|
+
makeSchema({ name: "audit", tables: [], tables_resolved: false }),
|
|
139
|
+
]),
|
|
140
|
+
]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("treats missing tables_resolved as resolved (backward compatible)", () => {
|
|
144
|
+
// Older payloads predating the new flag may omit it; default semantics
|
|
145
|
+
// treat the schema as resolved/authoritative.
|
|
146
|
+
const databases = [
|
|
147
|
+
makeDatabase("memory", [
|
|
148
|
+
{ name: "main", tables: [makeTable("t1")] },
|
|
149
|
+
{ name: "empty_schema", tables: [] },
|
|
150
|
+
] as DatabaseSchema[]),
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
expect(filterEmptyDatabases(databases)).toEqual([
|
|
154
|
+
makeDatabase("memory", [
|
|
155
|
+
{ name: "main", tables: [makeTable("t1")] },
|
|
156
|
+
] as DatabaseSchema[]),
|
|
157
|
+
]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("returns the same reference when nothing was filtered", () => {
|
|
161
|
+
const databases = [
|
|
162
|
+
makeDatabase("memory", [
|
|
163
|
+
makeSchema({ name: "main", tables: [makeTable("t1")] }),
|
|
164
|
+
]),
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
expect(filterEmptyDatabases(databases)).toBe(databases);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("does not mutate the input", () => {
|
|
171
|
+
const databases = [
|
|
172
|
+
makeDatabase("memory", [
|
|
173
|
+
makeSchema({ name: "main", tables: [makeTable("t1")] }),
|
|
174
|
+
makeSchema({ name: "empty_schema", tables: [] }),
|
|
175
|
+
]),
|
|
176
|
+
];
|
|
177
|
+
const snapshot = JSON.parse(JSON.stringify(databases));
|
|
178
|
+
|
|
179
|
+
filterEmptyDatabases(databases);
|
|
180
|
+
|
|
181
|
+
expect(databases).toEqual(snapshot);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { CommandList } from "cmdk";
|
|
4
|
-
import { atom, useAtomValue, useSetAtom } from "jotai";
|
|
4
|
+
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
|
|
5
|
+
import { atomWithStorage } from "jotai/utils";
|
|
5
6
|
import { PlusIcon, PlusSquareIcon, XIcon } from "lucide-react";
|
|
6
7
|
import React from "react";
|
|
7
8
|
import { dbDisplayName } from "@/components/databases/display";
|
|
8
9
|
import { EngineVariable } from "@/components/databases/engine-variable";
|
|
9
10
|
import { DatabaseLogo } from "@/components/databases/icon";
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
RefreshIconButton,
|
|
13
|
+
VisibilityToggleButton,
|
|
14
|
+
} from "@/components/editor/file-tree/tree-actions";
|
|
11
15
|
import { CopyClipboardIcon } from "@/components/icons/copy-icon";
|
|
12
16
|
import { Button } from "@/components/ui/button";
|
|
13
17
|
import { Command, CommandInput, CommandItem } from "@/components/ui/command";
|
|
@@ -52,6 +56,7 @@ import { sortBy } from "@/utils/arrays";
|
|
|
52
56
|
import { logNever } from "@/utils/assertNever";
|
|
53
57
|
import { cn } from "@/utils/cn";
|
|
54
58
|
import { Events } from "@/utils/events";
|
|
59
|
+
import { jotaiJsonStorage } from "@/utils/storage/jotai";
|
|
55
60
|
import {
|
|
56
61
|
DatabaseIcon,
|
|
57
62
|
SchemaIcon,
|
|
@@ -116,6 +121,63 @@ const sortedTablesAtom = atom((get) => {
|
|
|
116
121
|
});
|
|
117
122
|
});
|
|
118
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Whether to hide empty schemas and databases (those with no tables) in the
|
|
126
|
+
* datasources panel.
|
|
127
|
+
*/
|
|
128
|
+
export const hideEmptyDatasourcesAtom = atomWithStorage<boolean>(
|
|
129
|
+
"marimo:datasources:hideEmpty",
|
|
130
|
+
false,
|
|
131
|
+
jotaiJsonStorage,
|
|
132
|
+
{ getOnInit: true },
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
function isKnownEmptySchema(schema: DatabaseSchema): boolean {
|
|
136
|
+
return schema.tables_resolved !== false && schema.tables.length === 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Apply the "hide empty" filter to a connection's databases.
|
|
141
|
+
*
|
|
142
|
+
* - Schemas with confirmed-empty table lists are hidden.
|
|
143
|
+
* - Databases are hidden when either (a) their schemas have been enumerated
|
|
144
|
+
* and the list is empty, or (b) every schema in them was hidden by the
|
|
145
|
+
* schema-level filter.
|
|
146
|
+
* - Databases / schemas whose contents haven't been resolved yet (deferred
|
|
147
|
+
* discovery — `schemas_resolved === false` or `tables_resolved === false`)
|
|
148
|
+
* are preserved so the user can expand them to trigger a fetch.
|
|
149
|
+
*/
|
|
150
|
+
export function filterEmptyDatabases(databases: Database[]): Database[] {
|
|
151
|
+
let changed = false;
|
|
152
|
+
const result: Database[] = [];
|
|
153
|
+
for (const database of databases) {
|
|
154
|
+
// Known-empty database: schema list was enumerated and is empty.
|
|
155
|
+
if (database.schemas_resolved !== false && database.schemas.length === 0) {
|
|
156
|
+
changed = true;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
// Deferred schema discovery — keep so the user can expand and load.
|
|
160
|
+
if (database.schemas.length === 0) {
|
|
161
|
+
result.push(database);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const visibleSchemas = database.schemas.filter(
|
|
165
|
+
(schema) => !isKnownEmptySchema(schema),
|
|
166
|
+
);
|
|
167
|
+
if (visibleSchemas.length === 0) {
|
|
168
|
+
changed = true;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (visibleSchemas.length === database.schemas.length) {
|
|
172
|
+
result.push(database);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
changed = true;
|
|
176
|
+
result.push({ ...database, schemas: visibleSchemas });
|
|
177
|
+
}
|
|
178
|
+
return changed ? result : databases;
|
|
179
|
+
}
|
|
180
|
+
|
|
119
181
|
/**
|
|
120
182
|
* This atom is used to get the data connections that are available to the user.
|
|
121
183
|
* It filters out the internal engines if it has no databases or if it has only the in-memory database and no schemas.
|
|
@@ -152,10 +214,27 @@ export const connectionsAtom = atom((get) => {
|
|
|
152
214
|
|
|
153
215
|
export const DataSources: React.FC = () => {
|
|
154
216
|
const [searchValue, setSearchValue] = React.useState<string>("");
|
|
217
|
+
const [hideEmpty, setHideEmpty] = useAtom(hideEmptyDatasourcesAtom);
|
|
155
218
|
|
|
156
219
|
const closeAllColumns = useSetAtom(closeAllColumnsAtom);
|
|
157
220
|
const tables = useAtomValue(sortedTablesAtom);
|
|
158
|
-
const
|
|
221
|
+
const rawConnections = useAtomValue(connectionsAtom);
|
|
222
|
+
|
|
223
|
+
const dataConnections = React.useMemo(() => {
|
|
224
|
+
if (!hideEmpty) {
|
|
225
|
+
return rawConnections;
|
|
226
|
+
}
|
|
227
|
+
let changed = false;
|
|
228
|
+
const filtered = rawConnections.map((connection) => {
|
|
229
|
+
const databases = filterEmptyDatabases(connection.databases);
|
|
230
|
+
if (databases === connection.databases) {
|
|
231
|
+
return connection;
|
|
232
|
+
}
|
|
233
|
+
changed = true;
|
|
234
|
+
return { ...connection, databases };
|
|
235
|
+
});
|
|
236
|
+
return changed ? filtered : rawConnections;
|
|
237
|
+
}, [rawConnections, hideEmpty]);
|
|
159
238
|
|
|
160
239
|
if (tables.length === 0 && dataConnections.length === 0) {
|
|
161
240
|
return (
|
|
@@ -204,6 +283,16 @@ export const DataSources: React.FC = () => {
|
|
|
204
283
|
</button>
|
|
205
284
|
)}
|
|
206
285
|
|
|
286
|
+
<VisibilityToggleButton
|
|
287
|
+
data-testid="datasources-hide-empty-button"
|
|
288
|
+
isVisible={!hideEmpty}
|
|
289
|
+
onToggle={() => setHideEmpty(!hideEmpty)}
|
|
290
|
+
showTooltip="Show empty schemas and databases"
|
|
291
|
+
hideTooltip="Hide empty schemas and databases"
|
|
292
|
+
size="sm"
|
|
293
|
+
className="px-2 rounded-none focus-visible:outline-hidden"
|
|
294
|
+
/>
|
|
295
|
+
|
|
207
296
|
<AddConnectionDialog>
|
|
208
297
|
<Button
|
|
209
298
|
variant="ghost"
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import { historyField } from "@codemirror/commands";
|
|
3
3
|
import { EditorState, StateEffect } from "@codemirror/state";
|
|
4
4
|
import { EditorView, ViewPlugin } from "@codemirror/view";
|
|
5
|
+
import { useIntersectionObserver } from "@uidotdev/usehooks";
|
|
5
6
|
import { useAtom, useAtomValue } from "jotai";
|
|
6
|
-
import React, { memo, useEffect, useMemo, useRef } from "react";
|
|
7
|
+
import React, { memo, useEffect, useMemo, useRef, useState } from "react";
|
|
7
8
|
import useEvent from "react-use-event-hook";
|
|
8
9
|
import { Button } from "@/components/ui/button";
|
|
9
10
|
import { DelayMount } from "@/components/utils/delay-mount";
|
|
@@ -14,6 +15,7 @@ import { usePendingDeleteService } from "@/core/cells/pending-delete-service";
|
|
|
14
15
|
import type { CellData, CellRuntimeState } from "@/core/cells/types";
|
|
15
16
|
import { setupCodeMirror } from "@/core/codemirror/cm";
|
|
16
17
|
import { acceptCompletionOnEnterAtom } from "@/core/codemirror/completion/accept-on-enter-atom";
|
|
18
|
+
import { editorMountScheduler } from "@/core/codemirror/editor-mount-scheduler";
|
|
17
19
|
import {
|
|
18
20
|
getInitialLanguageAdapter,
|
|
19
21
|
languageAdapterState,
|
|
@@ -45,6 +47,7 @@ import {
|
|
|
45
47
|
} from "../../navigation/navigation";
|
|
46
48
|
import { useDeleteCellCallback } from "../useDeleteCell";
|
|
47
49
|
import { useSplitCellCallback } from "../useSplitCell";
|
|
50
|
+
import { CodePlaceholder } from "./code-placeholder";
|
|
48
51
|
import { LanguageToggles } from "./language-toggle";
|
|
49
52
|
|
|
50
53
|
export interface CellEditorProps
|
|
@@ -405,38 +408,45 @@ const CellEditorInternal = ({
|
|
|
405
408
|
// Clear the serialized state so that we don't re-create the editor next time
|
|
406
409
|
cellActions.clearSerializedEditorState({ cellId });
|
|
407
410
|
});
|
|
411
|
+
const [isEditorMounted, setIsEditorMounted] = useState(
|
|
412
|
+
serializedEditorState !== null,
|
|
413
|
+
);
|
|
408
414
|
|
|
415
|
+
// Build the editor. Deserialize eagerly; otherwise queue an async build so
|
|
416
|
+
// large notebooks stay responsive while editors come online progressively.
|
|
409
417
|
useEffect(() => {
|
|
410
|
-
if (
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
} else {
|
|
414
|
-
// If the editor already exists, reconfigure it with the new extensions.
|
|
415
|
-
handleReconfigureEditor();
|
|
416
|
-
}
|
|
417
|
-
} else {
|
|
418
|
-
handleDeserializeEditor();
|
|
418
|
+
if (editorViewRef.current !== null) {
|
|
419
|
+
setIsEditorMounted(true);
|
|
420
|
+
return;
|
|
419
421
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
editorViewParentRef.current !== null
|
|
425
|
-
) {
|
|
426
|
-
// Always replace the children in case the editor view was re-created.
|
|
427
|
-
editorViewParentRef.current.replaceChildren(editorViewRef.current.dom);
|
|
422
|
+
if (serializedEditorState !== null) {
|
|
423
|
+
handleDeserializeEditor();
|
|
424
|
+
setIsEditorMounted(true);
|
|
425
|
+
return;
|
|
428
426
|
}
|
|
427
|
+
editorMountScheduler.request(cellId, () => {
|
|
428
|
+
if (editorViewRef.current !== null) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
handleInitializeEditor();
|
|
432
|
+
setIsEditorMounted(true);
|
|
433
|
+
});
|
|
434
|
+
return () => editorMountScheduler.cancel(cellId);
|
|
429
435
|
}, [
|
|
430
|
-
|
|
431
|
-
handleReconfigureEditor,
|
|
432
|
-
handleDeserializeEditor,
|
|
436
|
+
cellId,
|
|
433
437
|
editorViewRef,
|
|
434
|
-
|
|
438
|
+
handleDeserializeEditor,
|
|
439
|
+
handleInitializeEditor,
|
|
435
440
|
serializedEditorState,
|
|
436
|
-
// Props to trigger reconfiguration
|
|
437
|
-
extensions,
|
|
438
441
|
]);
|
|
439
442
|
|
|
443
|
+
useEffect(() => {
|
|
444
|
+
if (editorViewRef.current === null) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
handleReconfigureEditor();
|
|
448
|
+
}, [handleReconfigureEditor, extensions, editorViewRef]);
|
|
449
|
+
|
|
440
450
|
// Destroy the editor when the component is unmounted
|
|
441
451
|
useEffect(() => {
|
|
442
452
|
const ev = editorViewRef.current;
|
|
@@ -445,6 +455,26 @@ const CellEditorInternal = ({
|
|
|
445
455
|
};
|
|
446
456
|
}, [editorViewRef]);
|
|
447
457
|
|
|
458
|
+
// Prioritize building this cell's editor when it scrolls into view, so the
|
|
459
|
+
// visible region fills in first instead of waiting for the top-to-bottom
|
|
460
|
+
// queue. The margin builds cells just before they reach the viewport.
|
|
461
|
+
// uidotdev sets threshold default to 1. Set it 0 so cells are prioritized
|
|
462
|
+
// at 300px.
|
|
463
|
+
const [intersectionRef, intersection] = useIntersectionObserver({
|
|
464
|
+
rootMargin: "300px 0px",
|
|
465
|
+
threshold: 0,
|
|
466
|
+
});
|
|
467
|
+
useEffect(() => {
|
|
468
|
+
if (isEditorMounted) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (intersection?.isIntersecting) {
|
|
472
|
+
editorMountScheduler.prioritize(cellId);
|
|
473
|
+
} else {
|
|
474
|
+
editorMountScheduler.deprioritize(cellId);
|
|
475
|
+
}
|
|
476
|
+
}, [isEditorMounted, intersection?.isIntersecting, cellId]);
|
|
477
|
+
|
|
448
478
|
const navigationProps = useCellEditorNavigationProps(cellId, editorViewRef);
|
|
449
479
|
|
|
450
480
|
let editorClassName = "";
|
|
@@ -489,14 +519,22 @@ const CellEditorInternal = ({
|
|
|
489
519
|
runCell={handleRunCell}
|
|
490
520
|
outputArea={outputArea}
|
|
491
521
|
>
|
|
492
|
-
<div
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
522
|
+
<div
|
|
523
|
+
className="relative w-full"
|
|
524
|
+
ref={intersectionRef}
|
|
525
|
+
{...navigationProps}
|
|
526
|
+
>
|
|
527
|
+
{isEditorMounted ? (
|
|
528
|
+
<CellCodeMirrorEditor
|
|
529
|
+
className={editorClassName}
|
|
530
|
+
editorView={editorViewRef.current}
|
|
531
|
+
ref={editorViewParentRef}
|
|
532
|
+
hidden={hidden}
|
|
533
|
+
showHiddenCode={showHiddenCode}
|
|
534
|
+
/>
|
|
535
|
+
) : (
|
|
536
|
+
<CodePlaceholder code={code} className={editorClassName} />
|
|
537
|
+
)}
|
|
500
538
|
{!hidden && showLanguageToggles && (
|
|
501
539
|
<div className="absolute top-1 right-5">
|
|
502
540
|
<LanguageToggles
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
.mo-code-placeholder {
|
|
3
|
+
--mo-skeleton-bar: color-mix(in srgb, var(--muted), var(--foreground) 18%);
|
|
4
|
+
cursor: default;
|
|
5
|
+
padding: 4px 10px;
|
|
6
|
+
font-size: var(--cm-font-size, 0.875rem);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.mo-code-placeholder-line {
|
|
10
|
+
display: flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
height: 1.5em;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.mo-code-placeholder-bar {
|
|
16
|
+
height: 0.7em;
|
|
17
|
+
background-color: var(--mo-skeleton-bar);
|
|
18
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import "./code-placeholder.css";
|
|
3
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
4
|
+
import { cn } from "@/utils/cn";
|
|
5
|
+
|
|
6
|
+
interface CodePlaceholderProps {
|
|
7
|
+
code: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Bound the rendered bars so a very long cell does not create excessive nodes.
|
|
12
|
+
const MAX_LINES = 50;
|
|
13
|
+
const MIN_LINE_WIDTH_CH = 4;
|
|
14
|
+
const MAX_LINE_WIDTH_CH = 60;
|
|
15
|
+
|
|
16
|
+
function getLineWidthCh(length: number): number {
|
|
17
|
+
return Math.min(MAX_LINE_WIDTH_CH, Math.max(MIN_LINE_WIDTH_CH, length));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Stand-in shown while a cell's CodeMirror editor has not been built yet.
|
|
22
|
+
* one skeleton bar per code line, so the final render doesn't cause a big jump.
|
|
23
|
+
*/
|
|
24
|
+
export const CodePlaceholder = ({ code, className }: CodePlaceholderProps) => {
|
|
25
|
+
const lines = code.split("\n", MAX_LINES);
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
className={cn("cm", "mo-code-placeholder", className)}
|
|
29
|
+
data-testid="cell-editor-placeholder"
|
|
30
|
+
aria-hidden={true}
|
|
31
|
+
>
|
|
32
|
+
{lines.map((line, index) => {
|
|
33
|
+
const length = line.trimEnd().length;
|
|
34
|
+
return (
|
|
35
|
+
<div key={index} className="mo-code-placeholder-line">
|
|
36
|
+
{length > 0 && (
|
|
37
|
+
<Skeleton
|
|
38
|
+
className="mo-code-placeholder-bar"
|
|
39
|
+
style={{ width: `${getLineWidthCh(length)}ch` }}
|
|
40
|
+
/>
|
|
41
|
+
)}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
})}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
@@ -17,6 +17,11 @@ interface LanguageTogglesProps {
|
|
|
17
17
|
code: string;
|
|
18
18
|
currentLanguageAdapter: LanguageAdapter["type"] | undefined;
|
|
19
19
|
onAfterToggle: () => void;
|
|
20
|
+
/**
|
|
21
|
+
* Classes for the wrapper element. Defaults to the absolutely-positioned,
|
|
22
|
+
* hover-revealed placement used inside the notebook cell editor.
|
|
23
|
+
*/
|
|
24
|
+
className?: string;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
export const LanguageToggles: React.FC<LanguageTogglesProps> = ({
|
|
@@ -24,6 +29,7 @@ export const LanguageToggles: React.FC<LanguageTogglesProps> = ({
|
|
|
24
29
|
code,
|
|
25
30
|
currentLanguageAdapter,
|
|
26
31
|
onAfterToggle,
|
|
32
|
+
className = "absolute right-3 top-2 z-20 flex hover-action gap-1",
|
|
27
33
|
}) => {
|
|
28
34
|
const canUseMarkdown = useMemo(
|
|
29
35
|
() => LanguageAdapters.markdown.isSupported(code) || code.trim() === "",
|
|
@@ -35,7 +41,7 @@ export const LanguageToggles: React.FC<LanguageTogglesProps> = ({
|
|
|
35
41
|
);
|
|
36
42
|
|
|
37
43
|
return (
|
|
38
|
-
<div className=
|
|
44
|
+
<div className={className}>
|
|
39
45
|
<LanguageToggle
|
|
40
46
|
editorView={editorView}
|
|
41
47
|
currentLanguageAdapter={currentLanguageAdapter}
|