@elaraai/e3-ui-cli 1.0.73 → 1.0.75
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/bin/e3-ui.mjs +6 -0
- package/dist/app/assets/{engine-Bm-cIDd_-DwUdGcTZ.js → engine-DAHqaY4g-CeN5Sr3o.js} +1 -1
- package/dist/app/assets/{index-mj-YVVN1.js → index-oIzAaxly.js} +107 -105
- package/dist/app/index.html +1 -1
- package/dist/commands/tui.d.ts.map +1 -1
- package/dist/commands/tui.js +4 -1
- package/dist/commands/tui.js.map +1 -1
- package/dist/tui/app.d.ts.map +1 -1
- package/dist/tui/app.js +3 -0
- package/dist/tui/app.js.map +1 -1
- package/dist/tui/controller.d.ts.map +1 -1
- package/dist/tui/controller.js +27 -3
- package/dist/tui/controller.js.map +1 -1
- package/dist/tui/model/tree.d.ts +16 -2
- package/dist/tui/model/tree.d.ts.map +1 -1
- package/dist/tui/model/tree.js +72 -33
- package/dist/tui/model/tree.js.map +1 -1
- package/dist/tui/render/text.d.ts +7 -3
- package/dist/tui/render/text.d.ts.map +1 -1
- package/dist/tui/render/text.js +64 -32
- package/dist/tui/render/text.js.map +1 -1
- package/dist/tui/ui/App.d.ts +6 -3
- package/dist/tui/ui/App.d.ts.map +1 -1
- package/dist/tui/ui/App.js +16 -3
- package/dist/tui/ui/App.js.map +1 -1
- package/dist/tui/ui/shell/widgets.d.ts +41 -0
- package/dist/tui/ui/shell/widgets.d.ts.map +1 -1
- package/dist/tui/ui/shell/widgets.js +63 -40
- package/dist/tui/ui/shell/widgets.js.map +1 -1
- package/dist/tui/ui/views/dashboard.d.ts +79 -24
- package/dist/tui/ui/views/dashboard.d.ts.map +1 -1
- package/dist/tui/ui/views/dashboard.js +221 -169
- package/dist/tui/ui/views/dashboard.js.map +1 -1
- package/package.json +14 -14
|
@@ -10,7 +10,7 @@ import { datasetStatusCell, eventCell, executionDuration, executionStatusCell, s
|
|
|
10
10
|
import { registerViewHooks } from '../../controller.js';
|
|
11
11
|
import { isRunLive, lockHolderText } from '../../data/dataflow.js';
|
|
12
12
|
import { b, blank, d, lineWidth, lrLine, t } from '../lines.js';
|
|
13
|
-
import { centredBlock,
|
|
13
|
+
import { centredBlock, sectionLine, tableLine, tablePlan, withScrollbar } from '../shell/widgets.js';
|
|
14
14
|
import { registerView } from './index.js';
|
|
15
15
|
/** Event rows the execution panel shows at most. */
|
|
16
16
|
export const MAX_EVENT_ROWS = 6;
|
|
@@ -147,7 +147,7 @@ function countsLines(status, dctx) {
|
|
|
147
147
|
return out;
|
|
148
148
|
}
|
|
149
149
|
// ---------------------------------------------------------------------------
|
|
150
|
-
//
|
|
150
|
+
// The model
|
|
151
151
|
// ---------------------------------------------------------------------------
|
|
152
152
|
/**
|
|
153
153
|
* The latest event of each task, in order of first appearance.
|
|
@@ -170,17 +170,149 @@ export function latestPerTask(events) {
|
|
|
170
170
|
function isLive(execution) {
|
|
171
171
|
return execution !== undefined && (execution.state?.status.type === 'running' || execution.settling || execution.stopping);
|
|
172
172
|
}
|
|
173
|
-
/**
|
|
174
|
-
function
|
|
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) {
|
|
175
307
|
const g = dctx.g;
|
|
176
308
|
const sep = g.sep;
|
|
177
309
|
const width = dctx.columns - 1;
|
|
178
310
|
const spin = g.spinner[dctx.spinner % g.spinner.length];
|
|
311
|
+
const execution = model.execution;
|
|
312
|
+
const tasksTotal = model.status?.tasks.length ?? 0;
|
|
179
313
|
const lines = [];
|
|
180
|
-
const logs = [];
|
|
181
314
|
let title = 'LAST EXECUTION';
|
|
182
315
|
let right;
|
|
183
|
-
let events = [];
|
|
184
316
|
if (execution === undefined) {
|
|
185
317
|
right = [d('…')];
|
|
186
318
|
}
|
|
@@ -189,13 +321,10 @@ function executionLines(execution, tasksTotal, sel, dctx) {
|
|
|
189
321
|
if (execution.settling)
|
|
190
322
|
title = 'EXECUTION';
|
|
191
323
|
}
|
|
192
|
-
else if (
|
|
324
|
+
else if (model.live) {
|
|
193
325
|
title = 'EXECUTION';
|
|
194
|
-
const state = execution.state;
|
|
195
|
-
const done = execution.events.filter(e => e.type !== 'start').length;
|
|
196
326
|
const head = execution.stopping ? b(`${g.square} STOPPING`, 'warn') : b(`${g.quarter} RUNNING`, 'info');
|
|
197
|
-
right = [head, d(` ${sep} started ${timeAgo(state.startedAt, dctx.now)} ${sep} ${done} of ${tasksTotal} tasks ${sep} ${spin}`)];
|
|
198
|
-
events = latestPerTask(execution.events).slice(-MAX_EVENT_ROWS);
|
|
327
|
+
right = [head, d(` ${sep} started ${timeAgo(execution.state.startedAt, dctx.now)} ${sep} ${model.done} of ${tasksTotal} tasks ${sep} ${spin}`)];
|
|
199
328
|
}
|
|
200
329
|
else {
|
|
201
330
|
const state = execution.state;
|
|
@@ -206,14 +335,13 @@ function executionLines(execution, tasksTotal, sel, dctx) {
|
|
|
206
335
|
detail += ` ${sep} ${formatDuration(executionDuration(state) ?? s.duration)} ${sep} executed ${s.executed} ${sep} cached ${s.cached} ${sep} failed ${s.failed} ${sep} skipped ${s.skipped}`;
|
|
207
336
|
}
|
|
208
337
|
right = [b(`${cell.glyph} ${cell.word}`, cell.tone), d(detail)];
|
|
209
|
-
events = latestPerTask(execution.events).filter(e => e.type === 'failed' || e.type === 'error');
|
|
210
338
|
}
|
|
211
339
|
lines.push(lrLine([t(' '), b(title)], [...right, t(' ')], width));
|
|
212
|
-
const shown = events.slice(0, MAX_EVENT_ROWS);
|
|
340
|
+
const shown = model.events.slice(0, MAX_EVENT_ROWS);
|
|
213
341
|
for (const event of shown) {
|
|
214
342
|
const cell = eventCell(event, g);
|
|
215
|
-
const
|
|
216
|
-
const selected =
|
|
343
|
+
const logs = opensLogs(event);
|
|
344
|
+
const selected = logs && sel?.kind === 'logs' && sel.name === cell.task;
|
|
217
345
|
const left = [
|
|
218
346
|
t(' '),
|
|
219
347
|
selected ? b(g.sel, 'brand') : t(' '),
|
|
@@ -228,20 +356,15 @@ function executionLines(execution, tasksTotal, sel, dctx) {
|
|
|
228
356
|
detail.push(d(`${spin} ${ageBare(cell.timestamp, dctx.now)}`));
|
|
229
357
|
else if (cell.detail !== '')
|
|
230
358
|
detail.push(d(cell.detail));
|
|
231
|
-
if (
|
|
232
|
-
logs.push({ task: cell.task, at: lines.length });
|
|
359
|
+
if (logs)
|
|
233
360
|
detail.push(t(' '), b(`${g.enter} logs`, 'brand'));
|
|
234
|
-
}
|
|
235
361
|
lines.push(lrLine(left, [...detail, t(' ')], width));
|
|
236
362
|
}
|
|
237
|
-
if (events.length > shown.length) {
|
|
238
|
-
lines.push([t(' '), d(`… ${events.length - shown.length} more failed ${sep} /logs <task>`)]);
|
|
363
|
+
if (model.events.length > shown.length) {
|
|
364
|
+
lines.push([t(' '), d(`… ${model.events.length - shown.length} more failed ${sep} /logs <task>`)]);
|
|
239
365
|
}
|
|
240
|
-
return
|
|
366
|
+
return lines;
|
|
241
367
|
}
|
|
242
|
-
// ---------------------------------------------------------------------------
|
|
243
|
-
// Tables
|
|
244
|
-
// ---------------------------------------------------------------------------
|
|
245
368
|
/** The `SIZE · LAST RUN` cell of a task row. */
|
|
246
369
|
function lastRunText(task, event, size, dctx) {
|
|
247
370
|
const g = dctx.g;
|
|
@@ -265,138 +388,66 @@ function lastRunText(task, event, size, dctx) {
|
|
|
265
388
|
}
|
|
266
389
|
}
|
|
267
390
|
/**
|
|
268
|
-
* The
|
|
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.
|
|
269
394
|
*
|
|
270
|
-
* @param
|
|
271
|
-
* @param ws - The workspace
|
|
272
|
-
* @param status - Its status
|
|
273
|
-
* @param dctx - The dashboard context
|
|
274
|
-
* @returns The rows, in the status order
|
|
275
|
-
*/
|
|
276
|
-
export function taskRows(state, ws, status, dctx) {
|
|
277
|
-
const g = dctx.g;
|
|
278
|
-
const entries = datasetEntries(state, ws);
|
|
279
|
-
const latest = new Map(latestPerTask(state.data.execution[ws]?.events ?? []).map(e => [e.value.task, e]));
|
|
280
|
-
return status.tasks.map(task => {
|
|
281
|
-
const cell = taskStatusCell(task.status, g);
|
|
282
|
-
// The reason / pid / cached detail lives in the last column; only a failure's exit code / message stays inline.
|
|
283
|
-
const bare = task.status.type !== 'failed' && task.status.type !== 'error';
|
|
284
|
-
const inputs = task.inputs.filter(p => p.startsWith('.inputs.')).map(p => p.slice('.inputs.'.length));
|
|
285
|
-
const entry = entries.get(task.output);
|
|
286
|
-
const size = entry?.size != null ? formatSize(entry.size) : '—';
|
|
287
|
-
return {
|
|
288
|
-
cells: {
|
|
289
|
-
name: task.name,
|
|
290
|
-
status: { text: bare ? `${cell.glyph} ${cell.word}` : statusText(cell), tone: cell.tone },
|
|
291
|
-
dependsOn: task.dependsOn.length > 0 ? task.dependsOn.join(', ') : '—',
|
|
292
|
-
inputs: inputs.length > 0 ? inputs.join(', ') : '—',
|
|
293
|
-
output: entry?.type ?? '—',
|
|
294
|
-
size: lastRunText(task, latest.get(task.name), size, dctx),
|
|
295
|
-
},
|
|
296
|
-
};
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* The inputs table rows (`.inputs.*` datasets that no task produces).
|
|
301
|
-
*
|
|
302
|
-
* @param state - The store state
|
|
303
|
-
* @param ws - The workspace
|
|
304
|
-
* @param status - Its status
|
|
305
|
-
* @param dctx - The dashboard context
|
|
306
|
-
* @returns The rows, in the status order
|
|
307
|
-
*/
|
|
308
|
-
export function inputRows(state, ws, status, dctx) {
|
|
309
|
-
const entries = datasetEntries(state, ws);
|
|
310
|
-
return status.datasets.filter(ds => !ds.isTaskOutput && ds.path.startsWith('.inputs.')).map(ds => {
|
|
311
|
-
const cell = datasetStatusCell(ds.status.type, dctx.g);
|
|
312
|
-
const entry = entries.get(ds.path);
|
|
313
|
-
return {
|
|
314
|
-
cells: {
|
|
315
|
-
name: ds.path.slice('.inputs.'.length),
|
|
316
|
-
status: { text: statusText(cell), tone: cell.tone },
|
|
317
|
-
type: entry?.type ?? '—',
|
|
318
|
-
size: entry?.size != null ? formatSize(entry.size) : '—',
|
|
319
|
-
hash: ds.hash.type === 'some' ? hashShort(ds.hash.value) : '—',
|
|
320
|
-
},
|
|
321
|
-
};
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
// ---------------------------------------------------------------------------
|
|
325
|
-
// The column
|
|
326
|
-
// ---------------------------------------------------------------------------
|
|
327
|
-
/**
|
|
328
|
-
* Builds the column under the title.
|
|
329
|
-
*
|
|
330
|
-
* @param state - The store state
|
|
331
|
-
* @param ws - The workspace
|
|
395
|
+
* @param model - The dashboard model
|
|
332
396
|
* @param dctx - The dashboard context
|
|
333
397
|
* @param sel - The selected row index
|
|
334
|
-
* @
|
|
398
|
+
* @param top - The first line
|
|
399
|
+
* @param visible - Lines in the window
|
|
400
|
+
* @returns The lines (fewer when the column ends first)
|
|
335
401
|
*/
|
|
336
|
-
export function
|
|
402
|
+
export function dashboardLines(model, dctx, sel, top, visible) {
|
|
337
403
|
const g = dctx.g;
|
|
338
404
|
const width = dctx.columns - 1;
|
|
339
|
-
const
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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));
|
|
350
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));
|
|
351
442
|
else {
|
|
352
|
-
|
|
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));
|
|
353
448
|
}
|
|
354
|
-
return { lines, rows };
|
|
355
449
|
}
|
|
356
|
-
|
|
357
|
-
// selected row is resolved from a first pass over the same data.
|
|
358
|
-
const tasks = taskRows(state, ws, status, dctx);
|
|
359
|
-
const inputs = inputRows(state, ws, status, dctx);
|
|
360
|
-
const execution = state.data.execution[ws];
|
|
361
|
-
const probe = executionLines(execution, status.tasks.length, undefined, dctx);
|
|
362
|
-
const order = [
|
|
363
|
-
...probe.logs.map(l => ({ kind: 'logs', name: l.task })),
|
|
364
|
-
...status.tasks.map(task => ({ kind: 'task', name: task.name })),
|
|
365
|
-
...status.datasets.filter(ds => !ds.isTaskOutput && ds.path.startsWith('.inputs.')).map(ds => ({ kind: 'input', name: ds.path.slice('.inputs.'.length) })),
|
|
366
|
-
];
|
|
367
|
-
const selected = order[Math.max(0, Math.min(sel, order.length - 1))];
|
|
368
|
-
const selectedRow = selected === undefined ? undefined : { ...selected, line: -1 };
|
|
369
|
-
lines.push(...countsLines(status, dctx));
|
|
370
|
-
lines.push(blank(width));
|
|
371
|
-
const panel = executionLines(execution, status.tasks.length, selectedRow, dctx);
|
|
372
|
-
const panelStart = lines.length;
|
|
373
|
-
for (const l of panel.logs)
|
|
374
|
-
rows.push({ kind: 'logs', name: l.task, line: panelStart + l.at });
|
|
375
|
-
lines.push(...panel.lines);
|
|
376
|
-
lines.push(blank(width));
|
|
377
|
-
lines.push(sectionLine('TASKS', '', width));
|
|
378
|
-
const taskSel = selectedRow?.kind === 'task' ? status.tasks.findIndex(task => task.name === selectedRow.name) : -1;
|
|
379
|
-
const taskTable = renderTable(columnPlan('tasks', dctx.bp), tasks, taskSel, 0, tasks.length, width, g);
|
|
380
|
-
lines.push(taskTable[0]);
|
|
381
|
-
status.tasks.forEach((task, i) => {
|
|
382
|
-
rows.push({ kind: 'task', name: task.name, line: lines.length });
|
|
383
|
-
lines.push(taskTable[i + 1]);
|
|
384
|
-
});
|
|
385
|
-
if (tasks.length === 0)
|
|
386
|
-
lines.push([t(' '), d('no tasks')]);
|
|
387
|
-
lines.push(blank(width));
|
|
388
|
-
lines.push(sectionLine('INPUTS', '', width));
|
|
389
|
-
const inputNames = order.filter(r => r.kind === 'input').map(r => r.name);
|
|
390
|
-
const inputSel = selectedRow?.kind === 'input' ? inputNames.indexOf(selectedRow.name) : -1;
|
|
391
|
-
const inputTable = renderTable(columnPlan('inputs', dctx.bp), inputs, inputSel, 0, inputs.length, width, g);
|
|
392
|
-
lines.push(inputTable[0]);
|
|
393
|
-
inputNames.forEach((name, i) => {
|
|
394
|
-
rows.push({ kind: 'input', name, line: lines.length });
|
|
395
|
-
lines.push(inputTable[i + 1]);
|
|
396
|
-
});
|
|
397
|
-
if (inputs.length === 0)
|
|
398
|
-
lines.push([t(' '), d('no inputs')]);
|
|
399
|
-
return { lines, rows };
|
|
450
|
+
return out;
|
|
400
451
|
}
|
|
401
452
|
/**
|
|
402
453
|
* The dashboard body: the fixed title, then the column window with its
|
|
@@ -412,15 +463,16 @@ export function renderDashboard(state, ctx) {
|
|
|
412
463
|
const ws = state.view.ws;
|
|
413
464
|
const width = ctx.layout.columns;
|
|
414
465
|
const out = [dashboardTitle(state, ws, ctx)];
|
|
415
|
-
const
|
|
466
|
+
const dctx = dashboardCtx(state, ctx);
|
|
467
|
+
const model = dashboardModel(state, ws, dctx);
|
|
416
468
|
const visible = Math.max(1, ctx.layout.bodyRows - 1);
|
|
417
|
-
const total =
|
|
469
|
+
const total = model.total;
|
|
418
470
|
const top = Math.max(0, Math.min(state.view.list.top, Math.max(0, total - visible)));
|
|
419
|
-
const window =
|
|
471
|
+
const window = dashboardLines(model, dctx, state.view.list.sel, top, visible);
|
|
420
472
|
while (window.length < visible)
|
|
421
473
|
window.push(blank(width - 1));
|
|
422
474
|
out.push(...withScrollbar(window, width, total, visible, top, ctx.g));
|
|
423
|
-
const hits =
|
|
475
|
+
const hits = model.rows
|
|
424
476
|
.map((row, index) => ({ row, index }))
|
|
425
477
|
.filter(({ row }) => row.line >= top && row.line < top + visible)
|
|
426
478
|
.map(({ row, index }) => ({ row: 1 + (row.line - top), x0: 0, x1: width, target: { kind: 'dashboard', index } }));
|
|
@@ -438,16 +490,16 @@ export function scrollDashboard(state, controller, to) {
|
|
|
438
490
|
const nav = navigation(state, controller);
|
|
439
491
|
if (nav === null || state.view.kind !== 'dashboard')
|
|
440
492
|
return;
|
|
441
|
-
const {
|
|
442
|
-
const total =
|
|
493
|
+
const { model, visible } = nav;
|
|
494
|
+
const total = model.total;
|
|
443
495
|
const current = state.view.list.top;
|
|
444
496
|
const top = Math.max(0, Math.min('delta' in to ? current + to.delta : to.top, Math.max(0, total - visible)));
|
|
445
497
|
let sel = state.view.list.sel;
|
|
446
|
-
if (
|
|
447
|
-
const line =
|
|
498
|
+
if (model.rows.length > 0) {
|
|
499
|
+
const line = model.rows[Math.max(0, Math.min(sel, model.rows.length - 1))].line;
|
|
448
500
|
if (line < top || line >= top + visible) {
|
|
449
501
|
// The selection follows the window: the first (or last) row inside it.
|
|
450
|
-
const inside =
|
|
502
|
+
const inside = model.rows.map((r, i) => ({ r, i })).filter(({ r }) => r.line >= top && r.line < top + visible);
|
|
451
503
|
const pick = line < top ? inside[0] : inside[inside.length - 1];
|
|
452
504
|
if (pick !== undefined)
|
|
453
505
|
sel = pick.i;
|
|
@@ -472,13 +524,13 @@ export function dashboardHints(state, ctx) {
|
|
|
472
524
|
// ---------------------------------------------------------------------------
|
|
473
525
|
// Navigation
|
|
474
526
|
// ---------------------------------------------------------------------------
|
|
475
|
-
/** The
|
|
527
|
+
/** The model and the window geometry for navigation (no theme needed; the model comes from the cache). */
|
|
476
528
|
function navigation(state, controller) {
|
|
477
529
|
if (state.view.kind !== 'dashboard')
|
|
478
530
|
return null;
|
|
479
531
|
const layout = layoutOf(state);
|
|
480
532
|
const dctx = { g: controller.deps.glyphs, now: controller.deps.now(), columns: layout.columns, bp: breakpoint(state.size), spinner: 0 };
|
|
481
|
-
return {
|
|
533
|
+
return { model: dashboardModel(state, state.view.ws, dctx), visible: Math.max(1, layout.bodyRows - 1) };
|
|
482
534
|
}
|
|
483
535
|
/**
|
|
484
536
|
* Selects a row of the column (by index) and scrolls it into view.
|
|
@@ -491,11 +543,11 @@ export function selectDashboardRow(state, controller, index) {
|
|
|
491
543
|
const nav = navigation(state, controller);
|
|
492
544
|
if (nav === null || state.view.kind !== 'dashboard')
|
|
493
545
|
return;
|
|
494
|
-
const {
|
|
495
|
-
if (
|
|
546
|
+
const { model, visible } = nav;
|
|
547
|
+
if (model.rows.length === 0)
|
|
496
548
|
return;
|
|
497
|
-
const sel = Math.max(0, Math.min(index,
|
|
498
|
-
const top = scrollIntoView(state.view.list.top,
|
|
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);
|
|
499
551
|
controller.dispatch({ type: 'view/set', view: { ...state.view, list: { sel, top } } });
|
|
500
552
|
}
|
|
501
553
|
/**
|
|
@@ -510,9 +562,9 @@ export function moveDashboard(state, controller, op) {
|
|
|
510
562
|
const nav = navigation(state, controller);
|
|
511
563
|
if (nav === null || state.view.kind !== 'dashboard')
|
|
512
564
|
return;
|
|
513
|
-
const {
|
|
514
|
-
const total =
|
|
515
|
-
const count =
|
|
565
|
+
const { model, visible } = nav;
|
|
566
|
+
const total = model.total;
|
|
567
|
+
const count = model.rows.length;
|
|
516
568
|
const page = Math.max(1, visible - 1);
|
|
517
569
|
if (count === 0) {
|
|
518
570
|
const delta = op === 'up' ? -1 : op === 'down' ? 1 : op === 'pageUp' ? -page : op === 'pageDown' ? page : op === 'home' ? -total : total;
|
|
@@ -522,7 +574,7 @@ export function moveDashboard(state, controller, op) {
|
|
|
522
574
|
}
|
|
523
575
|
const last = count - 1;
|
|
524
576
|
const current = Math.max(0, Math.min(state.view.list.sel, last));
|
|
525
|
-
const line =
|
|
577
|
+
const line = model.rows[current].line;
|
|
526
578
|
let sel = current;
|
|
527
579
|
switch (op) {
|
|
528
580
|
case 'up':
|
|
@@ -540,7 +592,7 @@ export function moveDashboard(state, controller, op) {
|
|
|
540
592
|
case 'pageUp': {
|
|
541
593
|
const target = line - page;
|
|
542
594
|
let i = current;
|
|
543
|
-
while (i > 0 &&
|
|
595
|
+
while (i > 0 && model.rows[i - 1].line >= target)
|
|
544
596
|
i--;
|
|
545
597
|
sel = i === current ? Math.max(0, current - 1) : i;
|
|
546
598
|
break;
|
|
@@ -548,7 +600,7 @@ export function moveDashboard(state, controller, op) {
|
|
|
548
600
|
case 'pageDown': {
|
|
549
601
|
const target = line + page;
|
|
550
602
|
let i = current;
|
|
551
|
-
while (i < last &&
|
|
603
|
+
while (i < last && model.rows[i + 1].line <= target)
|
|
552
604
|
i++;
|
|
553
605
|
sel = i === current ? Math.min(last, current + 1) : i;
|
|
554
606
|
break;
|
|
@@ -557,7 +609,7 @@ export function moveDashboard(state, controller, op) {
|
|
|
557
609
|
// Home shows the column from its start and End from its end; the other moves scroll minimally.
|
|
558
610
|
const top = op === 'home' ? 0
|
|
559
611
|
: op === 'end' ? Math.max(0, total - visible)
|
|
560
|
-
: scrollIntoView(state.view.list.top,
|
|
612
|
+
: scrollIntoView(state.view.list.top, model.rows[sel].line, visible, total);
|
|
561
613
|
controller.dispatch({ type: 'view/set', view: { ...state.view, list: { sel, top } } });
|
|
562
614
|
}
|
|
563
615
|
// The generic list model is not used: the column's rows sit at irregular
|
|
@@ -578,7 +630,7 @@ registerViewHooks('dashboard', {
|
|
|
578
630
|
const nav = navigation(state, controller);
|
|
579
631
|
if (nav === null || state.view.kind !== 'dashboard')
|
|
580
632
|
return;
|
|
581
|
-
const row = nav.
|
|
633
|
+
const row = nav.model.rows[Math.max(0, Math.min(state.view.list.sel, nav.model.rows.length - 1))];
|
|
582
634
|
if (row === undefined)
|
|
583
635
|
return;
|
|
584
636
|
const ws = state.view.ws;
|