@marimo-team/islands 0.20.5-dev44 → 0.20.5-dev48

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/main.js CHANGED
@@ -70710,7 +70710,7 @@ Image URL: ${r.imageUrl}`)), contextToXml({
70710
70710
  return Logger.warn("Failed to get version from mount config"), null;
70711
70711
  }
70712
70712
  }
70713
- const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.20.5-dev44"), showCodeInRunModeAtom = atom(true);
70713
+ const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.20.5-dev48"), showCodeInRunModeAtom = atom(true);
70714
70714
  atom(null);
70715
70715
  var import_compiler_runtime$89 = require_compiler_runtime();
70716
70716
  function useKeydownOnElement(e, r) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.20.5-dev44",
3
+ "version": "0.20.5-dev48",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -0,0 +1,253 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { describe, expect, it } from "vitest";
3
+ import {
4
+ STORAGE_SNIPPETS,
5
+ type StorageSnippetContext,
6
+ } from "../storage-snippets";
7
+
8
+ const readSnippet = STORAGE_SNIPPETS.find((s) => s.id === "read-file")!;
9
+ const downloadSnippet = STORAGE_SNIPPETS.find((s) => s.id === "download-file")!;
10
+
11
+ function makeCtx(
12
+ overrides: Partial<StorageSnippetContext> = {},
13
+ ): StorageSnippetContext {
14
+ return {
15
+ variableName: "store",
16
+ protocol: "s3",
17
+ entry: {
18
+ path: "data/file.csv",
19
+ kind: "object",
20
+ size: 1024,
21
+ lastModified: null,
22
+ },
23
+ backendType: "obstore",
24
+ ...overrides,
25
+ };
26
+ }
27
+
28
+ describe("read-file snippet", () => {
29
+ it("obstore backend", () => {
30
+ expect(readSnippet.getCode(makeCtx())).toMatchInlineSnapshot(`
31
+ "_data = store.get("data/file.csv").bytes()
32
+ _data"
33
+ `);
34
+ });
35
+
36
+ it("fsspec backend", () => {
37
+ expect(
38
+ readSnippet.getCode(
39
+ makeCtx({ backendType: "fsspec", variableName: "fs" }),
40
+ ),
41
+ ).toMatchInlineSnapshot(`
42
+ "_data = fs.cat_file("data/file.csv")
43
+ _data"
44
+ `);
45
+ });
46
+
47
+ it("returns null for directories", () => {
48
+ expect(
49
+ readSnippet.getCode(
50
+ makeCtx({
51
+ entry: {
52
+ path: "data/",
53
+ kind: "directory",
54
+ size: 0,
55
+ lastModified: null,
56
+ },
57
+ }),
58
+ ),
59
+ ).toBeNull();
60
+ });
61
+
62
+ it("escapes double quotes in paths", () => {
63
+ expect(
64
+ readSnippet.getCode(
65
+ makeCtx({
66
+ entry: {
67
+ path: 'data/"file".csv',
68
+ kind: "object",
69
+ size: 100,
70
+ lastModified: null,
71
+ },
72
+ }),
73
+ ),
74
+ ).toMatchInlineSnapshot(`
75
+ "_data = store.get("data/\\"file\\".csv").bytes()
76
+ _data"
77
+ `);
78
+ });
79
+
80
+ it("escapes backslashes in paths", () => {
81
+ expect(
82
+ readSnippet.getCode(
83
+ makeCtx({
84
+ entry: {
85
+ path: "data\\file.csv",
86
+ kind: "object",
87
+ size: 100,
88
+ lastModified: null,
89
+ },
90
+ }),
91
+ ),
92
+ ).toMatchInlineSnapshot(`
93
+ "_data = store.get("data\\\\file.csv").bytes()
94
+ _data"
95
+ `);
96
+ });
97
+
98
+ it("escapes newlines and tabs in paths", () => {
99
+ expect(
100
+ readSnippet.getCode(
101
+ makeCtx({
102
+ entry: {
103
+ path: "data/file\nname\there.csv",
104
+ kind: "object",
105
+ size: 100,
106
+ lastModified: null,
107
+ },
108
+ }),
109
+ ),
110
+ ).toMatchInlineSnapshot(`
111
+ "_data = store.get("data/file\\nname\\there.csv").bytes()
112
+ _data"
113
+ `);
114
+ });
115
+
116
+ it("escapes control characters in paths", () => {
117
+ expect(
118
+ readSnippet.getCode(
119
+ makeCtx({
120
+ entry: {
121
+ path: "data/\u0000\u001F.csv",
122
+ kind: "object",
123
+ size: 100,
124
+ lastModified: null,
125
+ },
126
+ }),
127
+ ),
128
+ ).toMatchInlineSnapshot(`
129
+ "_data = store.get("data/\\u0000\\u001f.csv").bytes()
130
+ _data"
131
+ `);
132
+ });
133
+ });
134
+
135
+ describe("download-file snippet", () => {
136
+ it("obstore s3 backend", () => {
137
+ expect(downloadSnippet.getCode(makeCtx())).toMatchInlineSnapshot(`
138
+ "from datetime import timedelta
139
+ from obstore import sign
140
+
141
+ signed_url = sign(
142
+ store, "GET", "data/file.csv",
143
+ expires_in=timedelta(hours=1),
144
+ )
145
+ signed_url"
146
+ `);
147
+ });
148
+
149
+ it("obstore gcs backend", () => {
150
+ expect(
151
+ downloadSnippet.getCode(makeCtx({ protocol: "gcs" })),
152
+ ).toMatchInlineSnapshot(`
153
+ "from datetime import timedelta
154
+ from obstore import sign
155
+
156
+ signed_url = sign(
157
+ store, "GET", "data/file.csv",
158
+ expires_in=timedelta(hours=1),
159
+ )
160
+ signed_url"
161
+ `);
162
+ });
163
+
164
+ it("obstore cloudflare backend", () => {
165
+ expect(
166
+ downloadSnippet.getCode(makeCtx({ protocol: "cloudflare" })),
167
+ ).toMatchInlineSnapshot(`
168
+ "from datetime import timedelta
169
+ from obstore import sign
170
+
171
+ signed_url = sign(
172
+ store, "GET", "data/file.csv",
173
+ expires_in=timedelta(hours=1),
174
+ )
175
+ signed_url"
176
+ `);
177
+ });
178
+
179
+ it("returns null for http obstore (not signable)", () => {
180
+ expect(downloadSnippet.getCode(makeCtx({ protocol: "http" }))).toBeNull();
181
+ });
182
+
183
+ it("returns null for file obstore (not signable)", () => {
184
+ expect(downloadSnippet.getCode(makeCtx({ protocol: "file" }))).toBeNull();
185
+ });
186
+
187
+ it("returns null for in-memory obstore (not signable)", () => {
188
+ expect(
189
+ downloadSnippet.getCode(makeCtx({ protocol: "in-memory" })),
190
+ ).toBeNull();
191
+ });
192
+
193
+ it("fsspec backend", () => {
194
+ expect(
195
+ downloadSnippet.getCode(
196
+ makeCtx({ backendType: "fsspec", variableName: "fs" }),
197
+ ),
198
+ ).toMatchInlineSnapshot(`"fs.get("data/file.csv", "file.csv")"`);
199
+ });
200
+
201
+ it("fsspec backend with nested path", () => {
202
+ expect(
203
+ downloadSnippet.getCode(
204
+ makeCtx({
205
+ backendType: "fsspec",
206
+ variableName: "fs",
207
+ entry: {
208
+ path: "nested/dir/report.parquet",
209
+ kind: "file",
210
+ size: 500,
211
+ lastModified: null,
212
+ },
213
+ }),
214
+ ),
215
+ ).toMatchInlineSnapshot(
216
+ `"fs.get("nested/dir/report.parquet", "report.parquet")"`,
217
+ );
218
+ });
219
+
220
+ it("returns null for directories", () => {
221
+ expect(
222
+ downloadSnippet.getCode(
223
+ makeCtx({
224
+ entry: {
225
+ path: "data/",
226
+ kind: "directory",
227
+ size: 0,
228
+ lastModified: null,
229
+ },
230
+ }),
231
+ ),
232
+ ).toBeNull();
233
+ });
234
+ });
235
+
236
+ describe("all snippets return null for directories", () => {
237
+ for (const snippet of STORAGE_SNIPPETS) {
238
+ it(`${snippet.id} returns null for directory entries`, () => {
239
+ expect(
240
+ snippet.getCode(
241
+ makeCtx({
242
+ entry: {
243
+ path: "some-dir/",
244
+ kind: "directory",
245
+ size: 0,
246
+ lastModified: null,
247
+ },
248
+ }),
249
+ ),
250
+ ).toBeNull();
251
+ });
252
+ }
253
+ });
@@ -15,6 +15,7 @@ import {
15
15
  import React, { useCallback, useState } from "react";
16
16
  import { useLocale } from "react-aria";
17
17
  import { EngineVariable } from "@/components/databases/engine-variable";
18
+ import { useAddCodeToNewCell } from "@/components/editor/cell/useAddCell";
18
19
  import { PanelEmptyState } from "@/components/editor/chrome/panels/empty-state";
19
20
  import { AddConnectionDialog } from "@/components/editor/connections/add-connection-dialog";
20
21
  import {
@@ -32,6 +33,7 @@ import {
32
33
  DropdownMenu,
33
34
  DropdownMenuContent,
34
35
  DropdownMenuItem,
36
+ DropdownMenuSeparator,
35
37
  DropdownMenuTrigger,
36
38
  } from "@/components/ui/dropdown-menu";
37
39
  import { Tooltip } from "@/components/ui/tooltip";
@@ -47,7 +49,7 @@ import type {
47
49
  StorageNamespace,
48
50
  StoragePathKey,
49
51
  } from "@/core/storage/types";
50
- import { storagePathKey, storageUrl } from "@/core/storage/types";
52
+ import { storagePathKey } from "@/core/storage/types";
51
53
  import type { VariableName } from "@/core/variables/types";
52
54
  import { cn } from "@/utils/cn";
53
55
  import { copyToClipboard } from "@/utils/copy";
@@ -58,6 +60,7 @@ import { ErrorState } from "../datasources/components";
58
60
  import { Button } from "../ui/button";
59
61
  import { ProtocolIcon } from "./components";
60
62
  import { StorageFileViewer } from "./storage-file-viewer";
63
+ import { STORAGE_SNIPPETS } from "./storage-snippets";
61
64
 
62
65
  interface OpenFileInfo {
63
66
  entry: StorageEntry;
@@ -145,6 +148,7 @@ const StorageEntryChildren: React.FC<{
145
148
  namespace: string;
146
149
  protocol: string;
147
150
  rootPath: string;
151
+ backendType: StorageNamespace["backendType"];
148
152
  prefix: string;
149
153
  depth: number;
150
154
  locale: string;
@@ -154,6 +158,7 @@ const StorageEntryChildren: React.FC<{
154
158
  namespace,
155
159
  protocol,
156
160
  rootPath,
161
+ backendType,
157
162
  prefix,
158
163
  depth,
159
164
  locale,
@@ -214,6 +219,7 @@ const StorageEntryChildren: React.FC<{
214
219
  namespace={namespace}
215
220
  protocol={protocol}
216
221
  rootPath={rootPath}
222
+ backendType={backendType}
217
223
  depth={depth}
218
224
  locale={locale}
219
225
  searchValue={searchValue}
@@ -229,6 +235,7 @@ const StorageEntryRow: React.FC<{
229
235
  namespace: string;
230
236
  protocol: string;
231
237
  rootPath: string;
238
+ backendType: StorageNamespace["backendType"];
232
239
  depth: number;
233
240
  locale: string;
234
241
  searchValue: string;
@@ -238,6 +245,7 @@ const StorageEntryRow: React.FC<{
238
245
  namespace,
239
246
  protocol,
240
247
  rootPath,
248
+ backendType,
241
249
  depth,
242
250
  locale,
243
251
  searchValue,
@@ -245,6 +253,7 @@ const StorageEntryRow: React.FC<{
245
253
  }) => {
246
254
  const [isExpanded, setIsExpanded] = useState(false);
247
255
  const { entriesByPath } = useStorage();
256
+ const addCodeToNewCell = useAddCodeToNewCell();
248
257
  const isDir = entry.kind === "directory";
249
258
  const name = displayName(entry.path);
250
259
  const hasSearch = !!searchValue.trim();
@@ -361,13 +370,12 @@ const StorageEntryRow: React.FC<{
361
370
  )}
362
371
  <DropdownMenuItem
363
372
  onSelect={async () => {
364
- const url = storageUrl(protocol, rootPath, entry.path);
365
- await copyToClipboard(url.toString());
373
+ await copyToClipboard(entry.path);
366
374
  toast({ title: "Copied to clipboard" });
367
375
  }}
368
376
  >
369
377
  <CopyIcon className={MENU_ITEM_ICON_CLASS} />
370
- Copy URL
378
+ Copy path
371
379
  </DropdownMenuItem>
372
380
  {!isDir && (
373
381
  <DropdownMenuItem onSelect={() => handleDownload()}>
@@ -375,6 +383,28 @@ const StorageEntryRow: React.FC<{
375
383
  Download
376
384
  </DropdownMenuItem>
377
385
  )}
386
+ <DropdownMenuSeparator />
387
+ {STORAGE_SNIPPETS.map((snippet) => {
388
+ const code = snippet.getCode({
389
+ variableName: namespace,
390
+ protocol,
391
+ entry,
392
+ backendType,
393
+ });
394
+ if (code === null) {
395
+ return null;
396
+ }
397
+ const Icon = snippet.icon;
398
+ return (
399
+ <DropdownMenuItem
400
+ key={snippet.id}
401
+ onSelect={() => addCodeToNewCell(code)}
402
+ >
403
+ <Icon className={MENU_ITEM_ICON_CLASS} />
404
+ {snippet.label}
405
+ </DropdownMenuItem>
406
+ );
407
+ })}
378
408
  </DropdownMenuContent>
379
409
  </DropdownMenu>
380
410
  </div>
@@ -384,6 +414,7 @@ const StorageEntryRow: React.FC<{
384
414
  namespace={namespace}
385
415
  protocol={protocol}
386
416
  rootPath={rootPath}
417
+ backendType={backendType}
387
418
  prefix={entry.path}
388
419
  depth={depth + 1}
389
420
  locale={locale}
@@ -498,6 +529,7 @@ const StorageNamespaceSection: React.FC<{
498
529
  namespace={namespaceName}
499
530
  protocol={namespace.protocol}
500
531
  rootPath={namespace.rootPath}
532
+ backendType={namespace.backendType}
501
533
  depth={1}
502
534
  locale={locale}
503
535
  searchValue={searchValue}
@@ -0,0 +1,67 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { LucideIcon } from "lucide-react";
4
+ import { BookOpenIcon, LinkIcon } from "lucide-react";
5
+ import type { StorageEntry, StorageNamespace } from "@/core/storage/types";
6
+
7
+ type BackendType = StorageNamespace["backendType"];
8
+
9
+ export interface StorageSnippetContext {
10
+ variableName: string;
11
+ protocol: string;
12
+ entry: StorageEntry;
13
+ backendType: BackendType;
14
+ }
15
+
16
+ export interface StorageSnippet {
17
+ id: string;
18
+ label: string;
19
+ icon: LucideIcon;
20
+ /** Return the code string, or null to hide the snippet for this context. */
21
+ getCode: (ctx: StorageSnippetContext) => string | null;
22
+ }
23
+
24
+ const NOT_SIGNABLE_PROTOCOLS = new Set(["http", "file", "in-memory"]);
25
+
26
+ function escapeForPythonString(value: string): string {
27
+ return JSON.stringify(value).slice(1, -1);
28
+ }
29
+
30
+ export const STORAGE_SNIPPETS: StorageSnippet[] = [
31
+ {
32
+ id: "read-file",
33
+ label: "Insert read snippet",
34
+ icon: BookOpenIcon,
35
+ getCode: (ctx) => {
36
+ if (ctx.entry.kind === "directory") {
37
+ return null;
38
+ }
39
+ const path = escapeForPythonString(ctx.entry.path);
40
+ if (ctx.backendType === "obstore") {
41
+ return `_data = ${ctx.variableName}.get("${path}").bytes()\n_data`;
42
+ }
43
+ return `_data = ${ctx.variableName}.cat_file("${path}")\n_data`;
44
+ },
45
+ },
46
+ {
47
+ id: "download-file",
48
+ label: "Insert download snippet",
49
+ icon: LinkIcon,
50
+ getCode: (ctx) => {
51
+ if (ctx.entry.kind === "directory") {
52
+ return null;
53
+ }
54
+ const path = escapeForPythonString(ctx.entry.path);
55
+ if (ctx.backendType === "obstore") {
56
+ if (NOT_SIGNABLE_PROTOCOLS.has(ctx.protocol)) {
57
+ return null;
58
+ }
59
+ return `from datetime import timedelta\nfrom obstore import sign\n\nsigned_url = sign(\n ${ctx.variableName}, "GET", "${path}",\n expires_in=timedelta(hours=1),\n)\nsigned_url`;
60
+ }
61
+ const filename = escapeForPythonString(
62
+ ctx.entry.path.split("/").pop() || "download",
63
+ );
64
+ return `${ctx.variableName}.get("${path}", "${filename}")`;
65
+ },
66
+ },
67
+ ];
@@ -10,6 +10,7 @@ function makeNamespace(
10
10
  overrides: Partial<StorageNamespace> & { name: string },
11
11
  ): StorageNamespace {
12
12
  return {
13
+ backendType: overrides.backendType ?? "obstore",
13
14
  displayName: overrides.displayName ?? overrides.name,
14
15
  name: overrides.name,
15
16
  protocol: overrides.protocol ?? "s3",