opencode-docker-panel 0.4.8 → 0.5.1
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/CHANGELOG.md +33 -1
- package/README.md +86 -194
- package/README.ru.md +76 -194
- package/dist/commands.js +9 -7
- package/dist/compose.js +36 -14
- package/dist/docker.js +26 -6
- package/dist/logs.js +3 -5
- package/dist/panel.js +232 -116
- package/dist/poll.js +68 -8
- package/dist/runtime.js +67 -0
- package/dist/tui.js +2 -2
- package/package.json +5 -2
package/dist/panel.js
CHANGED
|
@@ -6,15 +6,16 @@ import { createTextNode as _$createTextNode } from "@opentui/solid";
|
|
|
6
6
|
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
7
7
|
import { setProp as _$setProp } from "@opentui/solid";
|
|
8
8
|
import { createElement as _$createElement } from "@opentui/solid";
|
|
9
|
-
import { Index, Show, createSignal, onCleanup } from "solid-js";
|
|
9
|
+
import { Index, Show, createEffect, createSignal, onCleanup } from "solid-js";
|
|
10
10
|
import { ACTION_LABEL, availableActions, buildArgs, runDockerArgs } from "./commands.js";
|
|
11
11
|
import { findComposeFile } from "./compose.js";
|
|
12
12
|
import { runDockerLogs } from "./logs.js";
|
|
13
|
-
import { createDockerPolling, selectRows } from "./poll.js";
|
|
13
|
+
import { createDockerPolling, createEngineProbe, selectRows } from "./poll.js";
|
|
14
|
+
import { ENGINE_ACTION_LABEL, buildRuntimeArgs } from "./runtime.js";
|
|
14
15
|
const LOG_VIEW_LINES = 15;
|
|
15
16
|
const LOG_REFRESH_MS = 2000;
|
|
16
17
|
|
|
17
|
-
/**
|
|
18
|
+
/** One source for the argv, so the menu footer and the run cannot drift apart */
|
|
18
19
|
function stackArgs(target) {
|
|
19
20
|
return ["compose", "-f", target.file, "-p", target.project, "up", "-d"];
|
|
20
21
|
}
|
|
@@ -37,7 +38,7 @@ function hasStaleRows(state) {
|
|
|
37
38
|
return (state.kind === "unavailable" || state.kind === "stale") && state.containers.length > 0;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
/**
|
|
41
|
+
/** Fixed viewport: opentui has no scrollable container, so the height has to be predictable */
|
|
41
42
|
function LogsView(props) {
|
|
42
43
|
const lines = () => props.result?.lines ?? [];
|
|
43
44
|
const end = () => Math.max(0, lines().length - props.offset);
|
|
@@ -161,6 +162,7 @@ function LogsView(props) {
|
|
|
161
162
|
export function DockerPanel(props) {
|
|
162
163
|
const [open, setOpen] = createSignal(true);
|
|
163
164
|
const [busy, setBusy] = createSignal(false);
|
|
165
|
+
const [stopping, setStopping] = createSignal(false);
|
|
164
166
|
const [logs, setLogs] = createSignal(null);
|
|
165
167
|
const [offset, setOffset] = createSignal(0);
|
|
166
168
|
const theme = () => props.theme;
|
|
@@ -222,22 +224,45 @@ export function DockerPanel(props) {
|
|
|
222
224
|
if (logTimer) clearInterval(logTimer);
|
|
223
225
|
if (logs()) props.ui.dialog.clear();
|
|
224
226
|
});
|
|
227
|
+
const engine = createEngineProbe();
|
|
228
|
+
// The probe only has to be re-read when the poll changes its verdict: an available docker proves the
|
|
229
|
+
// engine answers, and a probe on every poll would stack several four second calls on the same pipe,
|
|
230
|
+
// while never re-reading it would leave a stale stopped engine once docker starts from the tray
|
|
231
|
+
let lastKind = "";
|
|
232
|
+
const polling = createDockerPolling(props.intervalMs, props.reload, next => {
|
|
233
|
+
refreshStack();
|
|
234
|
+
if (next.kind === lastKind) return;
|
|
235
|
+
lastKind = next.kind;
|
|
236
|
+
void engine.refresh();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// A stop the container poll could see beats a probe that has not answered yet: the poll knows the
|
|
240
|
+
// Desktop pipe is missing in a quarter second, the probe needs four seconds to admit the same
|
|
241
|
+
const engineDown = () => polling.state().kind === "stopped" || engine.state() === "stopped";
|
|
225
242
|
|
|
226
|
-
// Read
|
|
227
|
-
// assembling the render tree.
|
|
243
|
+
// Read on mount and on every poll, never while the host assembles the render tree
|
|
228
244
|
const [stackSignal, setStackSignal] = createSignal(null);
|
|
229
245
|
const [pendingStack, setPendingStack] = createSignal(null);
|
|
230
246
|
const derivePendingStack = stack => {
|
|
231
|
-
setPendingStack(stack && !polling.state().containers.some(item => item.composeProject === stack.project) ? stack : null);
|
|
247
|
+
setPendingStack(stack && !engineDown() && !polling.state().containers.some(item => item.composeProject === stack.project) ? stack : null);
|
|
232
248
|
};
|
|
233
249
|
const refreshStack = () => {
|
|
234
250
|
const stack = findComposeFile(props.agentDir);
|
|
235
251
|
setStackSignal(stack);
|
|
236
252
|
derivePendingStack(stack);
|
|
237
253
|
};
|
|
238
|
-
const polling = createDockerPolling(props.intervalMs, props.reload, refreshStack);
|
|
239
254
|
|
|
240
|
-
|
|
255
|
+
// A stopped engine makes every `docker logs` call fail, so the window keeps its last lines and the
|
|
256
|
+
// polling stops rather than spawning a doomed process every two seconds
|
|
257
|
+
createEffect(() => {
|
|
258
|
+
const state = engine.state();
|
|
259
|
+
if (state === "stopped" || state === "absent") {
|
|
260
|
+
if (logTimer) clearInterval(logTimer);
|
|
261
|
+
logTimer = undefined;
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
/** Single entry point, so the busy guard and the refresh cannot be skipped */
|
|
241
266
|
const run = async (action, title, args) => {
|
|
242
267
|
if (busy()) return;
|
|
243
268
|
setBusy(true);
|
|
@@ -250,53 +275,111 @@ export function DockerPanel(props) {
|
|
|
250
275
|
});
|
|
251
276
|
await polling.refreshAfterAction();
|
|
252
277
|
};
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
278
|
+
/**
|
|
279
|
+
* Opens the same actions for a container the sidebar does not show, and carries the one engine
|
|
280
|
+
* action last.
|
|
281
|
+
*
|
|
282
|
+
* The engine entry stays available with no containers at all, because that is exactly when Docker is
|
|
283
|
+
* down and it is the only way to turn it back on.
|
|
258
284
|
*/
|
|
259
|
-
|
|
260
|
-
/** The list of every container, from which picking one opens the same actions as its row does. */
|
|
261
285
|
const pickContainer = async all => {
|
|
262
|
-
const
|
|
286
|
+
const action = engine.state() === "running" ? "engine-stop" : undefined;
|
|
287
|
+
const pending = props.ui.dialog.select({
|
|
263
288
|
title: `All containers (${all.length})`,
|
|
264
289
|
placeholder: "Container",
|
|
265
|
-
options: all.map(container => ({
|
|
290
|
+
options: [...all.map(container => ({
|
|
266
291
|
title: container.name,
|
|
267
|
-
value:
|
|
292
|
+
value: {
|
|
293
|
+
kind: "container",
|
|
294
|
+
container
|
|
295
|
+
},
|
|
268
296
|
description: detailLabel(container, props.pinned),
|
|
269
297
|
footer: container.state
|
|
270
|
-
}))
|
|
298
|
+
})), ...(action ? [{
|
|
299
|
+
title: ENGINE_ACTION_LABEL[action],
|
|
300
|
+
value: {
|
|
301
|
+
kind: "action",
|
|
302
|
+
action
|
|
303
|
+
},
|
|
304
|
+
description: "stops every container of every project",
|
|
305
|
+
footer: `docker ${buildRuntimeArgs(action).join(" ")}`
|
|
306
|
+
}] : [])]
|
|
271
307
|
});
|
|
308
|
+
props.ui.dialog.set({
|
|
309
|
+
size: "large"
|
|
310
|
+
});
|
|
311
|
+
const choice = await pending;
|
|
272
312
|
if (!choice) return;
|
|
273
|
-
|
|
274
|
-
|
|
313
|
+
if (choice.kind === "action") {
|
|
314
|
+
await runRuntime(choice.action);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
// The list is a snapshot, a container can stop while it is open, so re-read before acting
|
|
318
|
+
const fresh = polling.state().containers.find(item => item.name === choice.container.name);
|
|
275
319
|
if (fresh) await openMenu(fresh);
|
|
276
320
|
};
|
|
277
321
|
|
|
278
|
-
/**
|
|
279
|
-
* The single door to the containers the sidebar does not show. Clicking the header lands on the
|
|
280
|
-
* list itself, so there is no menu to pick through first.
|
|
281
|
-
*/
|
|
322
|
+
/** Single door to the containers the sidebar does not show, so the header opens the list itself */
|
|
282
323
|
const openBrowse = async () => {
|
|
283
324
|
if (busy()) return;
|
|
284
|
-
|
|
285
|
-
|
|
325
|
+
await pickContainer(polling.state().containers);
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Both engine actions ask first, and Stop names what it is about to end.
|
|
330
|
+
*
|
|
331
|
+
* Start confirms too: it brings up a virtual machine and starts consuming memory, so a misclick is
|
|
332
|
+
* not free even though nothing is running that it would interrupt.
|
|
333
|
+
*/
|
|
334
|
+
const runRuntime = async action => {
|
|
335
|
+
if (busy()) return;
|
|
336
|
+
const args = buildRuntimeArgs(action);
|
|
337
|
+
const title = ENGINE_ACTION_LABEL[action];
|
|
338
|
+
if (action === "engine-stop") {
|
|
339
|
+
const running = polling.state().containers.filter(item => item.state === "running");
|
|
340
|
+
const names = running.length > 0 ? running.map(item => item.name).join(", ") : "nothing the panel knows about";
|
|
341
|
+
const confirmed = await props.ui.dialog.confirm({
|
|
342
|
+
title: "Stop Docker Desktop?",
|
|
343
|
+
message: `Stops the engine, so every container stops with it. Visible to the panel: ${names}.\n\nNot reversible from the panel.`,
|
|
344
|
+
label: {
|
|
345
|
+
confirm: "Stop",
|
|
346
|
+
cancel: "Cancel"
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
if (!confirmed) return;
|
|
350
|
+
} else {
|
|
351
|
+
const confirmed = await props.ui.dialog.confirm({
|
|
352
|
+
title: "Start Docker Desktop?",
|
|
353
|
+
message: `Brings up the engine and its virtual machine.\n\ndocker ${args.join(" ")}`,
|
|
354
|
+
label: {
|
|
355
|
+
confirm: "Start",
|
|
356
|
+
cancel: "Cancel"
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
if (!confirmed) return;
|
|
360
|
+
}
|
|
361
|
+
// The containers are about to go, so the rows go with them instead of staying on screen for a
|
|
362
|
+
// poll or two while the engine finishes shutting down
|
|
363
|
+
if (action === "engine-stop") setStopping(true);
|
|
364
|
+
setBusy(true);
|
|
365
|
+
try {
|
|
366
|
+
const result = await runDockerArgs(args);
|
|
286
367
|
props.ui.toast.show({
|
|
287
|
-
title
|
|
288
|
-
message:
|
|
289
|
-
variant: "
|
|
368
|
+
title,
|
|
369
|
+
message: result.ok ? `${title}: ${result.message}` : result.message,
|
|
370
|
+
variant: result.ok ? "success" : "error"
|
|
290
371
|
});
|
|
291
|
-
|
|
372
|
+
// busy stays on for the whole transition, otherwise working disappears while the VM boots
|
|
373
|
+
// A command that failed will never bring the engine up, and waiting for one costs two minutes
|
|
374
|
+
if (action === "engine-start" && result.ok) await engine.waitForRunning();
|
|
375
|
+
await polling.refreshAfterAction();
|
|
376
|
+
} finally {
|
|
377
|
+
setStopping(false);
|
|
378
|
+
setBusy(false);
|
|
292
379
|
}
|
|
293
|
-
await pickContainer(all);
|
|
294
380
|
};
|
|
295
381
|
|
|
296
|
-
/**
|
|
297
|
-
* A stack has no container to be the subject of the action, and its file carries build, command and
|
|
298
|
-
* entrypoint, so it asks once and shows the exact argv in the prompt.
|
|
299
|
-
*/
|
|
382
|
+
/** A stack has no container to act on and its file carries build and command lines, so confirm with the exact argv */
|
|
300
383
|
const openStack = async () => {
|
|
301
384
|
if (busy()) return;
|
|
302
385
|
const stack = pendingStack();
|
|
@@ -315,7 +398,7 @@ export function DockerPanel(props) {
|
|
|
315
398
|
const openMenu = async container => {
|
|
316
399
|
if (busy()) return;
|
|
317
400
|
const isPinned = props.pinned.includes(container.name);
|
|
318
|
-
// Read once per menu
|
|
401
|
+
// Read once per menu, compose can build images and the action must not wait on a second read
|
|
319
402
|
const stack = stackSignal()?.project === container.composeProject ? stackSignal() : null;
|
|
320
403
|
const options = [...availableActions(container, stack).map(item => ({
|
|
321
404
|
title: ACTION_LABEL[item],
|
|
@@ -334,11 +417,15 @@ export function DockerPanel(props) {
|
|
|
334
417
|
footer: "stored by the host, survives a restart"
|
|
335
418
|
}];
|
|
336
419
|
try {
|
|
337
|
-
const
|
|
420
|
+
const pending = props.ui.dialog.select({
|
|
338
421
|
title: container.name,
|
|
339
422
|
placeholder: "Action",
|
|
340
423
|
options
|
|
341
424
|
});
|
|
425
|
+
props.ui.dialog.set({
|
|
426
|
+
size: "large"
|
|
427
|
+
});
|
|
428
|
+
const choice = await pending;
|
|
342
429
|
if (!choice) return;
|
|
343
430
|
if (choice === "logs") {
|
|
344
431
|
openLogs(container);
|
|
@@ -375,14 +462,17 @@ export function DockerPanel(props) {
|
|
|
375
462
|
};
|
|
376
463
|
|
|
377
464
|
// A click in opentui is a mousedown plus a mouseup, and the host dismisses an overlay from the
|
|
378
|
-
// next mouse event it sees
|
|
379
|
-
// that release has finished propagating, so the release cannot land on its own backdrop.
|
|
465
|
+
// next mouse event it sees, so every dialog is armed on the release and mounted after it propagates
|
|
380
466
|
const arm = (open, event) => {
|
|
381
467
|
event.stopPropagation();
|
|
382
468
|
setTimeout(() => void open(), 0);
|
|
383
469
|
};
|
|
384
|
-
const visible = () => selectRows(polling.state().containers, props.pinned);
|
|
470
|
+
const visible = () => stopping() ? [] : selectRows(polling.state().containers, props.pinned);
|
|
385
471
|
const hasRows = () => visible().length > 0;
|
|
472
|
+
const hidden = () => Math.max(0, polling.state().containers.length - visible().length);
|
|
473
|
+
// The line stays when nothing is hidden because it is also the only way into the list, which is where
|
|
474
|
+
// stopping the engine lives
|
|
475
|
+
const browseLabel = () => hidden() > 0 ? `${hidden()} more, click for all` : `${polling.state().containers.length} container${polling.state().containers.length === 1 ? "" : "s"}, click for all`;
|
|
386
476
|
const pressHeader = () => {
|
|
387
477
|
if (hasRows()) {
|
|
388
478
|
setOpen(value => !value);
|
|
@@ -404,7 +494,7 @@ export function DockerPanel(props) {
|
|
|
404
494
|
_$insertNode(_el$21, _$createTextNode(`Docker`));
|
|
405
495
|
_$insert(_el$20, _$createComponent(Show, {
|
|
406
496
|
get when() {
|
|
407
|
-
return polling.state().containers.length > 0;
|
|
497
|
+
return _$memo(() => !!!stopping())() && polling.state().containers.length > 0;
|
|
408
498
|
},
|
|
409
499
|
get children() {
|
|
410
500
|
var _el$23 = _$createElement("span"),
|
|
@@ -419,52 +509,80 @@ export function DockerPanel(props) {
|
|
|
419
509
|
return _el$23;
|
|
420
510
|
}
|
|
421
511
|
}), null);
|
|
422
|
-
_$insert(_el$
|
|
512
|
+
_$insert(_el$19, _$createComponent(Show, {
|
|
423
513
|
get when() {
|
|
424
|
-
return
|
|
514
|
+
return hasStaleRows(polling.state());
|
|
425
515
|
},
|
|
426
516
|
get children() {
|
|
427
|
-
var _el$26 = _$createElement("
|
|
428
|
-
_$insertNode(_el$26, _$createTextNode(`
|
|
429
|
-
_$effect(_$p => _$setProp(_el$26, "
|
|
430
|
-
fg: theme().text.muted
|
|
431
|
-
}, _$p));
|
|
517
|
+
var _el$26 = _$createElement("text");
|
|
518
|
+
_$insertNode(_el$26, _$createTextNode(` stale`));
|
|
519
|
+
_$effect(_$p => _$setProp(_el$26, "fg", theme().text.feedback.warning.base, _$p));
|
|
432
520
|
return _el$26;
|
|
433
521
|
}
|
|
434
522
|
}), null);
|
|
435
523
|
_$insert(_el$19, _$createComponent(Show, {
|
|
436
524
|
get when() {
|
|
437
|
-
return
|
|
525
|
+
return busy();
|
|
438
526
|
},
|
|
439
527
|
get children() {
|
|
440
528
|
var _el$28 = _$createElement("text");
|
|
441
|
-
_$insertNode(_el$28, _$createTextNode(`
|
|
442
|
-
_$effect(_$p => _$setProp(_el$28, "fg", theme().text.
|
|
529
|
+
_$insertNode(_el$28, _$createTextNode(` working`));
|
|
530
|
+
_$effect(_$p => _$setProp(_el$28, "fg", theme().text.muted, _$p));
|
|
443
531
|
return _el$28;
|
|
444
532
|
}
|
|
445
533
|
}), null);
|
|
446
|
-
_$insert(_el$
|
|
534
|
+
_$insert(_el$18, _$createComponent(Show, {
|
|
447
535
|
get when() {
|
|
448
|
-
return
|
|
536
|
+
return _$memo(() => !!(!stopping() && !hasRows()))() && polling.state().containers.length > 0;
|
|
449
537
|
},
|
|
450
538
|
get children() {
|
|
451
|
-
var _el$30 = _$createElement("text")
|
|
452
|
-
|
|
539
|
+
var _el$30 = _$createElement("text"),
|
|
540
|
+
_el$31 = _$createTextNode(` click for the list`);
|
|
541
|
+
_$insertNode(_el$30, _el$31);
|
|
542
|
+
_$setProp(_el$30, "onMouseUp", event => arm(openBrowse, event));
|
|
453
543
|
_$effect(_$p => _$setProp(_el$30, "fg", theme().text.muted, _$p));
|
|
454
544
|
return _el$30;
|
|
455
545
|
}
|
|
456
546
|
}), null);
|
|
457
547
|
_$insert(_el$18, _$createComponent(Show, {
|
|
458
548
|
get when() {
|
|
459
|
-
return _$memo(() =>
|
|
549
|
+
return _$memo(() => !!engineDown())() && !busy();
|
|
550
|
+
},
|
|
551
|
+
get children() {
|
|
552
|
+
var _el$33 = _$createElement("text"),
|
|
553
|
+
_el$34 = _$createElement("b"),
|
|
554
|
+
_el$36 = _$createElement("span");
|
|
555
|
+
_$insertNode(_el$33, _el$34);
|
|
556
|
+
_$insertNode(_el$33, _el$36);
|
|
557
|
+
_$setProp(_el$33, "onMouseUp", event => arm(() => void runRuntime("engine-start"), event));
|
|
558
|
+
_$insertNode(_el$34, _$createTextNode(`Start Docker Desktop`));
|
|
559
|
+
_$insertNode(_el$36, _$createTextNode(` · click to start`));
|
|
560
|
+
_$effect(_p$ => {
|
|
561
|
+
var _v$6 = theme().text.base,
|
|
562
|
+
_v$7 = {
|
|
563
|
+
fg: theme().text.muted
|
|
564
|
+
};
|
|
565
|
+
_v$6 !== _p$.e && (_p$.e = _$setProp(_el$33, "fg", _v$6, _p$.e));
|
|
566
|
+
_v$7 !== _p$.t && (_p$.t = _$setProp(_el$36, "style", _v$7, _p$.t));
|
|
567
|
+
return _p$;
|
|
568
|
+
}, {
|
|
569
|
+
e: undefined,
|
|
570
|
+
t: undefined
|
|
571
|
+
});
|
|
572
|
+
return _el$33;
|
|
573
|
+
}
|
|
574
|
+
}), null);
|
|
575
|
+
_$insert(_el$18, _$createComponent(Show, {
|
|
576
|
+
get when() {
|
|
577
|
+
return _$memo(() => !!(!engineDown() && polling.state().containers.length === 0))() && polling.state().detail.length > 0;
|
|
460
578
|
},
|
|
461
579
|
get children() {
|
|
462
|
-
var _el$
|
|
463
|
-
_el$
|
|
464
|
-
_$insertNode(_el$
|
|
465
|
-
_$insert(_el$
|
|
466
|
-
_$effect(_$p => _$setProp(_el$
|
|
467
|
-
return _el$
|
|
580
|
+
var _el$38 = _$createElement("text"),
|
|
581
|
+
_el$39 = _$createTextNode(` `);
|
|
582
|
+
_$insertNode(_el$38, _el$39);
|
|
583
|
+
_$insert(_el$38, () => polling.state().detail, null);
|
|
584
|
+
_$effect(_$p => _$setProp(_el$38, "fg", theme().text.muted, _$p));
|
|
585
|
+
return _el$38;
|
|
468
586
|
}
|
|
469
587
|
}), null);
|
|
470
588
|
_$insert(_el$18, _$createComponent(Show, {
|
|
@@ -477,88 +595,86 @@ export function DockerPanel(props) {
|
|
|
477
595
|
return visible();
|
|
478
596
|
},
|
|
479
597
|
children: container => (() => {
|
|
480
|
-
var _el$
|
|
481
|
-
_el$
|
|
482
|
-
_el$
|
|
483
|
-
_el$
|
|
484
|
-
_el$
|
|
485
|
-
_$insertNode(_el$
|
|
486
|
-
_$insertNode(_el$
|
|
487
|
-
_$setProp(_el$
|
|
488
|
-
_$setProp(_el$
|
|
489
|
-
_$setProp(_el$
|
|
490
|
-
_$insertNode(_el$
|
|
491
|
-
_$setProp(_el$
|
|
492
|
-
_$insertNode(_el$
|
|
493
|
-
_$insertNode(_el$
|
|
494
|
-
_$setProp(_el$
|
|
495
|
-
_$insert(_el$
|
|
496
|
-
_$insert(_el$
|
|
598
|
+
var _el$48 = _$createElement("box"),
|
|
599
|
+
_el$49 = _$createElement("text"),
|
|
600
|
+
_el$51 = _$createElement("text"),
|
|
601
|
+
_el$52 = _$createTextNode(` `),
|
|
602
|
+
_el$53 = _$createElement("span");
|
|
603
|
+
_$insertNode(_el$48, _el$49);
|
|
604
|
+
_$insertNode(_el$48, _el$51);
|
|
605
|
+
_$setProp(_el$48, "flexDirection", "row");
|
|
606
|
+
_$setProp(_el$48, "gap", 1);
|
|
607
|
+
_$setProp(_el$48, "onMouseUp", event => arm(() => openMenu(container()), event));
|
|
608
|
+
_$insertNode(_el$49, _$createTextNode(`•`));
|
|
609
|
+
_$setProp(_el$49, "flexShrink", 0);
|
|
610
|
+
_$insertNode(_el$51, _el$52);
|
|
611
|
+
_$insertNode(_el$51, _el$53);
|
|
612
|
+
_$setProp(_el$51, "wrapMode", "word");
|
|
613
|
+
_$insert(_el$51, () => container().name, _el$52);
|
|
614
|
+
_$insert(_el$53, () => detailLabel(container(), props.pinned));
|
|
497
615
|
_$effect(_p$ => {
|
|
498
|
-
var _v$
|
|
499
|
-
_v$
|
|
500
|
-
_v$
|
|
616
|
+
var _v$0 = dotColor(theme(), container().state),
|
|
617
|
+
_v$1 = theme().text.base,
|
|
618
|
+
_v$10 = {
|
|
501
619
|
fg: theme().text.muted
|
|
502
620
|
};
|
|
503
|
-
_v$
|
|
504
|
-
_v$
|
|
505
|
-
_v$
|
|
621
|
+
_v$0 !== _p$.e && (_p$.e = _$setProp(_el$49, "fg", _v$0, _p$.e));
|
|
622
|
+
_v$1 !== _p$.t && (_p$.t = _$setProp(_el$51, "fg", _v$1, _p$.t));
|
|
623
|
+
_v$10 !== _p$.a && (_p$.a = _$setProp(_el$53, "style", _v$10, _p$.a));
|
|
506
624
|
return _p$;
|
|
507
625
|
}, {
|
|
508
626
|
e: undefined,
|
|
509
627
|
t: undefined,
|
|
510
628
|
a: undefined
|
|
511
629
|
});
|
|
512
|
-
return _el$
|
|
630
|
+
return _el$48;
|
|
513
631
|
})()
|
|
514
632
|
}), _$createComponent(Show, {
|
|
515
633
|
get when() {
|
|
516
|
-
return
|
|
634
|
+
return hasRows();
|
|
517
635
|
},
|
|
518
636
|
get children() {
|
|
519
|
-
var _el$
|
|
520
|
-
_el$
|
|
521
|
-
|
|
522
|
-
_$
|
|
523
|
-
_$
|
|
524
|
-
_$setProp(_el$
|
|
525
|
-
|
|
526
|
-
_$effect(_$p => _$setProp(_el$34, "fg", theme().text.muted, _$p));
|
|
527
|
-
return _el$34;
|
|
637
|
+
var _el$40 = _$createElement("text"),
|
|
638
|
+
_el$41 = _$createTextNode(` `);
|
|
639
|
+
_$insertNode(_el$40, _el$41);
|
|
640
|
+
_$setProp(_el$40, "onMouseUp", event => arm(openBrowse, event));
|
|
641
|
+
_$insert(_el$40, browseLabel, null);
|
|
642
|
+
_$effect(_$p => _$setProp(_el$40, "fg", theme().text.muted, _$p));
|
|
643
|
+
return _el$40;
|
|
528
644
|
}
|
|
529
645
|
})];
|
|
530
646
|
}
|
|
531
647
|
}), null);
|
|
532
648
|
_$insert(_el$18, _$createComponent(Show, {
|
|
533
649
|
get when() {
|
|
534
|
-
return pendingStack();
|
|
650
|
+
return _$memo(() => !!pendingStack())() && !engineDown();
|
|
535
651
|
},
|
|
536
652
|
get children() {
|
|
537
|
-
var _el$
|
|
538
|
-
_el$
|
|
539
|
-
_el$
|
|
540
|
-
_el$
|
|
541
|
-
_el$
|
|
542
|
-
_$insertNode(_el$
|
|
543
|
-
_$insertNode(_el$
|
|
544
|
-
_$setProp(_el$
|
|
545
|
-
_$insertNode(_el$
|
|
546
|
-
_$insertNode(_el$
|
|
547
|
-
_$insertNode(_el$
|
|
548
|
-
_$insert(_el$
|
|
653
|
+
var _el$42 = _$createElement("text"),
|
|
654
|
+
_el$43 = _$createElement("b"),
|
|
655
|
+
_el$45 = _$createElement("span"),
|
|
656
|
+
_el$46 = _$createTextNode(` · `),
|
|
657
|
+
_el$47 = _$createTextNode(` is not running here`);
|
|
658
|
+
_$insertNode(_el$42, _el$43);
|
|
659
|
+
_$insertNode(_el$42, _el$45);
|
|
660
|
+
_$setProp(_el$42, "onMouseUp", event => arm(() => void openStack(), event));
|
|
661
|
+
_$insertNode(_el$43, _$createTextNode(`Up stack`));
|
|
662
|
+
_$insertNode(_el$45, _el$46);
|
|
663
|
+
_$insertNode(_el$45, _el$47);
|
|
664
|
+
_$insert(_el$45, () => pendingStack()?.project, _el$47);
|
|
549
665
|
_$effect(_p$ => {
|
|
550
|
-
var _v$
|
|
551
|
-
_v$
|
|
666
|
+
var _v$8 = theme().text.base,
|
|
667
|
+
_v$9 = {
|
|
552
668
|
fg: theme().text.muted
|
|
553
669
|
};
|
|
554
|
-
_v$
|
|
555
|
-
_v$
|
|
670
|
+
_v$8 !== _p$.e && (_p$.e = _$setProp(_el$42, "fg", _v$8, _p$.e));
|
|
671
|
+
_v$9 !== _p$.t && (_p$.t = _$setProp(_el$45, "style", _v$9, _p$.t));
|
|
556
672
|
return _p$;
|
|
557
673
|
}, {
|
|
558
674
|
e: undefined,
|
|
559
675
|
t: undefined
|
|
560
676
|
});
|
|
561
|
-
return _el$
|
|
677
|
+
return _el$42;
|
|
562
678
|
}
|
|
563
679
|
}), null);
|
|
564
680
|
_$effect(_$p => _$setProp(_el$20, "fg", theme().text.base, _$p));
|
package/dist/poll.js
CHANGED
|
@@ -1,24 +1,37 @@
|
|
|
1
1
|
import { createSignal, onCleanup } from "solid-js";
|
|
2
2
|
import { runDockerPs } from "./docker.js";
|
|
3
|
+
import { runDesktopStatus } from "./runtime.js";
|
|
3
4
|
const INITIAL = {
|
|
4
5
|
kind: "empty",
|
|
5
6
|
containers: [],
|
|
6
7
|
detail: "loading"
|
|
7
8
|
};
|
|
8
9
|
|
|
9
|
-
/** docker reports the old state for a moment after start or stop, so an action is polled twice
|
|
10
|
+
/** docker reports the old state for a moment after start or stop, so an action is polled twice */
|
|
10
11
|
const SETTLE_MS = 1200;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A Docker Desktop cold start boots a virtual machine, which takes a minute and answers `unknown` while
|
|
15
|
+
* it goes. Polling faster than the container loop is what makes the panel notice it came up at all,
|
|
16
|
+
* and the deadline is what stops a dead engine from holding `working` forever.
|
|
17
|
+
*/
|
|
18
|
+
export const ENGINE_TRANSITION_MS = 2000;
|
|
19
|
+
export const ENGINE_DEADLINE_MS = 120000;
|
|
11
20
|
export function keepContainers(previous, next) {
|
|
21
|
+
// A stopped engine is not a blip, it means every container really is down, so the rows go rather than
|
|
22
|
+
// linger as a lie. Only a poll that could not reach docker keeps what was known
|
|
12
23
|
if (next.kind === "unavailable" || next.kind === "stale") return previous;
|
|
13
24
|
return next.containers;
|
|
14
25
|
}
|
|
15
26
|
|
|
16
|
-
/** The sidebar belongs to what runs right now,
|
|
27
|
+
/** The sidebar belongs to what runs right now, not to every container that exists */
|
|
17
28
|
export const MAX_RUNNING = 5;
|
|
18
29
|
|
|
19
|
-
/**
|
|
20
|
-
* A pinned container is shown whatever its state
|
|
21
|
-
* so pinning everything shows everything.
|
|
30
|
+
/**
|
|
31
|
+
* A pinned container is shown whatever its state and spends the running budget when it happens to
|
|
32
|
+
* run, so pinning everything shows everything.
|
|
33
|
+
*
|
|
34
|
+
* Stopped containers get no row, the dialog is their only door.
|
|
22
35
|
*/
|
|
23
36
|
export function selectRows(all, pinned) {
|
|
24
37
|
const isPinned = item => pinned.includes(item.name);
|
|
@@ -26,12 +39,59 @@ export function selectRows(all, pinned) {
|
|
|
26
39
|
const running = all.filter(item => !isPinned(item) && item.state === "running");
|
|
27
40
|
return [...fixed, ...running.slice(0, Math.max(0, MAX_RUNNING - fixed.length))];
|
|
28
41
|
}
|
|
29
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* The engine has its own probe because `docker ps` cannot report it: on a stopped engine the ps call
|
|
44
|
+
* fails to connect and says only `unavailable`, which is also what a missing CLI or a locked socket
|
|
45
|
+
* produces. `docker desktop status` is the only thing that tells those apart.
|
|
46
|
+
*/
|
|
47
|
+
export function createEngineProbe() {
|
|
48
|
+
const [state, setState] = createSignal("unknown");
|
|
49
|
+
let disposed = false;
|
|
50
|
+
let probing = false;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A stopped engine takes over four seconds to answer, which is longer than the container poll, so a
|
|
54
|
+
* probe per poll would leave several of them waiting on the same pipe at once. One at a time is the
|
|
55
|
+
* whole guard: a call that arrives while one is in flight reads the last known state instead.
|
|
56
|
+
*/
|
|
57
|
+
const refresh = async () => {
|
|
58
|
+
if (probing) return state();
|
|
59
|
+
probing = true;
|
|
60
|
+
try {
|
|
61
|
+
const next = await runDesktopStatus();
|
|
62
|
+
if (!disposed) setState(next);
|
|
63
|
+
return next;
|
|
64
|
+
} finally {
|
|
65
|
+
probing = false;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
onCleanup(() => {
|
|
69
|
+
disposed = true;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/** Polls on a deadline rather than on a timer, so disposal has nothing to cancel */
|
|
73
|
+
const waitForRunning = async (deadlineMs = ENGINE_DEADLINE_MS) => {
|
|
74
|
+
const until = Date.now() + deadlineMs;
|
|
75
|
+
for (;;) {
|
|
76
|
+
if ((await refresh()) === "running") return true;
|
|
77
|
+
if (disposed || Date.now() >= until) return false;
|
|
78
|
+
await new Promise(resolve => setTimeout(resolve, ENGINE_TRANSITION_MS));
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
void refresh();
|
|
82
|
+
return {
|
|
83
|
+
state,
|
|
84
|
+
refresh,
|
|
85
|
+
waitForRunning
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The host does not repaint this slot when only a signal changes, so a real change remounts the panel */
|
|
30
90
|
function signature(containers) {
|
|
31
91
|
return containers.map(item => `${item.name}:${item.state}`).join("|");
|
|
32
92
|
}
|
|
33
93
|
|
|
34
|
-
/** A container in a crash loop changes state on every poll
|
|
94
|
+
/** A container in a crash loop changes state on every poll and each remount costs a fresh docker ps */
|
|
35
95
|
const RELOAD_COOLDOWN_MS = 10000;
|
|
36
96
|
export function createDockerPolling(intervalMs, onContentChange, onPoll) {
|
|
37
97
|
const [state, setState] = createSignal(INITIAL);
|
|
@@ -49,7 +109,7 @@ export function createDockerPolling(intervalMs, onContentChange, onPoll) {
|
|
|
49
109
|
...next,
|
|
50
110
|
containers: visible
|
|
51
111
|
});
|
|
52
|
-
onPoll?.();
|
|
112
|
+
onPoll?.(next);
|
|
53
113
|
if (next.kind === "unavailable" || next.kind === "stale") return;
|
|
54
114
|
const current = signature(visible);
|
|
55
115
|
if (current === last) return;
|