@marimo-team/frontend 0.23.16-dev2 → 0.23.16-dev21
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-BalYv0pK.js → CellStatus-DwV4gbV3.js} +1 -1
- package/dist/assets/{ConnectedDataExplorerComponent-DI7mV-Gw.js → ConnectedDataExplorerComponent-CMaZapRd.js} +1 -1
- package/dist/assets/{ErrorBoundary-C7_5jjWi.js → ErrorBoundary-DzTD8LVq.js} +1 -1
- package/dist/assets/{ImperativeModal-BIavOiUv.js → ImperativeModal-fY-ZfT0b.js} +1 -1
- package/dist/assets/JsonOutput-BoDVlAbV.js +53 -0
- package/dist/assets/{LazyAnyLanguageCodeMirror-sVm_hz8A.js → LazyAnyLanguageCodeMirror-B2JvSXP-.js} +2 -2
- package/dist/assets/{MarimoErrorOutput-DO2k2fA5.js → MarimoErrorOutput-C2HrF0bD.js} +5 -5
- package/dist/assets/{RSPContexts-CgC81f0I.js → RSPContexts-WiCgGm__.js} +1 -1
- package/dist/assets/RenderHTML-DoCFSir5.js +1 -0
- package/dist/assets/{RunButton-BKRupqr7.js → RunButton-COtmJuup.js} +1 -1
- package/dist/assets/{add-cell-with-ai-CEZtaZCK.js → add-cell-with-ai-96GHR2R7.js} +16 -16
- package/dist/assets/add-connection-dialog-wLF3on0N.js +170 -0
- package/dist/assets/{agent-panel-By4A-Cqr.js → agent-panel-xLPKpIwr.js} +5 -5
- package/dist/assets/ai-model-dropdown-CR8HQGYf.js +5 -0
- package/dist/assets/{alert-dialog-BXegNC0I.js → alert-dialog-Bb8Lz8ko.js} +1 -1
- package/dist/assets/{any-language-editor-DCpZRrpx.js → any-language-editor--tRHs8C_.js} +1 -1
- package/dist/assets/{app-config-button-NrM9ywXA.js → app-config-button-Ch-fmU-u.js} +1 -1
- package/dist/assets/{button-eIihcbUw.js → button-DDGiD1NW.js} +1 -1
- package/dist/assets/{cache-panel-wllSTlbM.js → cache-panel-DcQUeUpg.js} +1 -1
- package/dist/assets/{capabilities-B09j1Eor.js → capabilities-DoEg0XwL.js} +1 -1
- package/dist/assets/{cell-editor-DdofA9oQ.js → cell-editor-CxP3SFwW.js} +10 -10
- package/dist/assets/cell-link-D1nZ5myN.js +1 -0
- package/dist/assets/cells-D9tCNe6o.js +231 -0
- package/dist/assets/{chat-display-BG08ltQ3.js → chat-display-PstK5lnL.js} +1 -1
- package/dist/assets/{chat-panel-P2CzCtXJ.js → chat-panel-XuGXjT-8.js} +3 -3
- package/dist/assets/{chat-ui-fAJZAoTD.js → chat-ui-Cme86OAK.js} +1 -1
- package/dist/assets/{column-preview-BgZLUEOj.js → column-preview-BYiduGzU.js} +1 -1
- package/dist/assets/{command-CAJLPEBq.js → command-Dqxie4gq.js} +1 -1
- package/dist/assets/{command-palette-DrI24FoL.js → command-palette-C7NlQPUx.js} +1 -1
- package/dist/assets/{common-CD6Mj8n6.js → common-XBd1qRDk.js} +1 -1
- package/dist/assets/components-D-aPO6md.js +1 -0
- package/dist/assets/config-B-Q7J_6d.js +1 -0
- package/dist/assets/{config-COUOIPEq.js → config-BJVx3eBW.js} +1 -1
- package/dist/assets/{copy-BtTOqPTS.js → copy-Bxb1_Gtw.js} +1 -1
- package/dist/assets/{copy-icon-C2pG0ZNK.js → copy-icon-neDQUo4Q.js} +1 -1
- package/dist/assets/{createReducer-C7kZ8zEe.js → createReducer-DGx5Af-C.js} +1 -1
- package/dist/assets/{datasource-ySAiC9gr.js → datasource-Pl4KKOUa.js} +2 -2
- package/dist/assets/{dates-D-kIKDYA.js → dates-C2LWzsCZ.js} +1 -1
- package/dist/assets/{dependency-graph-panel-BF23EwcD.js → dependency-graph-panel-98TlJ5y7.js} +4 -4
- package/dist/assets/{dialog-D1DEsRZ2.js → dialog-BCVepWLJ.js} +1 -1
- package/dist/assets/{dist-ByVeMWkr.js → dist-BLN8PNJV.js} +1 -1
- package/dist/assets/dist-CiVShZKM.js +2 -0
- package/dist/assets/documentation-panel-DSOMQtKU.js +1 -0
- package/dist/assets/{download-BomYt3nl.js → download-CnYLah74.js} +3 -3
- package/dist/assets/{dropdown-menu-BFZLnWeO.js → dropdown-menu-dHmZR8bI.js} +1 -1
- package/dist/assets/{edit-page-C37w54dQ.js → edit-page-CchyCkOl.js} +6 -6
- package/dist/assets/{error-banner-B6eIlxzg.js → error-banner-BSxEOIn2.js} +1 -1
- package/dist/assets/{error-entries-Birw-8VX.js → error-entries-CmfGHGq1.js} +3 -3
- package/dist/assets/{error-panel-DdsUL6b7.js → error-panel-BKb_UksO.js} +1 -1
- package/dist/assets/{es-1D83cgiu.js → es-gYgfF8y5.js} +1 -1
- package/dist/assets/{esm-BL7jQmzn.js → esm-B30N1GS0.js} +1 -1
- package/dist/assets/{field-BeZbVzXA.js → field-BsxD35VE.js} +1 -1
- package/dist/assets/file-explorer-panel-8_ps5wdU.js +26 -0
- package/dist/assets/{file-icons-Cb54mIL8.js → file-icons-Czm7CWoc.js} +1 -1
- package/dist/assets/{file-name-input-BWFcK1ey.js → file-name-input-CjWG0FWT.js} +1 -1
- package/dist/assets/floating-outline-BWk7h_Uf.js +1 -0
- package/dist/assets/focus-DTmSq3hN.js +1 -0
- package/dist/assets/{form-CgWNb44_.js → form-B5UJtZJr.js} +1 -1
- package/dist/assets/{formats-ClEFGPy-.js → formats-DzmLpc9A.js} +1 -1
- package/dist/assets/{formatting-B_41gSF5.js → formatting-CDf6w1Kt.js} +1 -1
- package/dist/assets/{fullscreen-dUjPYOmm.js → fullscreen-CTDqNQr6.js} +1 -1
- package/dist/assets/{gallery-page-CWQQAYpa.js → gallery-page-CJ3ms1Hn.js} +1 -1
- package/dist/assets/{glide-data-editor-lMARFWHS.js → glide-data-editor-j2igs9Y_.js} +1 -1
- package/dist/assets/home-page-cBU6sDBU.js +4 -0
- package/dist/assets/{html-to-image-CwmX28T9.js → html-to-image-1vflBYah.js} +2 -2
- package/dist/assets/index-B8eK-daS.js +38 -0
- package/dist/assets/index-xs-zSSrX.css +2 -0
- package/dist/assets/{input-CqrUjynb.js → input-JxFwu-3G.js} +1 -1
- package/dist/assets/kiosk-mode-BumOFEtY.js +1 -0
- package/dist/assets/{label-BElwWzf0.js → label-CqjrpwRC.js} +1 -1
- package/dist/assets/{layout-BOwE7yok.js → layout-DkpUSblm.js} +5 -5
- package/dist/assets/{links-CToBjIIj.js → links-CZr5bjvo.js} +1 -1
- package/dist/assets/{logs-panel-iFV4ZJ9b.js → logs-panel-BojuPimp.js} +1 -1
- package/dist/assets/{markdown-renderer-QvXG0CRk.js → markdown-renderer-C9vdWclk.js} +2 -2
- package/dist/assets/{menu-items-DbSLBfXR.js → menu-items-CCi6aOPm.js} +1 -1
- package/dist/assets/{mermaid-Bh0flEIb.js → mermaid-CYN0wOyG.js} +1 -1
- package/dist/assets/name-cell-input-BSFsnuTp.js +1 -0
- package/dist/assets/node-sql-parser-Si4oZ2P-.js +68 -0
- package/dist/assets/{numbers-CeeaFKqu.js → numbers-rmawRqQP.js} +1 -1
- package/dist/assets/outline-panel-B1f5a2yI.js +1 -0
- package/dist/assets/{packages-panel-BzQF60rG.js → packages-panel-BKuXx1gi.js} +1 -1
- package/dist/assets/{pair-with-agent-modal-CKOYwOYw.js → pair-with-agent-modal-nwd26aKZ.js} +1 -1
- package/dist/assets/panels-Cvyb3kny.js +4 -0
- package/dist/assets/{popover-DrHQ3KEt.js → popover-Bcz1Zikb.js} +1 -1
- package/dist/assets/process-output-Csd6PmBH.js +1 -0
- package/dist/assets/{radio-group-CLBjGkt5.js → radio-group-CdR34W0K.js} +1 -1
- package/dist/assets/readonly-code-display-XBAHhCYR.js +1 -0
- package/dist/assets/{readonly-python-code-CW_G5e4E.js → readonly-python-code-BqcMp63O.js} +1 -1
- package/dist/assets/{renderShortcut-BZLr8D7H.js → renderShortcut-Bl8_FVn8.js} +1 -1
- package/dist/assets/{reveal-component-DqMRxmUr.js → reveal-component-BvsuzhVi.js} +57 -57
- package/dist/assets/{run-page-s6Mf8p78.js → run-page-B9jHpo8w.js} +1 -1
- package/dist/assets/{runs-D51Xwrsk.js → runs-DBTXqJHS.js} +1 -1
- package/dist/assets/scratchpad-panel-D3slJkhR.js +1 -0
- package/dist/assets/{scroll-to-bottom-button-Cgr10kaJ.js → scroll-to-bottom-button-C8yfT4wq.js} +1 -1
- package/dist/assets/{secrets-panel-Cc5KDBk6.js → secrets-panel-6J6wcuVR.js} +1 -1
- package/dist/assets/{select-B5TFNjmU.js → select-BP75_5jJ.js} +1 -1
- package/dist/assets/{session-CEFnN9Cq.js → session-BmukRjSi.js} +1 -1
- package/dist/assets/{session-panel-CmHetqAb.js → session-panel-DO-4U6Ry.js} +1 -1
- package/dist/assets/{share-CYpcX4N4.js → share-852X_cqy.js} +1 -1
- package/dist/assets/{skeleton-B_4CZVEm.js → skeleton-C3fwwBky.js} +1 -1
- package/dist/assets/{snippets-panel-CXpGflMO.js → snippets-panel-ACMhA8oL.js} +1 -1
- package/dist/assets/{spec-CzqcmYQk.js → spec-C7opZZRp.js} +1 -1
- package/dist/assets/{state-BvdK8eiw.js → state-D10U2MwA.js} +1 -1
- package/dist/assets/{state-BXbiRXZk.js → state-D2ccy8VQ.js} +3 -3
- package/dist/assets/{state-WBgc7MiA.js → state-DDag0B_B.js} +1 -1
- package/dist/assets/{state-DBzEQtWz.js → state-N0U-K9F6.js} +1 -1
- package/dist/assets/{strings-Djnbwmfl.js → strings--BknHX9C.js} +1 -1
- package/dist/assets/{swiper-component-DInvCxhI.js → swiper-component-Che9MNs4.js} +1 -1
- package/dist/assets/{switch-CvweaCXq.js → switch-Drt3XC71.js} +1 -1
- package/dist/assets/{terminal-D3IQBBYo.js → terminal-217O8hlZ.js} +1 -1
- package/dist/assets/{textarea-CpFfEUet.js → textarea-BQE_pb7P.js} +1 -1
- package/dist/assets/{tooltip-CFX4fYHU.js → tooltip-4bZlU7jl.js} +1 -1
- package/dist/assets/{tooltip-HLb6RxVp.js → tooltip-B7U4xkIN.js} +1 -1
- package/dist/assets/{tracing-CKn3gWHw.js → tracing-CuJfR83E.js} +1 -1
- package/dist/assets/{tracing-panel-BU-o_LCd.js → tracing-panel-CmMG0R-H.js} +2 -2
- package/dist/assets/{tree-actions-CsC2XmGo.js → tree-actions-DJTGQWz0.js} +1 -1
- package/dist/assets/{useBoolean-D89u30n-.js → useBoolean-CFj86iac.js} +1 -1
- package/dist/assets/useCellActionButton-BhCYWOKS.js +1 -0
- package/dist/assets/{useDeleteCell-EPH4kcG7.js → useDeleteCell-DsWH8wmn.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-CZnK7N2L.js → useDependencyPanelTab-CAI77Oxj.js} +1 -1
- package/dist/assets/useEventListener-C0195JsF.js +1 -0
- package/dist/assets/{useHotkey-DIuRH8Wu.js → useHotkey-DLZvrRZT.js} +1 -1
- package/dist/assets/{useIframeCapabilities-Ccb4fvKc.js → useIframeCapabilities-j6fR_dBA.js} +1 -1
- package/dist/assets/{useInstallPackage-BRbUAUFn.js → useInstallPackage-WiYBwhUO.js} +1 -1
- package/dist/assets/{useInterval-CC0vKpWm.js → useInterval-CWBTUy5W.js} +1 -1
- package/dist/assets/{useNotebookActions-DJKDd0gM.js → useNotebookActions-9T9wvd8s.js} +4 -4
- package/dist/assets/useRunCells-BpMmDGqa.js +1 -0
- package/dist/assets/{useSplitCell-9Qz93yPZ.js → useSplitCell-CapBPXO1.js} +1 -1
- package/dist/assets/{useTheme-KXqAyDPC.js → useTheme-Bxv9B31g.js} +1 -1
- package/dist/assets/{utils-Z80SV4_d.js → utils-BWBBpCkT.js} +1 -1
- package/dist/assets/{vega-component-Cie3ag_T.js → vega-component-Db8KjZJG.js} +1 -1
- package/dist/assets/{write-secret-modal-DEEn7sZV.js → write-secret-modal-3-v-dHVj.js} +1 -1
- package/dist/index.html +68 -68
- package/package.json +2 -2
- package/src/components/data-table/column-formatting/feature.ts +2 -1
- package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +50 -0
- package/src/components/editor/connections/storage/__tests__/as-code.test.ts +87 -8
- package/src/components/editor/connections/storage/as-code.ts +40 -21
- package/src/components/editor/connections/storage/schemas.ts +83 -42
- package/src/components/editor/file-tree/__tests__/file-render-mode.test.ts +27 -0
- package/src/components/editor/file-tree/__tests__/file-viewer.test.tsx +150 -0
- package/src/components/editor/file-tree/file-preview-metadata.tsx +51 -0
- package/src/components/editor/file-tree/file-render-mode.ts +32 -0
- package/src/components/editor/file-tree/file-viewer.tsx +76 -50
- package/src/components/slides/__tests__/slide-scroll-hint.test.tsx +198 -0
- package/src/components/slides/reveal-component.tsx +3 -2
- package/src/components/slides/slide-scroll-hint.tsx +161 -0
- package/src/components/ui/input.tsx +1 -1
- package/src/core/codemirror/keymaps/__tests__/keymaps.test.ts +31 -2
- package/src/core/codemirror/keymaps/keymaps.ts +12 -0
- package/src/core/codemirror/language/__tests__/sql.test.ts +443 -61
- package/src/core/codemirror/language/languages/sql/completion-store.ts +5 -1
- package/src/core/codemirror/language/languages/sql/sql.ts +210 -3
- package/src/core/hotkeys/hotkeys.ts +0 -2
- package/src/core/i18n/__tests__/locale-provider.test.tsx +54 -118
- package/src/core/i18n/__tests__/locale.test.ts +113 -0
- package/src/core/i18n/locale-provider.tsx +1 -19
- package/src/core/i18n/locale.ts +71 -0
- package/src/core/islands/__tests__/bridge.test.ts +107 -0
- package/src/core/islands/bridge.ts +15 -0
- package/src/core/mode.ts +10 -0
- package/src/core/network/__tests__/connection.test.ts +45 -0
- package/src/core/network/__tests__/requests-network.test.ts +13 -0
- package/src/core/network/connection.ts +15 -0
- package/src/core/network/requests-network.ts +6 -3
- package/src/core/network/types.ts +3 -1
- package/src/plugins/impl/FormPlugin.tsx +1 -1
- package/src/plugins/impl/__tests__/FormPlugin.test.tsx +162 -0
- package/dist/assets/JsonOutput-DnGSkLeP.js +0 -53
- package/dist/assets/RenderHTML-BCvU9dYz.js +0 -1
- package/dist/assets/add-connection-dialog-CEp3SJR2.js +0 -170
- package/dist/assets/ai-model-dropdown-DXgLphFa.js +0 -5
- package/dist/assets/cell-link-DqBkyAFe.js +0 -1
- package/dist/assets/cells-Cf7bHHq3.js +0 -229
- package/dist/assets/components-BH6gDVv6.js +0 -1
- package/dist/assets/config-BbOTTCYY.js +0 -1
- package/dist/assets/dist-BCMBu0rO.js +0 -2
- package/dist/assets/documentation-panel-e9Fi9R7e.js +0 -1
- package/dist/assets/file-explorer-panel-DbdoJbbF.js +0 -26
- package/dist/assets/floating-outline-wz-WSRgP.js +0 -1
- package/dist/assets/focus-DiEyfiAG.js +0 -1
- package/dist/assets/home-page-DJ4jJsZj.js +0 -4
- package/dist/assets/index-A71LAt6h.css +0 -2
- package/dist/assets/index-Cl0lNhQU.js +0 -38
- package/dist/assets/kiosk-mode-ABm3oYgq.js +0 -1
- package/dist/assets/name-cell-input-C6cukQ9k.js +0 -1
- package/dist/assets/node-sql-parser-BWgUjcfV.js +0 -68
- package/dist/assets/outline-panel-BPlg9_CE.js +0 -1
- package/dist/assets/panels-CW-PUlv2.js +0 -4
- package/dist/assets/process-output-BEhIGfkO.js +0 -1
- package/dist/assets/readonly-code-display-D63oRtBP.js +0 -1
- package/dist/assets/scratchpad-panel-CXsxdMB-.js +0 -1
- package/dist/assets/useCellActionButton-CRfIEVB5.js +0 -1
- package/dist/assets/useEventListener-B_w3A283.js +0 -1
- package/dist/assets/useRunCells-CSSF_mJ0.js +0 -1
|
@@ -2,6 +2,83 @@
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { FieldOptions } from "@/components/forms/options";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Credentials shared by every S3-compatible store (Amazon S3, CoreWeave, ...).
|
|
7
|
+
*
|
|
8
|
+
* Rendered as tabs: either long-lived access keys, or a credential-vending
|
|
9
|
+
* endpoint (ECS/EKS task roles, CoreWeave sandboxes, ...) that obstore polls
|
|
10
|
+
* with a bearer token read from disk.
|
|
11
|
+
*/
|
|
12
|
+
const s3AuthField = () =>
|
|
13
|
+
z
|
|
14
|
+
.discriminatedUnion("type", [
|
|
15
|
+
z.object({
|
|
16
|
+
type: z.literal("Access keys"),
|
|
17
|
+
access_key_id: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe(
|
|
21
|
+
FieldOptions.of({
|
|
22
|
+
label: "Access Key ID",
|
|
23
|
+
description: "Leave empty to use the default credential chain",
|
|
24
|
+
inputType: "password",
|
|
25
|
+
optionRegex:
|
|
26
|
+
"(access.?key.?id|object.?storage.?key|aws.?access.?key)",
|
|
27
|
+
}),
|
|
28
|
+
),
|
|
29
|
+
secret_access_key: z
|
|
30
|
+
.string()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe(
|
|
33
|
+
FieldOptions.of({
|
|
34
|
+
label: "Secret Access Key",
|
|
35
|
+
inputType: "password",
|
|
36
|
+
optionRegex:
|
|
37
|
+
"(secret.?access.?key|object.?storage.?secret|aws.?secret)",
|
|
38
|
+
}),
|
|
39
|
+
),
|
|
40
|
+
}),
|
|
41
|
+
z.object({
|
|
42
|
+
type: z.literal("Container credentials"),
|
|
43
|
+
container_credentials_full_uri: z
|
|
44
|
+
.string()
|
|
45
|
+
.nonempty()
|
|
46
|
+
.describe(
|
|
47
|
+
FieldOptions.of({
|
|
48
|
+
label: "Credentials URI",
|
|
49
|
+
description: "Endpoint that vends credentials to the container",
|
|
50
|
+
placeholder: "http://169.254.170.23/v1/credentials",
|
|
51
|
+
optionRegex: "container.?credentials",
|
|
52
|
+
}),
|
|
53
|
+
),
|
|
54
|
+
container_authorization_token_file: z
|
|
55
|
+
.string()
|
|
56
|
+
.nonempty()
|
|
57
|
+
.describe(
|
|
58
|
+
FieldOptions.of({
|
|
59
|
+
label: "Authorization Token File",
|
|
60
|
+
description: "File holding the token sent to the credentials URI",
|
|
61
|
+
placeholder: "/var/run/secrets/token",
|
|
62
|
+
optionRegex:
|
|
63
|
+
"(container.?authorization.?token.?file|container.?auth.?token)",
|
|
64
|
+
}),
|
|
65
|
+
),
|
|
66
|
+
}),
|
|
67
|
+
])
|
|
68
|
+
.default({ type: "Access keys" })
|
|
69
|
+
.describe(FieldOptions.of({ label: "Credentials", special: "tabs" }));
|
|
70
|
+
|
|
71
|
+
const allowHttpField = () =>
|
|
72
|
+
z
|
|
73
|
+
.boolean()
|
|
74
|
+
.default(false)
|
|
75
|
+
.describe(
|
|
76
|
+
FieldOptions.of({
|
|
77
|
+
label: "Allow HTTP",
|
|
78
|
+
description: "Required for plain-http (non-TLS) endpoints",
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
|
|
5
82
|
export const S3StorageSchema = z
|
|
6
83
|
.object({
|
|
7
84
|
type: z.literal("s3"),
|
|
@@ -25,36 +102,20 @@ export const S3StorageSchema = z
|
|
|
25
102
|
optionRegex: "(region|aws.?region)",
|
|
26
103
|
}),
|
|
27
104
|
),
|
|
28
|
-
access_key_id: z
|
|
29
|
-
.string()
|
|
30
|
-
.optional()
|
|
31
|
-
.describe(
|
|
32
|
-
FieldOptions.of({
|
|
33
|
-
label: "Access Key ID",
|
|
34
|
-
inputType: "password",
|
|
35
|
-
optionRegex: "(access.?key.?id|aws.?access.?key)",
|
|
36
|
-
}),
|
|
37
|
-
),
|
|
38
|
-
secret_access_key: z
|
|
39
|
-
.string()
|
|
40
|
-
.optional()
|
|
41
|
-
.describe(
|
|
42
|
-
FieldOptions.of({
|
|
43
|
-
label: "Secret Access Key",
|
|
44
|
-
inputType: "password",
|
|
45
|
-
optionRegex: "(secret.?access.?key|aws.?secret)",
|
|
46
|
-
}),
|
|
47
|
-
),
|
|
48
105
|
endpoint_url: z
|
|
49
106
|
.string()
|
|
50
107
|
.optional()
|
|
51
108
|
.describe(
|
|
52
109
|
FieldOptions.of({
|
|
53
110
|
label: "Endpoint URL",
|
|
111
|
+
description:
|
|
112
|
+
"Ignored if the AWS_ENDPOINT_URL_S3 environment variable is set",
|
|
54
113
|
placeholder: "https://s3.amazonaws.com",
|
|
55
114
|
optionRegex: "(endpoint|s3.?url|s3.?endpoint)",
|
|
56
115
|
}),
|
|
57
116
|
),
|
|
117
|
+
allow_http: allowHttpField(),
|
|
118
|
+
auth: s3AuthField(),
|
|
58
119
|
})
|
|
59
120
|
.describe(FieldOptions.of({ direction: "two-columns" }));
|
|
60
121
|
|
|
@@ -139,28 +200,7 @@ export const CoreWeaveStorageSchema = z
|
|
|
139
200
|
placeholder: "US-EAST-04A",
|
|
140
201
|
}),
|
|
141
202
|
),
|
|
142
|
-
|
|
143
|
-
.string()
|
|
144
|
-
.optional()
|
|
145
|
-
.describe(
|
|
146
|
-
FieldOptions.of({
|
|
147
|
-
label: "Access Key ID",
|
|
148
|
-
inputType: "password",
|
|
149
|
-
optionRegex:
|
|
150
|
-
"(access.?key.?id|object.?storage.?key|aws.?access.?key)",
|
|
151
|
-
}),
|
|
152
|
-
),
|
|
153
|
-
secret_access_key: z
|
|
154
|
-
.string()
|
|
155
|
-
.optional()
|
|
156
|
-
.describe(
|
|
157
|
-
FieldOptions.of({
|
|
158
|
-
label: "Secret Access Key",
|
|
159
|
-
inputType: "password",
|
|
160
|
-
optionRegex:
|
|
161
|
-
"(secret.?access.?key|object.?storage.?secret|aws.?secret)",
|
|
162
|
-
}),
|
|
163
|
-
),
|
|
203
|
+
auth: s3AuthField(),
|
|
164
204
|
})
|
|
165
205
|
.describe(FieldOptions.of({ direction: "two-columns" }));
|
|
166
206
|
|
|
@@ -189,3 +229,4 @@ export const StorageConnectionSchema = z.discriminatedUnion("type", [
|
|
|
189
229
|
]);
|
|
190
230
|
|
|
191
231
|
export type StorageConnection = z.infer<typeof StorageConnectionSchema>;
|
|
232
|
+
export type S3Auth = Extract<StorageConnection, { type: "s3" }>["auth"];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { getFileRenderMode } from "../file-render-mode";
|
|
5
|
+
|
|
6
|
+
describe("getFileRenderMode", () => {
|
|
7
|
+
it.each([
|
|
8
|
+
["text/plain", false, "text"],
|
|
9
|
+
["text/x-rust", false, "text"],
|
|
10
|
+
["application/json", false, "text"],
|
|
11
|
+
["application/xml", false, "text"],
|
|
12
|
+
["text/csv", false, "csv"],
|
|
13
|
+
["image/png", true, "media"],
|
|
14
|
+
["video/mp4", true, "media"],
|
|
15
|
+
["application/pdf", true, "media"],
|
|
16
|
+
["application/x-apple-diskimage", true, "unsupported"],
|
|
17
|
+
["application/zip", true, "unsupported"],
|
|
18
|
+
["application/octet-stream", false, "unsupported"],
|
|
19
|
+
[null, false, "text"],
|
|
20
|
+
[null, true, "unsupported"],
|
|
21
|
+
] as const)(
|
|
22
|
+
"maps %s with isBase64=%s to %s",
|
|
23
|
+
(mimeType, isBase64, expected) => {
|
|
24
|
+
expect(getFileRenderMode(mimeType, isBase64)).toBe(expected);
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
4
|
+
import { Provider } from "jotai";
|
|
5
|
+
import type React from "react";
|
|
6
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import { MockRequestClient } from "@/__mocks__/requests";
|
|
8
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
9
|
+
import { requestClientAtom } from "@/core/network/requests";
|
|
10
|
+
import type { FileDetailsResponse, FileInfo } from "@/core/network/types";
|
|
11
|
+
import { store } from "@/core/state/jotai";
|
|
12
|
+
import { FileViewer, MAX_FILE_PREVIEW_BYTES } from "../file-viewer";
|
|
13
|
+
|
|
14
|
+
vi.mock("@/plugins/impl/code/LazyAnyLanguageCodeMirror", () => ({
|
|
15
|
+
LazyAnyLanguageCodeMirror: ({
|
|
16
|
+
value,
|
|
17
|
+
onChange,
|
|
18
|
+
}: {
|
|
19
|
+
value?: string;
|
|
20
|
+
onChange?: (value: string) => void;
|
|
21
|
+
}) => (
|
|
22
|
+
<textarea
|
|
23
|
+
data-testid="code-editor"
|
|
24
|
+
value={value ?? ""}
|
|
25
|
+
onChange={(evt) => onChange?.(evt.target.value)}
|
|
26
|
+
/>
|
|
27
|
+
),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock("@/core/wasm/utils", () => ({ isWasm: () => false }));
|
|
31
|
+
|
|
32
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
33
|
+
<Provider store={store}>
|
|
34
|
+
<TooltipProvider>{children}</TooltipProvider>
|
|
35
|
+
</Provider>
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const file: FileInfo = {
|
|
39
|
+
id: "/workspace/installer.dmg",
|
|
40
|
+
path: "/workspace/installer.dmg",
|
|
41
|
+
name: "installer.dmg",
|
|
42
|
+
isDirectory: false,
|
|
43
|
+
isMarimoFile: false,
|
|
44
|
+
size: 20 * 1024 * 1024,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function renderViewer(response: FileDetailsResponse) {
|
|
48
|
+
const client = MockRequestClient.create({
|
|
49
|
+
sendFileDetails: vi.fn().mockResolvedValue(response),
|
|
50
|
+
});
|
|
51
|
+
store.set(requestClientAtom, client);
|
|
52
|
+
render(<FileViewer file={file} onOpenNotebook={vi.fn()} />, { wrapper });
|
|
53
|
+
return client;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("FileViewer bounded previews", () => {
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
store.set(requestClientAtom, null);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("requests a bounded preview and shows oversized metadata", async () => {
|
|
62
|
+
const client = renderViewer({
|
|
63
|
+
file,
|
|
64
|
+
contents: null,
|
|
65
|
+
mimeType: "application/x-apple-diskimage",
|
|
66
|
+
isBase64: false,
|
|
67
|
+
isTooLarge: true,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(await screen.findByText(/too large to preview/i)).toBeVisible();
|
|
71
|
+
expect(screen.getByText("20 MB")).toBeVisible();
|
|
72
|
+
expect(screen.getByRole("button", { name: /download/i })).toBeVisible();
|
|
73
|
+
expect(screen.queryByTestId("code-editor")).not.toBeInTheDocument();
|
|
74
|
+
expect(client.sendFileDetails).toHaveBeenCalledWith({
|
|
75
|
+
path: file.path,
|
|
76
|
+
maxBytes: MAX_FILE_PREVIEW_BYTES,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("shows unsupported metadata with a download action, not base64 text", async () => {
|
|
81
|
+
renderViewer({
|
|
82
|
+
file: { ...file, size: 4 },
|
|
83
|
+
contents: "AAEC",
|
|
84
|
+
mimeType: "application/x-apple-diskimage",
|
|
85
|
+
isBase64: true,
|
|
86
|
+
isTooLarge: false,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(await screen.findByText(/cannot be previewed/i)).toBeVisible();
|
|
90
|
+
expect(screen.getByRole("button", { name: /download/i })).toBeVisible();
|
|
91
|
+
expect(screen.queryByTestId("code-editor")).not.toBeInTheDocument();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("continues to render small text files", async () => {
|
|
95
|
+
renderViewer({
|
|
96
|
+
file: { ...file, name: "notes.txt", size: 5 },
|
|
97
|
+
contents: "hello",
|
|
98
|
+
mimeType: "text/plain",
|
|
99
|
+
isBase64: false,
|
|
100
|
+
isTooLarge: false,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await waitFor(() => {
|
|
104
|
+
expect(screen.getByTestId("code-editor")).toBeInTheDocument();
|
|
105
|
+
});
|
|
106
|
+
expect(screen.queryByText(/too large to preview/i)).not.toBeInTheDocument();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("preserves edits to an initially empty text file across remount", async () => {
|
|
110
|
+
const emptyFile: FileInfo = {
|
|
111
|
+
id: "/workspace/empty.txt",
|
|
112
|
+
path: "/workspace/empty.txt",
|
|
113
|
+
name: "empty.txt",
|
|
114
|
+
isDirectory: false,
|
|
115
|
+
isMarimoFile: false,
|
|
116
|
+
size: 0,
|
|
117
|
+
};
|
|
118
|
+
const response: FileDetailsResponse = {
|
|
119
|
+
file: emptyFile,
|
|
120
|
+
contents: "",
|
|
121
|
+
mimeType: "text/plain",
|
|
122
|
+
isBase64: false,
|
|
123
|
+
isTooLarge: false,
|
|
124
|
+
};
|
|
125
|
+
const client = MockRequestClient.create({
|
|
126
|
+
sendFileDetails: vi.fn().mockResolvedValue(response),
|
|
127
|
+
});
|
|
128
|
+
store.set(requestClientAtom, client);
|
|
129
|
+
|
|
130
|
+
const { unmount } = render(
|
|
131
|
+
<FileViewer file={emptyFile} onOpenNotebook={vi.fn()} />,
|
|
132
|
+
{ wrapper },
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const editor =
|
|
136
|
+
await screen.findByTestId<HTMLTextAreaElement>("code-editor");
|
|
137
|
+
fireEvent.change(editor, { target: { value: "draft" } });
|
|
138
|
+
unmount();
|
|
139
|
+
|
|
140
|
+
render(<FileViewer file={emptyFile} onOpenNotebook={vi.fn()} />, {
|
|
141
|
+
wrapper,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const reopened =
|
|
145
|
+
await screen.findByTestId<HTMLTextAreaElement>("code-editor");
|
|
146
|
+
await waitFor(() => {
|
|
147
|
+
expect(reopened.value).toBe("draft");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { DownloadIcon } from "lucide-react";
|
|
4
|
+
import { useLocale } from "react-aria";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import type { FileInfo } from "@/core/network/types";
|
|
7
|
+
import { formatBytes } from "@/utils/formatting";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
file: FileInfo;
|
|
11
|
+
mimeType: string;
|
|
12
|
+
message: string;
|
|
13
|
+
onDownload?: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const FilePreviewMetadata = ({
|
|
17
|
+
file,
|
|
18
|
+
mimeType,
|
|
19
|
+
message,
|
|
20
|
+
onDownload,
|
|
21
|
+
}: Props) => {
|
|
22
|
+
const { locale } = useLocale();
|
|
23
|
+
return (
|
|
24
|
+
<div className="p-4 text-sm">
|
|
25
|
+
<div className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-2">
|
|
26
|
+
<span className="text-muted-foreground font-medium">Path</span>
|
|
27
|
+
<span className="font-mono text-xs break-all">{file.path}</span>
|
|
28
|
+
<span className="text-muted-foreground font-medium">Type</span>
|
|
29
|
+
<span>{mimeType}</span>
|
|
30
|
+
{file.size != null && (
|
|
31
|
+
<>
|
|
32
|
+
<span className="text-muted-foreground font-medium">Size</span>
|
|
33
|
+
<span>{formatBytes(file.size, locale)}</span>
|
|
34
|
+
</>
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
<div className="mt-4 text-muted-foreground italic">{message}</div>
|
|
38
|
+
{onDownload && (
|
|
39
|
+
<Button
|
|
40
|
+
variant="outline"
|
|
41
|
+
size="sm"
|
|
42
|
+
className="mt-4"
|
|
43
|
+
onClick={onDownload}
|
|
44
|
+
>
|
|
45
|
+
<DownloadIcon className="h-3.5 w-3.5 mr-2" />
|
|
46
|
+
Download
|
|
47
|
+
</Button>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { isMediaMime, MIME_TO_LANGUAGE } from "./renderers";
|
|
4
|
+
|
|
5
|
+
export type FileRenderMode = "text" | "csv" | "media" | "unsupported";
|
|
6
|
+
|
|
7
|
+
export function getFileRenderMode(
|
|
8
|
+
mimeType: string | null | undefined,
|
|
9
|
+
isBase64: boolean | undefined,
|
|
10
|
+
): FileRenderMode {
|
|
11
|
+
const mime = mimeType || "text/plain";
|
|
12
|
+
|
|
13
|
+
if (isMediaMime(mime)) {
|
|
14
|
+
return "media";
|
|
15
|
+
}
|
|
16
|
+
if (isBase64) {
|
|
17
|
+
return "unsupported";
|
|
18
|
+
}
|
|
19
|
+
if (mime === "text/csv") {
|
|
20
|
+
return "csv";
|
|
21
|
+
}
|
|
22
|
+
if (
|
|
23
|
+
mime.startsWith("text/") ||
|
|
24
|
+
(mime !== "default" && mime in MIME_TO_LANGUAGE)
|
|
25
|
+
) {
|
|
26
|
+
return "text";
|
|
27
|
+
}
|
|
28
|
+
if (mimeType == null) {
|
|
29
|
+
return "text";
|
|
30
|
+
}
|
|
31
|
+
return "unsupported";
|
|
32
|
+
}
|
|
@@ -25,11 +25,11 @@ import { copyToClipboard } from "@/utils/copy";
|
|
|
25
25
|
import type { Base64String } from "@/utils/json/base64";
|
|
26
26
|
import { downloadFile } from "./download";
|
|
27
27
|
import { FilePreviewHeader } from "./file-header";
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
import { FilePreviewMetadata } from "./file-preview-metadata";
|
|
29
|
+
import { getFileRenderMode } from "./file-render-mode";
|
|
30
|
+
import { FileContentRenderer } from "./renderers";
|
|
31
|
+
|
|
32
|
+
export const MAX_FILE_PREVIEW_BYTES = 10 * 1024 * 1024;
|
|
33
33
|
|
|
34
34
|
interface Props {
|
|
35
35
|
file: FileInfo;
|
|
@@ -50,9 +50,15 @@ export const FileViewer: React.FC<Props> = ({ file, onOpenNotebook }) => {
|
|
|
50
50
|
|
|
51
51
|
const { data, isPending, error, setData, refetch } =
|
|
52
52
|
useAsyncData(async () => {
|
|
53
|
-
const details = await sendFileDetails({
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
const details = await sendFileDetails({
|
|
54
|
+
path: file.path,
|
|
55
|
+
maxBytes: MAX_FILE_PREVIEW_BYTES,
|
|
56
|
+
});
|
|
57
|
+
const mode = getFileRenderMode(details.mimeType, details.isBase64);
|
|
58
|
+
if (!details.isTooLarge && mode === "text") {
|
|
59
|
+
const contents = details.contents || "";
|
|
60
|
+
setInternalValue(unsavedContentsForFile.get(file.path) ?? contents);
|
|
61
|
+
}
|
|
56
62
|
return details;
|
|
57
63
|
}, [file.path]);
|
|
58
64
|
|
|
@@ -72,13 +78,19 @@ export const FileViewer: React.FC<Props> = ({ file, onOpenNotebook }) => {
|
|
|
72
78
|
);
|
|
73
79
|
};
|
|
74
80
|
|
|
75
|
-
// On file change or unmount, save the unsaved contents
|
|
81
|
+
// On file change or unmount, save the unsaved contents. Only editable text
|
|
82
|
+
// previews participate in editor state; oversized and non-text responses
|
|
83
|
+
// carry no draft to preserve.
|
|
76
84
|
// We use a ref for internalValue so we don't call this effect on each keystroke
|
|
77
85
|
const internalValueRef = useRef<string>(internalValue);
|
|
78
86
|
internalValueRef.current = internalValue;
|
|
79
87
|
useEffect(() => {
|
|
80
88
|
return () => {
|
|
81
|
-
if (
|
|
89
|
+
if (
|
|
90
|
+
data?.contents == null ||
|
|
91
|
+
data.isTooLarge ||
|
|
92
|
+
getFileRenderMode(data.mimeType, data.isBase64) !== "text"
|
|
93
|
+
) {
|
|
82
94
|
return;
|
|
83
95
|
}
|
|
84
96
|
const draft = internalValueRef.current;
|
|
@@ -88,7 +100,13 @@ export const FileViewer: React.FC<Props> = ({ file, onOpenNotebook }) => {
|
|
|
88
100
|
unsavedContentsForFile.set(file.path, draft);
|
|
89
101
|
}
|
|
90
102
|
};
|
|
91
|
-
}, [
|
|
103
|
+
}, [
|
|
104
|
+
file.path,
|
|
105
|
+
data?.contents,
|
|
106
|
+
data?.isTooLarge,
|
|
107
|
+
data?.mimeType,
|
|
108
|
+
data?.isBase64,
|
|
109
|
+
]);
|
|
92
110
|
|
|
93
111
|
if (error) {
|
|
94
112
|
return <ErrorBanner error={error} />;
|
|
@@ -99,7 +117,10 @@ export const FileViewer: React.FC<Props> = ({ file, onOpenNotebook }) => {
|
|
|
99
117
|
}
|
|
100
118
|
|
|
101
119
|
const mimeType = data.mimeType || "text/plain";
|
|
102
|
-
const
|
|
120
|
+
const renderMode = getFileRenderMode(data.mimeType, data.isBase64);
|
|
121
|
+
const isText = renderMode === "text";
|
|
122
|
+
const isCsv = renderMode === "csv";
|
|
123
|
+
const isMedia = renderMode === "media";
|
|
103
124
|
const isActiveNotebook =
|
|
104
125
|
currentNotebookFilename &&
|
|
105
126
|
data.file.isMarimoFile &&
|
|
@@ -108,22 +129,27 @@ export const FileViewer: React.FC<Props> = ({ file, onOpenNotebook }) => {
|
|
|
108
129
|
// but this is an okay heuristic for now.
|
|
109
130
|
file.path.endsWith(`/${currentNotebookFilename}`));
|
|
110
131
|
|
|
111
|
-
if (!data.contents && !isEditable) {
|
|
112
|
-
// Show details instead of contents
|
|
113
|
-
return (
|
|
114
|
-
<div className="grid grid-cols-2 gap-2 p-6">
|
|
115
|
-
<div className="font-bold text-muted-foreground">Name</div>
|
|
116
|
-
<div>{data.file.name}</div>
|
|
117
|
-
<div className="font-bold text-muted-foreground">Type</div>
|
|
118
|
-
<div>{mimeType}</div>
|
|
119
|
-
</div>
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
132
|
const handleDownload = async () => {
|
|
124
133
|
await downloadFile(file.path, data.file.name);
|
|
125
134
|
};
|
|
126
135
|
|
|
136
|
+
const saveKeymapExtension = keymap.of([
|
|
137
|
+
{
|
|
138
|
+
key: hotkeys.getHotkey("global.save").key,
|
|
139
|
+
stopPropagation: true,
|
|
140
|
+
run: () => {
|
|
141
|
+
if (internalValue !== data.contents) {
|
|
142
|
+
void handleSaveFile();
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
// Defined before the metadata-only returns so refresh, download, and
|
|
151
|
+
// open-notebook stay available while edit-only copy/save are withheld from
|
|
152
|
+
// non-text previews.
|
|
127
153
|
const header = (
|
|
128
154
|
<FilePreviewHeader
|
|
129
155
|
filename={data.file.name}
|
|
@@ -142,7 +168,7 @@ export const FileViewer: React.FC<Props> = ({ file, onOpenNotebook }) => {
|
|
|
142
168
|
</Button>
|
|
143
169
|
</Tooltip>
|
|
144
170
|
)}
|
|
145
|
-
{
|
|
171
|
+
{isText && (
|
|
146
172
|
<>
|
|
147
173
|
<Tooltip content="Copy contents to clipboard">
|
|
148
174
|
<Button
|
|
@@ -172,8 +198,23 @@ export const FileViewer: React.FC<Props> = ({ file, onOpenNotebook }) => {
|
|
|
172
198
|
/>
|
|
173
199
|
);
|
|
174
200
|
|
|
175
|
-
|
|
176
|
-
|
|
201
|
+
if (data.isTooLarge || renderMode === "unsupported") {
|
|
202
|
+
return (
|
|
203
|
+
<>
|
|
204
|
+
{header}
|
|
205
|
+
<FilePreviewMetadata
|
|
206
|
+
file={data.file}
|
|
207
|
+
mimeType={mimeType}
|
|
208
|
+
message={
|
|
209
|
+
data.isTooLarge
|
|
210
|
+
? "File is too large to preview."
|
|
211
|
+
: "This file type cannot be previewed."
|
|
212
|
+
}
|
|
213
|
+
onDownload={disableFileDownloads ? undefined : handleDownload}
|
|
214
|
+
/>
|
|
215
|
+
</>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
177
218
|
|
|
178
219
|
const warningBanner = isText && isActiveNotebook && (
|
|
179
220
|
<Alert variant="warning" className="rounded-none">
|
|
@@ -192,35 +233,20 @@ export const FileViewer: React.FC<Props> = ({ file, onOpenNotebook }) => {
|
|
|
192
233
|
<FileContentRenderer
|
|
193
234
|
mimeType={mimeType}
|
|
194
235
|
contents={
|
|
195
|
-
|
|
196
|
-
?
|
|
197
|
-
:
|
|
198
|
-
?
|
|
199
|
-
:
|
|
236
|
+
isText
|
|
237
|
+
? internalValue
|
|
238
|
+
: isCsv
|
|
239
|
+
? (data.contents ?? undefined)
|
|
240
|
+
: undefined
|
|
200
241
|
}
|
|
201
242
|
mediaSource={
|
|
202
|
-
isMedia
|
|
243
|
+
isMedia && data.contents
|
|
203
244
|
? { base64: data.contents as Base64String, mime: mimeType }
|
|
204
245
|
: undefined
|
|
205
246
|
}
|
|
206
247
|
readOnly={!isText}
|
|
207
|
-
onChange={setInternalValue}
|
|
208
|
-
extensions={[
|
|
209
|
-
// Command S for save
|
|
210
|
-
keymap.of([
|
|
211
|
-
{
|
|
212
|
-
key: hotkeys.getHotkey("global.save").key,
|
|
213
|
-
stopPropagation: true,
|
|
214
|
-
run: () => {
|
|
215
|
-
if (internalValue !== data.contents) {
|
|
216
|
-
handleSaveFile();
|
|
217
|
-
return true;
|
|
218
|
-
}
|
|
219
|
-
return false;
|
|
220
|
-
},
|
|
221
|
-
},
|
|
222
|
-
]),
|
|
223
|
-
]}
|
|
248
|
+
onChange={isText ? setInternalValue : undefined}
|
|
249
|
+
extensions={isText ? [saveKeymapExtension] : []}
|
|
224
250
|
/>
|
|
225
251
|
</>
|
|
226
252
|
);
|