@marimo-team/islands 0.24.3-dev82 → 0.24.3-dev83
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/{ErrorBoundary-CegtSjfd.js → ErrorBoundary-Uf_LvjvY.js} +1 -1
- package/dist/{cells-BIfOnGBK.js → cells-DYIkSxs6.js} +4 -0
- package/dist/{chat-ui-LWkjhObe.js → chat-ui-BXj3uYjy.js} +4 -4
- package/dist/{code-visibility-Blq7oGrC.js → code-visibility-DxgQWu1j.js} +2 -2
- package/dist/{common-BQZDWy8O.js → common-ByAtncLr.js} +3 -3
- package/dist/{data-grid-overlay-editor-DsU0Zpgt.js → data-grid-overlay-editor-DL_blFyL.js} +1 -1
- package/dist/{error-entries-CR0oClSL.js → error-entries-C0NsUqXl.js} +1 -1
- package/dist/{feedback-button-pGaDcCJS.js → feedback-button-AZcBqfoV.js} +4 -4
- package/dist/{glide-data-editor-DtJQCRsf.js → glide-data-editor-BJdZncJk.js} +3 -3
- package/dist/{globals-DBAC4Ihd.js → globals-yWarpyN4.js} +1 -1
- package/dist/{html-to-image-_i03yR4q.js → html-to-image-CeYYvQNb.js} +1 -1
- package/dist/main.js +12 -11
- package/dist/{number-overlay-editor-9M7FnzxL.js → number-overlay-editor-BatBVYxr.js} +1 -1
- package/dist/{process-output-D3zQyTAS.js → process-output-BCTjlyuH.js} +2 -2
- package/dist/{reveal-component-ovaRyk0R.js → reveal-component-D02jcpiJ.js} +3 -3
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/editor/alerts/__tests__/connection-notice.test.tsx +186 -0
- package/src/components/editor/alerts/__tests__/startup-progress.test.tsx +185 -0
- package/src/components/editor/alerts/connection-notice.tsx +100 -54
- package/src/components/editor/alerts/startup-output.tsx +173 -0
- package/src/components/editor/alerts/startup-progress.tsx +204 -0
- package/src/components/editor/chrome/panels/__tests__/packages-panel.test.tsx +53 -4
- package/src/components/editor/chrome/panels/__tests__/sandbox-details.test.tsx +358 -0
- package/src/components/editor/chrome/panels/__tests__/sandbox-recovery.test.tsx +5 -2
- package/src/components/editor/chrome/panels/packages-panel.tsx +32 -21
- package/src/components/editor/chrome/panels/sandbox-details-state.ts +38 -0
- package/src/components/editor/chrome/panels/sandbox-panel.tsx +188 -131
- package/src/components/editor/chrome/panels/sandbox-toggle.tsx +52 -0
- package/src/components/editor/chrome/wrapper/app-chrome.tsx +7 -0
- package/src/components/editor/package-alert.tsx +112 -66
- package/src/core/alerts/__tests__/environment.test.ts +183 -0
- package/src/core/alerts/__tests__/state.test.tsx +240 -0
- package/src/core/alerts/environment.ts +57 -0
- package/src/core/alerts/state.ts +155 -99
- package/src/core/islands/bootstrap.ts +2 -1
- package/src/core/kernel/messages.ts +2 -2
- package/src/core/network/connection-notice.ts +45 -12
- package/src/core/network/connection.ts +19 -0
- package/src/core/network/useConnectionNotice.ts +53 -0
- package/src/core/packages/__tests__/useSandboxController.test.tsx +86 -5
- package/src/core/packages/sandbox-state.ts +56 -4
- package/src/core/packages/useSandboxController.ts +27 -7
- package/src/core/websocket/__tests__/useMarimoKernelConnection.hook.test.tsx +199 -8
- package/src/core/websocket/types.ts +1 -1
- package/src/core/websocket/useMarimoKernelConnection.tsx +31 -19
- package/src/stories/sandbox.stories.tsx +230 -50
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
// @vitest-environment jsdom
|
|
3
|
+
|
|
4
|
+
import { act, renderHook } from "@testing-library/react";
|
|
5
|
+
import { createStore, Provider } from "jotai";
|
|
6
|
+
import { expect, it } from "vitest";
|
|
7
|
+
import type { EnvironmentOperation } from "../environment";
|
|
8
|
+
import { alertAtom, getPackageAlert, useAlertActions } from "../state";
|
|
9
|
+
|
|
10
|
+
it.each(["install", "remove"] as const)(
|
|
11
|
+
"restores active %s progress without announcing old successes",
|
|
12
|
+
(action) => {
|
|
13
|
+
const store = createStore();
|
|
14
|
+
const { result } = renderHook(() => useAlertActions(), {
|
|
15
|
+
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
|
|
16
|
+
});
|
|
17
|
+
const operation: EnvironmentOperation = {
|
|
18
|
+
operation_id: "package",
|
|
19
|
+
action,
|
|
20
|
+
source: "kernel",
|
|
21
|
+
status: { kind: "succeeded" },
|
|
22
|
+
packages: { numpy: "succeeded" },
|
|
23
|
+
logs: { numpy: "Done\n" },
|
|
24
|
+
};
|
|
25
|
+
const snapshot = (value: EnvironmentOperation) =>
|
|
26
|
+
result.current.setEnvironment({
|
|
27
|
+
source: value.source,
|
|
28
|
+
state: { restart_required: false, operations: [value] },
|
|
29
|
+
});
|
|
30
|
+
act(() => snapshot(operation));
|
|
31
|
+
expect(getPackageAlert(store.get(alertAtom))).toBeNull();
|
|
32
|
+
expect(store.get(alertAtom).environments.kernel.operations).toEqual([
|
|
33
|
+
operation,
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const active: EnvironmentOperation = {
|
|
37
|
+
...operation,
|
|
38
|
+
status: { kind: "running" },
|
|
39
|
+
packages: { numpy: "running" },
|
|
40
|
+
logs: { numpy: "Working\n" },
|
|
41
|
+
};
|
|
42
|
+
act(() => snapshot(active));
|
|
43
|
+
expect(getPackageAlert(store.get(alertAtom))).toEqual({
|
|
44
|
+
...active,
|
|
45
|
+
id: active.operation_id,
|
|
46
|
+
kind: "environment",
|
|
47
|
+
restartRequired: false,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Reconnecting can complete work that was already visible on this page.
|
|
51
|
+
act(() => snapshot(operation));
|
|
52
|
+
expect(getPackageAlert(store.get(alertAtom))).toEqual({
|
|
53
|
+
...operation,
|
|
54
|
+
id: operation.operation_id,
|
|
55
|
+
kind: "environment",
|
|
56
|
+
restartRequired: false,
|
|
57
|
+
});
|
|
58
|
+
act(() => result.current.clearPackageAlert(operation.operation_id));
|
|
59
|
+
act(() => snapshot(operation));
|
|
60
|
+
expect(getPackageAlert(store.get(alertAtom))).toBeNull();
|
|
61
|
+
|
|
62
|
+
// A live result still gives feedback for a new package action.
|
|
63
|
+
act(() =>
|
|
64
|
+
result.current.updateEnvironment({
|
|
65
|
+
...operation,
|
|
66
|
+
operation_id: "new",
|
|
67
|
+
log_mode: "replace",
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
expect(getPackageAlert(store.get(alertAtom))?.id).toBe("new");
|
|
71
|
+
|
|
72
|
+
// Earlier changes can still require a restart after a later action succeeds.
|
|
73
|
+
act(() => result.current.clearPackageAlert("new"));
|
|
74
|
+
act(() =>
|
|
75
|
+
result.current.setEnvironment({
|
|
76
|
+
source: operation.source,
|
|
77
|
+
state: { restart_required: true, operations: [operation] },
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
expect(getPackageAlert(store.get(alertAtom))).toEqual({
|
|
81
|
+
...operation,
|
|
82
|
+
id: operation.operation_id,
|
|
83
|
+
kind: "environment",
|
|
84
|
+
restartRequired: true,
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
it.each(["prepare", "sync"] as const)(
|
|
90
|
+
"retains %s progress without turning it into a package alert",
|
|
91
|
+
(action) => {
|
|
92
|
+
const store = createStore();
|
|
93
|
+
const { result } = renderHook(() => useAlertActions(), {
|
|
94
|
+
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
|
|
95
|
+
});
|
|
96
|
+
const operation: EnvironmentOperation = {
|
|
97
|
+
operation_id: "sandbox",
|
|
98
|
+
action,
|
|
99
|
+
source: "kernel",
|
|
100
|
+
status: { kind: "succeeded" },
|
|
101
|
+
packages: {},
|
|
102
|
+
logs: { environment: "Environment ready\n" },
|
|
103
|
+
};
|
|
104
|
+
const environment = { restart_required: false, operations: [operation] };
|
|
105
|
+
|
|
106
|
+
// A refreshed browser restores the result without announcing it again.
|
|
107
|
+
act(() =>
|
|
108
|
+
result.current.setEnvironment({ source: "kernel", state: environment }),
|
|
109
|
+
);
|
|
110
|
+
expect(getPackageAlert(store.get(alertAtom))).toBeNull();
|
|
111
|
+
expect(store.get(alertAtom).environments.kernel).toEqual(environment);
|
|
112
|
+
|
|
113
|
+
const install: EnvironmentOperation = {
|
|
114
|
+
...operation,
|
|
115
|
+
operation_id: "install",
|
|
116
|
+
action: "install",
|
|
117
|
+
status: { kind: "running" },
|
|
118
|
+
packages: { numpy: "running" },
|
|
119
|
+
logs: { numpy: "Installing\n" },
|
|
120
|
+
};
|
|
121
|
+
act(() =>
|
|
122
|
+
result.current.updateEnvironment({ ...install, log_mode: "replace" }),
|
|
123
|
+
);
|
|
124
|
+
const packageAlert = getPackageAlert(store.get(alertAtom));
|
|
125
|
+
for (const status of [
|
|
126
|
+
{ kind: "running" },
|
|
127
|
+
{ kind: "failed", error: "Sync failed" },
|
|
128
|
+
] as const) {
|
|
129
|
+
act(() =>
|
|
130
|
+
result.current.updateEnvironment({
|
|
131
|
+
...operation,
|
|
132
|
+
status,
|
|
133
|
+
log_mode: "replace",
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
expect(getPackageAlert(store.get(alertAtom))).toEqual(packageAlert);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
act(() => result.current.clearPackageAlert(install.operation_id));
|
|
140
|
+
act(() =>
|
|
141
|
+
result.current.updateEnvironment({ ...operation, log_mode: "replace" }),
|
|
142
|
+
);
|
|
143
|
+
expect(getPackageAlert(store.get(alertAtom))).toBeNull();
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
it.each(["kernel", "server"] as const)(
|
|
148
|
+
"preserves the selected %s result across snapshots and follows new work",
|
|
149
|
+
(source) => {
|
|
150
|
+
const store = createStore();
|
|
151
|
+
const { result } = renderHook(() => useAlertActions(), {
|
|
152
|
+
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
|
|
153
|
+
});
|
|
154
|
+
const selected: EnvironmentOperation = {
|
|
155
|
+
operation_id: "selected",
|
|
156
|
+
action: "install",
|
|
157
|
+
source,
|
|
158
|
+
status: { kind: "failed", error: "Network unavailable" },
|
|
159
|
+
packages: { numpy: "failed" },
|
|
160
|
+
logs: { numpy: "Selected logs\n" },
|
|
161
|
+
};
|
|
162
|
+
const other: EnvironmentOperation = {
|
|
163
|
+
...selected,
|
|
164
|
+
operation_id: "other",
|
|
165
|
+
source: source === "kernel" ? "server" : "kernel",
|
|
166
|
+
status: { kind: "succeeded" },
|
|
167
|
+
packages: { numpy: "succeeded" },
|
|
168
|
+
logs: { numpy: "Other logs\n" },
|
|
169
|
+
};
|
|
170
|
+
const snapshot = (operation: EnvironmentOperation) =>
|
|
171
|
+
result.current.setEnvironment({
|
|
172
|
+
source: operation.source,
|
|
173
|
+
state: { restart_required: false, operations: [operation] },
|
|
174
|
+
});
|
|
175
|
+
act(() => snapshot(selected));
|
|
176
|
+
for (const operations of [
|
|
177
|
+
[selected, other],
|
|
178
|
+
[other, selected],
|
|
179
|
+
]) {
|
|
180
|
+
act(() => operations.forEach(snapshot));
|
|
181
|
+
expect(getPackageAlert(store.get(alertAtom))).toEqual({
|
|
182
|
+
...selected,
|
|
183
|
+
id: selected.operation_id,
|
|
184
|
+
kind: "environment",
|
|
185
|
+
restartRequired: false,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Live work in the other environment should still take over the banner.
|
|
190
|
+
const active: EnvironmentOperation = {
|
|
191
|
+
...other,
|
|
192
|
+
operation_id: "new-work",
|
|
193
|
+
status: { kind: "running" },
|
|
194
|
+
packages: { numpy: "running" },
|
|
195
|
+
logs: { numpy: "Installing\n" },
|
|
196
|
+
};
|
|
197
|
+
act(() =>
|
|
198
|
+
result.current.updateEnvironment({ ...active, log_mode: "replace" }),
|
|
199
|
+
);
|
|
200
|
+
act(() => snapshot(selected));
|
|
201
|
+
expect(getPackageAlert(store.get(alertAtom))?.id).toBe(active.operation_id);
|
|
202
|
+
act(() =>
|
|
203
|
+
result.current.updateEnvironment({
|
|
204
|
+
...active,
|
|
205
|
+
status: { kind: "succeeded" },
|
|
206
|
+
packages: { numpy: "succeeded" },
|
|
207
|
+
logs: {},
|
|
208
|
+
log_mode: "append",
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
act(() => snapshot(selected));
|
|
212
|
+
expect(getPackageAlert(store.get(alertAtom))).toEqual({
|
|
213
|
+
...active,
|
|
214
|
+
id: active.operation_id,
|
|
215
|
+
kind: "environment",
|
|
216
|
+
restartRequired: false,
|
|
217
|
+
status: { kind: "succeeded" },
|
|
218
|
+
packages: { numpy: "succeeded" },
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Replacing that environment with an empty snapshot removes its selection.
|
|
222
|
+
act(() =>
|
|
223
|
+
result.current.setEnvironment({
|
|
224
|
+
source: active.source,
|
|
225
|
+
state: { restart_required: false, operations: [] },
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
expect(getPackageAlert(store.get(alertAtom))?.id).toBe(
|
|
229
|
+
selected.operation_id,
|
|
230
|
+
);
|
|
231
|
+
act(() =>
|
|
232
|
+
result.current.updateEnvironment({
|
|
233
|
+
...other,
|
|
234
|
+
operation_id: "latest-result",
|
|
235
|
+
log_mode: "replace",
|
|
236
|
+
}),
|
|
237
|
+
);
|
|
238
|
+
expect(getPackageAlert(store.get(alertAtom))?.id).toBe("latest-result");
|
|
239
|
+
},
|
|
240
|
+
);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type { NotificationMessageData, schemas } from "@/core/kernel/messages";
|
|
4
|
+
|
|
5
|
+
export type EnvironmentState = schemas["EnvironmentState"];
|
|
6
|
+
export type EnvironmentOperation = schemas["EnvironmentOperation"];
|
|
7
|
+
export type EnvironmentSource = EnvironmentOperation["source"];
|
|
8
|
+
|
|
9
|
+
export function emptyEnvironmentState(): EnvironmentState {
|
|
10
|
+
return { restart_required: false, operations: [] };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Apply a live delta after the connection's authoritative snapshot. */
|
|
14
|
+
export function reduceEnvironmentState(
|
|
15
|
+
state: EnvironmentState,
|
|
16
|
+
update: NotificationMessageData<"environment-operation">,
|
|
17
|
+
): EnvironmentState {
|
|
18
|
+
const previous = state.operations.find(
|
|
19
|
+
(operation) => operation.operation_id === update.operation_id,
|
|
20
|
+
);
|
|
21
|
+
const logs = { ...previous?.logs };
|
|
22
|
+
for (const [name, content] of Object.entries(update.logs)) {
|
|
23
|
+
logs[name] =
|
|
24
|
+
update.log_mode === "replace" ? content : (logs[name] ?? "") + content;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const operation: EnvironmentOperation = {
|
|
28
|
+
operation_id: update.operation_id,
|
|
29
|
+
action: update.action,
|
|
30
|
+
status: update.status,
|
|
31
|
+
packages: update.packages,
|
|
32
|
+
logs,
|
|
33
|
+
source: update.source,
|
|
34
|
+
};
|
|
35
|
+
const operations = state.operations.filter(
|
|
36
|
+
(item) =>
|
|
37
|
+
(previous && update.status.kind === "running") ||
|
|
38
|
+
item.status.kind === "running" ||
|
|
39
|
+
(item.action === "prepare" && update.action !== "prepare"),
|
|
40
|
+
);
|
|
41
|
+
const index = operations.findIndex(
|
|
42
|
+
(item) => item.operation_id === update.operation_id,
|
|
43
|
+
);
|
|
44
|
+
if (index === -1) {
|
|
45
|
+
operations.push(operation);
|
|
46
|
+
} else {
|
|
47
|
+
operations[index] = operation;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
restart_required:
|
|
52
|
+
state.restart_required ||
|
|
53
|
+
update.status.kind === "restart-required" ||
|
|
54
|
+
Object.values(update.packages).includes("restart-required"),
|
|
55
|
+
operations,
|
|
56
|
+
};
|
|
57
|
+
}
|
package/src/core/alerts/state.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { useAtomValue } from "jotai";
|
|
4
|
-
import type {
|
|
4
|
+
import type { NotificationMessageData } from "@/core/kernel/messages";
|
|
5
5
|
import { createReducerAndAtoms } from "@/utils/createReducer";
|
|
6
6
|
import { generateUUID } from "@/utils/uuid";
|
|
7
|
+
import {
|
|
8
|
+
emptyEnvironmentState,
|
|
9
|
+
type EnvironmentOperation,
|
|
10
|
+
type EnvironmentSource,
|
|
11
|
+
type EnvironmentState,
|
|
12
|
+
reduceEnvironmentState,
|
|
13
|
+
} from "./environment";
|
|
7
14
|
|
|
8
15
|
type Identified<T> = { id: string } & T;
|
|
9
16
|
|
|
@@ -14,12 +21,18 @@ export interface MissingPackageAlert {
|
|
|
14
21
|
source?: "kernel" | "server";
|
|
15
22
|
}
|
|
16
23
|
|
|
17
|
-
export interface
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
export interface EnvironmentOperationAlert extends EnvironmentOperation {
|
|
25
|
+
action: "install" | "remove";
|
|
26
|
+
kind: "environment";
|
|
27
|
+
restartRequired: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isPackageOperation(
|
|
31
|
+
operation: EnvironmentOperation,
|
|
32
|
+
): operation is EnvironmentOperation & {
|
|
33
|
+
action: EnvironmentOperationAlert["action"];
|
|
34
|
+
} {
|
|
35
|
+
return operation.action === "install" || operation.action === "remove";
|
|
23
36
|
}
|
|
24
37
|
|
|
25
38
|
export interface StartupLogsAlert {
|
|
@@ -28,118 +41,161 @@ export interface StartupLogsAlert {
|
|
|
28
41
|
}
|
|
29
42
|
|
|
30
43
|
export function isMissingPackageAlert(
|
|
31
|
-
alert: MissingPackageAlert |
|
|
44
|
+
alert: MissingPackageAlert | EnvironmentOperationAlert,
|
|
32
45
|
): alert is MissingPackageAlert {
|
|
33
46
|
return alert.kind === "missing";
|
|
34
47
|
}
|
|
35
48
|
|
|
36
|
-
export function
|
|
37
|
-
alert: MissingPackageAlert |
|
|
38
|
-
): alert is
|
|
39
|
-
return alert.kind === "
|
|
49
|
+
export function isEnvironmentOperationAlert(
|
|
50
|
+
alert: MissingPackageAlert | EnvironmentOperationAlert,
|
|
51
|
+
): alert is EnvironmentOperationAlert {
|
|
52
|
+
return alert.kind === "environment";
|
|
40
53
|
}
|
|
41
54
|
|
|
42
|
-
/** Prominent alerts.
|
|
43
|
-
*
|
|
44
|
-
* Right now we only have one type of alert.
|
|
45
|
-
*/
|
|
46
55
|
interface AlertState {
|
|
47
56
|
packageAlert:
|
|
48
57
|
| Identified<MissingPackageAlert>
|
|
49
|
-
|
|
|
58
|
+
| { kind: "environment"; id: string; source: EnvironmentSource }
|
|
50
59
|
| null;
|
|
60
|
+
environments: Record<EnvironmentSource, EnvironmentState>;
|
|
51
61
|
startupLogsAlert: StartupLogsAlert | null;
|
|
52
|
-
packageLogs: { [packageName: string]: string };
|
|
53
62
|
}
|
|
54
63
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// No default
|
|
94
|
-
}
|
|
64
|
+
function setEnvironment(
|
|
65
|
+
state: AlertState,
|
|
66
|
+
source: EnvironmentSource,
|
|
67
|
+
environment: EnvironmentState,
|
|
68
|
+
): AlertState {
|
|
69
|
+
// Sandbox preparation and sync use the existing sandbox UI.
|
|
70
|
+
const environments = { ...state.environments, [source]: environment };
|
|
71
|
+
const operations = Object.values(environments)
|
|
72
|
+
.flatMap((item) => item.operations)
|
|
73
|
+
.filter(isPackageOperation);
|
|
74
|
+
const incoming = environment.operations.filter(isPackageOperation);
|
|
75
|
+
const alert = state.packageAlert;
|
|
76
|
+
const selected =
|
|
77
|
+
alert?.kind === "environment"
|
|
78
|
+
? operations.find(
|
|
79
|
+
(item) =>
|
|
80
|
+
item.operation_id === alert.id && item.source === alert.source,
|
|
81
|
+
)
|
|
82
|
+
: undefined;
|
|
83
|
+
// A snapshot restores work and unresolved issues, not old success banners.
|
|
84
|
+
const needsAttention = (operation: EnvironmentOperation) =>
|
|
85
|
+
operation.status.kind !== "succeeded" ||
|
|
86
|
+
environments[operation.source].restart_required;
|
|
87
|
+
const operation =
|
|
88
|
+
(selected?.status.kind === "running" ? selected : undefined) ??
|
|
89
|
+
incoming.findLast((item) => item.status.kind === "running") ??
|
|
90
|
+
operations.findLast((item) => item.status.kind === "running") ??
|
|
91
|
+
selected ??
|
|
92
|
+
incoming.findLast(needsAttention) ??
|
|
93
|
+
operations.findLast(needsAttention);
|
|
94
|
+
return {
|
|
95
|
+
...state,
|
|
96
|
+
environments,
|
|
97
|
+
packageAlert: operation
|
|
98
|
+
? {
|
|
99
|
+
kind: "environment",
|
|
100
|
+
id: operation.operation_id,
|
|
101
|
+
source: operation.source,
|
|
95
102
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
: state.packageAlert?.kind === "missing"
|
|
104
|
+
? state.packageAlert
|
|
105
|
+
: null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
100
108
|
|
|
101
|
-
|
|
109
|
+
export const { valueAtom: alertAtom, useActions: useAlertActions } =
|
|
110
|
+
createReducerAndAtoms(
|
|
111
|
+
(): AlertState => ({
|
|
112
|
+
packageAlert: null,
|
|
113
|
+
startupLogsAlert: null,
|
|
114
|
+
environments: {
|
|
115
|
+
kernel: emptyEnvironmentState(),
|
|
116
|
+
server: emptyEnvironmentState(),
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
{
|
|
120
|
+
addMissingPackageAlert: (state, alert: MissingPackageAlert) => ({
|
|
102
121
|
...state,
|
|
103
|
-
packageAlert: { id:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
packageAlert: { id: generateUUID(), ...alert },
|
|
123
|
+
}),
|
|
124
|
+
|
|
125
|
+
updateEnvironment: (
|
|
126
|
+
state,
|
|
127
|
+
update: NotificationMessageData<"environment-operation">,
|
|
128
|
+
) => {
|
|
129
|
+
const source = update.source;
|
|
130
|
+
const environment = reduceEnvironmentState(
|
|
131
|
+
state.environments[source],
|
|
132
|
+
update,
|
|
133
|
+
);
|
|
134
|
+
if (!isPackageOperation(update)) {
|
|
135
|
+
return {
|
|
136
|
+
...state,
|
|
137
|
+
environments: { ...state.environments, [source]: environment },
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return setEnvironment(
|
|
141
|
+
{
|
|
142
|
+
...state,
|
|
143
|
+
packageAlert: {
|
|
144
|
+
kind: "environment",
|
|
145
|
+
id: update.operation_id,
|
|
146
|
+
source,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
source,
|
|
150
|
+
environment,
|
|
151
|
+
);
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
setEnvironment: (
|
|
155
|
+
state,
|
|
156
|
+
snapshot: NotificationMessageData<"environment-state">,
|
|
157
|
+
) => setEnvironment(state, snapshot.source, snapshot.state),
|
|
158
|
+
|
|
159
|
+
clearPackageAlert: (state, id: string) =>
|
|
160
|
+
state.packageAlert?.id === id
|
|
161
|
+
? { ...state, packageAlert: null }
|
|
162
|
+
: state,
|
|
163
|
+
|
|
164
|
+
addStartupLog: (
|
|
165
|
+
state,
|
|
166
|
+
logData: { content: string; status: "append" | "start" | "done" },
|
|
167
|
+
) => ({
|
|
120
168
|
...state,
|
|
121
169
|
startupLogsAlert: {
|
|
122
|
-
|
|
123
|
-
content: prevContent + logData.content,
|
|
170
|
+
content: (state.startupLogsAlert?.content ?? "") + logData.content,
|
|
124
171
|
status: logData.status,
|
|
125
172
|
},
|
|
126
|
-
}
|
|
127
|
-
},
|
|
173
|
+
}),
|
|
128
174
|
|
|
129
|
-
|
|
130
|
-
return { ...state, startupLogsAlert: null };
|
|
175
|
+
clearStartupLogsAlert: (state) => ({ ...state, startupLogsAlert: null }),
|
|
131
176
|
},
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
export function getPackageAlert(
|
|
180
|
+
state: AlertState,
|
|
181
|
+
): Identified<MissingPackageAlert | EnvironmentOperationAlert> | null {
|
|
182
|
+
const alert = state.packageAlert;
|
|
183
|
+
if (alert?.kind !== "environment") {
|
|
184
|
+
return alert;
|
|
185
|
+
}
|
|
186
|
+
const environment = state.environments[alert.source];
|
|
187
|
+
const operation = environment.operations.find(
|
|
188
|
+
(item) => item.operation_id === alert.id,
|
|
189
|
+
);
|
|
190
|
+
return operation && isPackageOperation(operation)
|
|
191
|
+
? { ...operation, ...alert, restartRequired: environment.restart_required }
|
|
192
|
+
: null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function useAlerts() {
|
|
196
|
+
const state = useAtomValue(alertAtom);
|
|
197
|
+
return {
|
|
198
|
+
packageAlert: getPackageAlert(state),
|
|
199
|
+
startupLogsAlert: state.startupLogsAlert,
|
|
200
|
+
};
|
|
145
201
|
}
|
|
@@ -181,7 +181,8 @@ function handleMessage(
|
|
|
181
181
|
// Unsupported operations in islands mode
|
|
182
182
|
case "banner":
|
|
183
183
|
case "missing-package-alert":
|
|
184
|
-
case "
|
|
184
|
+
case "environment-operation":
|
|
185
|
+
case "environment-state":
|
|
185
186
|
case "completion-result":
|
|
186
187
|
case "reload":
|
|
187
188
|
case "focus-cell":
|
|
@@ -36,8 +36,8 @@ export type CompletionResultMessage =
|
|
|
36
36
|
export type HumanReadableStatus = schemas["HumanReadableStatus"];
|
|
37
37
|
export type FunctionCallResultMessage =
|
|
38
38
|
NotificationMessageData<"function-call-result">;
|
|
39
|
-
export type
|
|
40
|
-
schemas["
|
|
39
|
+
export type PackageOperationStatus =
|
|
40
|
+
schemas["EnvironmentOperationNotification"]["packages"];
|
|
41
41
|
export type DataColumnPreview = NotificationMessageData<"data-column-preview">;
|
|
42
42
|
export type SQLTablePreview = NotificationMessageData<"sql-table-preview">;
|
|
43
43
|
export type SQLTableListPreview =
|