@marimo-team/frontend 0.23.2-dev49 → 0.23.2-dev51

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.
Files changed (76) hide show
  1. package/dist/assets/{CellStatus-sFlO649o.js → CellStatus-Dl2cuz44.js} +1 -1
  2. package/dist/assets/JsonOutput-C9sdMiTb.js +49 -0
  3. package/dist/assets/{MarimoErrorOutput-BgCbGEKp.js → MarimoErrorOutput-B58oLtN_.js} +3 -3
  4. package/dist/assets/{RenderHTML-CEX3AZCQ.js → RenderHTML-Drt_i2fd.js} +1 -1
  5. package/dist/assets/{add-cell-with-ai-DZT9iD3u.js → add-cell-with-ai-BMzH0JuZ.js} +16 -16
  6. package/dist/assets/{add-connection-dialog-BkOw_6xV.js → add-connection-dialog-D99_HhRY.js} +6 -6
  7. package/dist/assets/{agent-panel-BqveKByM.js → agent-panel-D3tw0ZVQ.js} +4 -4
  8. package/dist/assets/{ai-model-dropdown-kUBiVpDU.js → ai-model-dropdown-CFk3mSaX.js} +4 -4
  9. package/dist/assets/{app-config-button-BIP9636P.js → app-config-button-dGsspe59.js} +1 -1
  10. package/dist/assets/{cell-editor-BvG1FS9o.js → cell-editor-4qf8B3EV.js} +11 -11
  11. package/dist/assets/cell-link-Df1ljd4V.js +1 -0
  12. package/dist/assets/{cells-C-zt9vSm.js → cells-8P3lr_U-.js} +53 -53
  13. package/dist/assets/{chat-display-Cy1vHbo1.js → chat-display-C_teRjCW.js} +1 -1
  14. package/dist/assets/{chat-panel-DuR1jvOQ.js → chat-panel-JtHFdSiv.js} +1 -1
  15. package/dist/assets/{chat-ui-CKOBvIhw.js → chat-ui-M6oxwFAv.js} +1 -1
  16. package/dist/assets/column-preview-Axu0HnwM.js +1 -0
  17. package/dist/assets/{command-palette-5nbOrWhm.js → command-palette-Cy62Xen5.js} +1 -1
  18. package/dist/assets/{common-BSe2tDOe.js → common-B-NUAA5M.js} +1 -1
  19. package/dist/assets/{components-CYlgzOH1.js → components-B7Yfph8S.js} +1 -1
  20. package/dist/assets/{components-Cndifajt.js → components-xJCIMxt1.js} +1 -1
  21. package/dist/assets/{datasource-B5Gx1j-U.js → datasource-CLc7VKPR.js} +2 -2
  22. package/dist/assets/{dependency-graph-panel-BPtH_wJ1.js → dependency-graph-panel-CTP4_7yE.js} +4 -4
  23. package/dist/assets/{documentation-panel-QMpJX-io.js → documentation-panel-CuZ0vIRJ.js} +1 -1
  24. package/dist/assets/{download-CHp_KlYC.js → download-C9hPuN_P.js} +4 -4
  25. package/dist/assets/{edit-page-tJU1mQbt.js → edit-page-DVwbBFLu.js} +6 -6
  26. package/dist/assets/{error-panel-CiIhCuXf.js → error-panel-w2nF9mem.js} +1 -1
  27. package/dist/assets/{file-explorer-panel-Dasma46_.js → file-explorer-panel-gGGNwR-_.js} +4 -4
  28. package/dist/assets/{file-icons-1cJrwij8.js → file-icons-1S5VCaFF.js} +1 -1
  29. package/dist/assets/{floating-outline-2vMPOy7R.js → floating-outline-BZXnTZ1r.js} +1 -1
  30. package/dist/assets/{focus-aICI9il4.js → focus-D4FN7qb2.js} +1 -1
  31. package/dist/assets/{form-B4Y7m0Y8.js → form-BfXyiimk.js} +2 -2
  32. package/dist/assets/{home-page-l2Uwlgx8.js → home-page-Bgh9Dueq.js} +2 -2
  33. package/dist/assets/{hooks-h06-I7oI.js → hooks-D_QT1i4z.js} +1 -1
  34. package/dist/assets/{html-to-image-DdeXbx5f.js → html-to-image-1NtYwXkp.js} +1 -1
  35. package/dist/assets/index-BFiDkTwh.css +2 -0
  36. package/dist/assets/{index-DHRoJnig.js → index-D87tudCA.js} +14 -14
  37. package/dist/assets/{kiosk-mode-B3GfdoMW.js → kiosk-mode-CIxmIlNA.js} +1 -1
  38. package/dist/assets/{layout-D_yZuR1P.js → layout-j6GRgN2K.js} +4 -4
  39. package/dist/assets/{logs-panel-QGqHp8Lf.js → logs-panel-BUepHA7e.js} +1 -1
  40. package/dist/assets/{markdown-renderer-DIkByGeb.js → markdown-renderer-1cPM8PkF.js} +2 -2
  41. package/dist/assets/{name-cell-input-CupzvuGZ.js → name-cell-input-BQJD6TKP.js} +1 -1
  42. package/dist/assets/{outline-panel-Do1J9aXm.js → outline-panel-DccaAsRc.js} +1 -1
  43. package/dist/assets/{packages-panel-BA5ljE7v.js → packages-panel-abeJJVa6.js} +1 -1
  44. package/dist/assets/{panels-Cfv16ydA.js → panels--d4wmuBV.js} +1 -1
  45. package/dist/assets/{process-output-BuQnmsaO.js → process-output-Co0mD7M7.js} +1 -1
  46. package/dist/assets/{readonly-python-code-DtngV3fU.js → readonly-python-code-BYdGKgJO.js} +1 -1
  47. package/dist/assets/{run-page-Chg0jNDK.js → run-page-D4Vzm-Eb.js} +1 -1
  48. package/dist/assets/{scratchpad-panel-CLseGAxm.js → scratchpad-panel-CqsZSlFQ.js} +1 -1
  49. package/dist/assets/{session-panel-CCaqRzAi.js → session-panel-C2KoZApR.js} +1 -1
  50. package/dist/assets/{snippets-panel-ejQ4jU5T.js → snippets-panel-dNanbU6P.js} +1 -1
  51. package/dist/assets/{state-1T5LfJM0.js → state-8mx-Gzpe.js} +1 -1
  52. package/dist/assets/{state-tjRHDzCg.js → state-BjyYO_pj.js} +2 -2
  53. package/dist/assets/{textarea-zk31mf1n.js → textarea-BlamcRdb.js} +1 -1
  54. package/dist/assets/{tracing-DvOtM0ZS.js → tracing-B8NKIKOU.js} +1 -1
  55. package/dist/assets/{tracing-panel-B8UydmGI.js → tracing-panel-BqQ3Q7g4.js} +2 -2
  56. package/dist/assets/{useAddCell-Bj6kS9rn.js → useAddCell-CgMJXG5u.js} +1 -1
  57. package/dist/assets/useCellActionButton-DUVPJ4Hl.js +1 -0
  58. package/dist/assets/{useDeleteCell-lj49IMPc.js → useDeleteCell-dIpO8Ciq.js} +1 -1
  59. package/dist/assets/{useDependencyPanelTab-CHu-36YS.js → useDependencyPanelTab-D3NiPzz8.js} +1 -1
  60. package/dist/assets/{useNotebookActions-CthH4-0C.js → useNotebookActions-1VNlr8Vn.js} +1 -1
  61. package/dist/assets/useRunCells-AtsnERdg.js +1 -0
  62. package/dist/assets/useSplitCell-D9U8UlYq.js +1 -0
  63. package/dist/index.html +23 -23
  64. package/package.json +1 -1
  65. package/src/components/data-table/export-actions.tsx +62 -23
  66. package/src/components/data-table/schemas.ts +8 -1
  67. package/src/components/datasources/__tests__/missing-package-prompt.test.tsx +103 -0
  68. package/src/components/datasources/missing-package-prompt.tsx +49 -0
  69. package/src/core/mode.ts +10 -1
  70. package/dist/assets/JsonOutput-DDoiOoWw.js +0 -49
  71. package/dist/assets/cell-link-Dh2V_x75.js +0 -1
  72. package/dist/assets/column-preview-C2kPae07.js +0 -1
  73. package/dist/assets/index-CKRn_SiB.css +0 -2
  74. package/dist/assets/useCellActionButton-eFsG32d8.js +0 -1
  75. package/dist/assets/useRunCells-C1d4GNkJ.js +0 -1
  76. package/dist/assets/useSplitCell-Dpecjgrm.js +0 -1
