@elaraai/e3-ui-cli 1.0.72 → 1.0.74
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/README.md +46 -21
- package/bin/e3-ui.mjs +6 -0
- package/dist/app/assets/{engine-BKZbmXkC-DmcCFcnb.js → engine-Bm-cIDd_-DwUdGcTZ.js} +1 -1
- package/dist/app/assets/{index-BJNWHnRv.js → index-mj-YVVN1.js} +91 -91
- package/dist/app/index.html +1 -1
- package/dist/cli.js +36 -5
- package/dist/cli.js.map +1 -1
- package/dist/commands/tui.d.ts +84 -0
- package/dist/commands/tui.d.ts.map +1 -0
- package/dist/commands/tui.js +89 -0
- package/dist/commands/tui.js.map +1 -0
- package/dist/tui/api.d.ts +108 -0
- package/dist/tui/api.d.ts.map +1 -0
- package/dist/tui/api.js +130 -0
- package/dist/tui/api.js.map +1 -0
- package/dist/tui/app.d.ts +23 -0
- package/dist/tui/app.d.ts.map +1 -0
- package/dist/tui/app.js +244 -0
- package/dist/tui/app.js.map +1 -0
- package/dist/tui/controller.d.ts +116 -0
- package/dist/tui/controller.d.ts.map +1 -0
- package/dist/tui/controller.js +644 -0
- package/dist/tui/controller.js.map +1 -0
- package/dist/tui/data/dataflow.d.ts +68 -0
- package/dist/tui/data/dataflow.d.ts.map +1 -0
- package/dist/tui/data/dataflow.js +137 -0
- package/dist/tui/data/dataflow.js.map +1 -0
- package/dist/tui/data/dataset.d.ts +80 -0
- package/dist/tui/data/dataset.d.ts.map +1 -0
- package/dist/tui/data/dataset.js +297 -0
- package/dist/tui/data/dataset.js.map +1 -0
- package/dist/tui/data/edit-buffer.d.ts +74 -0
- package/dist/tui/data/edit-buffer.d.ts.map +1 -0
- package/dist/tui/data/edit-buffer.js +120 -0
- package/dist/tui/data/edit-buffer.js.map +1 -0
- package/dist/tui/data/feeds.d.ts +45 -0
- package/dist/tui/data/feeds.d.ts.map +1 -0
- package/dist/tui/data/feeds.js +318 -0
- package/dist/tui/data/feeds.js.map +1 -0
- package/dist/tui/data/logs.d.ts +68 -0
- package/dist/tui/data/logs.d.ts.map +1 -0
- package/dist/tui/data/logs.js +134 -0
- package/dist/tui/data/logs.js.map +1 -0
- package/dist/tui/input/commands.d.ts +159 -0
- package/dist/tui/input/commands.d.ts.map +1 -0
- package/dist/tui/input/commands.js +259 -0
- package/dist/tui/input/commands.js.map +1 -0
- package/dist/tui/input/completion.d.ts +63 -0
- package/dist/tui/input/completion.d.ts.map +1 -0
- package/dist/tui/input/completion.js +128 -0
- package/dist/tui/input/completion.js.map +1 -0
- package/dist/tui/input/keymap.d.ts +153 -0
- package/dist/tui/input/keymap.d.ts.map +1 -0
- package/dist/tui/input/keymap.js +206 -0
- package/dist/tui/input/keymap.js.map +1 -0
- package/dist/tui/input/mouse.d.ts +52 -0
- package/dist/tui/input/mouse.d.ts.map +1 -0
- package/dist/tui/input/mouse.js +55 -0
- package/dist/tui/input/mouse.js.map +1 -0
- package/dist/tui/lifecycle.d.ts +61 -0
- package/dist/tui/lifecycle.d.ts.map +1 -0
- package/dist/tui/lifecycle.js +99 -0
- package/dist/tui/lifecycle.js.map +1 -0
- package/dist/tui/model/catalogue.d.ts +28 -0
- package/dist/tui/model/catalogue.d.ts.map +1 -0
- package/dist/tui/model/catalogue.js +94 -0
- package/dist/tui/model/catalogue.js.map +1 -0
- package/dist/tui/model/help.d.ts +37 -0
- package/dist/tui/model/help.d.ts.map +1 -0
- package/dist/tui/model/help.js +210 -0
- package/dist/tui/model/help.js.map +1 -0
- package/dist/tui/model/index.d.ts +42 -0
- package/dist/tui/model/index.d.ts.map +1 -0
- package/dist/tui/model/index.js +50 -0
- package/dist/tui/model/index.js.map +1 -0
- package/dist/tui/model/status.d.ts +88 -0
- package/dist/tui/model/status.d.ts.map +1 -0
- package/dist/tui/model/status.js +137 -0
- package/dist/tui/model/status.js.map +1 -0
- package/dist/tui/model/tree.d.ts +126 -0
- package/dist/tui/model/tree.d.ts.map +1 -0
- package/dist/tui/model/tree.js +217 -0
- package/dist/tui/model/tree.js.map +1 -0
- package/dist/tui/model/types.d.ts +21 -0
- package/dist/tui/model/types.d.ts.map +1 -0
- package/dist/tui/model/types.js +46 -0
- package/dist/tui/model/types.js.map +1 -0
- package/dist/tui/render/glyphs.d.ts +122 -0
- package/dist/tui/render/glyphs.d.ts.map +1 -0
- package/dist/tui/render/glyphs.js +122 -0
- package/dist/tui/render/glyphs.js.map +1 -0
- package/dist/tui/render/layout.d.ts +145 -0
- package/dist/tui/render/layout.d.ts.map +1 -0
- package/dist/tui/render/layout.js +209 -0
- package/dist/tui/render/layout.js.map +1 -0
- package/dist/tui/render/text.d.ts +153 -0
- package/dist/tui/render/text.d.ts.map +1 -0
- package/dist/tui/render/text.js +320 -0
- package/dist/tui/render/text.js.map +1 -0
- package/dist/tui/render/theme.d.ts +88 -0
- package/dist/tui/render/theme.d.ts.map +1 -0
- package/dist/tui/render/theme.js +145 -0
- package/dist/tui/render/theme.js.map +1 -0
- package/dist/tui/session.d.ts +78 -0
- package/dist/tui/session.d.ts.map +1 -0
- package/dist/tui/session.js +237 -0
- package/dist/tui/session.js.map +1 -0
- package/dist/tui/state/actions.d.ts +584 -0
- package/dist/tui/state/actions.d.ts.map +1 -0
- package/dist/tui/state/actions.js +62 -0
- package/dist/tui/state/actions.js.map +1 -0
- package/dist/tui/state/persist.d.ts +114 -0
- package/dist/tui/state/persist.d.ts.map +1 -0
- package/dist/tui/state/persist.js +187 -0
- package/dist/tui/state/persist.js.map +1 -0
- package/dist/tui/state/poll.d.ts +99 -0
- package/dist/tui/state/poll.d.ts.map +1 -0
- package/dist/tui/state/poll.js +160 -0
- package/dist/tui/state/poll.js.map +1 -0
- package/dist/tui/state/reducer.d.ts +16 -0
- package/dist/tui/state/reducer.d.ts.map +1 -0
- package/dist/tui/state/reducer.js +363 -0
- package/dist/tui/state/reducer.js.map +1 -0
- package/dist/tui/state/store.d.ts +37 -0
- package/dist/tui/state/store.d.ts.map +1 -0
- package/dist/tui/state/store.js +84 -0
- package/dist/tui/state/store.js.map +1 -0
- package/dist/tui/suspend.d.ts +17 -0
- package/dist/tui/suspend.d.ts.map +1 -0
- package/dist/tui/suspend.js +33 -0
- package/dist/tui/suspend.js.map +1 -0
- package/dist/tui/ui/App.d.ts +44 -0
- package/dist/tui/ui/App.d.ts.map +1 -0
- package/dist/tui/ui/App.js +105 -0
- package/dist/tui/ui/App.js.map +1 -0
- package/dist/tui/ui/frame.d.ts +86 -0
- package/dist/tui/ui/frame.d.ts.map +1 -0
- package/dist/tui/ui/frame.js +42 -0
- package/dist/tui/ui/frame.js.map +1 -0
- package/dist/tui/ui/lines.d.ts +92 -0
- package/dist/tui/ui/lines.d.ts.map +1 -0
- package/dist/tui/ui/lines.js +94 -0
- package/dist/tui/ui/lines.js.map +1 -0
- package/dist/tui/ui/shell/chrome.d.ts +61 -0
- package/dist/tui/ui/shell/chrome.d.ts.map +1 -0
- package/dist/tui/ui/shell/chrome.js +272 -0
- package/dist/tui/ui/shell/chrome.js.map +1 -0
- package/dist/tui/ui/shell/widgets.d.ts +116 -0
- package/dist/tui/ui/shell/widgets.d.ts.map +1 -0
- package/dist/tui/ui/shell/widgets.js +183 -0
- package/dist/tui/ui/shell/widgets.js.map +1 -0
- package/dist/tui/ui/views/about.d.ts +20 -0
- package/dist/tui/ui/views/about.d.ts.map +1 -0
- package/dist/tui/ui/views/about.js +40 -0
- package/dist/tui/ui/views/about.js.map +1 -0
- package/dist/tui/ui/views/all.d.ts +17 -0
- package/dist/tui/ui/views/all.d.ts.map +1 -0
- package/dist/tui/ui/views/all.js +17 -0
- package/dist/tui/ui/views/all.js.map +1 -0
- package/dist/tui/ui/views/dashboard.d.ts +211 -0
- package/dist/tui/ui/views/dashboard.d.ts.map +1 -0
- package/dist/tui/ui/views/dashboard.js +651 -0
- package/dist/tui/ui/views/dashboard.js.map +1 -0
- package/dist/tui/ui/views/help.d.ts +17 -0
- package/dist/tui/ui/views/help.d.ts.map +1 -0
- package/dist/tui/ui/views/help.js +65 -0
- package/dist/tui/ui/views/help.js.map +1 -0
- package/dist/tui/ui/views/index.d.ts +42 -0
- package/dist/tui/ui/views/index.d.ts.map +1 -0
- package/dist/tui/ui/views/index.js +42 -0
- package/dist/tui/ui/views/index.js.map +1 -0
- package/dist/tui/ui/views/input.d.ts +6 -0
- package/dist/tui/ui/views/input.d.ts.map +1 -0
- package/dist/tui/ui/views/input.js +494 -0
- package/dist/tui/ui/views/input.js.map +1 -0
- package/dist/tui/ui/views/launch.d.ts +16 -0
- package/dist/tui/ui/views/launch.d.ts.map +1 -0
- package/dist/tui/ui/views/launch.js +43 -0
- package/dist/tui/ui/views/launch.js.map +1 -0
- package/dist/tui/ui/views/refusal.d.ts +20 -0
- package/dist/tui/ui/views/refusal.d.ts.map +1 -0
- package/dist/tui/ui/views/refusal.js +47 -0
- package/dist/tui/ui/views/refusal.js.map +1 -0
- package/dist/tui/ui/views/repos.d.ts +10 -0
- package/dist/tui/ui/views/repos.d.ts.map +1 -0
- package/dist/tui/ui/views/repos.js +93 -0
- package/dist/tui/ui/views/repos.js.map +1 -0
- package/dist/tui/ui/views/task.d.ts +17 -0
- package/dist/tui/ui/views/task.d.ts.map +1 -0
- package/dist/tui/ui/views/task.js +309 -0
- package/dist/tui/ui/views/task.js.map +1 -0
- package/dist/tui/ui/views/wordmark.d.ts +15 -0
- package/dist/tui/ui/views/wordmark.d.ts.map +1 -0
- package/dist/tui/ui/views/wordmark.js +50 -0
- package/dist/tui/ui/views/wordmark.js.map +1 -0
- package/dist/tui/ui/views/workspaces.d.ts +17 -0
- package/dist/tui/ui/views/workspaces.d.ts.map +1 -0
- package/dist/tui/ui/views/workspaces.js +127 -0
- package/dist/tui/ui/views/workspaces.js.map +1 -0
- package/dist/tui/ui/widgets/leaf-editor.d.ts +58 -0
- package/dist/tui/ui/widgets/leaf-editor.d.ts.map +1 -0
- package/dist/tui/ui/widgets/leaf-editor.js +119 -0
- package/dist/tui/ui/widgets/leaf-editor.js.map +1 -0
- package/dist/tui/ui/widgets/logs.d.ts +99 -0
- package/dist/tui/ui/widgets/logs.d.ts.map +1 -0
- package/dist/tui/ui/widgets/logs.js +291 -0
- package/dist/tui/ui/widgets/logs.js.map +1 -0
- package/dist/tui/ui/widgets/reads.d.ts +62 -0
- package/dist/tui/ui/widgets/reads.d.ts.map +1 -0
- package/dist/tui/ui/widgets/reads.js +122 -0
- package/dist/tui/ui/widgets/reads.js.map +1 -0
- package/dist/tui/ui/widgets/runs.d.ts +51 -0
- package/dist/tui/ui/widgets/runs.d.ts.map +1 -0
- package/dist/tui/ui/widgets/runs.js +86 -0
- package/dist/tui/ui/widgets/runs.js.map +1 -0
- package/dist/tui/ui/widgets/tree.d.ts +149 -0
- package/dist/tui/ui/widgets/tree.d.ts.map +1 -0
- package/dist/tui/ui/widgets/tree.js +584 -0
- package/dist/tui/ui/widgets/tree.js.map +1 -0
- package/package.json +22 -12
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Elara AI Pty Ltd
|
|
3
|
+
* Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
|
|
4
|
+
*/
|
|
5
|
+
import { breakpoint, columnPlan, scrollIntoView } from '../../render/layout.js';
|
|
6
|
+
import { agoShort, formatDuration, formatSize, hashShort, padEnd, padStart, timeAgo } from '../../render/text.js';
|
|
7
|
+
import { layoutOf, registerListModel } from '../../model/index.js';
|
|
8
|
+
import { datasetEntries } from '../../model/catalogue.js';
|
|
9
|
+
import { datasetStatusCell, eventCell, executionDuration, executionStatusCell, statusText, taskStatusCell } from '../../model/status.js';
|
|
10
|
+
import { registerViewHooks } from '../../controller.js';
|
|
11
|
+
import { isRunLive, lockHolderText } from '../../data/dataflow.js';
|
|
12
|
+
import { b, blank, d, lineWidth, lrLine, t } from '../lines.js';
|
|
13
|
+
import { centredBlock, sectionLine, tableLine, tablePlan, withScrollbar } from '../shell/widgets.js';
|
|
14
|
+
import { registerView } from './index.js';
|
|
15
|
+
/** Event rows the execution panel shows at most. */
|
|
16
|
+
export const MAX_EVENT_ROWS = 6;
|
|
17
|
+
/** Cells of the accounted bar at most (longer workspaces are sampled). */
|
|
18
|
+
const MAX_BAR_CELLS = 40;
|
|
19
|
+
/**
|
|
20
|
+
* The column builder's context from a render context.
|
|
21
|
+
*
|
|
22
|
+
* @param state - The store state
|
|
23
|
+
* @param ctx - The render context
|
|
24
|
+
* @returns The dashboard context
|
|
25
|
+
*/
|
|
26
|
+
export function dashboardCtx(state, ctx) {
|
|
27
|
+
return { g: ctx.g, now: ctx.now, columns: ctx.layout.columns, bp: breakpoint(state.size), spinner: ctx.spinner };
|
|
28
|
+
}
|
|
29
|
+
/** `12s` / `1m` / `3h` — an event's age without the `ago`. */
|
|
30
|
+
function ageBare(then, now) {
|
|
31
|
+
const ago = timeAgo(then, now);
|
|
32
|
+
return ago === 'just now' ? '0s' : ago.replace(/ ago$/, '');
|
|
33
|
+
}
|
|
34
|
+
/** The reason with its first letter lowered (`Waiting for task 'x'` → `waiting for task 'x'`). */
|
|
35
|
+
function lowerFirst(text) {
|
|
36
|
+
return text.length === 0 ? text : text[0].toLowerCase() + text.slice(1);
|
|
37
|
+
}
|
|
38
|
+
/** The dashboard title line: ` main ● DEPLOYED · demand@1.4.2 · deployed 3d ago · lock: none`. */
|
|
39
|
+
export function dashboardTitle(state, ws, ctx) {
|
|
40
|
+
const g = ctx.g;
|
|
41
|
+
const info = (state.data.workspaces ?? []).find(w => w.name === ws);
|
|
42
|
+
const wsState = state.data.workspaceState[ws];
|
|
43
|
+
const status = state.data.status[ws]?.result;
|
|
44
|
+
const right = [];
|
|
45
|
+
if (info === undefined || !info.deployed) {
|
|
46
|
+
right.push(b(`${g.empty} EMPTY`, 'muted'));
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const pkg = info.packageName.type === 'some' ? `${info.packageName.value}${info.packageVersion.type === 'some' ? `@${info.packageVersion.value}` : ''}` : '';
|
|
50
|
+
right.push(b(`${g.dot} DEPLOYED`, 'pos'), d(` ${g.sep} ${pkg}`));
|
|
51
|
+
if (wsState !== undefined && wsState !== null)
|
|
52
|
+
right.push(d(` ${g.sep} deployed ${timeAgo(wsState.deployedAt, ctx.now)}`));
|
|
53
|
+
}
|
|
54
|
+
const lock = status?.lock;
|
|
55
|
+
if (lock !== undefined) {
|
|
56
|
+
if (lock.type === 'some') {
|
|
57
|
+
right.push(t(` ${g.sep} `), b(`lock: ${lockHolderText(lock.value, ctx.now, g)}`, 'warn'));
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
right.push(d(` ${g.sep} lock: none`));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
right.push(t(' '));
|
|
64
|
+
return lrLine([t(' '), b(ws)], right, ctx.layout.columns);
|
|
65
|
+
}
|
|
66
|
+
/** The task stats: the four the design shows, plus in-progress / stale-running when non-zero. */
|
|
67
|
+
function taskStats(s, g) {
|
|
68
|
+
const stats = [
|
|
69
|
+
{ glyph: g.dot, tone: 'pos', word: 'up-to-date', count: Number(s.upToDate) },
|
|
70
|
+
{ glyph: g.half, tone: 'warn', word: 'waiting', count: Number(s.waiting) },
|
|
71
|
+
{ glyph: g.cross, tone: 'neg', word: 'failed', count: Number(s.failed) + Number(s.error) },
|
|
72
|
+
{ glyph: g.empty, tone: 'muted', word: 'ready', count: Number(s.ready) },
|
|
73
|
+
];
|
|
74
|
+
if (Number(s.inProgress) > 0)
|
|
75
|
+
stats.push({ glyph: g.quarter, tone: 'info', word: 'in-progress', count: Number(s.inProgress) });
|
|
76
|
+
if (Number(s.staleRunning) > 0)
|
|
77
|
+
stats.push({ glyph: g.half, tone: 'warn', word: 'stale-running', count: Number(s.staleRunning) });
|
|
78
|
+
return stats;
|
|
79
|
+
}
|
|
80
|
+
/** The dataset stats. */
|
|
81
|
+
function datasetStats(s, g) {
|
|
82
|
+
return [
|
|
83
|
+
{ glyph: g.dot, tone: 'pos', word: 'up-to-date', count: Number(s.upToDate) },
|
|
84
|
+
{ glyph: g.half, tone: 'warn', word: 'stale', count: Number(s.stale) },
|
|
85
|
+
{ glyph: g.empty, tone: 'muted', word: 'unset', count: Number(s.unset) },
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
/** A two-wide grid of stat cells (`● up-to-date 4 ◐ waiting 1`). */
|
|
89
|
+
function statGrid(stats) {
|
|
90
|
+
const lines = [];
|
|
91
|
+
for (let i = 0; i < stats.length; i += 2) {
|
|
92
|
+
const first = stats[i];
|
|
93
|
+
const second = stats[i + 1];
|
|
94
|
+
const line = [b(first.glyph, first.tone), t(padEnd(` ${first.word}`, 17)), t(padStart(String(first.count), 4))];
|
|
95
|
+
if (second !== undefined)
|
|
96
|
+
line.push(t(' '), b(second.glyph, second.tone), t(padEnd(` ${second.word}`, 13)), t(padStart(String(second.count), 4)));
|
|
97
|
+
lines.push(line);
|
|
98
|
+
}
|
|
99
|
+
return lines;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The accounted bar: one cell per task (sampled past 40), lowest first —
|
|
103
|
+
* `✗` failed / error, `▁` ready, `▃` waiting, `▅` in progress, `▇`
|
|
104
|
+
* up-to-date — and `N of M accounted`, where a task is accounted for once
|
|
105
|
+
* the dataflow has touched it (anything but `ready`).
|
|
106
|
+
*
|
|
107
|
+
* @param tasks - The workspace's tasks
|
|
108
|
+
* @param g - The glyph set
|
|
109
|
+
* @returns The bar's spans and the counts
|
|
110
|
+
*/
|
|
111
|
+
export function accountedBar(tasks, g) {
|
|
112
|
+
const height = (status) => status === 'ready' ? 0 : status === 'waiting' ? 1 : status === 'in-progress' || status === 'stale-running' ? 2 : status === 'up-to-date' ? 3 : -1;
|
|
113
|
+
const heights = tasks.map(task => height(task.status.type)).sort((x, y) => x - y);
|
|
114
|
+
const n = Math.min(heights.length, MAX_BAR_CELLS);
|
|
115
|
+
const bar = [];
|
|
116
|
+
for (let i = 0; i < n; i++) {
|
|
117
|
+
const h = heights[Math.floor((i * heights.length) / n)];
|
|
118
|
+
bar.push(h < 0 ? b(g.cross, 'neg') : b(g.bars[h], 'brand'));
|
|
119
|
+
}
|
|
120
|
+
return { bar, accounted: tasks.filter(task => task.status.type !== 'ready').length, total: tasks.length };
|
|
121
|
+
}
|
|
122
|
+
/** The counts block: the two headed groups side by side (or stacked when narrow) and the accounted bar. */
|
|
123
|
+
function countsLines(status, dctx) {
|
|
124
|
+
const g = dctx.g;
|
|
125
|
+
const tasks = statGrid(taskStats(status.summary.tasks, g));
|
|
126
|
+
const datasets = statGrid(datasetStats(status.summary.datasets, g));
|
|
127
|
+
const taskHead = [b(`TASKS ${status.summary.tasks.total}`)];
|
|
128
|
+
const datasetHead = [b(`DATASETS ${status.summary.datasets.total}`)];
|
|
129
|
+
const colL = Math.floor((dctx.columns - 2) / 2);
|
|
130
|
+
const out = [];
|
|
131
|
+
if (colL >= 44) {
|
|
132
|
+
const left = [taskHead, ...tasks];
|
|
133
|
+
const right = [datasetHead, ...datasets];
|
|
134
|
+
const n = Math.max(left.length, right.length);
|
|
135
|
+
for (let i = 0; i < n; i++) {
|
|
136
|
+
const l = left[i] ?? [];
|
|
137
|
+
const r = right[i] ?? [];
|
|
138
|
+
out.push([t(' '), ...l, t(' '.repeat(Math.max(0, colL - lineWidth(l)))), ...r]);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
for (const line of [taskHead, ...tasks, datasetHead, ...datasets])
|
|
143
|
+
out.push([t(' '), ...line]);
|
|
144
|
+
}
|
|
145
|
+
const { bar, accounted, total } = accountedBar(status.tasks, g);
|
|
146
|
+
out.push([t(' '), ...bar, d(`${bar.length > 0 ? ' ' : ''}${accounted} of ${total} accounted`)]);
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// The model
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
/**
|
|
153
|
+
* The latest event of each task, in order of first appearance.
|
|
154
|
+
*
|
|
155
|
+
* @param events - The execution's events
|
|
156
|
+
* @returns One event per task
|
|
157
|
+
*/
|
|
158
|
+
export function latestPerTask(events) {
|
|
159
|
+
const order = [];
|
|
160
|
+
const latest = new Map();
|
|
161
|
+
for (const event of events) {
|
|
162
|
+
const task = event.value.task;
|
|
163
|
+
if (!latest.has(task))
|
|
164
|
+
order.push(task);
|
|
165
|
+
latest.set(task, event);
|
|
166
|
+
}
|
|
167
|
+
return order.map(task => latest.get(task));
|
|
168
|
+
}
|
|
169
|
+
/** Whether an execution is live (running, launched and not yet seen running, or being stopped). */
|
|
170
|
+
function isLive(execution) {
|
|
171
|
+
return execution !== undefined && (execution.state?.status.type === 'running' || execution.settling || execution.stopping);
|
|
172
|
+
}
|
|
173
|
+
/** Whether an event's row opens the task's logs. */
|
|
174
|
+
function opensLogs(event) {
|
|
175
|
+
return event.type === 'failed' || event.type === 'error';
|
|
176
|
+
}
|
|
177
|
+
let cached = null;
|
|
178
|
+
function sameKey(a, b) {
|
|
179
|
+
return a.ws === b.ws && a.status === b.status && a.statusError === b.statusError && a.workspaceState === b.workspaceState
|
|
180
|
+
&& a.execution === b.execution && a.datasets === b.datasets && a.taskList === b.taskList
|
|
181
|
+
&& a.columns === b.columns && a.bp === b.bp && a.g === b.g;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The dashboard model for a workspace — built once per data change and
|
|
185
|
+
* returned as the same object until the status, the execution, the
|
|
186
|
+
* dataset list, the task list, the width or the breakpoint changes. The
|
|
187
|
+
* clock, the spinner and the selection are not inputs: they restyle lines
|
|
188
|
+
* at render ({@link dashboardLines}).
|
|
189
|
+
*
|
|
190
|
+
* @param state - The store state
|
|
191
|
+
* @param ws - The workspace
|
|
192
|
+
* @param dctx - The dashboard context (its width and breakpoint key the cache)
|
|
193
|
+
* @returns The model
|
|
194
|
+
*/
|
|
195
|
+
export function dashboardModel(state, ws, dctx) {
|
|
196
|
+
const key = {
|
|
197
|
+
ws,
|
|
198
|
+
status: state.data.status[ws]?.result,
|
|
199
|
+
statusError: state.data.statusError[ws],
|
|
200
|
+
workspaceState: state.data.workspaceState[ws],
|
|
201
|
+
execution: state.data.execution[ws],
|
|
202
|
+
datasets: state.data.datasets[ws],
|
|
203
|
+
taskList: state.data.taskList[ws],
|
|
204
|
+
columns: dctx.columns,
|
|
205
|
+
bp: dctx.bp,
|
|
206
|
+
g: dctx.g,
|
|
207
|
+
};
|
|
208
|
+
if (cached !== null && sameKey(cached.key, key))
|
|
209
|
+
return cached.model;
|
|
210
|
+
const model = buildModel(state, ws, dctx, key);
|
|
211
|
+
cached = { key, model };
|
|
212
|
+
return model;
|
|
213
|
+
}
|
|
214
|
+
/** Builds the model (the cache's miss path). */
|
|
215
|
+
function buildModel(state, ws, dctx, key) {
|
|
216
|
+
const g = dctx.g;
|
|
217
|
+
const width = dctx.columns - 1;
|
|
218
|
+
const status = key.status;
|
|
219
|
+
const execution = key.execution;
|
|
220
|
+
const empty = {
|
|
221
|
+
status, placeholder: [], counts: [], execution, live: false, done: 0, events: [], latest: new Map(),
|
|
222
|
+
taskPlan: [], tasks: [], inputPlan: [], inputs: [], inputNames: [], rows: [], total: 0, panelAt: 0, tasksAt: 0, inputsAt: 0,
|
|
223
|
+
};
|
|
224
|
+
if (status === undefined) {
|
|
225
|
+
const placeholder = [];
|
|
226
|
+
if (key.workspaceState === null) {
|
|
227
|
+
placeholder.push(...centredBlock(g.empty, 'muted', 'NOTHING DEPLOYED', [`${ws} has no package yet`, `e3 workspace deploy <repo> ${ws} <package>[@version] deploy one`, '/workspaces pick another workspace'], width));
|
|
228
|
+
}
|
|
229
|
+
else if (key.statusError !== undefined) {
|
|
230
|
+
placeholder.push([t(' '), b(`${g.cross} ${key.statusError}`, 'neg')]);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
placeholder.push([t(' '), d('loading…')]);
|
|
234
|
+
}
|
|
235
|
+
return { ...empty, placeholder, total: placeholder.length };
|
|
236
|
+
}
|
|
237
|
+
// The dataset map and the latest event per task: once per build, shared by every row.
|
|
238
|
+
const entries = datasetEntries(state, ws);
|
|
239
|
+
const latest = new Map(latestPerTask(execution?.events ?? []).map(e => [e.value.task, e]));
|
|
240
|
+
const live = isLive(execution);
|
|
241
|
+
const done = execution === undefined ? 0 : execution.events.filter(e => e.type !== 'start').length;
|
|
242
|
+
const events = execution === undefined || execution.state === null ? []
|
|
243
|
+
: live ? [...latest.values()].slice(-MAX_EVENT_ROWS)
|
|
244
|
+
: [...latest.values()].filter(opensLogs);
|
|
245
|
+
const tasks = status.tasks.map(task => {
|
|
246
|
+
const cell = taskStatusCell(task.status, g);
|
|
247
|
+
// The reason / pid / cached detail lives in the last column; only a failure's exit code / message stays inline.
|
|
248
|
+
const bare = task.status.type !== 'failed' && task.status.type !== 'error';
|
|
249
|
+
const inputs = task.inputs.filter(p => p.startsWith('.inputs.')).map(p => p.slice('.inputs.'.length));
|
|
250
|
+
const entry = entries.get(task.output);
|
|
251
|
+
return {
|
|
252
|
+
task,
|
|
253
|
+
event: latest.get(task.name),
|
|
254
|
+
size: entry?.size != null ? formatSize(entry.size) : '—',
|
|
255
|
+
cells: {
|
|
256
|
+
name: task.name,
|
|
257
|
+
status: { text: bare ? `${cell.glyph} ${cell.word}` : statusText(cell), tone: cell.tone },
|
|
258
|
+
dependsOn: task.dependsOn.length > 0 ? task.dependsOn.join(', ') : '—',
|
|
259
|
+
inputs: inputs.length > 0 ? inputs.join(', ') : '—',
|
|
260
|
+
output: entry?.type ?? '—',
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
});
|
|
264
|
+
const inputDatasets = status.datasets.filter(ds => !ds.isTaskOutput && ds.path.startsWith('.inputs.'));
|
|
265
|
+
const inputNames = inputDatasets.map(ds => ds.path.slice('.inputs.'.length));
|
|
266
|
+
const inputs = inputDatasets.map((ds, i) => {
|
|
267
|
+
const cell = datasetStatusCell(ds.status.type, g);
|
|
268
|
+
const entry = entries.get(ds.path);
|
|
269
|
+
return {
|
|
270
|
+
cells: {
|
|
271
|
+
name: inputNames[i],
|
|
272
|
+
status: { text: statusText(cell), tone: cell.tone },
|
|
273
|
+
type: entry?.type ?? '—',
|
|
274
|
+
size: entry?.size != null ? formatSize(entry.size) : '—',
|
|
275
|
+
hash: ds.hash.type === 'some' ? hashShort(ds.hash.value) : '—',
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
});
|
|
279
|
+
const taskPlan = tablePlan(columnPlan('tasks', dctx.bp), tasks, width);
|
|
280
|
+
const inputPlan = tablePlan(columnPlan('inputs', dctx.bp), inputs, width);
|
|
281
|
+
const counts = countsLines(status, dctx);
|
|
282
|
+
// The geometry: every selectable row's line, numbered in column order — failures, tasks, inputs.
|
|
283
|
+
const rows = [];
|
|
284
|
+
let line = counts.length + 1;
|
|
285
|
+
const panelAt = line;
|
|
286
|
+
const shown = events.slice(0, MAX_EVENT_ROWS);
|
|
287
|
+
shown.forEach((event, i) => {
|
|
288
|
+
if (opensLogs(event))
|
|
289
|
+
rows.push({ kind: 'logs', name: event.value.task, line: panelAt + 1 + i });
|
|
290
|
+
});
|
|
291
|
+
line += 1 + shown.length + (events.length > shown.length ? 1 : 0) + 1;
|
|
292
|
+
const tasksAt = line;
|
|
293
|
+
line += 2;
|
|
294
|
+
status.tasks.forEach((task, i) => rows.push({ kind: 'task', name: task.name, line: line + i }));
|
|
295
|
+
line += Math.max(1, tasks.length) + 1;
|
|
296
|
+
const inputsAt = line;
|
|
297
|
+
line += 2;
|
|
298
|
+
inputNames.forEach((name, i) => rows.push({ kind: 'input', name, line: line + i }));
|
|
299
|
+
line += Math.max(1, inputs.length);
|
|
300
|
+
return { ...empty, counts, live, done, events, latest, taskPlan, tasks, inputPlan, inputs, inputNames, rows, total: line, panelAt, tasksAt, inputsAt };
|
|
301
|
+
}
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
// Rendering
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
/** The execution panel: a header line, then the event rows (the header and the ages carry the clock and the spinner). */
|
|
306
|
+
function executionLines(model, sel, dctx) {
|
|
307
|
+
const g = dctx.g;
|
|
308
|
+
const sep = g.sep;
|
|
309
|
+
const width = dctx.columns - 1;
|
|
310
|
+
const spin = g.spinner[dctx.spinner % g.spinner.length];
|
|
311
|
+
const execution = model.execution;
|
|
312
|
+
const tasksTotal = model.status?.tasks.length ?? 0;
|
|
313
|
+
const lines = [];
|
|
314
|
+
let title = 'LAST EXECUTION';
|
|
315
|
+
let right;
|
|
316
|
+
if (execution === undefined) {
|
|
317
|
+
right = [d('…')];
|
|
318
|
+
}
|
|
319
|
+
else if (execution.state === null) {
|
|
320
|
+
right = execution.settling ? [b(`${g.quarter} STARTING`, 'info'), d(` ${sep} ${spin}`)] : [d(`${g.empty} never run ${sep} r run`)];
|
|
321
|
+
if (execution.settling)
|
|
322
|
+
title = 'EXECUTION';
|
|
323
|
+
}
|
|
324
|
+
else if (model.live) {
|
|
325
|
+
title = 'EXECUTION';
|
|
326
|
+
const head = execution.stopping ? b(`${g.square} STOPPING`, 'warn') : b(`${g.quarter} RUNNING`, 'info');
|
|
327
|
+
right = [head, d(` ${sep} started ${timeAgo(execution.state.startedAt, dctx.now)} ${sep} ${model.done} of ${tasksTotal} tasks ${sep} ${spin}`)];
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
const state = execution.state;
|
|
331
|
+
const cell = executionStatusCell(state.status.type, g);
|
|
332
|
+
let detail = ` ${sep} started ${timeAgo(state.startedAt, dctx.now)}`;
|
|
333
|
+
if (state.summary.type === 'some') {
|
|
334
|
+
const s = state.summary.value;
|
|
335
|
+
detail += ` ${sep} ${formatDuration(executionDuration(state) ?? s.duration)} ${sep} executed ${s.executed} ${sep} cached ${s.cached} ${sep} failed ${s.failed} ${sep} skipped ${s.skipped}`;
|
|
336
|
+
}
|
|
337
|
+
right = [b(`${cell.glyph} ${cell.word}`, cell.tone), d(detail)];
|
|
338
|
+
}
|
|
339
|
+
lines.push(lrLine([t(' '), b(title)], [...right, t(' ')], width));
|
|
340
|
+
const shown = model.events.slice(0, MAX_EVENT_ROWS);
|
|
341
|
+
for (const event of shown) {
|
|
342
|
+
const cell = eventCell(event, g);
|
|
343
|
+
const logs = opensLogs(event);
|
|
344
|
+
const selected = logs && sel?.kind === 'logs' && sel.name === cell.task;
|
|
345
|
+
const left = [
|
|
346
|
+
t(' '),
|
|
347
|
+
selected ? b(g.sel, 'brand') : t(' '),
|
|
348
|
+
t(padStart(ageBare(cell.timestamp, dctx.now), 4)),
|
|
349
|
+
t(' '),
|
|
350
|
+
b(cell.glyph, cell.tone),
|
|
351
|
+
t(padEnd(` ${cell.word}`, 13)),
|
|
352
|
+
selected ? b(cell.task) : t(cell.task),
|
|
353
|
+
];
|
|
354
|
+
const detail = [];
|
|
355
|
+
if (event.type === 'start')
|
|
356
|
+
detail.push(d(`${spin} ${ageBare(cell.timestamp, dctx.now)}`));
|
|
357
|
+
else if (cell.detail !== '')
|
|
358
|
+
detail.push(d(cell.detail));
|
|
359
|
+
if (logs)
|
|
360
|
+
detail.push(t(' '), b(`${g.enter} logs`, 'brand'));
|
|
361
|
+
lines.push(lrLine(left, [...detail, t(' ')], width));
|
|
362
|
+
}
|
|
363
|
+
if (model.events.length > shown.length) {
|
|
364
|
+
lines.push([t(' '), d(`… ${model.events.length - shown.length} more failed ${sep} /logs <task>`)]);
|
|
365
|
+
}
|
|
366
|
+
return lines;
|
|
367
|
+
}
|
|
368
|
+
/** The `SIZE · LAST RUN` cell of a task row. */
|
|
369
|
+
function lastRunText(task, event, size, dctx) {
|
|
370
|
+
const g = dctx.g;
|
|
371
|
+
const spin = g.spinner[dctx.spinner % g.spinner.length];
|
|
372
|
+
switch (task.status.type) {
|
|
373
|
+
case 'in-progress': {
|
|
374
|
+
const since = task.status.value.startedAt.type === 'some' ? task.status.value.startedAt.value : event?.type === 'start' ? event.value.timestamp : null;
|
|
375
|
+
return `${spin} ${since !== null ? ageBare(since, dctx.now) : 'running'}`;
|
|
376
|
+
}
|
|
377
|
+
case 'waiting':
|
|
378
|
+
return `— ${g.sep} ${lowerFirst(task.status.value.reason)}`;
|
|
379
|
+
default: {
|
|
380
|
+
const run = event === undefined ? (task.status.type === 'ready' ? 'never' : '—')
|
|
381
|
+
: event.type === 'complete' || event.type === 'failed' ? formatDuration(event.value.duration)
|
|
382
|
+
: event.type === 'cached' ? 'cached'
|
|
383
|
+
: event.type === 'error' ? 'error'
|
|
384
|
+
: event.type === 'input_unavailable' ? 'skipped'
|
|
385
|
+
: '—';
|
|
386
|
+
return `${size} ${g.sep} ${run}`;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* The column's lines in `[top, top + visible)` — the window the screen
|
|
392
|
+
* shows, rendered from the model with the selected row restyled and the
|
|
393
|
+
* clock and the spinner stamped in.
|
|
394
|
+
*
|
|
395
|
+
* @param model - The dashboard model
|
|
396
|
+
* @param dctx - The dashboard context
|
|
397
|
+
* @param sel - The selected row index
|
|
398
|
+
* @param top - The first line
|
|
399
|
+
* @param visible - Lines in the window
|
|
400
|
+
* @returns The lines (fewer when the column ends first)
|
|
401
|
+
*/
|
|
402
|
+
export function dashboardLines(model, dctx, sel, top, visible) {
|
|
403
|
+
const g = dctx.g;
|
|
404
|
+
const width = dctx.columns - 1;
|
|
405
|
+
const first = Math.max(0, top);
|
|
406
|
+
const end = Math.min(model.total, first + visible);
|
|
407
|
+
if (model.status === undefined)
|
|
408
|
+
return model.placeholder.slice(first, end);
|
|
409
|
+
const selected = model.rows[Math.max(0, Math.min(sel, model.rows.length - 1))];
|
|
410
|
+
const panel = executionLines(model, selected, dctx);
|
|
411
|
+
const taskSel = selected?.kind === 'task' ? model.tasks.findIndex(row => row.task.name === selected.name) : -1;
|
|
412
|
+
const inputSel = selected?.kind === 'input' ? model.inputNames.indexOf(selected.name) : -1;
|
|
413
|
+
const tasksFirst = model.tasksAt + 2;
|
|
414
|
+
const inputsFirst = model.inputsAt + 2;
|
|
415
|
+
const out = [];
|
|
416
|
+
for (let i = first; i < end; i++) {
|
|
417
|
+
if (i < model.counts.length)
|
|
418
|
+
out.push(model.counts[i]);
|
|
419
|
+
else if (i < model.panelAt)
|
|
420
|
+
out.push(blank(width));
|
|
421
|
+
else if (i < model.panelAt + panel.length)
|
|
422
|
+
out.push(panel[i - model.panelAt]);
|
|
423
|
+
else if (i < model.tasksAt)
|
|
424
|
+
out.push(blank(width));
|
|
425
|
+
else if (i === model.tasksAt)
|
|
426
|
+
out.push(sectionLine('TASKS', '', width));
|
|
427
|
+
else if (i === model.tasksAt + 1)
|
|
428
|
+
out.push(tableLine(model.taskPlan, null, false, width, g));
|
|
429
|
+
else if (i < model.inputsAt - 1) {
|
|
430
|
+
const row = model.tasks[i - tasksFirst];
|
|
431
|
+
if (row === undefined)
|
|
432
|
+
out.push([t(' '), d('no tasks')]);
|
|
433
|
+
else
|
|
434
|
+
out.push(tableLine(model.taskPlan, { cells: { ...row.cells, size: lastRunText(row.task, row.event, row.size, dctx) } }, i - tasksFirst === taskSel, width, g));
|
|
435
|
+
}
|
|
436
|
+
else if (i < model.inputsAt)
|
|
437
|
+
out.push(blank(width));
|
|
438
|
+
else if (i === model.inputsAt)
|
|
439
|
+
out.push(sectionLine('INPUTS', '', width));
|
|
440
|
+
else if (i === model.inputsAt + 1)
|
|
441
|
+
out.push(tableLine(model.inputPlan, null, false, width, g));
|
|
442
|
+
else {
|
|
443
|
+
const row = model.inputs[i - inputsFirst];
|
|
444
|
+
if (row === undefined)
|
|
445
|
+
out.push([t(' '), d('no inputs')]);
|
|
446
|
+
else
|
|
447
|
+
out.push(tableLine(model.inputPlan, row, i - inputsFirst === inputSel, width, g));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return out;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* The dashboard body: the fixed title, then the column window with its
|
|
454
|
+
* scrollbar — plus the window's clickable rows and pane for the mouse.
|
|
455
|
+
*
|
|
456
|
+
* @param state - The store state
|
|
457
|
+
* @param ctx - The render context
|
|
458
|
+
* @returns The body lines, hits and pane
|
|
459
|
+
*/
|
|
460
|
+
export function renderDashboard(state, ctx) {
|
|
461
|
+
if (state.view.kind !== 'dashboard')
|
|
462
|
+
return { body: [], hits: [], pane: null };
|
|
463
|
+
const ws = state.view.ws;
|
|
464
|
+
const width = ctx.layout.columns;
|
|
465
|
+
const out = [dashboardTitle(state, ws, ctx)];
|
|
466
|
+
const dctx = dashboardCtx(state, ctx);
|
|
467
|
+
const model = dashboardModel(state, ws, dctx);
|
|
468
|
+
const visible = Math.max(1, ctx.layout.bodyRows - 1);
|
|
469
|
+
const total = model.total;
|
|
470
|
+
const top = Math.max(0, Math.min(state.view.list.top, Math.max(0, total - visible)));
|
|
471
|
+
const window = dashboardLines(model, dctx, state.view.list.sel, top, visible);
|
|
472
|
+
while (window.length < visible)
|
|
473
|
+
window.push(blank(width - 1));
|
|
474
|
+
out.push(...withScrollbar(window, width, total, visible, top, ctx.g));
|
|
475
|
+
const hits = model.rows
|
|
476
|
+
.map((row, index) => ({ row, index }))
|
|
477
|
+
.filter(({ row }) => row.line >= top && row.line < top + visible)
|
|
478
|
+
.map(({ row, index }) => ({ row: 1 + (row.line - top), x0: 0, x1: width, target: { kind: 'dashboard', index } }));
|
|
479
|
+
return { body: out, hits, pane: { top: 1, rows: visible, total, visible, scrollTop: top } };
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Scrolls the column by lines (the wheel), or to a line (a thumb drag),
|
|
483
|
+
* keeping the selection inside the window.
|
|
484
|
+
*
|
|
485
|
+
* @param state - The store state
|
|
486
|
+
* @param controller - The controller
|
|
487
|
+
* @param to - `{ delta }` lines, or `{ top }` absolute
|
|
488
|
+
*/
|
|
489
|
+
export function scrollDashboard(state, controller, to) {
|
|
490
|
+
const nav = navigation(state, controller);
|
|
491
|
+
if (nav === null || state.view.kind !== 'dashboard')
|
|
492
|
+
return;
|
|
493
|
+
const { model, visible } = nav;
|
|
494
|
+
const total = model.total;
|
|
495
|
+
const current = state.view.list.top;
|
|
496
|
+
const top = Math.max(0, Math.min('delta' in to ? current + to.delta : to.top, Math.max(0, total - visible)));
|
|
497
|
+
let sel = state.view.list.sel;
|
|
498
|
+
if (model.rows.length > 0) {
|
|
499
|
+
const line = model.rows[Math.max(0, Math.min(sel, model.rows.length - 1))].line;
|
|
500
|
+
if (line < top || line >= top + visible) {
|
|
501
|
+
// The selection follows the window: the first (or last) row inside it.
|
|
502
|
+
const inside = model.rows.map((r, i) => ({ r, i })).filter(({ r }) => r.line >= top && r.line < top + visible);
|
|
503
|
+
const pick = line < top ? inside[0] : inside[inside.length - 1];
|
|
504
|
+
if (pick !== undefined)
|
|
505
|
+
sel = pick.i;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
controller.dispatch({ type: 'view/set', view: { ...state.view, list: { sel, top } } });
|
|
509
|
+
}
|
|
510
|
+
registerView('dashboard', (state, ctx) => {
|
|
511
|
+
const { body, hits, pane } = renderDashboard(state, ctx);
|
|
512
|
+
return { body, hits, pane: pane ?? undefined, hints: dashboardHints(state, ctx) };
|
|
513
|
+
});
|
|
514
|
+
/** The dashboard hints. */
|
|
515
|
+
export function dashboardHints(state, ctx) {
|
|
516
|
+
const polled = state.data.polledAt;
|
|
517
|
+
const ws = state.view.kind === 'dashboard' ? state.view.ws : null;
|
|
518
|
+
const running = ws !== null && isRunLive(state, ws);
|
|
519
|
+
return {
|
|
520
|
+
left: running ? '↑↓ move ⏎ open x stop / commands' : '↑↓ move ⏎ open r run x stop w workspaces / commands',
|
|
521
|
+
right: polled !== null ? `polled ${agoShort(polled, ctx.now)}` : '',
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
// ---------------------------------------------------------------------------
|
|
525
|
+
// Navigation
|
|
526
|
+
// ---------------------------------------------------------------------------
|
|
527
|
+
/** The model and the window geometry for navigation (no theme needed; the model comes from the cache). */
|
|
528
|
+
function navigation(state, controller) {
|
|
529
|
+
if (state.view.kind !== 'dashboard')
|
|
530
|
+
return null;
|
|
531
|
+
const layout = layoutOf(state);
|
|
532
|
+
const dctx = { g: controller.deps.glyphs, now: controller.deps.now(), columns: layout.columns, bp: breakpoint(state.size), spinner: 0 };
|
|
533
|
+
return { model: dashboardModel(state, state.view.ws, dctx), visible: Math.max(1, layout.bodyRows - 1) };
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Selects a row of the column (by index) and scrolls it into view.
|
|
537
|
+
*
|
|
538
|
+
* @param state - The store state
|
|
539
|
+
* @param controller - The controller
|
|
540
|
+
* @param index - The row index
|
|
541
|
+
*/
|
|
542
|
+
export function selectDashboardRow(state, controller, index) {
|
|
543
|
+
const nav = navigation(state, controller);
|
|
544
|
+
if (nav === null || state.view.kind !== 'dashboard')
|
|
545
|
+
return;
|
|
546
|
+
const { model, visible } = nav;
|
|
547
|
+
if (model.rows.length === 0)
|
|
548
|
+
return;
|
|
549
|
+
const sel = Math.max(0, Math.min(index, model.rows.length - 1));
|
|
550
|
+
const top = scrollIntoView(state.view.list.top, model.rows[sel].line, visible, model.total);
|
|
551
|
+
controller.dispatch({ type: 'view/set', view: { ...state.view, list: { sel, top } } });
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Moves the selection (or scrolls the column when it has no selectable
|
|
555
|
+
* rows); page moves step by a window of lines.
|
|
556
|
+
*
|
|
557
|
+
* @param state - The store state
|
|
558
|
+
* @param controller - The controller
|
|
559
|
+
* @param op - The navigation
|
|
560
|
+
*/
|
|
561
|
+
export function moveDashboard(state, controller, op) {
|
|
562
|
+
const nav = navigation(state, controller);
|
|
563
|
+
if (nav === null || state.view.kind !== 'dashboard')
|
|
564
|
+
return;
|
|
565
|
+
const { model, visible } = nav;
|
|
566
|
+
const total = model.total;
|
|
567
|
+
const count = model.rows.length;
|
|
568
|
+
const page = Math.max(1, visible - 1);
|
|
569
|
+
if (count === 0) {
|
|
570
|
+
const delta = op === 'up' ? -1 : op === 'down' ? 1 : op === 'pageUp' ? -page : op === 'pageDown' ? page : op === 'home' ? -total : total;
|
|
571
|
+
const top = Math.max(0, Math.min(state.view.list.top + delta, Math.max(0, total - visible)));
|
|
572
|
+
controller.dispatch({ type: 'view/set', view: { ...state.view, list: { sel: 0, top } } });
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
const last = count - 1;
|
|
576
|
+
const current = Math.max(0, Math.min(state.view.list.sel, last));
|
|
577
|
+
const line = model.rows[current].line;
|
|
578
|
+
let sel = current;
|
|
579
|
+
switch (op) {
|
|
580
|
+
case 'up':
|
|
581
|
+
sel = Math.max(0, current - 1);
|
|
582
|
+
break;
|
|
583
|
+
case 'down':
|
|
584
|
+
sel = Math.min(last, current + 1);
|
|
585
|
+
break;
|
|
586
|
+
case 'home':
|
|
587
|
+
sel = 0;
|
|
588
|
+
break;
|
|
589
|
+
case 'end':
|
|
590
|
+
sel = last;
|
|
591
|
+
break;
|
|
592
|
+
case 'pageUp': {
|
|
593
|
+
const target = line - page;
|
|
594
|
+
let i = current;
|
|
595
|
+
while (i > 0 && model.rows[i - 1].line >= target)
|
|
596
|
+
i--;
|
|
597
|
+
sel = i === current ? Math.max(0, current - 1) : i;
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
case 'pageDown': {
|
|
601
|
+
const target = line + page;
|
|
602
|
+
let i = current;
|
|
603
|
+
while (i < last && model.rows[i + 1].line <= target)
|
|
604
|
+
i++;
|
|
605
|
+
sel = i === current ? Math.min(last, current + 1) : i;
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
// Home shows the column from its start and End from its end; the other moves scroll minimally.
|
|
610
|
+
const top = op === 'home' ? 0
|
|
611
|
+
: op === 'end' ? Math.max(0, total - visible)
|
|
612
|
+
: scrollIntoView(state.view.list.top, model.rows[sel].line, visible, total);
|
|
613
|
+
controller.dispatch({ type: 'view/set', view: { ...state.view, list: { sel, top } } });
|
|
614
|
+
}
|
|
615
|
+
// The generic list model is not used: the column's rows sit at irregular
|
|
616
|
+
// line offsets, so the view moves and scrolls itself (`moveDashboard`).
|
|
617
|
+
registerListModel('dashboard', () => ({ count: 0, visible: 0 }));
|
|
618
|
+
registerViewHooks('dashboard', {
|
|
619
|
+
click: (target, _event, state, controller) => {
|
|
620
|
+
if (target.kind !== 'dashboard')
|
|
621
|
+
return false;
|
|
622
|
+
selectDashboardRow(state, controller, target.index);
|
|
623
|
+
return true;
|
|
624
|
+
},
|
|
625
|
+
scroll: (to, state, controller) => {
|
|
626
|
+
scrollDashboard(state, controller, to);
|
|
627
|
+
return true;
|
|
628
|
+
},
|
|
629
|
+
open: (state, controller) => {
|
|
630
|
+
const nav = navigation(state, controller);
|
|
631
|
+
if (nav === null || state.view.kind !== 'dashboard')
|
|
632
|
+
return;
|
|
633
|
+
const row = nav.model.rows[Math.max(0, Math.min(state.view.list.sel, nav.model.rows.length - 1))];
|
|
634
|
+
if (row === undefined)
|
|
635
|
+
return;
|
|
636
|
+
const ws = state.view.ws;
|
|
637
|
+
if (row.kind === 'logs')
|
|
638
|
+
controller.openTask(ws, row.name, 'stdout');
|
|
639
|
+
else if (row.kind === 'task')
|
|
640
|
+
controller.openTask(ws, row.name);
|
|
641
|
+
else
|
|
642
|
+
controller.openInput(ws, row.name);
|
|
643
|
+
},
|
|
644
|
+
key: (action, state, controller) => {
|
|
645
|
+
if (action.kind !== 'move' || state.view.kind !== 'dashboard')
|
|
646
|
+
return false;
|
|
647
|
+
moveDashboard(state, controller, action.op);
|
|
648
|
+
return true;
|
|
649
|
+
},
|
|
650
|
+
});
|
|
651
|
+
//# sourceMappingURL=dashboard.js.map
|