@marimo-team/islands 0.23.17-dev1 → 0.23.17-dev13
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-DsZJj75A.js → chat-ui-DH3hwWoI.js} +2 -2
- package/dist/{common-BGCQJb-W.js → common-D8DGPf0N.js} +5 -5
- package/dist/{html-to-image-CZ1kLKkq.js → html-to-image-CxlSazqu.js} +2095 -2088
- package/dist/main.js +5 -5
- package/dist/{process-output-bVfbcx5_.js → process-output-B24cDGzV.js} +1 -1
- package/dist/{reveal-component-NNi374so.js → reveal-component-UfXDaVe-.js} +2 -2
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/__mocks__/requests.ts +5 -0
- package/src/components/databases/icons/huggingface.svg +8 -0
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +1 -1
- package/src/components/editor/actions/export-dialog/__tests__/export-command.test.ts +115 -0
- package/src/components/editor/actions/export-dialog/__tests__/export-dialog.test.tsx +458 -0
- package/src/components/editor/actions/export-dialog/__tests__/export-notebook.test.ts +178 -0
- package/src/components/editor/actions/export-dialog/__tests__/state.test.ts +295 -0
- package/src/components/editor/actions/export-dialog/__tests__/use-export-dialog.test.tsx +287 -0
- package/src/components/editor/actions/export-dialog/export-command.ts +149 -0
- package/src/components/editor/actions/export-dialog/export-dialog.tsx +194 -0
- package/src/components/editor/actions/export-dialog/export-notebook.ts +98 -0
- package/src/components/editor/actions/export-dialog/format-notice.tsx +166 -0
- package/src/components/editor/actions/export-dialog/format-options.tsx +531 -0
- package/src/components/editor/actions/export-dialog/state.ts +339 -0
- package/src/components/editor/actions/export-dialog/use-export-dialog.ts +372 -0
- package/src/components/editor/actions/pair-with-agent-commands.ts +1 -21
- package/src/components/editor/actions/useNotebookActions.tsx +72 -170
- package/src/components/editor/chrome/panels/outline/__tests__/useActiveOutline.test.ts +26 -0
- package/src/components/editor/connections/add-connection-dialog.tsx +1 -1
- package/src/components/editor/connections/quick-add-data-sources.tsx +14 -7
- package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +14 -0
- package/src/components/editor/connections/storage/__tests__/as-code.test.ts +20 -0
- package/src/components/editor/connections/storage/add-storage-form.tsx +10 -0
- package/src/components/editor/connections/storage/as-code.ts +19 -1
- package/src/components/editor/connections/storage/schemas.ts +19 -0
- package/src/components/editor/controls/__tests__/notebook-menu-dropdown.test.tsx +187 -0
- package/src/components/editor/controls/notebook-menu-dropdown.tsx +4 -2
- package/src/components/storage/__tests__/storage-snippets.test.ts +88 -0
- package/src/components/storage/components.tsx +2 -0
- package/src/components/storage/storage-snippets.ts +58 -0
- package/src/core/__tests__/mode.test.ts +85 -0
- package/src/core/dom/outline.ts +25 -2
- package/src/core/mode.ts +11 -9
- package/src/core/network/__tests__/requests-lazy.test.ts +1 -0
- package/src/core/storage/types.ts +1 -0
- package/src/utils/__tests__/download.test.tsx +6 -4
- package/src/utils/download.ts +3 -1
- package/src/utils/shell.ts +17 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { assertNever } from "@/utils/assertNever";
|
|
4
4
|
import { KnownQueryParams } from "@/core/constants";
|
|
5
|
+
import { shellQuote } from "@/utils/shell";
|
|
5
6
|
|
|
6
7
|
export type AgentTab = "claude" | "codex" | "opencode" | "prompt";
|
|
7
8
|
|
|
@@ -23,27 +24,6 @@ export function getMarimoCommand(): string {
|
|
|
23
24
|
return import.meta.env.DEV ? "uv run marimo" : "uvx marimo@latest";
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
/**
|
|
27
|
-
* POSIX-quote a value for safe embedding in a shell command. These commands are
|
|
28
|
-
* meant to be copied into a terminal, so a url/token containing shell
|
|
29
|
-
* metacharacters (`'`, `&`, `$(...)`, ...) must not break out of its argument.
|
|
30
|
-
*
|
|
31
|
-
* Mirrors Python's `shlex.quote` (used on the CLI side in
|
|
32
|
-
* `marimo/_cli/pair/commands.py`) so both sides produce identical commands:
|
|
33
|
-
* values that are already shell-safe are left as-is for readability, and
|
|
34
|
-
* anything else is single-quoted with embedded quotes escaped as `'"'"'`.
|
|
35
|
-
*/
|
|
36
|
-
export function shellQuote(value: string): string {
|
|
37
|
-
if (value === "") {
|
|
38
|
-
return "''";
|
|
39
|
-
}
|
|
40
|
-
// Same "safe" character set as CPython's shlex.quote (ASCII \w plus a few).
|
|
41
|
-
if (/^[\w@%+=:,./-]+$/.test(value)) {
|
|
42
|
-
return value;
|
|
43
|
-
}
|
|
44
|
-
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
27
|
/** Return the server file key from a page URL, preserving its decoded value. */
|
|
48
28
|
export function getFileFromURL(href: string): string | undefined {
|
|
49
29
|
const file = new URL(href).searchParams.get(KnownQueryParams.filePath);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
|
4
|
+
import type { RefObject } from "react";
|
|
4
5
|
import {
|
|
5
6
|
BookMarkedIcon,
|
|
6
7
|
CheckIcon,
|
|
@@ -51,7 +52,6 @@ import { GitHubIcon } from "@/components/icons/github";
|
|
|
51
52
|
import { MarimoPlusIcon } from "@/components/icons/marimo-icons";
|
|
52
53
|
import { YouTubeIcon } from "@/components/icons/youtube";
|
|
53
54
|
import { useImperativeModal } from "@/components/modal/ImperativeModal";
|
|
54
|
-
import { renderShortcut } from "@/components/shortcuts/renderShortcut";
|
|
55
55
|
import { PairWithAgentModal } from "@/components/editor/actions/pair-with-agent-modal";
|
|
56
56
|
import { ShareStaticNotebookModal } from "@/components/static-html/share-modal";
|
|
57
57
|
import { toast } from "@/components/ui/use-toast";
|
|
@@ -66,30 +66,15 @@ import { disabledCellIds } from "@/core/cells/utils";
|
|
|
66
66
|
import { capabilitiesAtom } from "@/core/config/capabilities";
|
|
67
67
|
import { aiEnabledAtom, useResolvedMarimoConfig } from "@/core/config/config";
|
|
68
68
|
import { Constants } from "@/core/constants";
|
|
69
|
-
import {
|
|
70
|
-
updateCellOutputsWithScreenshots,
|
|
71
|
-
useEnrichCellOutputs,
|
|
72
|
-
} from "@/core/export/hooks";
|
|
73
69
|
import { useLayoutActions, useLayoutState } from "@/core/layout/layout";
|
|
74
70
|
import { useTogglePresenting } from "@/core/layout/useTogglePresenting";
|
|
75
71
|
import { kioskModeAtom, viewStateAtom } from "@/core/mode";
|
|
76
72
|
import { useRequestClient } from "@/core/network/requests";
|
|
77
73
|
import { useFilename } from "@/core/saving/filename";
|
|
78
|
-
import { downloadAsHTML } from "@/core/static/download-html";
|
|
79
74
|
import { createShareableLink } from "@/core/wasm/share";
|
|
80
75
|
import { isWasm } from "@/core/wasm/utils";
|
|
81
76
|
import { copyToClipboard } from "@/utils/copy";
|
|
82
|
-
import {
|
|
83
|
-
ADD_PRINTING_CLASS,
|
|
84
|
-
downloadAsPDF,
|
|
85
|
-
downloadBlob,
|
|
86
|
-
downloadExportedFile,
|
|
87
|
-
downloadHTMLAsImage,
|
|
88
|
-
withLoadingToast,
|
|
89
|
-
} from "@/utils/download";
|
|
90
|
-
import { Filenames } from "@/utils/filenames";
|
|
91
77
|
import { Objects } from "@/utils/objects";
|
|
92
|
-
import type { ProgressState } from "@/utils/progress";
|
|
93
78
|
import { Strings } from "@/utils/strings";
|
|
94
79
|
import { newNotebookURL } from "@/utils/urls";
|
|
95
80
|
import { useRunAllCells } from "../cell/useRunCells";
|
|
@@ -100,7 +85,13 @@ import { keyboardShortcutsAtom } from "../controls/keyboard-shortcuts";
|
|
|
100
85
|
import { commandPaletteAtom } from "../controls/state";
|
|
101
86
|
import { displayLayoutName, getLayoutIcon } from "../renderers/layout-select";
|
|
102
87
|
import { LAYOUT_TYPES } from "../renderers/types";
|
|
103
|
-
import {
|
|
88
|
+
import { ExportDialog } from "./export-dialog/export-dialog";
|
|
89
|
+
import {
|
|
90
|
+
applyExportOptionOverrides,
|
|
91
|
+
exportOptionsAtom,
|
|
92
|
+
type ExportFormat,
|
|
93
|
+
type ExportOptionOverrides,
|
|
94
|
+
} from "./export-dialog/state";
|
|
104
95
|
import type { ActionButton } from "./types";
|
|
105
96
|
import { useCopyNotebook } from "./useCopyNotebook";
|
|
106
97
|
import { useRestartKernel } from "./useRestartKernel";
|
|
@@ -111,7 +102,11 @@ const NOOP_HANDLER = (event?: Event) => {
|
|
|
111
102
|
event?.stopPropagation();
|
|
112
103
|
};
|
|
113
104
|
|
|
114
|
-
export function useNotebookActions(
|
|
105
|
+
export function useNotebookActions({
|
|
106
|
+
exportDialogReturnFocusRef,
|
|
107
|
+
}: {
|
|
108
|
+
exportDialogReturnFocusRef?: RefObject<HTMLElement | null>;
|
|
109
|
+
} = {}) {
|
|
115
110
|
const filename = useFilename();
|
|
116
111
|
const { openModal, closeModal } = useImperativeModal();
|
|
117
112
|
const { toggleApplication } = useChromeActions();
|
|
@@ -138,15 +133,8 @@ export function useNotebookActions() {
|
|
|
138
133
|
const setSettingsDialogOpen = useSetAtom(settingDialogAtom);
|
|
139
134
|
const { handleClick: openSettings } = useOpenSettingsToTab();
|
|
140
135
|
const setKeyboardShortcutsOpen = useSetAtom(keyboardShortcutsAtom);
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
exportAsMarkdown,
|
|
144
|
-
exportAsScript,
|
|
145
|
-
readCode,
|
|
146
|
-
saveCellConfig,
|
|
147
|
-
updateCellOutputs,
|
|
148
|
-
} = useRequestClient();
|
|
149
|
-
const takeScreenshots = useEnrichCellOutputs();
|
|
136
|
+
const setExportOptions = useSetAtom(exportOptionsAtom);
|
|
137
|
+
const { readCode, saveCellConfig } = useRequestClient();
|
|
150
138
|
|
|
151
139
|
const hasDisabledCells = useAtomValue(hasDisabledCellsAtom);
|
|
152
140
|
const canUndoDeletes = useAtomValue(canUndoDeletesAtom);
|
|
@@ -158,10 +146,6 @@ export function useNotebookActions() {
|
|
|
158
146
|
const sharingHtmlEnabled = resolvedConfig.sharing?.html ?? true;
|
|
159
147
|
const sharingWasmEnabled = resolvedConfig.sharing?.wasm ?? true;
|
|
160
148
|
const sharingMolabEnabled = resolvedConfig.sharing?.molab ?? true;
|
|
161
|
-
|
|
162
|
-
// Server-side PDF export is always available outside WASM.
|
|
163
|
-
// Browser print fallback is used in WASM.
|
|
164
|
-
const serverSidePdfEnabled = !isWasm();
|
|
165
149
|
const isSlidesLayout = selectedLayout === "slides";
|
|
166
150
|
|
|
167
151
|
const renderCheckboxElement = (checked: boolean) => (
|
|
@@ -170,82 +154,39 @@ export function useNotebookActions() {
|
|
|
170
154
|
</div>
|
|
171
155
|
);
|
|
172
156
|
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
157
|
+
const openExportDialog = (
|
|
158
|
+
initialFormat?: ExportFormat,
|
|
159
|
+
optionOverrides?: ExportOptionOverrides,
|
|
160
|
+
) => {
|
|
161
|
+
if (optionOverrides) {
|
|
162
|
+
setExportOptions((current) =>
|
|
163
|
+
applyExportOptionOverrides(current, optionOverrides),
|
|
164
|
+
);
|
|
176
165
|
}
|
|
177
|
-
|
|
178
|
-
<
|
|
179
|
-
|
|
180
|
-
|
|
166
|
+
openModal(
|
|
167
|
+
<ExportDialog
|
|
168
|
+
initialFormat={initialFormat}
|
|
169
|
+
onClose={closeModal}
|
|
170
|
+
returnFocusRef={exportDialogReturnFocusRef}
|
|
171
|
+
/>,
|
|
181
172
|
);
|
|
182
173
|
};
|
|
183
174
|
|
|
184
|
-
const downloadServerSidePDF = async ({
|
|
185
|
-
preset,
|
|
186
|
-
title,
|
|
187
|
-
}: {
|
|
188
|
-
preset: "document" | "slides";
|
|
189
|
-
title: string;
|
|
190
|
-
}) => {
|
|
191
|
-
if (!filename) {
|
|
192
|
-
toastNotebookMustBeNamed();
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const runDownload = async (progress: ProgressState) => {
|
|
197
|
-
await runServerSidePDFDownload({
|
|
198
|
-
exportOptions: {
|
|
199
|
-
webpdf: false,
|
|
200
|
-
preset,
|
|
201
|
-
includeInputs: true,
|
|
202
|
-
includeOutputs: true,
|
|
203
|
-
},
|
|
204
|
-
captureOutputs: () =>
|
|
205
|
-
updateCellOutputsWithScreenshots({
|
|
206
|
-
takeScreenshots: () => takeScreenshots({ progress }),
|
|
207
|
-
updateCellOutputs,
|
|
208
|
-
}),
|
|
209
|
-
downloadPDF: downloadAsPDF,
|
|
210
|
-
});
|
|
211
|
-
};
|
|
212
|
-
await withLoadingToast(title, runDownload);
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
const handleDocumentPDF = async () => {
|
|
216
|
-
if (serverSidePdfEnabled) {
|
|
217
|
-
await downloadServerSidePDF({
|
|
218
|
-
preset: "document",
|
|
219
|
-
title: "Downloading Document PDF...",
|
|
220
|
-
});
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
const beforeprint = new Event("export-beforeprint");
|
|
224
|
-
const afterprint = new Event("export-afterprint");
|
|
225
|
-
window.dispatchEvent(beforeprint);
|
|
226
|
-
setTimeout(() => window.print(), 0);
|
|
227
|
-
setTimeout(() => window.dispatchEvent(afterprint), 0);
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
const handleDownloadAsIPYNB = async () => {
|
|
231
|
-
if (!filename) {
|
|
232
|
-
toastNotebookMustBeNamed();
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const runDownload = async (progress: ProgressState) => {
|
|
237
|
-
await updateCellOutputsWithScreenshots({
|
|
238
|
-
takeScreenshots: () => takeScreenshots({ progress }),
|
|
239
|
-
updateCellOutputs,
|
|
240
|
-
});
|
|
241
|
-
const exportedFile = await exportAsIPYNB({ download: false });
|
|
242
|
-
downloadExportedFile(exportedFile);
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
await withLoadingToast("Downloading IPYNB...", runDownload);
|
|
246
|
-
};
|
|
247
|
-
|
|
248
175
|
const actions: ActionButton[] = [
|
|
176
|
+
{
|
|
177
|
+
icon: <DownloadIcon size={14} strokeWidth={1.5} />,
|
|
178
|
+
label: "Export…",
|
|
179
|
+
additionalKeywords: [
|
|
180
|
+
"download",
|
|
181
|
+
"html",
|
|
182
|
+
"markdown",
|
|
183
|
+
"ipynb",
|
|
184
|
+
"pdf",
|
|
185
|
+
"script",
|
|
186
|
+
"png",
|
|
187
|
+
],
|
|
188
|
+
handle: () => openExportDialog(),
|
|
189
|
+
},
|
|
249
190
|
{
|
|
250
191
|
icon: <DownloadIcon size={14} strokeWidth={1.5} />,
|
|
251
192
|
label: "Download",
|
|
@@ -254,83 +195,44 @@ export function useNotebookActions() {
|
|
|
254
195
|
{
|
|
255
196
|
icon: <FolderDownIcon size={14} strokeWidth={1.5} />,
|
|
256
197
|
label: "Download as HTML",
|
|
257
|
-
handle:
|
|
258
|
-
|
|
259
|
-
toastNotebookMustBeNamed();
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
await downloadAsHTML({ includeCode: true });
|
|
263
|
-
},
|
|
198
|
+
handle: () =>
|
|
199
|
+
openExportDialog("html", { html: { includeCode: true } }),
|
|
264
200
|
},
|
|
265
201
|
{
|
|
266
202
|
icon: <FolderDownIcon size={14} strokeWidth={1.5} />,
|
|
267
203
|
label: "Download as HTML (exclude code)",
|
|
268
|
-
handle:
|
|
269
|
-
|
|
270
|
-
toastNotebookMustBeNamed();
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
await downloadAsHTML({ includeCode: false });
|
|
274
|
-
},
|
|
204
|
+
handle: () =>
|
|
205
|
+
openExportDialog("html", { html: { includeCode: false } }),
|
|
275
206
|
},
|
|
276
207
|
{
|
|
277
208
|
icon: (
|
|
278
209
|
<MarkdownIcon strokeWidth={1.5} style={{ width: 14, height: 14 }} />
|
|
279
210
|
),
|
|
280
211
|
label: "Download as Markdown",
|
|
281
|
-
handle:
|
|
282
|
-
const exportedFile = await exportAsMarkdown({ download: false });
|
|
283
|
-
downloadExportedFile(exportedFile);
|
|
284
|
-
},
|
|
212
|
+
handle: () => openExportDialog("markdown"),
|
|
285
213
|
},
|
|
286
214
|
{
|
|
287
215
|
icon: <NotebookIcon size={14} strokeWidth={1.5} />,
|
|
288
216
|
label: "Download as ipynb",
|
|
289
|
-
handle:
|
|
217
|
+
handle: () => openExportDialog("ipynb"),
|
|
290
218
|
},
|
|
291
219
|
{
|
|
292
220
|
icon: <CodeIcon size={14} strokeWidth={1.5} />,
|
|
293
221
|
label: "Download notebook source",
|
|
294
|
-
handle:
|
|
295
|
-
|
|
296
|
-
downloadBlob(
|
|
297
|
-
new Blob([code.contents], { type: "text/plain" }),
|
|
298
|
-
Filenames.toPY(document.title),
|
|
299
|
-
);
|
|
300
|
-
},
|
|
222
|
+
handle: () =>
|
|
223
|
+
openExportDialog("script", { script: { type: "source" } }),
|
|
301
224
|
},
|
|
302
225
|
{
|
|
303
226
|
icon: <CodeIcon size={14} strokeWidth={1.5} />,
|
|
304
227
|
label: "Download flat script",
|
|
305
|
-
handle:
|
|
306
|
-
|
|
307
|
-
downloadExportedFile(exportedFile);
|
|
308
|
-
},
|
|
228
|
+
handle: () =>
|
|
229
|
+
openExportDialog("script", { script: { type: "flat" } }),
|
|
309
230
|
},
|
|
310
231
|
{
|
|
311
232
|
divider: true,
|
|
312
233
|
icon: <ImageIcon size={14} strokeWidth={1.5} />,
|
|
313
234
|
label: "Download as PNG",
|
|
314
|
-
|
|
315
|
-
tooltip:
|
|
316
|
-
viewState.mode === "present" ? undefined : (
|
|
317
|
-
<span>
|
|
318
|
-
Only available in app view. <br />
|
|
319
|
-
Toggle with: {renderShortcut("global.hideCode", false)}
|
|
320
|
-
</span>
|
|
321
|
-
),
|
|
322
|
-
handle: async () => {
|
|
323
|
-
const app = document.getElementById("App");
|
|
324
|
-
if (!app) {
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
await downloadHTMLAsImage({
|
|
328
|
-
element: app,
|
|
329
|
-
filename: document.title,
|
|
330
|
-
// Add body.printing ONLY when converting the whole notebook to a screenshot
|
|
331
|
-
prepare: ADD_PRINTING_CLASS,
|
|
332
|
-
});
|
|
333
|
-
},
|
|
235
|
+
handle: () => openExportDialog("png"),
|
|
334
236
|
},
|
|
335
237
|
isSlidesLayout
|
|
336
238
|
? {
|
|
@@ -342,19 +244,24 @@ export function useNotebookActions() {
|
|
|
342
244
|
{
|
|
343
245
|
icon: <FileIcon size={14} strokeWidth={1.5} />,
|
|
344
246
|
label: "Document Layout",
|
|
345
|
-
handle:
|
|
247
|
+
handle: () =>
|
|
248
|
+
openExportDialog("pdf", {
|
|
249
|
+
pdf: { preset: "document" },
|
|
250
|
+
}),
|
|
346
251
|
},
|
|
347
252
|
{
|
|
348
253
|
icon: <FileIcon size={14} strokeWidth={1.5} />,
|
|
349
254
|
label: "Slides Layout",
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
255
|
+
hidden: isWasm(),
|
|
256
|
+
rightElement: (
|
|
257
|
+
<span className="ml-3 shrink-0 rounded-full border border-emerald-200 bg-emerald-50 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-emerald-700">
|
|
258
|
+
Recommended
|
|
259
|
+
</span>
|
|
260
|
+
),
|
|
261
|
+
handle: () =>
|
|
262
|
+
openExportDialog("pdf", {
|
|
263
|
+
pdf: { preset: "slides" },
|
|
264
|
+
}),
|
|
358
265
|
},
|
|
359
266
|
],
|
|
360
267
|
}
|
|
@@ -362,7 +269,10 @@ export function useNotebookActions() {
|
|
|
362
269
|
divider: true,
|
|
363
270
|
icon: <FileIcon size={14} strokeWidth={1.5} />,
|
|
364
271
|
label: "Download as PDF",
|
|
365
|
-
handle:
|
|
272
|
+
handle: () =>
|
|
273
|
+
openExportDialog("pdf", {
|
|
274
|
+
pdf: { preset: "document" },
|
|
275
|
+
}),
|
|
366
276
|
},
|
|
367
277
|
],
|
|
368
278
|
},
|
|
@@ -740,11 +650,3 @@ export function useNotebookActions() {
|
|
|
740
650
|
return action;
|
|
741
651
|
});
|
|
742
652
|
}
|
|
743
|
-
|
|
744
|
-
function toastNotebookMustBeNamed() {
|
|
745
|
-
toast({
|
|
746
|
-
title: "Error",
|
|
747
|
-
description: "Notebooks must be named to be exported.",
|
|
748
|
-
variant: "danger",
|
|
749
|
-
});
|
|
750
|
-
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { parseOutline } from "@/core/dom/outline";
|
|
4
|
+
import { findOutlineElements } from "../useActiveOutline";
|
|
5
|
+
|
|
6
|
+
describe("findOutlineElements", () => {
|
|
7
|
+
it.each([
|
|
8
|
+
"Design from the portfolio",
|
|
9
|
+
'Decorative Art of "Spanish California"',
|
|
10
|
+
`The painter's "Study"`,
|
|
11
|
+
])("finds an id-less heading named %s", (name) => {
|
|
12
|
+
const html = `<h3>${name}</h3>`;
|
|
13
|
+
document.body.innerHTML = html;
|
|
14
|
+
|
|
15
|
+
const outline = parseOutline({
|
|
16
|
+
mimetype: "text/html",
|
|
17
|
+
timestamp: 0,
|
|
18
|
+
channel: "output",
|
|
19
|
+
data: html,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(
|
|
23
|
+
findOutlineElements(outline?.items ?? []).map(([element]) => element),
|
|
24
|
+
).toEqual([document.querySelector("h3")]);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -87,7 +87,7 @@ export const AddConnectionDialogContent: React.FC<{
|
|
|
87
87
|
<span className="block">{codeSnippetHint}</span>
|
|
88
88
|
</DialogDescription>
|
|
89
89
|
</DialogHeader>
|
|
90
|
-
<AutoDiscoveredDataSources onSubmit={onClose} />
|
|
90
|
+
<AutoDiscoveredDataSources onSubmit={onClose} className="-mt-2" />
|
|
91
91
|
<Tabs
|
|
92
92
|
value={activeTab}
|
|
93
93
|
onValueChange={(v) => setActiveTab(v as ConnectionTab)}
|
|
@@ -5,11 +5,13 @@ import { Tooltip, TooltipProvider } from "@/components/ui/tooltip";
|
|
|
5
5
|
import type { DetectedDataSource } from "@/core/datasets/data-source-discovery";
|
|
6
6
|
import { useDataSourceDiscovery } from "@/hooks/useDataSourceDiscovery";
|
|
7
7
|
import { useInsertCode } from "./components";
|
|
8
|
+
import { cn } from "@/utils/cn";
|
|
8
9
|
|
|
9
10
|
export const QuickAddDataSources: React.FC<{
|
|
11
|
+
className?: string;
|
|
10
12
|
sources: DetectedDataSource[];
|
|
11
13
|
onAdd: (source: DetectedDataSource) => void;
|
|
12
|
-
}> = ({ sources, onAdd }) => {
|
|
14
|
+
}> = ({ className, sources, onAdd }) => {
|
|
13
15
|
if (sources.length === 0) {
|
|
14
16
|
return null;
|
|
15
17
|
}
|
|
@@ -17,12 +19,15 @@ export const QuickAddDataSources: React.FC<{
|
|
|
17
19
|
return (
|
|
18
20
|
<section
|
|
19
21
|
aria-labelledby="quick-add-data-sources-title"
|
|
20
|
-
className=
|
|
22
|
+
className={cn(
|
|
23
|
+
"rounded-full bg-[linear-gradient(135deg,var(--blue-2),var(--purple-3))] px-3 py-2",
|
|
24
|
+
className,
|
|
25
|
+
)}
|
|
21
26
|
>
|
|
22
27
|
<div className="flex flex-wrap items-center gap-2">
|
|
23
28
|
<div className="mr-1 flex items-center gap-1.5">
|
|
24
|
-
<SparklesIcon className="h-3.5 w-3.5 text-
|
|
25
|
-
<h3 id="quick-add-data-sources-title" className="text-sm
|
|
29
|
+
<SparklesIcon className="h-3.5 w-3.5 text-(--blue-9)" />
|
|
30
|
+
<h3 id="quick-add-data-sources-title" className="text-sm">
|
|
26
31
|
Quick add
|
|
27
32
|
</h3>
|
|
28
33
|
</div>
|
|
@@ -36,10 +41,10 @@ export const QuickAddDataSources: React.FC<{
|
|
|
36
41
|
<button
|
|
37
42
|
type="button"
|
|
38
43
|
aria-label={`Add ${source.displayName} connection`}
|
|
39
|
-
className="inline-flex items-center gap-1 rounded-full border border-
|
|
44
|
+
className="inline-flex items-center gap-1 rounded-full border border-border/60 bg-background px-2.5 py-1 text-xs font-medium text-foreground/90 transition-colors hover:border-(--blue-7) focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
|
40
45
|
onClick={() => onAdd(source)}
|
|
41
46
|
>
|
|
42
|
-
<PlusIcon className="h-3 w-3" />
|
|
47
|
+
<PlusIcon className="h-3 w-3 text-muted-foreground" />
|
|
43
48
|
{source.displayName}
|
|
44
49
|
</button>
|
|
45
50
|
</Tooltip>
|
|
@@ -90,12 +95,14 @@ const DetectedDataSourceDetails: React.FC<{
|
|
|
90
95
|
|
|
91
96
|
export const AutoDiscoveredDataSources: React.FC<{
|
|
92
97
|
onSubmit: () => void;
|
|
93
|
-
|
|
98
|
+
className?: string;
|
|
99
|
+
}> = ({ onSubmit, className }) => {
|
|
94
100
|
const insertCode = useInsertCode();
|
|
95
101
|
const { data } = useDataSourceDiscovery();
|
|
96
102
|
|
|
97
103
|
return (
|
|
98
104
|
<QuickAddDataSources
|
|
105
|
+
className={className}
|
|
99
106
|
sources={data ?? []}
|
|
100
107
|
onAdd={(source) => {
|
|
101
108
|
insertCode(source.code);
|
package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap
CHANGED
|
@@ -154,6 +154,20 @@ _creds = json.loads(_credentials_json)
|
|
|
154
154
|
fs = GoogleDriveFileSystem(creds=_creds, token="service_account", use_listings_cache=False, skip_instance_cache=True)"
|
|
155
155
|
`;
|
|
156
156
|
|
|
157
|
+
exports[`generateStorageCode > Hugging Face > default connection 1`] = `
|
|
158
|
+
"from huggingface_hub import HfApi
|
|
159
|
+
|
|
160
|
+
hf = HfApi()"
|
|
161
|
+
`;
|
|
162
|
+
|
|
163
|
+
exports[`generateStorageCode > Hugging Face > with token from secrets 1`] = `
|
|
164
|
+
"from huggingface_hub import HfApi
|
|
165
|
+
import os
|
|
166
|
+
|
|
167
|
+
_token = os.environ.get("HF_TOKEN")
|
|
168
|
+
hf = HfApi(token=_token)"
|
|
169
|
+
`;
|
|
170
|
+
|
|
157
171
|
exports[`generateStorageCode > S3 > basic connection with all fields 1`] = `
|
|
158
172
|
"from obstore.store import S3Store
|
|
159
173
|
|
|
@@ -309,6 +309,26 @@ describe("generateStorageCode", () => {
|
|
|
309
309
|
});
|
|
310
310
|
});
|
|
311
311
|
|
|
312
|
+
describe("Hugging Face", () => {
|
|
313
|
+
it("default connection", () => {
|
|
314
|
+
expect(
|
|
315
|
+
generateStorageCode({ type: "huggingface" }, { library: "fsspec" }),
|
|
316
|
+
).toMatchSnapshot();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("with token from secrets", () => {
|
|
320
|
+
expect(
|
|
321
|
+
generateStorageCode(
|
|
322
|
+
{
|
|
323
|
+
type: "huggingface",
|
|
324
|
+
token: prefixSecret("HF_TOKEN"),
|
|
325
|
+
},
|
|
326
|
+
{ library: "fsspec" },
|
|
327
|
+
),
|
|
328
|
+
).toMatchSnapshot();
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
312
332
|
describe("invalid cases", () => {
|
|
313
333
|
it("throws for empty S3 bucket", () => {
|
|
314
334
|
expect(() =>
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
CoreWeaveStorageSchema,
|
|
18
18
|
GCSStorageSchema,
|
|
19
19
|
GoogleDriveStorageSchema,
|
|
20
|
+
HuggingfaceStorageSchema,
|
|
20
21
|
S3StorageSchema,
|
|
21
22
|
type StorageConnection,
|
|
22
23
|
} from "./schemas";
|
|
@@ -79,6 +80,15 @@ const STORAGE_PROVIDERS = [
|
|
|
79
80
|
preferred: "fsspec",
|
|
80
81
|
},
|
|
81
82
|
},
|
|
83
|
+
{
|
|
84
|
+
name: "Hugging Face Hub",
|
|
85
|
+
schema: HuggingfaceStorageSchema,
|
|
86
|
+
protocol: "hf",
|
|
87
|
+
storageLibraries: {
|
|
88
|
+
libraries: ["huggingface_hub"],
|
|
89
|
+
preferred: "huggingface_hub",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
82
92
|
] satisfies StorageProviderSchema[];
|
|
83
93
|
|
|
84
94
|
const StorageProviderSelector: React.FC<{
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
StorageConnectionSchema,
|
|
11
11
|
} from "./schemas";
|
|
12
12
|
|
|
13
|
-
export type StorageLibrary = "obstore" | "fsspec";
|
|
13
|
+
export type StorageLibrary = "obstore" | "fsspec" | "huggingface_hub";
|
|
14
14
|
|
|
15
15
|
export interface StorageCodeOptions {
|
|
16
16
|
library: StorageLibrary;
|
|
@@ -20,6 +20,7 @@ export interface StorageCodeOptions {
|
|
|
20
20
|
export const StorageLibraryDisplayNames: Record<StorageLibrary, string> = {
|
|
21
21
|
obstore: "obstore",
|
|
22
22
|
fsspec: "fsspec",
|
|
23
|
+
huggingface_hub: "huggingface_hub",
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
class SecretContainer {
|
|
@@ -245,6 +246,20 @@ function generateGDriveCode(
|
|
|
245
246
|
return { imports, code };
|
|
246
247
|
}
|
|
247
248
|
|
|
249
|
+
function generateHuggingfaceCode(
|
|
250
|
+
connection: Extract<StorageConnection, { type: "huggingface" }>,
|
|
251
|
+
secrets: SecretContainer,
|
|
252
|
+
): { imports: Set<string>; code: string } {
|
|
253
|
+
const imports = new Set(["from huggingface_hub import HfApi"]);
|
|
254
|
+
|
|
255
|
+
if (!connection.token) {
|
|
256
|
+
return { imports, code: "hf = HfApi()" };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const token = secrets.print("token", connection.token);
|
|
260
|
+
return { imports, code: `hf = HfApi(token=${token})` };
|
|
261
|
+
}
|
|
262
|
+
|
|
248
263
|
export function generateStorageCode(
|
|
249
264
|
connection: StorageConnection,
|
|
250
265
|
options: StorageCodeOptions,
|
|
@@ -273,6 +288,9 @@ export function generateStorageCode(
|
|
|
273
288
|
isEmbedded: options.isEmbedded,
|
|
274
289
|
});
|
|
275
290
|
break;
|
|
291
|
+
case "huggingface":
|
|
292
|
+
result = generateHuggingfaceCode(connection, secrets);
|
|
293
|
+
break;
|
|
276
294
|
default:
|
|
277
295
|
assertNever(connection);
|
|
278
296
|
}
|
|
@@ -220,12 +220,31 @@ export const GoogleDriveStorageSchema = z
|
|
|
220
220
|
})
|
|
221
221
|
.describe(FieldOptions.of({ direction: "two-columns" }));
|
|
222
222
|
|
|
223
|
+
export const HuggingfaceStorageSchema = z
|
|
224
|
+
.object({
|
|
225
|
+
type: z.literal("huggingface"),
|
|
226
|
+
token: z
|
|
227
|
+
.string()
|
|
228
|
+
.optional()
|
|
229
|
+
.describe(
|
|
230
|
+
FieldOptions.of({
|
|
231
|
+
label: "Access Token",
|
|
232
|
+
description:
|
|
233
|
+
"Leave empty to use the HF_TOKEN environment variable or cached login",
|
|
234
|
+
inputType: "password",
|
|
235
|
+
optionRegex: "(hf.?token|hugging.?face.?token|hub.?token)",
|
|
236
|
+
}),
|
|
237
|
+
),
|
|
238
|
+
})
|
|
239
|
+
.describe(FieldOptions.of({ direction: "two-columns" }));
|
|
240
|
+
|
|
223
241
|
export const StorageConnectionSchema = z.discriminatedUnion("type", [
|
|
224
242
|
S3StorageSchema,
|
|
225
243
|
GCSStorageSchema,
|
|
226
244
|
AzureStorageSchema,
|
|
227
245
|
CoreWeaveStorageSchema,
|
|
228
246
|
GoogleDriveStorageSchema,
|
|
247
|
+
HuggingfaceStorageSchema,
|
|
229
248
|
]);
|
|
230
249
|
|
|
231
250
|
export type StorageConnection = z.infer<typeof StorageConnectionSchema>;
|