@marimo-team/frontend 0.23.2-dev48 → 0.23.2-dev50
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/assets/{CellStatus-sFlO649o.js → CellStatus-Dl2cuz44.js} +1 -1
- package/dist/assets/JsonOutput-C9sdMiTb.js +49 -0
- package/dist/assets/{MarimoErrorOutput-BgCbGEKp.js → MarimoErrorOutput-B58oLtN_.js} +3 -3
- package/dist/assets/{RenderHTML-CEX3AZCQ.js → RenderHTML-Drt_i2fd.js} +1 -1
- package/dist/assets/{add-cell-with-ai-DZT9iD3u.js → add-cell-with-ai-BMzH0JuZ.js} +16 -16
- package/dist/assets/{add-connection-dialog-BkOw_6xV.js → add-connection-dialog-D99_HhRY.js} +6 -6
- package/dist/assets/{agent-panel-BqveKByM.js → agent-panel-D3tw0ZVQ.js} +4 -4
- package/dist/assets/{ai-model-dropdown-kUBiVpDU.js → ai-model-dropdown-CFk3mSaX.js} +4 -4
- package/dist/assets/{app-config-button-BIP9636P.js → app-config-button-dGsspe59.js} +1 -1
- package/dist/assets/{cell-editor-BvG1FS9o.js → cell-editor-4qf8B3EV.js} +11 -11
- package/dist/assets/cell-link-Df1ljd4V.js +1 -0
- package/dist/assets/{cells-C-zt9vSm.js → cells-8P3lr_U-.js} +53 -53
- package/dist/assets/{chat-display-Cy1vHbo1.js → chat-display-C_teRjCW.js} +1 -1
- package/dist/assets/{chat-panel-DuR1jvOQ.js → chat-panel-JtHFdSiv.js} +1 -1
- package/dist/assets/{chat-ui-CKOBvIhw.js → chat-ui-M6oxwFAv.js} +1 -1
- package/dist/assets/column-preview-Axu0HnwM.js +1 -0
- package/dist/assets/{command-palette-5nbOrWhm.js → command-palette-Cy62Xen5.js} +1 -1
- package/dist/assets/{common-BSe2tDOe.js → common-B-NUAA5M.js} +1 -1
- package/dist/assets/{components-CYlgzOH1.js → components-B7Yfph8S.js} +1 -1
- package/dist/assets/{components-Cndifajt.js → components-xJCIMxt1.js} +1 -1
- package/dist/assets/{datasource-B5Gx1j-U.js → datasource-CLc7VKPR.js} +2 -2
- package/dist/assets/{dependency-graph-panel-BPtH_wJ1.js → dependency-graph-panel-CTP4_7yE.js} +4 -4
- package/dist/assets/{documentation-panel-QMpJX-io.js → documentation-panel-CuZ0vIRJ.js} +1 -1
- package/dist/assets/{download-CHp_KlYC.js → download-C9hPuN_P.js} +4 -4
- package/dist/assets/{edit-page-tJU1mQbt.js → edit-page-DVwbBFLu.js} +6 -6
- package/dist/assets/{error-panel-CiIhCuXf.js → error-panel-w2nF9mem.js} +1 -1
- package/dist/assets/{file-explorer-panel-Dasma46_.js → file-explorer-panel-gGGNwR-_.js} +4 -4
- package/dist/assets/{file-icons-1cJrwij8.js → file-icons-1S5VCaFF.js} +1 -1
- package/dist/assets/{floating-outline-2vMPOy7R.js → floating-outline-BZXnTZ1r.js} +1 -1
- package/dist/assets/{focus-aICI9il4.js → focus-D4FN7qb2.js} +1 -1
- package/dist/assets/{form-B4Y7m0Y8.js → form-BfXyiimk.js} +2 -2
- package/dist/assets/{home-page-l2Uwlgx8.js → home-page-Bgh9Dueq.js} +2 -2
- package/dist/assets/{hooks-h06-I7oI.js → hooks-D_QT1i4z.js} +1 -1
- package/dist/assets/{html-to-image-DdeXbx5f.js → html-to-image-1NtYwXkp.js} +1 -1
- package/dist/assets/index-BFiDkTwh.css +2 -0
- package/dist/assets/{index-DHRoJnig.js → index-D87tudCA.js} +14 -14
- package/dist/assets/{kiosk-mode-B3GfdoMW.js → kiosk-mode-CIxmIlNA.js} +1 -1
- package/dist/assets/{layout-D_yZuR1P.js → layout-j6GRgN2K.js} +4 -4
- package/dist/assets/{logs-panel-QGqHp8Lf.js → logs-panel-BUepHA7e.js} +1 -1
- package/dist/assets/{markdown-renderer-DIkByGeb.js → markdown-renderer-1cPM8PkF.js} +2 -2
- package/dist/assets/{name-cell-input-CupzvuGZ.js → name-cell-input-BQJD6TKP.js} +1 -1
- package/dist/assets/{outline-panel-Do1J9aXm.js → outline-panel-DccaAsRc.js} +1 -1
- package/dist/assets/{packages-panel-BA5ljE7v.js → packages-panel-abeJJVa6.js} +1 -1
- package/dist/assets/{panels-Cfv16ydA.js → panels--d4wmuBV.js} +1 -1
- package/dist/assets/{process-output-BuQnmsaO.js → process-output-Co0mD7M7.js} +1 -1
- package/dist/assets/{readonly-python-code-DtngV3fU.js → readonly-python-code-BYdGKgJO.js} +1 -1
- package/dist/assets/{run-page-Chg0jNDK.js → run-page-D4Vzm-Eb.js} +1 -1
- package/dist/assets/{scratchpad-panel-CLseGAxm.js → scratchpad-panel-CqsZSlFQ.js} +1 -1
- package/dist/assets/{session-panel-CCaqRzAi.js → session-panel-C2KoZApR.js} +1 -1
- package/dist/assets/{snippets-panel-ejQ4jU5T.js → snippets-panel-dNanbU6P.js} +1 -1
- package/dist/assets/{state-1T5LfJM0.js → state-8mx-Gzpe.js} +1 -1
- package/dist/assets/{state-tjRHDzCg.js → state-BjyYO_pj.js} +2 -2
- package/dist/assets/{textarea-zk31mf1n.js → textarea-BlamcRdb.js} +1 -1
- package/dist/assets/{tracing-DvOtM0ZS.js → tracing-B8NKIKOU.js} +1 -1
- package/dist/assets/{tracing-panel-B8UydmGI.js → tracing-panel-BqQ3Q7g4.js} +2 -2
- package/dist/assets/{useAddCell-Bj6kS9rn.js → useAddCell-CgMJXG5u.js} +1 -1
- package/dist/assets/useCellActionButton-DUVPJ4Hl.js +1 -0
- package/dist/assets/{useDeleteCell-lj49IMPc.js → useDeleteCell-dIpO8Ciq.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-CHu-36YS.js → useDependencyPanelTab-D3NiPzz8.js} +1 -1
- package/dist/assets/{useNotebookActions-CthH4-0C.js → useNotebookActions-1VNlr8Vn.js} +1 -1
- package/dist/assets/useRunCells-AtsnERdg.js +1 -0
- package/dist/assets/useSplitCell-D9U8UlYq.js +1 -0
- package/dist/index.html +23 -23
- package/package.json +1 -1
- package/src/components/data-table/export-actions.tsx +62 -23
- package/src/components/data-table/schemas.ts +8 -1
- package/src/components/datasources/__tests__/missing-package-prompt.test.tsx +103 -0
- package/src/components/datasources/missing-package-prompt.tsx +49 -0
- package/src/core/mode.ts +10 -1
- package/dist/assets/JsonOutput-DDoiOoWw.js +0 -49
- package/dist/assets/cell-link-Dh2V_x75.js +0 -1
- package/dist/assets/column-preview-C2kPae07.js +0 -1
- package/dist/assets/index-CKRn_SiB.css +0 -2
- package/dist/assets/useCellActionButton-eFsG32d8.js +0 -1
- package/dist/assets/useRunCells-C1d4GNkJ.js +0 -1
- 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
|
|
105
|
-
|
|
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:
|
|
119
|
+
description:
|
|
120
|
+
error != null && typeof error === "object" && "message" in error
|
|
121
|
+
? String(error.message)
|
|
122
|
+
: String(error),
|
|
109
123
|
});
|
|
110
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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={
|
|
168
|
-
|
|
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<{
|
|
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
|
+
}
|