@elaraai/e3-ui-cli 1.0.79 → 1.0.81
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 +8 -6
- package/dist/app/assets/{engine-DAHqaY4g-B9nTMQVT.js → engine-D7VXHt7P-TStdXyXV.js} +1 -1
- package/dist/app/assets/{index-CIIzwdrV.js → index-DUOL-ROM.js} +113 -115
- package/dist/app/index.html +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/tui.d.ts +15 -3
- package/dist/commands/tui.d.ts.map +1 -1
- package/dist/commands/tui.js +14 -4
- package/dist/commands/tui.js.map +1 -1
- package/dist/e3-server.d.ts +6 -1
- package/dist/e3-server.d.ts.map +1 -1
- package/dist/e3-server.js +6 -1
- package/dist/e3-server.js.map +1 -1
- package/dist/load-source.d.ts +13 -11
- package/dist/load-source.d.ts.map +1 -1
- package/dist/load-source.js +9 -10
- package/dist/load-source.js.map +1 -1
- package/dist/tui/api.d.ts +17 -1
- package/dist/tui/api.d.ts.map +1 -1
- package/dist/tui/api.js +6 -1
- package/dist/tui/api.js.map +1 -1
- package/dist/tui/app.d.ts.map +1 -1
- package/dist/tui/app.js +19 -1
- package/dist/tui/app.js.map +1 -1
- package/dist/tui/controller.d.ts +3 -1
- package/dist/tui/controller.d.ts.map +1 -1
- package/dist/tui/controller.js +48 -20
- package/dist/tui/controller.js.map +1 -1
- package/dist/tui/data/dataflow.d.ts +1 -2
- package/dist/tui/data/dataflow.d.ts.map +1 -1
- package/dist/tui/data/dataflow.js +1 -3
- package/dist/tui/data/dataflow.js.map +1 -1
- package/dist/tui/data/dataset.d.ts +24 -5
- package/dist/tui/data/dataset.d.ts.map +1 -1
- package/dist/tui/data/dataset.js +100 -25
- package/dist/tui/data/dataset.js.map +1 -1
- package/dist/tui/data/feeds.d.ts +3 -0
- package/dist/tui/data/feeds.d.ts.map +1 -1
- package/dist/tui/data/feeds.js +60 -7
- package/dist/tui/data/feeds.js.map +1 -1
- package/dist/tui/data/records.d.ts +63 -0
- package/dist/tui/data/records.d.ts.map +1 -0
- package/dist/tui/data/records.js +144 -0
- package/dist/tui/data/records.js.map +1 -0
- package/dist/tui/input/commands.d.ts +14 -11
- package/dist/tui/input/commands.d.ts.map +1 -1
- package/dist/tui/input/commands.js +20 -15
- package/dist/tui/input/commands.js.map +1 -1
- package/dist/tui/input/completion.d.ts +10 -4
- package/dist/tui/input/completion.d.ts.map +1 -1
- package/dist/tui/input/completion.js +6 -0
- package/dist/tui/input/completion.js.map +1 -1
- package/dist/tui/model/catalogue.d.ts.map +1 -1
- package/dist/tui/model/catalogue.js +39 -3
- package/dist/tui/model/catalogue.js.map +1 -1
- package/dist/tui/model/help.d.ts.map +1 -1
- package/dist/tui/model/help.js +44 -4
- package/dist/tui/model/help.js.map +1 -1
- package/dist/tui/model/status.d.ts +55 -3
- package/dist/tui/model/status.d.ts.map +1 -1
- package/dist/tui/model/status.js +126 -4
- package/dist/tui/model/status.js.map +1 -1
- package/dist/tui/model/tree.d.ts +36 -1
- package/dist/tui/model/tree.d.ts.map +1 -1
- package/dist/tui/model/tree.js +58 -8
- package/dist/tui/model/tree.js.map +1 -1
- package/dist/tui/render/glyphs.d.ts +2 -0
- package/dist/tui/render/glyphs.d.ts.map +1 -1
- package/dist/tui/render/glyphs.js +2 -0
- package/dist/tui/render/glyphs.js.map +1 -1
- package/dist/tui/render/layout.d.ts +1 -1
- package/dist/tui/render/layout.d.ts.map +1 -1
- package/dist/tui/render/layout.js +50 -5
- package/dist/tui/render/layout.js.map +1 -1
- package/dist/tui/session.d.ts.map +1 -1
- package/dist/tui/session.js +7 -0
- package/dist/tui/session.js.map +1 -1
- package/dist/tui/state/actions.d.ts +70 -5
- package/dist/tui/state/actions.d.ts.map +1 -1
- package/dist/tui/state/actions.js +10 -1
- package/dist/tui/state/actions.js.map +1 -1
- package/dist/tui/state/persist.d.ts +1 -1
- package/dist/tui/state/persist.d.ts.map +1 -1
- package/dist/tui/state/reducer.d.ts.map +1 -1
- package/dist/tui/state/reducer.js +24 -2
- package/dist/tui/state/reducer.js.map +1 -1
- package/dist/tui/ui/App.js +1 -1
- package/dist/tui/ui/App.js.map +1 -1
- package/dist/tui/ui/shell/chrome.d.ts +6 -1
- package/dist/tui/ui/shell/chrome.d.ts.map +1 -1
- package/dist/tui/ui/shell/chrome.js +26 -23
- package/dist/tui/ui/shell/chrome.js.map +1 -1
- package/dist/tui/ui/views/all.d.ts +1 -0
- package/dist/tui/ui/views/all.d.ts.map +1 -1
- package/dist/tui/ui/views/all.js +1 -0
- package/dist/tui/ui/views/all.js.map +1 -1
- package/dist/tui/ui/views/dashboard.d.ts +75 -18
- package/dist/tui/ui/views/dashboard.d.ts.map +1 -1
- package/dist/tui/ui/views/dashboard.js +265 -40
- package/dist/tui/ui/views/dashboard.js.map +1 -1
- package/dist/tui/ui/views/help.js +1 -1
- package/dist/tui/ui/views/record.d.ts +38 -0
- package/dist/tui/ui/views/record.d.ts.map +1 -0
- package/dist/tui/ui/views/record.js +298 -0
- package/dist/tui/ui/views/record.js.map +1 -0
- package/dist/tui/ui/widgets/reads.d.ts +7 -7
- package/dist/tui/ui/widgets/reads.d.ts.map +1 -1
- package/dist/tui/ui/widgets/reads.js +9 -21
- package/dist/tui/ui/widgets/reads.js.map +1 -1
- package/dist/tui/ui/widgets/runs.d.ts +2 -2
- package/dist/tui/ui/widgets/runs.d.ts.map +1 -1
- package/dist/tui/ui/widgets/runs.js +3 -2
- package/dist/tui/ui/widgets/runs.js.map +1 -1
- package/dist/tui/ui/widgets/tree.d.ts +11 -6
- package/dist/tui/ui/widgets/tree.d.ts.map +1 -1
- package/dist/tui/ui/widgets/tree.js +28 -17
- package/dist/tui/ui/widgets/tree.js.map +1 -1
- package/package.json +14 -14
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
* Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
|
|
4
4
|
*/
|
|
5
5
|
import { breakpoint, columnPlan, scrollIntoView } from '../../render/layout.js';
|
|
6
|
-
import { agoShort, formatDuration, formatSize, hashShort, padEnd, padStart, timeAgo } from '../../render/text.js';
|
|
6
|
+
import { agoShort, formatDuration, formatInt, formatSize, hashShort, padEnd, padStart, timeAgo } from '../../render/text.js';
|
|
7
7
|
import { layoutOf, registerListModel } from '../../model/index.js';
|
|
8
8
|
import { datasetEntries } from '../../model/catalogue.js';
|
|
9
|
-
import { datasetStatusCell, eventCell, executionDuration, executionStatusCell, statusText, taskStatusCell } from '../../model/status.js';
|
|
9
|
+
import { datasetStatusCell, eventCell, executionDuration, executionStatusCell, intakeCell, recordDeployCell, splitPlace, statusText, taskStatusCell, unitPlace, waitCell, } from '../../model/status.js';
|
|
10
10
|
import { registerViewHooks } from '../../controller.js';
|
|
11
11
|
import { isRunLive, lockHolderText } from '../../data/dataflow.js';
|
|
12
|
+
import { recordEntries } from '../../data/records.js';
|
|
12
13
|
import { b, blank, d, lineWidth, lrLine, t } from '../lines.js';
|
|
13
14
|
import { centredBlock, sectionLine, tableLine, tablePlan, withScrollbar } from '../shell/widgets.js';
|
|
14
15
|
import { registerView } from './index.js';
|
|
@@ -35,15 +36,21 @@ function ageBare(then, now) {
|
|
|
35
36
|
function lowerFirst(text) {
|
|
36
37
|
return text.length === 0 ? text : text[0].toLowerCase() + text.slice(1);
|
|
37
38
|
}
|
|
38
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* The dashboard title line: ` main ● DEPLOYED · demand@1.4.2 · deployed 3d ago · lock: none`;
|
|
41
|
+
* while a deploy holds the workspace, the package it deploys, who deploys it
|
|
42
|
+
* and since when, in place of the lock (`◔ DEPLOYING · demand@1.5.0 · pid 4242 · started 9s ago`).
|
|
43
|
+
*/
|
|
39
44
|
export function dashboardTitle(state, ws, ctx) {
|
|
40
45
|
const g = ctx.g;
|
|
41
46
|
const info = (state.data.workspaces ?? []).find(w => w.name === ws);
|
|
42
47
|
const wsState = state.data.workspaceState[ws];
|
|
43
48
|
const status = state.data.status[ws]?.result;
|
|
49
|
+
const deploying = deployOf(state.data.lock[ws]);
|
|
44
50
|
const right = [];
|
|
45
51
|
if (info === undefined || !info.deployed) {
|
|
46
|
-
|
|
52
|
+
if (deploying === null)
|
|
53
|
+
right.push(b(`${g.empty} EMPTY`, 'muted'));
|
|
47
54
|
}
|
|
48
55
|
else {
|
|
49
56
|
const pkg = info.packageName.type === 'some' ? `${info.packageName.value}${info.packageVersion.type === 'some' ? `@${info.packageVersion.value}` : ''}` : '';
|
|
@@ -52,7 +59,18 @@ export function dashboardTitle(state, ws, ctx) {
|
|
|
52
59
|
right.push(d(` ${g.sep} deployed ${timeAgo(wsState.deployedAt, ctx.now)}`));
|
|
53
60
|
}
|
|
54
61
|
const lock = status?.lock;
|
|
55
|
-
if (
|
|
62
|
+
if (deploying !== null) {
|
|
63
|
+
if (right.length > 0)
|
|
64
|
+
right.push(t(` ${g.sep} `));
|
|
65
|
+
right.push(b(`${g.quarter} DEPLOYING`, 'info'));
|
|
66
|
+
const progress = deployProgressOf(deploying);
|
|
67
|
+
const holder = deploying.state.holder;
|
|
68
|
+
// The process's pid, not its command: the command is the whole argv, paths and all.
|
|
69
|
+
const who = holder.type === 'process' ? `pid ${holder.value.pid}` : holder.value.functionName;
|
|
70
|
+
const pkg = progress !== null ? ` ${g.sep} ${progress.package.name}@${progress.package.version}` : '';
|
|
71
|
+
right.push(d(`${pkg} ${g.sep} ${who} ${g.sep} started ${timeAgo(deploying.state.acquiredAt, ctx.now)}`));
|
|
72
|
+
}
|
|
73
|
+
else if (lock !== undefined) {
|
|
56
74
|
if (lock.type === 'some') {
|
|
57
75
|
right.push(t(` ${g.sep} `), b(`lock: ${lockHolderText(lock.value, ctx.now, g)}`, 'warn'));
|
|
58
76
|
}
|
|
@@ -63,6 +81,83 @@ export function dashboardTitle(state, ws, ctx) {
|
|
|
63
81
|
right.push(t(' '));
|
|
64
82
|
return lrLine([t(' '), b(ws)], right, ctx.layout.columns);
|
|
65
83
|
}
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// A deploy in progress
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
/** The lock, when a deploy holds the workspace; null otherwise. */
|
|
88
|
+
function deployOf(lock) {
|
|
89
|
+
return lock !== undefined && lock !== null && lock.state.operation.type === 'deployment' ? lock : null;
|
|
90
|
+
}
|
|
91
|
+
/** How far a deploy says it has got, once it has reported. */
|
|
92
|
+
function deployProgressOf(lock) {
|
|
93
|
+
return lock.progress.type === 'some' && lock.progress.value.type === 'deployment' ? lock.progress.value.value : null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The DEPLOY header's right side — what the deploy is doing and how far it has
|
|
97
|
+
* got: `◔ TAKING IN · 2 of 6 files · 134.9 of 202.4 MB · 14.3 MB/s · ~5.2s left`
|
|
98
|
+
* while its files come in, then `◔ MIGRATING` or `◔ BUILDING INDEXES` while a
|
|
99
|
+
* record's step runs, and `◔ FINISHING` once its files and records are done.
|
|
100
|
+
*
|
|
101
|
+
* @param progress - What the deploy last reported
|
|
102
|
+
* @param dctx - The dashboard context
|
|
103
|
+
* @returns The spans
|
|
104
|
+
*/
|
|
105
|
+
function deployPhase(progress, dctx) {
|
|
106
|
+
const { sep, quarter } = dctx.g;
|
|
107
|
+
const files = progress.files;
|
|
108
|
+
const done = files.filter(file => file.step.type === 'done').length;
|
|
109
|
+
const total = files.reduce((sum, file) => sum + Number(file.total), 0);
|
|
110
|
+
const plural = `${files.length} file${files.length === 1 ? '' : 's'}`;
|
|
111
|
+
if (done < files.length) {
|
|
112
|
+
// The bytes past their hash: a file taking in, and every file in.
|
|
113
|
+
const moved = files.reduce((sum, file) => sum + (file.step.type === 'done' ? Number(file.total) : file.step.type === 'taking_in' ? Number(file.bytes) : 0), 0);
|
|
114
|
+
// `134.9 of 202.4 MB`: the unit once when both share it.
|
|
115
|
+
const whole = formatSize(total);
|
|
116
|
+
const part = formatSize(moved);
|
|
117
|
+
const unit = whole.slice(whole.indexOf(' '));
|
|
118
|
+
const seconds = (dctx.now - progress.startedAt.getTime()) / 1000;
|
|
119
|
+
const rate = seconds > 0 ? moved / seconds : 0;
|
|
120
|
+
const pace = rate > 0 ? ` ${sep} ${formatSize(rate)}/s ${sep} ~${formatDuration(((total - moved) / rate) * 1000)} left` : '';
|
|
121
|
+
return [b(`${quarter} TAKING IN`, 'info'), d(` ${sep} ${done} of ${plural} ${sep} ${part.endsWith(unit) ? part.slice(0, -unit.length) : part} of ${whole}${pace}`)];
|
|
122
|
+
}
|
|
123
|
+
const running = progress.records.find(record => record.step.type === 'migrating' || record.step.type === 'indexing')?.step.type;
|
|
124
|
+
const head = running === 'migrating' ? 'MIGRATING' : running === 'indexing' ? 'BUILDING INDEXES' : 'FINISHING';
|
|
125
|
+
return [b(`${quarter} ${head}`, 'info'), ...(files.length > 0 ? [d(` ${sep} took in ${plural} ${sep} ${formatSize(total)}`)] : [])];
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The column while a deploy holds the workspace: the DEPLOY header, then the
|
|
129
|
+
* normal screen's tables by type — INPUTS, a row per file source the deploy
|
|
130
|
+
* takes in, and RECORDS, a row per record it deploys — each row with the step
|
|
131
|
+
* the deploy is at with it.
|
|
132
|
+
*
|
|
133
|
+
* @param lock - The deploy's lock
|
|
134
|
+
* @param dctx - The dashboard context
|
|
135
|
+
* @returns The lines
|
|
136
|
+
*/
|
|
137
|
+
function deployLines(lock, dctx) {
|
|
138
|
+
const g = dctx.g;
|
|
139
|
+
const width = dctx.columns - 1;
|
|
140
|
+
const progress = deployProgressOf(lock);
|
|
141
|
+
if (progress === null)
|
|
142
|
+
return [lrLine([t(' '), b('DEPLOY')], [b(`${g.quarter} STARTING`, 'info'), t(' ')], width)];
|
|
143
|
+
const lines = [lrLine([t(' '), b('DEPLOY')], [...deployPhase(progress, dctx), t(' ')], width)];
|
|
144
|
+
const table = (title, kind, rows) => {
|
|
145
|
+
if (rows.length === 0)
|
|
146
|
+
return;
|
|
147
|
+
const plan = tablePlan(columnPlan(kind, dctx.bp), rows, width);
|
|
148
|
+
lines.push(blank(width), sectionLine(title, '', width), tableLine(plan, null, false, width, g), ...rows.map(row => tableLine(plan, row, false, width, g)));
|
|
149
|
+
};
|
|
150
|
+
table('INPUTS', 'deployInputs', progress.files.map(file => {
|
|
151
|
+
const cell = intakeCell(file, g);
|
|
152
|
+
return { cells: { name: file.path.split('/').pop() ?? file.path, status: { text: statusText(cell, ' '), tone: cell.tone }, size: formatSize(Number(file.total)) } };
|
|
153
|
+
}));
|
|
154
|
+
table('RECORDS', 'deployRecords', progress.records.map(record => {
|
|
155
|
+
const cell = recordDeployCell(record, g);
|
|
156
|
+
const path = record.plan.record;
|
|
157
|
+
return { cells: { name: path.split('/').pop() ?? path, status: { text: statusText(cell), tone: cell.tone }, indexes: record.indexes.length > 0 ? record.indexes.join(', ') : '—' } };
|
|
158
|
+
}));
|
|
159
|
+
return lines;
|
|
160
|
+
}
|
|
66
161
|
/** The task stats: the four the design shows, plus in-progress / stale-running when non-zero. */
|
|
67
162
|
function taskStats(s, g) {
|
|
68
163
|
const stats = [
|
|
@@ -150,7 +245,8 @@ function countsLines(status, dctx) {
|
|
|
150
245
|
// The model
|
|
151
246
|
// ---------------------------------------------------------------------------
|
|
152
247
|
/**
|
|
153
|
-
* The latest event of each task, in order of first appearance.
|
|
248
|
+
* The latest event of each task, in order of first appearance. A unit's
|
|
249
|
+
* requeue is not a task's event, so it never stands for its task.
|
|
154
250
|
*
|
|
155
251
|
* @param events - The execution's events
|
|
156
252
|
* @returns One event per task
|
|
@@ -159,6 +255,8 @@ export function latestPerTask(events) {
|
|
|
159
255
|
const order = [];
|
|
160
256
|
const latest = new Map();
|
|
161
257
|
for (const event of events) {
|
|
258
|
+
if (event.type === 'requeued')
|
|
259
|
+
continue;
|
|
162
260
|
const task = event.value.task;
|
|
163
261
|
if (!latest.has(task))
|
|
164
262
|
order.push(task);
|
|
@@ -174,16 +272,67 @@ function isLive(execution) {
|
|
|
174
272
|
function opensLogs(event) {
|
|
175
273
|
return event.type === 'failed' || event.type === 'error';
|
|
176
274
|
}
|
|
275
|
+
/** An event's row, with nothing after the task's name. */
|
|
276
|
+
function eventRow(event, g) {
|
|
277
|
+
const cell = eventCell(event, g);
|
|
278
|
+
return { cell, task: cell.task, place: '', timestamp: cell.timestamp, running: event.type === 'start', logs: opensLogs(event) };
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* The rows of the live panel, in the order each happened or began: each
|
|
282
|
+
* task's latest event, a split task's start naming its progress; each unit's
|
|
283
|
+
* latest requeue; and each task or unit waiting for room, whose wait takes the
|
|
284
|
+
* place of the start of a task that waits whole.
|
|
285
|
+
*
|
|
286
|
+
* @param execution - The execution
|
|
287
|
+
* @param latest - Each task's latest event
|
|
288
|
+
* @param g - The glyph set
|
|
289
|
+
* @returns The rows
|
|
290
|
+
*/
|
|
291
|
+
export function liveRows(execution, latest, g) {
|
|
292
|
+
const state = execution.state;
|
|
293
|
+
const budget = state !== null && state.budget.type === 'some' ? state.budget.value : null;
|
|
294
|
+
const waiting = state?.waiting ?? [];
|
|
295
|
+
const splits = new Map((state?.splits ?? []).map(split => [split.task, split]));
|
|
296
|
+
const waitsWhole = new Set(waiting.filter(wait => wait.unit.type === 'none').map(wait => wait.task));
|
|
297
|
+
const rows = [];
|
|
298
|
+
for (const event of latest) {
|
|
299
|
+
if (event.type === 'start' && waitsWhole.has(event.value.task))
|
|
300
|
+
continue;
|
|
301
|
+
const split = event.type === 'start' ? splits.get(event.value.task) : undefined;
|
|
302
|
+
rows.push({ ...eventRow(event, g), place: split === undefined ? '' : splitPlace(split, g.sep) });
|
|
303
|
+
}
|
|
304
|
+
// A unit requeued more than once shows its latest.
|
|
305
|
+
const requeues = new Map();
|
|
306
|
+
for (const event of execution.events) {
|
|
307
|
+
if (event.type !== 'requeued')
|
|
308
|
+
continue;
|
|
309
|
+
const place = unitPlace(event.value.unit, g.sep);
|
|
310
|
+
requeues.set(`${event.value.task} ${place}`, { ...eventRow(event, g), place });
|
|
311
|
+
}
|
|
312
|
+
rows.push(...requeues.values());
|
|
313
|
+
for (const wait of waiting) {
|
|
314
|
+
rows.push({
|
|
315
|
+
cell: waitCell(wait, budget, g),
|
|
316
|
+
task: wait.task,
|
|
317
|
+
place: wait.unit.type === 'some' ? unitPlace(wait.unit.value, g.sep) : '',
|
|
318
|
+
timestamp: wait.since,
|
|
319
|
+
running: false,
|
|
320
|
+
logs: false,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return rows.sort((a, c) => Date.parse(a.timestamp) - Date.parse(c.timestamp));
|
|
324
|
+
}
|
|
177
325
|
let cached = null;
|
|
178
326
|
function sameKey(a, b) {
|
|
179
327
|
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
|
|
328
|
+
&& a.lock === b.lock && a.execution === b.execution && a.datasets === b.datasets && a.taskList === b.taskList && a.records === b.records
|
|
181
329
|
&& a.columns === b.columns && a.bp === b.bp && a.g === b.g;
|
|
182
330
|
}
|
|
183
331
|
/**
|
|
184
332
|
* The dashboard model for a workspace — built once per data change and
|
|
185
333
|
* returned as the same object until the status, the execution, the
|
|
186
|
-
* dataset list, the task list, the
|
|
334
|
+
* dataset list, the task list, the records read, the width or the
|
|
335
|
+
* breakpoint changes. The
|
|
187
336
|
* clock, the spinner and the selection are not inputs: they restyle lines
|
|
188
337
|
* at render ({@link dashboardLines}).
|
|
189
338
|
*
|
|
@@ -198,9 +347,11 @@ export function dashboardModel(state, ws, dctx) {
|
|
|
198
347
|
status: state.data.status[ws]?.result,
|
|
199
348
|
statusError: state.data.statusError[ws],
|
|
200
349
|
workspaceState: state.data.workspaceState[ws],
|
|
350
|
+
lock: state.data.lock[ws],
|
|
201
351
|
execution: state.data.execution[ws],
|
|
202
352
|
datasets: state.data.datasets[ws],
|
|
203
353
|
taskList: state.data.taskList[ws],
|
|
354
|
+
records: state.data.records[ws],
|
|
204
355
|
columns: dctx.columns,
|
|
205
356
|
bp: dctx.bp,
|
|
206
357
|
g: dctx.g,
|
|
@@ -218,9 +369,17 @@ function buildModel(state, ws, dctx, key) {
|
|
|
218
369
|
const status = key.status;
|
|
219
370
|
const execution = key.execution;
|
|
220
371
|
const empty = {
|
|
221
|
-
status, placeholder: [], counts: [], execution, live: false, done: 0,
|
|
222
|
-
taskPlan: [], tasks: [], inputPlan: [], inputs: [], inputNames: [],
|
|
372
|
+
status, placeholder: [], counts: [], execution, live: false, done: 0, feed: [], latest: new Map(),
|
|
373
|
+
taskPlan: [], tasks: [], inputPlan: [], inputs: [], inputNames: [], recordPlan: [], records: [], recordNames: [],
|
|
374
|
+
rows: [], total: 0, panelAt: 0, tasksAt: 0, inputsAt: 0, recordsAt: -1,
|
|
223
375
|
};
|
|
376
|
+
// While a deploy holds the workspace, the column is the deploy: what the
|
|
377
|
+
// workspace held before is going, and a first deploy has nothing else.
|
|
378
|
+
const deploying = deployOf(key.lock);
|
|
379
|
+
if (deploying !== null) {
|
|
380
|
+
const placeholder = deployLines(deploying, dctx);
|
|
381
|
+
return { ...empty, status: undefined, placeholder, total: placeholder.length };
|
|
382
|
+
}
|
|
224
383
|
if (status === undefined) {
|
|
225
384
|
const placeholder = [];
|
|
226
385
|
if (key.workspaceState === null) {
|
|
@@ -236,12 +395,14 @@ function buildModel(state, ws, dctx, key) {
|
|
|
236
395
|
}
|
|
237
396
|
// The dataset map and the latest event per task: once per build, shared by every row.
|
|
238
397
|
const entries = datasetEntries(state, ws);
|
|
239
|
-
const
|
|
398
|
+
const latestEvents = latestPerTask(execution?.events ?? []);
|
|
399
|
+
const latest = new Map(latestEvents.map(e => [e.value.task, e]));
|
|
240
400
|
const live = isLive(execution);
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
401
|
+
// A requeue is a unit's, and ends no task.
|
|
402
|
+
const done = execution === undefined ? 0 : execution.events.filter(e => e.type !== 'start' && e.type !== 'requeued').length;
|
|
403
|
+
const feed = execution === undefined || execution.state === null ? []
|
|
404
|
+
: live ? liveRows(execution, latestEvents, g).slice(-MAX_EVENT_ROWS)
|
|
405
|
+
: latestEvents.filter(opensLogs).map(event => eventRow(event, g));
|
|
245
406
|
const tasks = status.tasks.map(task => {
|
|
246
407
|
const cell = taskStatusCell(task.status, g);
|
|
247
408
|
// The reason / pid / cached detail lives in the last column; only a failure's exit code / message stays inline.
|
|
@@ -258,6 +419,7 @@ function buildModel(state, ws, dctx, key) {
|
|
|
258
419
|
dependsOn: task.dependsOn.length > 0 ? task.dependsOn.join(', ') : '—',
|
|
259
420
|
inputs: inputs.length > 0 ? inputs.join(', ') : '—',
|
|
260
421
|
output: entry?.type ?? '—',
|
|
422
|
+
peak: task.peakBytes.type === 'some' ? formatSize(Number(task.peakBytes.value)) : '—',
|
|
261
423
|
},
|
|
262
424
|
};
|
|
263
425
|
});
|
|
@@ -276,19 +438,36 @@ function buildModel(state, ws, dctx, key) {
|
|
|
276
438
|
},
|
|
277
439
|
};
|
|
278
440
|
});
|
|
441
|
+
// The dataset list names the records; what the records loader read of each fills its row.
|
|
442
|
+
const recordNames = recordEntries(key.datasets ?? []).map(entry => entry.name);
|
|
443
|
+
const records = recordNames.map(name => {
|
|
444
|
+
const facts = key.records?.[name];
|
|
445
|
+
const entry = entries.get(`.records.${name}`);
|
|
446
|
+
const indexes = facts?.signature?.indexes.map(index => index.name) ?? null;
|
|
447
|
+
return {
|
|
448
|
+
head: facts?.head ?? null,
|
|
449
|
+
cells: {
|
|
450
|
+
name,
|
|
451
|
+
rows: facts?.rows != null ? formatInt(facts.rows) : '—',
|
|
452
|
+
size: entry?.size != null ? formatSize(entry.size) : '—',
|
|
453
|
+
indexes: indexes === null ? '…' : indexes.length > 0 ? indexes.join(', ') : '—',
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
});
|
|
279
457
|
const taskPlan = tablePlan(columnPlan('tasks', dctx.bp), tasks, width);
|
|
280
458
|
const inputPlan = tablePlan(columnPlan('inputs', dctx.bp), inputs, width);
|
|
459
|
+
const recordPlan = records.length > 0 ? tablePlan(columnPlan('records', dctx.bp), records, width) : [];
|
|
281
460
|
const counts = countsLines(status, dctx);
|
|
282
|
-
// The geometry: every selectable row's line, numbered in column order — failures, tasks, inputs.
|
|
461
|
+
// The geometry: every selectable row's line, numbered in column order — failures, tasks, inputs, records.
|
|
283
462
|
const rows = [];
|
|
284
463
|
let line = counts.length + 1;
|
|
285
464
|
const panelAt = line;
|
|
286
|
-
const shown =
|
|
287
|
-
shown.forEach((
|
|
288
|
-
if (
|
|
289
|
-
rows.push({ kind: 'logs', name:
|
|
465
|
+
const shown = feed.slice(0, MAX_EVENT_ROWS);
|
|
466
|
+
shown.forEach((row, i) => {
|
|
467
|
+
if (row.logs)
|
|
468
|
+
rows.push({ kind: 'logs', name: row.task, line: panelAt + 1 + i });
|
|
290
469
|
});
|
|
291
|
-
line += 1 + shown.length + (
|
|
470
|
+
line += 1 + shown.length + (feed.length > shown.length ? 1 : 0) + 1;
|
|
292
471
|
const tasksAt = line;
|
|
293
472
|
line += 2;
|
|
294
473
|
status.tasks.forEach((task, i) => rows.push({ kind: 'task', name: task.name, line: line + i }));
|
|
@@ -297,7 +476,19 @@ function buildModel(state, ws, dctx, key) {
|
|
|
297
476
|
line += 2;
|
|
298
477
|
inputNames.forEach((name, i) => rows.push({ kind: 'input', name, line: line + i }));
|
|
299
478
|
line += Math.max(1, inputs.length);
|
|
300
|
-
|
|
479
|
+
// A workspace without records has no RECORDS section at all.
|
|
480
|
+
let recordsAt = -1;
|
|
481
|
+
if (records.length > 0) {
|
|
482
|
+
line += 1;
|
|
483
|
+
recordsAt = line;
|
|
484
|
+
line += 2;
|
|
485
|
+
recordNames.forEach((name, i) => rows.push({ kind: 'record', name, line: line + i }));
|
|
486
|
+
line += records.length;
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
...empty, counts, live, done, feed, latest, taskPlan, tasks, inputPlan, inputs, inputNames, recordPlan, records, recordNames,
|
|
490
|
+
rows, total: line, panelAt, tasksAt, inputsAt, recordsAt,
|
|
491
|
+
};
|
|
301
492
|
}
|
|
302
493
|
// ---------------------------------------------------------------------------
|
|
303
494
|
// Rendering
|
|
@@ -324,7 +515,21 @@ function executionLines(model, sel, dctx) {
|
|
|
324
515
|
else if (model.live) {
|
|
325
516
|
title = 'EXECUTION';
|
|
326
517
|
const head = execution.stopping ? b(`${g.square} STOPPING`, 'warn') : b(`${g.quarter} RUNNING`, 'info');
|
|
327
|
-
|
|
518
|
+
// What the server's budget holds now, where it has one: `memory 9.4 of 14 GB`, the unit once when both share it.
|
|
519
|
+
const budget = execution.state.budget.type === 'some' ? execution.state.budget.value : null;
|
|
520
|
+
let room = '';
|
|
521
|
+
if (budget !== null) {
|
|
522
|
+
const memory = formatSize(Number(budget.memory));
|
|
523
|
+
const inUse = formatSize(Number(budget.memoryInUse));
|
|
524
|
+
const unit = memory.slice(memory.indexOf(' '));
|
|
525
|
+
room = ` ${sep} cores ${budget.coresInUse} of ${budget.cores} ${sep} memory ${inUse.endsWith(unit) ? inUse.slice(0, -unit.length) : inUse} of ${memory}`;
|
|
526
|
+
}
|
|
527
|
+
const age = ` ${sep} started ${timeAgo(execution.state.startedAt, dctx.now)}`;
|
|
528
|
+
const count = ` ${sep} ${model.done} of ${tasksTotal} tasks`;
|
|
529
|
+
const end = ` ${sep} ${spin}`;
|
|
530
|
+
// Short of room, the run's age gives way to the budget (each feed row carries its own age), then the budget to the age.
|
|
531
|
+
const detail = [age + count + room, count + room, age + count].find(text => lineWidth([t(` ${title} `), head, t(`${text}${end} `)]) <= width) ?? age + count;
|
|
532
|
+
right = [head, d(detail + end)];
|
|
328
533
|
}
|
|
329
534
|
else {
|
|
330
535
|
const state = execution.state;
|
|
@@ -337,31 +542,31 @@ function executionLines(model, sel, dctx) {
|
|
|
337
542
|
right = [b(`${cell.glyph} ${cell.word}`, cell.tone), d(detail)];
|
|
338
543
|
}
|
|
339
544
|
lines.push(lrLine([t(' '), b(title)], [...right, t(' ')], width));
|
|
340
|
-
const shown = model.
|
|
341
|
-
for (const
|
|
342
|
-
const
|
|
343
|
-
const logs = opensLogs(event);
|
|
344
|
-
const selected = logs && sel?.kind === 'logs' && sel.name === cell.task;
|
|
545
|
+
const shown = model.feed.slice(0, MAX_EVENT_ROWS);
|
|
546
|
+
for (const row of shown) {
|
|
547
|
+
const selected = row.logs && sel?.kind === 'logs' && sel.name === row.task;
|
|
345
548
|
const left = [
|
|
346
549
|
t(' '),
|
|
347
550
|
selected ? b(g.sel, 'brand') : t(' '),
|
|
348
|
-
t(padStart(ageBare(
|
|
551
|
+
t(padStart(ageBare(row.timestamp, dctx.now), 4)),
|
|
349
552
|
t(' '),
|
|
350
|
-
b(cell.glyph, cell.tone),
|
|
351
|
-
t(padEnd(` ${cell.word}`, 13)),
|
|
352
|
-
selected ? b(
|
|
553
|
+
b(row.cell.glyph, row.cell.tone),
|
|
554
|
+
t(padEnd(` ${row.cell.word}`, 13)),
|
|
555
|
+
selected ? b(row.task) : t(row.task),
|
|
353
556
|
];
|
|
557
|
+
if (row.place !== '')
|
|
558
|
+
left.push(d(` ${sep} ${row.place}`));
|
|
354
559
|
const detail = [];
|
|
355
|
-
if (
|
|
356
|
-
detail.push(d(`${spin} ${ageBare(
|
|
357
|
-
else if (cell.detail !== '')
|
|
358
|
-
detail.push(d(cell.detail));
|
|
359
|
-
if (logs)
|
|
560
|
+
if (row.running)
|
|
561
|
+
detail.push(d(`${spin} ${ageBare(row.timestamp, dctx.now)}`));
|
|
562
|
+
else if (row.cell.detail !== '')
|
|
563
|
+
detail.push(d(row.cell.detail));
|
|
564
|
+
if (row.logs)
|
|
360
565
|
detail.push(t(' '), b(`${g.enter} logs`, 'brand'));
|
|
361
566
|
lines.push(lrLine(left, [...detail, t(' ')], width));
|
|
362
567
|
}
|
|
363
|
-
if (model.
|
|
364
|
-
lines.push([t(' '), d(`… ${model.
|
|
568
|
+
if (model.feed.length > shown.length) {
|
|
569
|
+
lines.push([t(' '), d(`… ${model.feed.length - shown.length} more failed ${sep} /logs <task>`)]);
|
|
365
570
|
}
|
|
366
571
|
return lines;
|
|
367
572
|
}
|
|
@@ -387,6 +592,12 @@ function lastRunText(task, event, size, dctx) {
|
|
|
387
592
|
}
|
|
388
593
|
}
|
|
389
594
|
}
|
|
595
|
+
/** The `LAST COMMIT` cell of a record row: `set_status · alice · 3m ago`. */
|
|
596
|
+
function lastCommitText(head, dctx) {
|
|
597
|
+
if (head === null)
|
|
598
|
+
return '—';
|
|
599
|
+
return `${head.mutation} ${dctx.g.sep} ${head.actor} ${dctx.g.sep} ${timeAgo(head.at, dctx.now)}`;
|
|
600
|
+
}
|
|
390
601
|
/**
|
|
391
602
|
* The column's lines in `[top, top + visible)` — the window the screen
|
|
392
603
|
* shows, rendered from the model with the selected row restyled and the
|
|
@@ -410,8 +621,10 @@ export function dashboardLines(model, dctx, sel, top, visible) {
|
|
|
410
621
|
const panel = executionLines(model, selected, dctx);
|
|
411
622
|
const taskSel = selected?.kind === 'task' ? model.tasks.findIndex(row => row.task.name === selected.name) : -1;
|
|
412
623
|
const inputSel = selected?.kind === 'input' ? model.inputNames.indexOf(selected.name) : -1;
|
|
624
|
+
const recordSel = selected?.kind === 'record' ? model.recordNames.indexOf(selected.name) : -1;
|
|
413
625
|
const tasksFirst = model.tasksAt + 2;
|
|
414
626
|
const inputsFirst = model.inputsAt + 2;
|
|
627
|
+
const recordsFirst = model.recordsAt + 2;
|
|
415
628
|
const out = [];
|
|
416
629
|
for (let i = first; i < end; i++) {
|
|
417
630
|
if (i < model.counts.length)
|
|
@@ -439,13 +652,23 @@ export function dashboardLines(model, dctx, sel, top, visible) {
|
|
|
439
652
|
out.push(sectionLine('INPUTS', '', width));
|
|
440
653
|
else if (i === model.inputsAt + 1)
|
|
441
654
|
out.push(tableLine(model.inputPlan, null, false, width, g));
|
|
442
|
-
else {
|
|
655
|
+
else if (model.recordsAt === -1 || i < model.recordsAt - 1) {
|
|
443
656
|
const row = model.inputs[i - inputsFirst];
|
|
444
657
|
if (row === undefined)
|
|
445
658
|
out.push([t(' '), d('no inputs')]);
|
|
446
659
|
else
|
|
447
660
|
out.push(tableLine(model.inputPlan, row, i - inputsFirst === inputSel, width, g));
|
|
448
661
|
}
|
|
662
|
+
else if (i < model.recordsAt)
|
|
663
|
+
out.push(blank(width));
|
|
664
|
+
else if (i === model.recordsAt)
|
|
665
|
+
out.push(sectionLine('RECORDS', '', width));
|
|
666
|
+
else if (i === model.recordsAt + 1)
|
|
667
|
+
out.push(tableLine(model.recordPlan, null, false, width, g));
|
|
668
|
+
else {
|
|
669
|
+
const row = model.records[i - recordsFirst];
|
|
670
|
+
out.push(tableLine(model.recordPlan, { cells: { ...row.cells, lastCommit: lastCommitText(row.head, dctx) } }, i - recordsFirst === recordSel, width, g));
|
|
671
|
+
}
|
|
449
672
|
}
|
|
450
673
|
return out;
|
|
451
674
|
}
|
|
@@ -638,6 +861,8 @@ registerViewHooks('dashboard', {
|
|
|
638
861
|
controller.openTask(ws, row.name, 'stdout');
|
|
639
862
|
else if (row.kind === 'task')
|
|
640
863
|
controller.openTask(ws, row.name);
|
|
864
|
+
else if (row.kind === 'record')
|
|
865
|
+
controller.openRecord(ws, row.name);
|
|
641
866
|
else
|
|
642
867
|
controller.openInput(ws, row.name);
|
|
643
868
|
},
|