@marimo-team/frontend 0.25.3-dev2 → 0.25.3-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-BP2mavAq.js → CellStatus-O0rU3_5j.js} +1 -1
- package/dist/assets/{ConnectedDataExplorerComponent-t8fH_TNc.js → ConnectedDataExplorerComponent-TprnxZeJ.js} +1 -1
- package/dist/assets/{ErrorBoundary-Gl5nCA8C.js → ErrorBoundary-BkSCfv3B.js} +2 -2
- package/dist/assets/JsonOutput-Dq6MGIMV.js +11 -0
- package/dist/assets/{LazyAnyLanguageCodeMirror-CKcCbG2G.js → LazyAnyLanguageCodeMirror-B4fCknq1.js} +2 -2
- package/dist/assets/MarimoErrorOutput-BLtCVffa.js +10 -0
- package/dist/assets/{RenderHTML-BbOqmHy3.js → RenderHTML-PtQ99V0D.js} +1 -1
- package/dist/assets/{RunButton-CK3LhnP5.js → RunButton-B8KRdPZ-.js} +1 -1
- package/dist/assets/{add-cell-with-ai-8UBC0xwR.js → add-cell-with-ai-BOBZjUxI.js} +13 -13
- package/dist/assets/{add-connection-dialog-DXQnd6eB.js → add-connection-dialog-DP62zXw2.js} +2 -2
- package/dist/assets/{agent-panel-D7kMZLG-.js → agent-panel-BFul7UiF.js} +5 -5
- package/dist/assets/ai-model-dropdown-sTKkdrbX.js +13 -0
- package/dist/assets/{any-language-editor-BLCAD3yx.js → any-language-editor-BdTWdMCI.js} +1 -1
- package/dist/assets/{app-config-button-BKEG4x9u.js → app-config-button-Bp44UZdD.js} +1 -1
- package/dist/assets/{blob-D5nfNHOo.js → blob-ykpU69Lk.js} +1 -1
- package/dist/assets/{cell-editor-ADVfycpN.js → cell-editor-B7YlotyH.js} +9 -9
- package/dist/assets/{cell-link-LSth_8hT.js → cell-link-CmO9D2a2.js} +1 -1
- package/dist/assets/{cells-BRkCaSsh.js → cells-BMs-CYDt.js} +31 -31
- package/dist/assets/{chat-display-Dnagy4Nv.js → chat-display-Dw8HQAAY.js} +1 -1
- package/dist/assets/{chat-panel-DQ6HkWEF.js → chat-panel-DPDZrfoJ.js} +2 -2
- package/dist/assets/{chat-ui-Df8Vcj6n.js → chat-ui-BxnFw9Za.js} +1 -1
- package/dist/assets/code-visibility-CGVmpSdE.js +1 -0
- package/dist/assets/coerce-DWjFeXOY.js +1 -0
- package/dist/assets/{column-preview-Cm2D6vyX.js → column-preview-BQ4k86Lv.js} +1 -1
- package/dist/assets/{command-palette-DTSQ7gOC.js → command-palette-BfxYnBUD.js} +1 -1
- package/dist/assets/{common-Cu6CDbuw.js → common-CzWTWhcY.js} +1 -1
- package/dist/assets/{components-CGw16R_N.js → components-C-p2nJ78.js} +1 -1
- package/dist/assets/{components-CP_UoFtT.js → components-CskjK6xZ.js} +1 -1
- package/dist/assets/{components-CXfO-F47.js → components-YBmtV0p6.js} +1 -1
- package/dist/assets/config-BtRLgrlt.js +104 -0
- package/dist/assets/{config-VPSq73n8.js → config-CA1V-wjh.js} +1 -1
- package/dist/assets/config-CMUeR8Wr.js +1 -0
- package/dist/assets/{datasource-Cq6x_5ML.js → datasource-CEDUN47W.js} +1 -1
- package/dist/assets/{dates-BbFQ2oE4.js → dates-CfmdTL6B.js} +1 -1
- package/dist/assets/{dependency-graph-panel--Kgtktcw.js → dependency-graph-panel-KWuUrABU.js} +1 -1
- package/dist/assets/{documentation-panel-BHmbeznw.js → documentation-panel-T1wYs9YS.js} +1 -1
- package/dist/assets/{download-DCJXy66H.js → download-Bt4rM2gS.js} +1 -1
- package/dist/assets/edit-page-DIxL37jf.js +9 -0
- package/dist/assets/{error-banner-Cd69vvyX.js → error-banner-COZ7SfYV.js} +1 -1
- package/dist/assets/{error-entries-Dk3VZ-OK.js → error-entries-CQTemuon.js} +1 -1
- package/dist/assets/{error-panel-BPRYmC5J.js → error-panel-BmlN965r.js} +1 -1
- package/dist/assets/{errors-DCrsa1nH.js → errors-CNsgAw6m.js} +1 -1
- package/dist/assets/{feedback-button-Bik2-e33.js → feedback-button-CO89l9HJ.js} +2 -2
- package/dist/assets/feedback-button-QWfd9e1X.js +1 -0
- package/dist/assets/{file-explorer-panel-BEEK5lcl.js → file-explorer-panel-Bs25Xwng.js} +1 -1
- package/dist/assets/{file-name-input-Cdc-TVqO.js → file-name-input-DgR3ynbz.js} +1 -1
- package/dist/assets/{floating-outline-XD9FAV-f.js → floating-outline-kOJ_XOxZ.js} +1 -1
- package/dist/assets/{gallery-page-Dm6mGwBi.js → gallery-page--AiS7R2S.js} +1 -1
- package/dist/assets/{glide-data-editor-J9CAb70d.js → glide-data-editor-FopeFe04.js} +1 -1
- package/dist/assets/{home-page-DMHnsRxm.js → home-page-DEUEDKZQ.js} +1 -1
- package/dist/assets/hooks-CsCHI0xl.js +1 -0
- package/dist/assets/index-6b4mf8Ai.css +2 -0
- package/dist/assets/index-BXheV3uz.js +30 -0
- package/dist/assets/{layout-DmcNEu3P.js → layout-CcQIAduN.js} +5 -5
- package/dist/assets/{logs-panel-ABM16htT.js → logs-panel-Cj-mRfbP.js} +1 -1
- package/dist/assets/{markdown-renderer-BG3sp2rs.js → markdown-renderer-Dg2Tj8xg.js} +1 -1
- package/dist/assets/{mermaid-a3xYymTy.js → mermaid-C4EiVqcd.js} +1 -1
- package/dist/assets/{name-cell-input-BgEBmqyL.js → name-cell-input-DhWcLBL9.js} +1 -1
- package/dist/assets/{outline-panel-DCXQ3_F6.js → outline-panel-DCCN7CCz.js} +1 -1
- package/dist/assets/{packages-panel-BMGOmZkR.js → packages-panel-cI3VRnI0.js} +1 -1
- package/dist/assets/{panel-accordion-state-C1sdPp0m.js → panel-accordion-state-CrUAB8rV.js} +1 -1
- package/dist/assets/panels-DeK2CjCU.js +4 -0
- package/dist/assets/{process-output-D0KGq04R.js → process-output-C0AVlTcL.js} +1 -1
- package/dist/assets/{readonly-code-display-Do50PHI8.js → readonly-code-display-CEGKPKlf.js} +1 -1
- package/dist/assets/{readonly-python-code-DCSMIsTX.js → readonly-python-code-kOLuuvuL.js} +1 -1
- package/dist/assets/{renderShortcut-BkVJQ_Zh.js → renderShortcut-C1JwYcqW.js} +1 -1
- package/dist/assets/{reveal-component-ltaepAqw.js → reveal-component-m_Q_Dhsa.js} +3 -3
- package/dist/assets/{run-page-DF3c5cDk.js → run-page-ZPnT0tT8.js} +1 -1
- package/dist/assets/{runtime-details-state-CSPobEdR.js → runtime-details-state-DRpKx1d7.js} +1 -1
- package/dist/assets/{schemas-Dv42wLFq.js → schemas-ChRqMOTL.js} +1 -1
- package/dist/assets/{scratchpad-panel-Dwd6W5PE.js → scratchpad-panel-4r5I7_4T.js} +1 -1
- package/dist/assets/{secrets-panel-Bctbtkrp.js → secrets-panel-D7KN4tVs.js} +1 -1
- package/dist/assets/session-panel-B-fUHivk.js +1 -0
- package/dist/assets/{snippets-panel-Bh1oTw4m.js → snippets-panel-QqliOy2l.js} +1 -1
- package/dist/assets/state-BAKG3nB6.js +3 -0
- package/dist/assets/{state-XsPO8hNi.js → state-BiuybHTd.js} +1 -1
- package/dist/assets/{terminal-Ck_vK-34.js → terminal-CP30at9D.js} +3 -3
- package/dist/assets/{tracing-Di6pUmX8.js → tracing-Cg6nJ9oa.js} +1 -1
- package/dist/assets/{tracing-panel-PeRXHD8_.js → tracing-panel-_OkwL3ST.js} +2 -2
- package/dist/assets/{tree-actions-GM5E_bUs.js → tree-actions-D5U7Yym8.js} +1 -1
- package/dist/assets/{useAddCell-GR7eEQrT.js → useAddCell-C3jdhSc9.js} +1 -1
- package/dist/assets/{useBoolean-BDD6_xQQ.js → useBoolean-BHCcNH_F.js} +1 -1
- package/dist/assets/{useCellActionButton-B-9eG5z_.js → useCellActionButton-Db-2kvJ0.js} +1 -1
- package/dist/assets/{useDeleteCell-2_bMClHd.js → useDeleteCell-D0dwhgwH.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-k3C3M18S.js → useDependencyPanelTab-BjQVPzm0.js} +1 -1
- package/dist/assets/{useInstallPackage-iX1eufdl.js → useInstallPackage-B-krIeGp.js} +1 -1
- package/dist/assets/useNotebookActions-CY_gTNzg.js +1 -0
- package/dist/assets/{useRunCells-B2vUhREg.js → useRunCells-Dyf_vzJl.js} +1 -1
- package/dist/assets/{useSplitCell-DEb03htp.js → useSplitCell-DA8ujlOv.js} +1 -1
- package/dist/assets/{useTheme-COhSHkCj.js → useTheme-n8Dj1zH7.js} +1 -1
- package/dist/assets/{vega-component-CnadTgoE.js → vega-component-CaZvZfgt.js} +1 -1
- package/dist/index.html +40 -41
- package/package.json +1 -1
- package/src/__tests__/fixtures/{pair-preview.ts → pair-config.ts} +3 -2
- package/src/__tests__/mount.test.ts +28 -8
- package/src/components/data-table/__tests__/context-menu.test.ts +176 -0
- package/src/components/data-table/__tests__/date-filter-inputs.test.tsx +24 -1
- package/src/components/data-table/__tests__/export-actions.test.tsx +329 -6
- package/src/components/data-table/context-menu.tsx +125 -26
- package/src/components/data-table/date-filter-inputs.tsx +15 -42
- package/src/components/data-table/export-actions.tsx +192 -116
- package/src/components/data-table/schemas.ts +14 -36
- package/src/components/data-table/utils.ts +18 -0
- package/src/components/editor/Output.tsx +9 -2
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +80 -252
- package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +186 -34
- package/src/components/editor/actions/pair-with-agent-commands.ts +42 -51
- package/src/components/editor/actions/pair-with-agent-modal.tsx +96 -36
- package/src/components/editor/chrome/panels/error-panel.tsx +1 -0
- package/src/components/editor/chrome/wrapper/__tests__/attachment-status.test.tsx +366 -0
- package/src/components/editor/chrome/wrapper/footer-item.tsx +32 -24
- package/src/components/editor/chrome/wrapper/footer-items/attachment-status.tsx +237 -0
- package/src/components/editor/chrome/wrapper/footer.tsx +2 -0
- package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +537 -0
- package/src/components/editor/errors/send-error-report-button.tsx +69 -0
- package/src/components/editor/output/MarimoErrorOutput.tsx +21 -5
- package/src/components/editor/output/MarimoTracebackOutput.tsx +35 -20
- package/src/components/editor/output/__tests__/ansi-reduce.test.ts +18 -0
- package/src/components/editor/output/ansi-reduce.ts +6 -0
- package/src/core/attachments/__tests__/toast.test.tsx +58 -0
- package/src/core/attachments/state.ts +18 -0
- package/src/core/attachments/toast.ts +15 -0
- package/src/core/cells/__tests__/cells.test.ts +52 -0
- package/src/core/cells/__tests__/error-report.test.ts +139 -0
- package/src/core/cells/__tests__/session.test.ts +50 -1
- package/src/core/cells/error-report.ts +63 -0
- package/src/core/cells/session.ts +28 -1
- package/src/core/config/pair.ts +5 -3
- package/src/core/edit-app.tsx +2 -2
- package/src/core/host/AGENTS.md +5 -0
- package/src/core/host/__tests__/attachments.test.ts +57 -0
- package/src/core/host/__tests__/document.test.ts +124 -0
- package/src/core/host/__tests__/events.test.ts +46 -0
- package/src/core/host/__tests__/fixtures.ts +39 -0
- package/src/core/host/__tests__/notebook-client.test.ts +433 -0
- package/src/core/host/__tests__/stream.test.ts +211 -0
- package/src/core/host/connection.ts +81 -0
- package/src/core/host/events.ts +61 -0
- package/src/core/host/notebook-client.ts +290 -0
- package/src/core/host/resources/attachments.ts +19 -0
- package/src/core/host/resources/document.ts +101 -0
- package/src/core/host/resources/resource.ts +59 -0
- package/src/core/host/stream.ts +213 -0
- package/src/core/network/connection.ts +4 -2
- package/src/core/run-app.tsx +2 -2
- package/src/core/runtime/__tests__/runtime.test.ts +55 -11
- package/src/core/runtime/runtime.ts +15 -1
- package/src/core/runtime/types.ts +2 -0
- package/src/core/websocket/__tests__/{useMarimoKernelConnection.test.ts → connection-state.test.ts} +15 -17
- package/src/core/websocket/__tests__/{useWebSocket.test.ts → createConnectionTransport.test.ts} +1 -1
- package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +170 -0
- package/src/core/websocket/__tests__/{useMarimoKernelConnection.hook.test.tsx → useNotebookSession.hook.test.tsx} +181 -65
- package/src/core/websocket/connection-state.ts +70 -0
- package/src/core/websocket/state.ts +7 -0
- package/src/core/websocket/transports/__tests__/sse.test.ts +1 -4
- package/src/core/websocket/transports/create.ts +31 -0
- package/src/core/websocket/transports/transport.ts +12 -23
- package/src/core/websocket/types.ts +9 -39
- package/src/core/websocket/useNotebookClient.ts +88 -0
- package/src/core/websocket/{useMarimoKernelConnection.tsx → useNotebookSession.tsx} +59 -225
- package/src/mount.tsx +7 -3
- package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +84 -0
- package/src/utils/__tests__/dates.test.ts +116 -0
- package/src/utils/dates.ts +94 -0
- package/dist/assets/JsonOutput-D6HIIsCb.js +0 -11
- package/dist/assets/MarimoErrorOutput-D60XkCny.js +0 -6
- package/dist/assets/ai-model-dropdown-B0z6-zhp.js +0 -5
- package/dist/assets/api-BeC_VSGv.js +0 -1
- package/dist/assets/code-visibility-B-_GvwR5.js +0 -1
- package/dist/assets/coerce-CpE6kUdX.js +0 -1
- package/dist/assets/config-BOKzA3tF.js +0 -1
- package/dist/assets/config-DX8Y2WbJ.js +0 -1
- package/dist/assets/edit-page-ChcSgpke.js +0 -9
- package/dist/assets/feedback-button-B2gKC5cn.js +0 -1
- package/dist/assets/hooks-93IwHd2u.js +0 -1
- package/dist/assets/index-Bcdzex-3.css +0 -2
- package/dist/assets/index-s-M1OXjs.js +0 -29
- package/dist/assets/pair-CaQyK6En.js +0 -1
- package/dist/assets/panels-CxVG8-v2.js +0 -4
- package/dist/assets/session-panel-VdDhEBa-.js +0 -1
- package/dist/assets/state-_Qai_QyX.js +0 -3
- package/dist/assets/useNotebookActions-NHN6A-cN.js +0 -1
- package/src/core/websocket/useWebSocket.tsx +0 -88
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { invalidatePackageData } from "@/core/packages/package-data";
|
|
4
|
-
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
|
5
|
-
import {
|
|
4
|
+
import { useAtom, useAtomValue, useSetAtom, useStore } from "jotai";
|
|
5
|
+
import { useLayoutEffect } from "react";
|
|
6
6
|
import { useErrorBoundary } from "react-error-boundary";
|
|
7
7
|
import { toast } from "@/components/ui/use-toast";
|
|
8
8
|
import { getNotebook, useCellActions } from "@/core/cells/cells";
|
|
@@ -12,8 +12,7 @@ import type {
|
|
|
12
12
|
NotificationMessageData,
|
|
13
13
|
NotificationPayload,
|
|
14
14
|
} from "@/core/kernel/messages";
|
|
15
|
-
import {
|
|
16
|
-
import { useConnectionTransport } from "@/core/websocket/useWebSocket";
|
|
15
|
+
import { useNotebookClient } from "@/core/websocket/useNotebookClient";
|
|
17
16
|
import { renderHTML } from "@/plugins/core/RenderHTML";
|
|
18
17
|
import {
|
|
19
18
|
handleWidgetMessage,
|
|
@@ -21,11 +20,7 @@ import {
|
|
|
21
20
|
} from "@/plugins/impl/anywidget/registry";
|
|
22
21
|
import { logNever } from "@/utils/assertNever";
|
|
23
22
|
import { prettyError } from "@/utils/errors";
|
|
24
|
-
import {
|
|
25
|
-
type JsonString,
|
|
26
|
-
safeExtractSetUIElementMessageBuffers,
|
|
27
|
-
} from "@/utils/json/base64";
|
|
28
|
-
import { jsonParseWithSpecialChar } from "@/utils/json/json-parser";
|
|
23
|
+
import { safeExtractSetUIElementMessageBuffers } from "@/utils/json/base64";
|
|
29
24
|
import { Logger } from "@/utils/Logger";
|
|
30
25
|
import { reloadSafe } from "@/utils/reload-safe";
|
|
31
26
|
import { useAlertActions } from "../alerts/state";
|
|
@@ -70,7 +65,7 @@ import { type LayoutState, useLayoutActions } from "../layout/state";
|
|
|
70
65
|
import { kioskModeAtom } from "../mode";
|
|
71
66
|
import { connectionAtom, startupProgressAtom } from "../network/connection";
|
|
72
67
|
import type { RequestId } from "../network/DeferredRequestRegistry";
|
|
73
|
-
import { useRuntimeManager } from "../runtime/config";
|
|
68
|
+
import { runtimeConfigAtom, useRuntimeManager } from "../runtime/config";
|
|
74
69
|
import { SECRETS_REGISTRY } from "../secrets/request-registry";
|
|
75
70
|
import { isStaticNotebook } from "../static/static-state";
|
|
76
71
|
import {
|
|
@@ -81,104 +76,10 @@ import { useStorageActions } from "../storage/state";
|
|
|
81
76
|
import { useVariablesActions } from "../variables/state";
|
|
82
77
|
import type { VariableName } from "../variables/types";
|
|
83
78
|
import { isWasm } from "../wasm/utils";
|
|
84
|
-
import {
|
|
85
|
-
type ConnectionStatus,
|
|
86
|
-
WebSocketClosedReason,
|
|
87
|
-
ConnectionState,
|
|
88
|
-
} from "./types";
|
|
79
|
+
import { WebSocketClosedReason, ConnectionState } from "./types";
|
|
89
80
|
|
|
90
81
|
const SUPPORTS_LAZY_KERNELS = true;
|
|
91
82
|
|
|
92
|
-
// All MARIMO_* reasons except TRANSPORT_EXHAUSTED are emitted by the backend
|
|
93
|
-
// (marimo/_server/api/endpoints/ws_endpoint.py and ws/*.py). Keep in sync with
|
|
94
|
-
// the backend literals. Over the SSE transport these arrive as in-band
|
|
95
|
-
// `close` events rather than WebSocket close frames, so every terminal
|
|
96
|
-
// reason must be classified here — an unclassified reason falls into the
|
|
97
|
-
// retry path.
|
|
98
|
-
export type CloseReason =
|
|
99
|
-
| "MARIMO_NO_FILE_KEY"
|
|
100
|
-
| "MARIMO_NO_SESSION_ID"
|
|
101
|
-
| "MARIMO_NO_SESSION"
|
|
102
|
-
| "MARIMO_SHUTDOWN"
|
|
103
|
-
| "MARIMO_KERNEL_STARTUP_ERROR"
|
|
104
|
-
| "MARIMO_UNAUTHORIZED"
|
|
105
|
-
| "MARIMO_KIOSK_NOT_ALLOWED"
|
|
106
|
-
| typeof TRANSPORT_EXHAUSTED_REASON;
|
|
107
|
-
|
|
108
|
-
export type CloseDecision =
|
|
109
|
-
| { kind: "terminal"; status: ConnectionStatus; closeTransport: boolean }
|
|
110
|
-
| { kind: "gave-up"; status: ConnectionStatus }
|
|
111
|
-
| { kind: "retry"; status: ConnectionStatus };
|
|
112
|
-
|
|
113
|
-
export function classifyCloseEvent(event: { reason?: string }): CloseDecision {
|
|
114
|
-
switch (event.reason as CloseReason | undefined) {
|
|
115
|
-
case TRANSPORT_EXHAUSTED_REASON:
|
|
116
|
-
return {
|
|
117
|
-
kind: "gave-up",
|
|
118
|
-
status: {
|
|
119
|
-
state: ConnectionState.CLOSED,
|
|
120
|
-
code: WebSocketClosedReason.KERNEL_DISCONNECTED,
|
|
121
|
-
reason: "kernel not found",
|
|
122
|
-
},
|
|
123
|
-
};
|
|
124
|
-
case "MARIMO_NO_FILE_KEY":
|
|
125
|
-
case "MARIMO_NO_SESSION_ID":
|
|
126
|
-
case "MARIMO_NO_SESSION":
|
|
127
|
-
case "MARIMO_SHUTDOWN":
|
|
128
|
-
return {
|
|
129
|
-
kind: "terminal",
|
|
130
|
-
status: {
|
|
131
|
-
state: ConnectionState.CLOSED,
|
|
132
|
-
code: WebSocketClosedReason.KERNEL_DISCONNECTED,
|
|
133
|
-
reason: "kernel not found",
|
|
134
|
-
},
|
|
135
|
-
closeTransport: true,
|
|
136
|
-
};
|
|
137
|
-
case "MARIMO_UNAUTHORIZED":
|
|
138
|
-
return {
|
|
139
|
-
kind: "terminal",
|
|
140
|
-
status: {
|
|
141
|
-
state: ConnectionState.CLOSED,
|
|
142
|
-
code: WebSocketClosedReason.KERNEL_DISCONNECTED,
|
|
143
|
-
reason: "not authorized",
|
|
144
|
-
},
|
|
145
|
-
closeTransport: true,
|
|
146
|
-
};
|
|
147
|
-
case "MARIMO_KIOSK_NOT_ALLOWED":
|
|
148
|
-
return {
|
|
149
|
-
kind: "terminal",
|
|
150
|
-
status: {
|
|
151
|
-
state: ConnectionState.CLOSED,
|
|
152
|
-
code: WebSocketClosedReason.KERNEL_DISCONNECTED,
|
|
153
|
-
reason: "kiosk mode is not available for this session",
|
|
154
|
-
},
|
|
155
|
-
closeTransport: true,
|
|
156
|
-
};
|
|
157
|
-
case "MARIMO_KERNEL_STARTUP_ERROR":
|
|
158
|
-
return {
|
|
159
|
-
kind: "terminal",
|
|
160
|
-
status: {
|
|
161
|
-
state: ConnectionState.CLOSED,
|
|
162
|
-
code: WebSocketClosedReason.KERNEL_STARTUP_ERROR,
|
|
163
|
-
reason: "Failed to start kernel sandbox",
|
|
164
|
-
},
|
|
165
|
-
closeTransport: true,
|
|
166
|
-
};
|
|
167
|
-
default:
|
|
168
|
-
// Empty/undefined reasons are normal transient closes. Anything else is
|
|
169
|
-
// an unknown server reason; warn so a new MARIMO_* reason doesn't fall
|
|
170
|
-
// silently into the retry path.
|
|
171
|
-
if (event.reason) {
|
|
172
|
-
logNever(event.reason as never);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
kind: "retry",
|
|
178
|
-
status: { state: ConnectionState.CONNECTING },
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
83
|
function getExistingCells(): CellData[] | undefined {
|
|
183
84
|
if (!SUPPORTS_LAZY_KERNELS) {
|
|
184
85
|
return undefined;
|
|
@@ -190,16 +91,12 @@ function getExistingCells(): CellData[] | undefined {
|
|
|
190
91
|
);
|
|
191
92
|
}
|
|
192
93
|
|
|
193
|
-
/**
|
|
194
|
-
|
|
195
|
-
*/
|
|
196
|
-
export function useMarimoKernelConnection(opts: {
|
|
94
|
+
/** Coordinate session startup and apply notifications to the editor. */
|
|
95
|
+
export function useNotebookSession(opts: {
|
|
197
96
|
sessionId: SessionId;
|
|
198
97
|
autoInstantiate: boolean;
|
|
199
98
|
setCells: (cells: CellData[], layout: LayoutState) => void;
|
|
200
99
|
}) {
|
|
201
|
-
// Track whether we want to try reconnecting.
|
|
202
|
-
const shouldTryReconnecting = useRef<boolean>(true);
|
|
203
100
|
const { autoInstantiate, sessionId, setCells } = opts;
|
|
204
101
|
const { showBoundary } = useErrorBoundary();
|
|
205
102
|
|
|
@@ -238,6 +135,7 @@ export function useMarimoKernelConnection(opts: {
|
|
|
238
135
|
const setStableSessionId = useSetAtom(stableSessionIdAtom);
|
|
239
136
|
const setCapabilities = useSetAtom(capabilitiesAtom);
|
|
240
137
|
const runtimeManager = useRuntimeManager();
|
|
138
|
+
const { notebookId } = useAtomValue(runtimeConfigAtom);
|
|
241
139
|
const transportType = useAtomValue(connectionTransportTypeAtom);
|
|
242
140
|
const setCacheInfo = useSetAtom(cacheInfoAtom);
|
|
243
141
|
const setKernelStartupError = useSetAtom(kernelStartupErrorAtom);
|
|
@@ -246,8 +144,7 @@ export function useMarimoKernelConnection(opts: {
|
|
|
246
144
|
filterFromVariables: filterStorageFromVariables,
|
|
247
145
|
} = useStorageActions();
|
|
248
146
|
|
|
249
|
-
const
|
|
250
|
-
const msg = jsonParseWithSpecialChar(e.data);
|
|
147
|
+
const handleNotification = (msg: NotificationPayload) => {
|
|
251
148
|
switch (msg.data.op) {
|
|
252
149
|
case "reload":
|
|
253
150
|
reloadSafe();
|
|
@@ -505,107 +402,58 @@ export function useMarimoKernelConnection(opts: {
|
|
|
505
402
|
}
|
|
506
403
|
};
|
|
507
404
|
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
}
|
|
516
|
-
};
|
|
517
|
-
|
|
518
|
-
// Manual reconnect. Probes /health first to fail fast when the runtime
|
|
519
|
-
// is unreachable, instead of waiting on partysocket's retry budget.
|
|
520
|
-
const reconnect = async () => {
|
|
521
|
-
if (
|
|
522
|
-
ws.readyState === WebSocket.OPEN ||
|
|
523
|
-
ws.readyState === WebSocket.CONNECTING
|
|
524
|
-
) {
|
|
525
|
-
return;
|
|
526
|
-
}
|
|
527
|
-
shouldTryReconnecting.current = true;
|
|
528
|
-
setKernelStartupError(null);
|
|
529
|
-
setConnection({ state: ConnectionState.CONNECTING });
|
|
530
|
-
const healthy = await runtimeManager.reconcileFromHealth();
|
|
531
|
-
if (!healthy) {
|
|
532
|
-
shouldTryReconnecting.current = false;
|
|
533
|
-
setConnection({
|
|
534
|
-
state: ConnectionState.CLOSED,
|
|
535
|
-
code: WebSocketClosedReason.KERNEL_DISCONNECTED,
|
|
536
|
-
reason: "kernel not found",
|
|
537
|
-
});
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
ws.reconnect();
|
|
405
|
+
const store = useStore();
|
|
406
|
+
const onMessageError = (error: unknown) => {
|
|
407
|
+
toast({
|
|
408
|
+
title: "Failed to handle message",
|
|
409
|
+
description: prettyError(error),
|
|
410
|
+
variant: "danger",
|
|
411
|
+
});
|
|
541
412
|
};
|
|
542
|
-
|
|
543
|
-
|
|
413
|
+
const notebook = useNotebookClient({
|
|
414
|
+
sessionId,
|
|
544
415
|
static: isStaticNotebook(),
|
|
545
416
|
transportType,
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
*/
|
|
417
|
+
messageFormat:
|
|
418
|
+
notebookId && transportType === "websocket" ? "host" : "session",
|
|
549
419
|
url: () =>
|
|
550
420
|
transportType === "sse"
|
|
551
421
|
? runtimeManager.getSseURL(sessionId).toString()
|
|
552
422
|
: runtimeManager.getWsURL(sessionId).toString(),
|
|
553
|
-
|
|
554
|
-
* Auth headers for the SSE transport. WebSockets cannot send headers
|
|
555
|
-
* and instead put the access token in the URL.
|
|
556
|
-
*/
|
|
423
|
+
// SSE uses headers for auth. WebSockets carry the token in the URL.
|
|
557
424
|
headers: () => runtimeManager.headers(),
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
*/
|
|
562
|
-
onOpen: async () => {
|
|
563
|
-
updateStartupProgress(null);
|
|
564
|
-
// If we are open, we can reset our reconnecting flag.
|
|
565
|
-
shouldTryReconnecting.current = true;
|
|
566
|
-
},
|
|
567
|
-
|
|
568
|
-
/**
|
|
569
|
-
* Wait to connect, in case the remote kernel still starting up.
|
|
570
|
-
*/
|
|
571
|
-
waitToConnect: async () => {
|
|
572
|
-
if (isStaticNotebook()) {
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
if (isWasm()) {
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
if (runtimeManager.isSameOrigin) {
|
|
580
|
-
return;
|
|
425
|
+
waitUntilReady: async () => {
|
|
426
|
+
if (!isStaticNotebook() && !isWasm() && !runtimeManager.isSameOrigin) {
|
|
427
|
+
await runtimeManager.waitForHealthy();
|
|
581
428
|
}
|
|
582
|
-
await runtimeManager.waitForHealthy();
|
|
583
429
|
},
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
*/
|
|
588
|
-
onMessage: (e) => {
|
|
430
|
+
checkHealth: () => runtimeManager.reconcileFromHealth(),
|
|
431
|
+
onMessageError,
|
|
432
|
+
onNotification: (notification) => {
|
|
589
433
|
try {
|
|
590
|
-
|
|
434
|
+
handleNotification(notification);
|
|
591
435
|
} catch (error) {
|
|
592
|
-
Logger.error("Failed to handle
|
|
593
|
-
|
|
594
|
-
title: "Failed to handle message",
|
|
595
|
-
description: prettyError(error),
|
|
596
|
-
variant: "danger",
|
|
597
|
-
});
|
|
436
|
+
Logger.error("Failed to handle notification", notification, error);
|
|
437
|
+
onMessageError(error);
|
|
598
438
|
}
|
|
599
439
|
},
|
|
440
|
+
});
|
|
600
441
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
442
|
+
// Subscribe before passive connection setup: static/WASM transports can emit synchronously.
|
|
443
|
+
useLayoutEffect(() => {
|
|
444
|
+
const updateConnection = () => {
|
|
445
|
+
const status = store.get(notebook.connectionAtom);
|
|
446
|
+
if (status.state === ConnectionState.NOT_STARTED) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (status.state === ConnectionState.OPEN) {
|
|
450
|
+
updateStartupProgress(null);
|
|
451
|
+
return; // kernel-ready, not the socket, establishes session readiness.
|
|
452
|
+
}
|
|
453
|
+
if (status.state === ConnectionState.CONNECTING) {
|
|
454
|
+
setKernelStartupError(null);
|
|
455
|
+
}
|
|
607
456
|
setConnection((previous) => {
|
|
608
|
-
const status = decision.status;
|
|
609
457
|
if (
|
|
610
458
|
status.state === ConnectionState.CLOSED &&
|
|
611
459
|
status.code === WebSocketClosedReason.KERNEL_STARTUP_ERROR &&
|
|
@@ -624,31 +472,17 @@ export function useMarimoKernelConnection(opts: {
|
|
|
624
472
|
}
|
|
625
473
|
return status;
|
|
626
474
|
});
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
* When we encounter an error, we should close the connection.
|
|
641
|
-
*/
|
|
642
|
-
onError: (e) => {
|
|
643
|
-
Logger.warn("WebSocket error", e);
|
|
644
|
-
setConnection({
|
|
645
|
-
state: ConnectionState.CLOSED,
|
|
646
|
-
code: WebSocketClosedReason.KERNEL_DISCONNECTED,
|
|
647
|
-
reason: "kernel not found",
|
|
648
|
-
});
|
|
649
|
-
tryReconnecting();
|
|
650
|
-
},
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
return { connection, reconnect };
|
|
475
|
+
};
|
|
476
|
+
const unsubscribe = store.sub(notebook.connectionAtom, updateConnection);
|
|
477
|
+
updateConnection();
|
|
478
|
+
return unsubscribe;
|
|
479
|
+
}, [
|
|
480
|
+
notebook,
|
|
481
|
+
store,
|
|
482
|
+
setConnection,
|
|
483
|
+
setKernelStartupError,
|
|
484
|
+
updateStartupProgress,
|
|
485
|
+
]);
|
|
486
|
+
|
|
487
|
+
return { notebook, connection, reconnect: notebook.reconnect };
|
|
654
488
|
}
|
package/src/mount.tsx
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
userConfigAtom,
|
|
11
11
|
} from "@/core/config/config";
|
|
12
12
|
import { KnownQueryParams } from "@/core/constants";
|
|
13
|
-
import {
|
|
13
|
+
import { pairConfigAtom, pairConfigSchema } from "@/core/config/pair";
|
|
14
14
|
import { getFilenameFromDOM } from "@/core/dom/htmlUtils";
|
|
15
15
|
import { getMarimoCode } from "@/core/meta/globals";
|
|
16
16
|
import {
|
|
@@ -209,7 +209,7 @@ const mountOptionsSchema = z.object({
|
|
|
209
209
|
* marimo config overrides
|
|
210
210
|
*/
|
|
211
211
|
configOverrides: passthroughObject,
|
|
212
|
-
|
|
212
|
+
pairConfig: pairConfigSchema.optional(),
|
|
213
213
|
/**
|
|
214
214
|
* marimo app config
|
|
215
215
|
*/
|
|
@@ -274,6 +274,8 @@ const mountOptionsSchema = z.object({
|
|
|
274
274
|
.transform((val) => val as api.Notebook["NotebookV1"]),
|
|
275
275
|
]),
|
|
276
276
|
|
|
277
|
+
notebookId: z.string().optional(),
|
|
278
|
+
|
|
277
279
|
/**
|
|
278
280
|
* Runtime configs
|
|
279
281
|
*/
|
|
@@ -284,6 +286,7 @@ const mountOptionsSchema = z.object({
|
|
|
284
286
|
// Lazy by default, but can be overridden by the runtime config
|
|
285
287
|
lazy: z.boolean().default(true),
|
|
286
288
|
authToken: z.string().nullish(),
|
|
289
|
+
notebookId: z.string().optional(),
|
|
287
290
|
}),
|
|
288
291
|
)
|
|
289
292
|
.nullish()
|
|
@@ -347,7 +350,7 @@ function initStore(options: unknown) {
|
|
|
347
350
|
);
|
|
348
351
|
store.set(userConfigAtom, parseUserConfig(parsedOptions.data.config));
|
|
349
352
|
store.set(appConfigAtom, parseAppConfig(parsedOptions.data.appConfig));
|
|
350
|
-
store.set(
|
|
353
|
+
store.set(pairConfigAtom, parsedOptions.data.pairConfig);
|
|
351
354
|
|
|
352
355
|
// Runtime config
|
|
353
356
|
if (parsedOptions.data.runtimeConfig.length > 0) {
|
|
@@ -364,6 +367,7 @@ function initStore(options: unknown) {
|
|
|
364
367
|
} else {
|
|
365
368
|
store.set(runtimeConfigAtom, {
|
|
366
369
|
...DEFAULT_RUNTIME_CONFIG,
|
|
370
|
+
notebookId: parsedOptions.data.notebookId,
|
|
367
371
|
lazy: false,
|
|
368
372
|
serverToken: parsedOptions.data.serverToken,
|
|
369
373
|
});
|
|
@@ -136,6 +136,90 @@ describe("LoadingDataTableComponent", () => {
|
|
|
136
136
|
expect(downloadGeoJSON).toHaveBeenCalledTimes(1);
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
+
it("refreshes export metadata when a dataframe transform changes the data", async () => {
|
|
140
|
+
const host = document.createElement("div");
|
|
141
|
+
const metadataFor = (name: string) => ({
|
|
142
|
+
geometry_columns: [{ name, encoding: "objects", crs: "EPSG:4326" }],
|
|
143
|
+
primary_geometry_column: name,
|
|
144
|
+
default_geometry_column: name,
|
|
145
|
+
formats: {
|
|
146
|
+
parquet: { available: true, reason: null, missing_packages: [] },
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
const getExportMetadata = vi
|
|
150
|
+
.fn()
|
|
151
|
+
.mockResolvedValueOnce(metadataFor("before"))
|
|
152
|
+
.mockResolvedValueOnce(metadataFor("after"));
|
|
153
|
+
const view = (data: string) => (
|
|
154
|
+
<Provider store={store}>
|
|
155
|
+
<TooltipProvider>
|
|
156
|
+
<LoadingDataTableComponent
|
|
157
|
+
label={null}
|
|
158
|
+
totalRows={1}
|
|
159
|
+
pagination={false}
|
|
160
|
+
pageSize={10}
|
|
161
|
+
selection={null}
|
|
162
|
+
showDownload={true}
|
|
163
|
+
showFilters={false}
|
|
164
|
+
showColumnSummaries={false}
|
|
165
|
+
showDataTypes={false}
|
|
166
|
+
showPageSizeSelector={false}
|
|
167
|
+
showColumnExplorer={false}
|
|
168
|
+
showRowExplorer={false}
|
|
169
|
+
showChartBuilder={false}
|
|
170
|
+
rowHeaders={[]}
|
|
171
|
+
fieldTypes={undefined}
|
|
172
|
+
totalColumns={1}
|
|
173
|
+
maxColumns="all"
|
|
174
|
+
hasStableRowId={false}
|
|
175
|
+
lazy={false}
|
|
176
|
+
host={host}
|
|
177
|
+
showSearch={false}
|
|
178
|
+
value={[]}
|
|
179
|
+
setValue={vi.fn()}
|
|
180
|
+
data={data}
|
|
181
|
+
search={vi.fn().mockResolvedValue({
|
|
182
|
+
data,
|
|
183
|
+
total_rows: 1,
|
|
184
|
+
cell_styles: null,
|
|
185
|
+
cell_hover_texts: null,
|
|
186
|
+
})}
|
|
187
|
+
download_as={vi.fn()}
|
|
188
|
+
download_geojson={vi.fn()}
|
|
189
|
+
get_export_metadata={getExportMetadata}
|
|
190
|
+
get_column_summaries={vi.fn()}
|
|
191
|
+
get_data_url={vi.fn() as GetDataUrl}
|
|
192
|
+
get_row_ids={vi.fn() as GetRowIds}
|
|
193
|
+
get_size_bytes={vi.fn().mockResolvedValue({ size_bytes: null })}
|
|
194
|
+
/>
|
|
195
|
+
</TooltipProvider>
|
|
196
|
+
</Provider>
|
|
197
|
+
);
|
|
198
|
+
const { rerender } = render(
|
|
199
|
+
view(JSON.stringify([{ before: "POINT (1 2)" }])),
|
|
200
|
+
);
|
|
201
|
+
await waitFor(() =>
|
|
202
|
+
expect(
|
|
203
|
+
screen.getByRole("columnheader", { name: "before" }),
|
|
204
|
+
).toBeInTheDocument(),
|
|
205
|
+
);
|
|
206
|
+
fireEvent.click(screen.getByTestId("export-button"));
|
|
207
|
+
await waitFor(() =>
|
|
208
|
+
expect(screen.getByTestId("export-summary-parquet")).toHaveTextContent(
|
|
209
|
+
"before",
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
rerender(view(JSON.stringify([{ after: "POINT (3 4)" }])));
|
|
214
|
+
|
|
215
|
+
await waitFor(() =>
|
|
216
|
+
expect(screen.getByTestId("export-summary-parquet")).toHaveTextContent(
|
|
217
|
+
"after",
|
|
218
|
+
),
|
|
219
|
+
);
|
|
220
|
+
expect(getExportMetadata).toHaveBeenCalledTimes(2);
|
|
221
|
+
});
|
|
222
|
+
|
|
139
223
|
it("does not log duplicate keys when a pivot becomes empty", async () => {
|
|
140
224
|
const host = document.createElement("div");
|
|
141
225
|
const consoleError = vi
|
|
@@ -4,6 +4,9 @@ import {
|
|
|
4
4
|
exactDateTime,
|
|
5
5
|
getDateFormat,
|
|
6
6
|
getShortTimeZone,
|
|
7
|
+
parseLocalISODate,
|
|
8
|
+
parseLocalISODateTime,
|
|
9
|
+
parseTimeToDate,
|
|
7
10
|
prettyDate,
|
|
8
11
|
timeAgo,
|
|
9
12
|
} from "../dates";
|
|
@@ -191,6 +194,119 @@ describe("dates", () => {
|
|
|
191
194
|
});
|
|
192
195
|
});
|
|
193
196
|
|
|
197
|
+
describe("parseTimeToDate", () => {
|
|
198
|
+
it("parses 24-hour times", () => {
|
|
199
|
+
expect(parseTimeToDate("14:30")).toEqual(new Date(1970, 0, 1, 14, 30));
|
|
200
|
+
expect(parseTimeToDate("14:30:05")).toEqual(
|
|
201
|
+
new Date(1970, 0, 1, 14, 30, 5),
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("parses times with an am/pm suffix", () => {
|
|
206
|
+
expect(parseTimeToDate("2:30 PM")).toEqual(new Date(1970, 0, 1, 14, 30));
|
|
207
|
+
expect(parseTimeToDate("12:00 am")).toEqual(new Date(1970, 0, 1, 0, 0));
|
|
208
|
+
expect(parseTimeToDate("12:00 pm")).toEqual(new Date(1970, 0, 1, 12, 0));
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("rejects an out-of-range hour instead of letting Date normalize it", () => {
|
|
212
|
+
// new Date(1970, 0, 1, 25, 0) would silently become 01:00 the next day.
|
|
213
|
+
expect(parseTimeToDate("25:00")).toBeUndefined();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("rejects an out-of-range minute instead of letting Date normalize it", () => {
|
|
217
|
+
// new Date(1970, 0, 1, 12, 99) would silently become 13:39.
|
|
218
|
+
expect(parseTimeToDate("12:99")).toBeUndefined();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("rejects an out-of-range second", () => {
|
|
222
|
+
expect(parseTimeToDate("12:30:99")).toBeUndefined();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("requires an hour from 1 to 12 with an am/pm suffix", () => {
|
|
226
|
+
expect(parseTimeToDate("0:30 am")).toBeUndefined();
|
|
227
|
+
expect(parseTimeToDate("13:30 pm")).toBeUndefined();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("parses milliseconds", () => {
|
|
231
|
+
expect(parseTimeToDate("14:30:05.123")).toEqual(
|
|
232
|
+
new Date(1970, 0, 1, 14, 30, 5, 123),
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("pads a short millisecond fraction and truncates a long one", () => {
|
|
237
|
+
expect(parseTimeToDate("14:30:05.5")).toEqual(
|
|
238
|
+
new Date(1970, 0, 1, 14, 30, 5, 500),
|
|
239
|
+
);
|
|
240
|
+
expect(parseTimeToDate("14:30:05.123456")).toEqual(
|
|
241
|
+
new Date(1970, 0, 1, 14, 30, 5, 123),
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe("parseLocalISODate", () => {
|
|
247
|
+
it("parses a date-only string into local midnight", () => {
|
|
248
|
+
expect(parseLocalISODate("2024-01-15")).toEqual(new Date(2024, 0, 15));
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("returns undefined for a non-date-only string", () => {
|
|
252
|
+
expect(parseLocalISODate("2024-01-15T00:00:00")).toBeUndefined();
|
|
253
|
+
expect(parseLocalISODate("")).toBeUndefined();
|
|
254
|
+
expect(parseLocalISODate("not-a-date")).toBeUndefined();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("returns undefined for a calendar date that doesn't exist", () => {
|
|
258
|
+
expect(parseLocalISODate("2024-02-30")).toBeUndefined();
|
|
259
|
+
expect(parseLocalISODate("2024-13-01")).toBeUndefined();
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe("parseLocalISODateTime", () => {
|
|
264
|
+
it("parses a 'T'-separated datetime into local wall-clock time", () => {
|
|
265
|
+
expect(parseLocalISODateTime("2024-01-15T08:30:00")).toEqual(
|
|
266
|
+
new Date(2024, 0, 15, 8, 30, 0),
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("parses a space-separated datetime", () => {
|
|
271
|
+
expect(parseLocalISODateTime("2024-01-15 08:30:00")).toEqual(
|
|
272
|
+
new Date(2024, 0, 15, 8, 30, 0),
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("defaults seconds to 0 when omitted", () => {
|
|
277
|
+
expect(parseLocalISODateTime("2024-01-15T08:30")).toEqual(
|
|
278
|
+
new Date(2024, 0, 15, 8, 30, 0),
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("ignores a trailing Z instead of shifting to UTC", () => {
|
|
283
|
+
expect(parseLocalISODateTime("2024-01-15T08:30:00Z")).toEqual(
|
|
284
|
+
new Date(2024, 0, 15, 8, 30, 0),
|
|
285
|
+
);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("ignores a trailing UTC offset instead of shifting the time", () => {
|
|
289
|
+
expect(parseLocalISODateTime("2024-01-15T08:30:00+05:00")).toEqual(
|
|
290
|
+
new Date(2024, 0, 15, 8, 30, 0),
|
|
291
|
+
);
|
|
292
|
+
expect(parseLocalISODateTime("2024-01-15T08:30:00-0500")).toEqual(
|
|
293
|
+
new Date(2024, 0, 15, 8, 30, 0),
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("ignores sub-second precision", () => {
|
|
298
|
+
expect(parseLocalISODateTime("2024-01-15T08:30:00.123")).toEqual(
|
|
299
|
+
new Date(2024, 0, 15, 8, 30, 0),
|
|
300
|
+
);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("returns undefined for a date-only or unparsable string", () => {
|
|
304
|
+
expect(parseLocalISODateTime("2024-01-15")).toBeUndefined();
|
|
305
|
+
expect(parseLocalISODateTime("not-a-datetime")).toBeUndefined();
|
|
306
|
+
expect(parseLocalISODateTime("")).toBeUndefined();
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
194
310
|
describe("getDateFormat", () => {
|
|
195
311
|
it("returns the correct format for date", () => {
|
|
196
312
|
expect(getDateFormat("2023-05-15")).toBe("yyyy-MM-dd");
|