@@ -20,6 +20,7 @@ import {
20
20
  jsonToMarkdown,
21
21
  jsonToTSV,
22
22
  } from "@/utils/json/json-parser";
23
+ import { MissingPackagePrompt } from "../datasources/missing-package-prompt";
23
24
  import { Button } from "../ui/button";
24
25
  import {
25
26
  DropdownMenu,
@@ -38,6 +39,8 @@ export interface ExportActionProps {
38
39
  downloadAs: (req: { format: DownloadFormat }) => Promise<{
39
40
  url: string;
40
41
  filename: string;
42
+ error?: string | null;
43
+ missing_packages?: string[] | null;
41
44
  }>;
42
45
  }
43
46
 
@@ -81,6 +84,8 @@ const copyOptions = [
81
84
  FILE_TYPES.CSV,
82
85
  FILE_TYPES.MARKDOWN,
83
86
  ];
87
+ const labelForDownloadFormat = (format: DownloadFormat): string =>
88
+ downloadOptions.find((opt) => opt.format === format)?.label ?? format;
84
89
 
85
90
  export const ExportMenu: React.FC<ExportActionProps> = (props) => {
86
91
  const { locale } = useLocale();
@@ -101,43 +106,82 @@ export const ExportMenu: React.FC<ExportActionProps> = (props) => {
101
106
  </Button>
102
107
  );
103
108
 
104
- const getDownloadResult = (format: DownloadFormat) => {
105
- return props.downloadAs({ format }).catch((error) => {
109
+ const resolveDownloadUrl = async (
110
+ format: DownloadFormat,
111
+ onRetry: () => void,
112
+ ): Promise<{ url: string; filename: string } | null> => {
113
+ let response: Awaited<ReturnType<typeof props.downloadAs>>;
114
+ try {
115
+ response = await props.downloadAs({ format });
116
+ } catch (error) {
106
117
  toast({
107
118
  title: "Failed to download",
108
- description: "message" in error ? error.message : String(error),
119
+ description:
120
+ error != null && typeof error === "object" && "message" in error
121
+ ? String(error.message)
122
+ : String(error),
109
123
  });
110
- throw error;
124
+ return null;
125
+ }
126
+
127
+ if (response.missing_packages && response.missing_packages.length > 0) {
128
+ toast({
129
+ title: "Export failed",
130
+ description: (
131
+ <MissingPackagePrompt
132
+ packages={response.missing_packages}
133
+ featureName={`${labelForDownloadFormat(format)} export`}
134
+ description={response.error}
135
+ onInstall={onRetry}
136
+ />
137
+ ),
138
+ });
139
+ return null;
140
+ }
141
+
142
+ return { url: response.url, filename: response.filename };
143
+ };
144
+
145
+ const handleDownload = async (format: DownloadFormat) => {
146
+ const result = await resolveDownloadUrl(format, () => {
147
+ void handleDownload(format);
111
148
  });
149
+ if (!result) {
150
+ return;
151
+ }
152
+ const rawName = (result.filename ?? "").trim();
153
+ const baseName = Filenames.withoutExtension(rawName) || "download";
154
+ downloadByURL(result.url, `${baseName}.${format}`);
112
155
  };
113
156
 
114
157
  const handleClipboardCopy = async (
115
158
  format: (typeof copyOptions)[number]["format"],
116
159
  ) => {
117
- let text: string;
160
+ const sourceFormat: DownloadFormat = format === "csv" ? "csv" : "json";
161
+ const result = await resolveDownloadUrl(sourceFormat, () => {
162
+ void handleClipboardCopy(format);
163
+ });
164
+ if (!result) {
165
+ return;
166
+ }
118
167
 
168
+ let text: string;
119
169
  switch (format) {
120
170
  case "tsv": {
121
- const { url } = await getDownloadResult("json");
122
- const json = await fetchJson(url);
171
+ const json = await fetchJson(result.url);
123
172
  text = jsonToTSV(json, locale);
124
173
  break;
125
174
  }
126
175
  case "json": {
127
- const { url } = await getDownloadResult("json");
128
- const json = await fetchJson(url);
176
+ const json = await fetchJson(result.url);
129
177
  text = JSON.stringify(json, null, 2);
130
178
  break;
131
179
  }
132
- case "csv": {
133
- const { url } = await getDownloadResult("csv");
134
- const csv = await fetchText(url);
135
- text = csv;
180
+ case "csv":
181
+ text = await fetchText(result.url);
136
182
  break;
137
- }
138
183
  case "markdown": {
139
- const { url } = await getDownloadResult("json");
140
- const json = await fetchJson(url);
184
+ const json = await fetchJson(result.url);
141
185
  text = jsonToMarkdown(json);
142
186
  break;
143
187
  }
@@ -164,13 +208,8 @@ export const ExportMenu: React.FC<ExportActionProps> = (props) => {
164
208
  {downloadOptions.map((option) => (
165
209
  <DropdownMenuItem
166
210
  key={option.label}
167
- onSelect={async () => {
168
- const { url, filename } = await getDownloadResult(option.format);
169
- const ext = option.format;
170
- const rawName = (filename ?? "").trim();
171
- const baseName =
172
- Filenames.withoutExtension(rawName) || "download";
173
- downloadByURL(url, `${baseName}.${ext}`);
211
+ onSelect={() => {
212
+ void handleDownload(option.format);
174
213
  }}
175
214
  >
176
215
  <option.icon className="mo-dropdown-icon" />
@@ -5,7 +5,12 @@ import { rpc } from "@/plugins/core/rpc";
5
5
 
6
6
  export type DownloadAsArgs = (req: {
7
7
  format: "csv" | "json" | "parquet";
8
- }) => Promise<{ url: string; filename: string }>;
8
+ }) => Promise<{
9
+ url: string;
10
+ filename: string;
11
+ error?: string | null;
12
+ missing_packages?: string[] | null;
13
+ }>;
9
14
 
10
15
  export const DownloadAsSchema = rpc
11
16
  .input(
@@ -17,5 +22,7 @@ export const DownloadAsSchema = rpc
17
22
  z.object({
18
23
  url: z.string(),
19
24
  filename: z.string(),
25
+ error: z.string().nullish(),
26
+ missing_packages: z.array(z.string()).nullish(),
20
27
  }),
21
28
  );
@@ -0,0 +1,103 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { render, screen } from "@testing-library/react";
4
+ import { Provider } from "jotai";
5
+ import React from "react";
6
+ import { beforeEach, describe, expect, it, vi } from "vitest";
7
+ import { MockRequestClient } from "@/__mocks__/requests";
8
+ import { viewStateAtom } from "@/core/mode";
9
+ import { requestClientAtom } from "@/core/network/requests";
10
+ import { store } from "@/core/state/jotai";
11
+ import { MissingPackagePrompt } from "../missing-package-prompt";
12
+
13
+ const mockOpenApplication = vi.fn();
14
+ vi.mock("@/components/editor/chrome/state", () => ({
15
+ useChromeActions: () => ({
16
+ openApplication: mockOpenApplication,
17
+ }),
18
+ }));
19
+
20
+ vi.mock("@/components/editor/chrome/panels/packages-state", () => ({
21
+ packagesToInstallAtom: {},
22
+ }));
23
+
24
+ const mockRequestAnimationFrame = vi.fn((callback) => {
25
+ callback();
26
+ return 1;
27
+ });
28
+ vi.stubGlobal("requestAnimationFrame", mockRequestAnimationFrame);
29
+
30
+ const mockInput = {
31
+ focus: vi.fn(),
32
+ value: "",
33
+ dispatchEvent: vi.fn(),
34
+ };
35
+ vi.spyOn(document, "getElementById").mockReturnValue(
36
+ mockInput as unknown as HTMLInputElement,
37
+ );
38
+
39
+ function createTestWrapper() {
40
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
41
+ <Provider store={store}>{children}</Provider>
42
+ );
43
+ return { wrapper };
44
+ }
45
+
46
+ describe("MissingPackagePrompt", () => {
47
+ const { wrapper } = createTestWrapper();
48
+
49
+ beforeEach(() => {
50
+ vi.clearAllMocks();
51
+ store.set(requestClientAtom, MockRequestClient.create());
52
+ store.set(viewStateAtom, { mode: "edit", cellAnchor: null });
53
+ });
54
+
55
+ it("should render backend description and install button in edit mode", () => {
56
+ render(
57
+ <MissingPackagePrompt
58
+ packages={["polars"]}
59
+ featureName="Parquet export"
60
+ description="Parquet export requires a DataFrame library."
61
+ />,
62
+ { wrapper },
63
+ );
64
+ expect(
65
+ screen.getByText("Parquet export requires a DataFrame library."),
66
+ ).toBeInTheDocument();
67
+ expect(screen.getByText("Install polars")).toBeInTheDocument();
68
+ });
69
+
70
+ it("should fall back to generic copy when description is absent", () => {
71
+ render(
72
+ <MissingPackagePrompt
73
+ packages={["polars"]}
74
+ featureName="Parquet export"
75
+ />,
76
+ { wrapper },
77
+ );
78
+ expect(
79
+ screen.getByText("Parquet export requires polars"),
80
+ ).toBeInTheDocument();
81
+ expect(screen.getByText("Install polars")).toBeInTheDocument();
82
+ });
83
+
84
+ it("should render generic line in read mode and not leak packages or description", () => {
85
+ store.set(viewStateAtom, { mode: "read", cellAnchor: null });
86
+ render(
87
+ <MissingPackagePrompt
88
+ packages={["polars"]}
89
+ featureName="Parquet export"
90
+ description="Install polars to enable Parquet."
91
+ />,
92
+ { wrapper },
93
+ );
94
+ expect(
95
+ screen.getByText("Parquet export isn't available in this notebook"),
96
+ ).toBeInTheDocument();
97
+ expect(
98
+ screen.queryByText("Install polars to enable Parquet."),
99
+ ).not.toBeInTheDocument();
100
+ expect(screen.queryByText("Install polars")).not.toBeInTheDocument();
101
+ expect(screen.queryByText(/polars/)).not.toBeInTheDocument();
102
+ });
103
+ });
@@ -0,0 +1,49 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { useInstallAllowed } from "@/core/mode";
4
+ import { cn } from "@/utils/cn";
5
+ import { InstallPackageButton } from "./install-package-button";
6
+
7
+ interface MissingPackagePromptProps {
8
+ packages: string[];
9
+ featureName: string;
10
+ description?: string | null; // server message, suppressing this in read-only mode
11
+ onInstall?: () => void;
12
+ className?: string;
13
+ }
14
+
15
+ /**
16
+ * display missing-package requirement with mode-aware copy. In edit mode,
17
+ * shows the backend explanation (if any) plus an install button for missing-packages.
18
+ * read mode, shows a generic "isn't available"
19
+ */
20
+ export const MissingPackagePrompt: React.FC<MissingPackagePromptProps> = ({
21
+ packages,
22
+ featureName,
23
+ description,
24
+ onInstall,
25
+ className,
26
+ }) => {
27
+ const installAllowed = useInstallAllowed();
28
+
29
+ if (!installAllowed) {
30
+ return (
31
+ <span className={cn("text-xs", className)}>
32
+ {featureName} isn't available in this notebook
33
+ </span>
34
+ );
35
+ }
36
+
37
+ return (
38
+ <div className={cn("text-xs flex flex-col items-end gap-2", className)}>
39
+ <span className="self-start">
40
+ {description || `${featureName} requires ${packages.join(", ")}`}
41
+ </span>
42
+ <InstallPackageButton
43
+ packages={packages}
44
+ onInstall={onInstall}
45
+ className="ml-0"
46
+ />
47
+ </div>
48
+ );
49
+ };
package/src/core/mode.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { atom } from "jotai";
3
+ import { atom, useAtomValue } from "jotai";
4
4
  import { isIslands } from "@/core/islands/utils";
5
5
  import { assertExists } from "@/utils/assertExists";
6
6
  import { invariant } from "@/utils/invariant";
@@ -79,3 +79,12 @@ export const viewStateAtom = atom<ViewState>({
79
79
  export const initialModeAtom = atom<AppMode | undefined>(undefined);
80
80
 
81
81
  export const kioskModeAtom = atom<boolean>(false);
82
+
83
+ /**
84
+ * Whether installing packages is allowed in the current view. False in read
85
+ * mode, since end-users of a deployed notebook cannot mutate its environment.
86
+ */
87
+ export function useInstallAllowed(): boolean {
88
+ const { mode } = useAtomValue(viewStateAtom);
89
+ return mode !== "read";
90
+ }