@gaia-ai/addon-gaia-ui 0.12.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/Entity/conductor/Data/conductors.d.ts +10 -0
- package/dist/src/Entity/conductor/Data/conductors.js +21 -0
- package/dist/src/Entity/conductor/Data/process.d.ts +20 -0
- package/dist/src/Entity/conductor/Data/process.js +30 -1
- package/dist/src/Entity/conductor/Screen/conductors.js +3 -0
- package/dist/src/Entity/statistics/Data/statistics.d.ts +18 -13
- package/dist/src/Entity/statistics/Data/statistics.js +35 -11
- package/dist/src/Entity/statistics/render.js +13 -11
- package/dist/src/Entity/team/Screen/team-users.js +16 -2
- package/dist/src/Entity/ticket/Component/ticket-detail/index.d.ts +42 -3
- package/dist/src/Entity/ticket/Component/ticket-detail/index.js +48 -1
- package/dist/src/Entity/ticket/Screen/ticket-detail.d.ts +92 -3
- package/dist/src/Entity/ticket/Screen/ticket-detail.js +380 -52
- package/dist/src/Entity/workspace/Screen/workspace-picker.js +4 -4
- package/dist/src/Extension/contract.d.ts +149 -3
- package/dist/src/Extension/store.js +10 -2
- package/dist/src/types.d.ts +2 -2
- package/dist/src/ui.d.ts +1 -1
- package/package.json +3 -3
|
@@ -402,6 +402,16 @@ export interface ConductorQuery {
|
|
|
402
402
|
* is the convention's cost, not this read's decision to make.
|
|
403
403
|
*/
|
|
404
404
|
export declare const listConductorRowPage: (client: DropshClient, ctx: ConductorRowContext, query: ConductorQuery, page?: number) => Promise<Page<ConductorRow>>;
|
|
405
|
+
/**
|
|
406
|
+
* A single conductor's own `last_seen`, or `undefined` when the record has
|
|
407
|
+
* none or the read fails.
|
|
408
|
+
*
|
|
409
|
+
* GAIA-477 AC-5: the cockpit's `act('start', …)` polls this after a spawn,
|
|
410
|
+
* comparing against the row's `lastSeen` captured before the start, to tell
|
|
411
|
+
* a genuine new heartbeat apart from a stale one still in the window — the
|
|
412
|
+
* same distinction the CLI's `waitForFreshHeartbeat` makes server-side.
|
|
413
|
+
*/
|
|
414
|
+
export declare const fetchConductorLastSeen: (client: DropshClient, id: string) => Promise<number | undefined>;
|
|
405
415
|
/**
|
|
406
416
|
* How large the collection under `query` is, or `null` when that is not
|
|
407
417
|
* established (GAIA-460 D4).
|
|
@@ -428,6 +428,27 @@ export const listConductorRowPage = async (client, ctx, query, page = 0) => {
|
|
|
428
428
|
return data.map((raw) => toConductorRow(raw, ctx, machinesById));
|
|
429
429
|
}, page, window);
|
|
430
430
|
};
|
|
431
|
+
/**
|
|
432
|
+
* A single conductor's own `last_seen`, or `undefined` when the record has
|
|
433
|
+
* none or the read fails.
|
|
434
|
+
*
|
|
435
|
+
* GAIA-477 AC-5: the cockpit's `act('start', …)` polls this after a spawn,
|
|
436
|
+
* comparing against the row's `lastSeen` captured before the start, to tell
|
|
437
|
+
* a genuine new heartbeat apart from a stale one still in the window — the
|
|
438
|
+
* same distinction the CLI's `waitForFreshHeartbeat` makes server-side.
|
|
439
|
+
*/
|
|
440
|
+
export const fetchConductorLastSeen = async (client, id) => {
|
|
441
|
+
try {
|
|
442
|
+
const doc = (await client.get(`gaia_conductor/gaia_conductor/${id}`));
|
|
443
|
+
const raw = doc?.data;
|
|
444
|
+
if (!raw)
|
|
445
|
+
return undefined;
|
|
446
|
+
return toUnixSeconds(asResource(raw).attr('last_seen')) ?? undefined;
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
return undefined;
|
|
450
|
+
}
|
|
451
|
+
};
|
|
431
452
|
/**
|
|
432
453
|
* How large the collection under `query` is, or `null` when that is not
|
|
433
454
|
* established (GAIA-460 D4).
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type SignalOutcome } from '@gaia-ai/core';
|
|
2
|
+
import type { DropshClient } from '../../../types.js';
|
|
2
3
|
/** What a cockpit action reports back to its row. */
|
|
3
4
|
export interface ConductorActionResult {
|
|
4
5
|
ok: boolean;
|
|
@@ -35,6 +36,16 @@ export interface ConductorProcessDeps {
|
|
|
35
36
|
* stop needs this machine's own value to compare against).
|
|
36
37
|
*/
|
|
37
38
|
thisHostKey?: () => Promise<string>;
|
|
39
|
+
/**
|
|
40
|
+
* GAIA-477 AC-5: how `start` learns whether the spawn actually came online,
|
|
41
|
+
* injected so tests need no timers or network. Defaults to
|
|
42
|
+
* {@link fetchConductorLastSeen} against the real control plane.
|
|
43
|
+
*/
|
|
44
|
+
fetchLastSeen?: (client: DropshClient, id: string) => Promise<number | undefined>;
|
|
45
|
+
sleep?: (ms: number) => Promise<void>;
|
|
46
|
+
/** Bounded poll budget; small defaults so a real failure resolves quickly. */
|
|
47
|
+
heartbeatPollAttempts?: number;
|
|
48
|
+
heartbeatPollIntervalMs?: number;
|
|
38
49
|
}
|
|
39
50
|
export interface ConductorProcessHost {
|
|
40
51
|
start(input: {
|
|
@@ -43,6 +54,15 @@ export interface ConductorProcessHost {
|
|
|
43
54
|
/** The row's `host_key` (GAIA-450 review B5) — verified against this
|
|
44
55
|
* machine's own before anything is spawned. */
|
|
45
56
|
hostKey: string | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* GAIA-477 AC-5: the conductor's own record id and its `last_seen`
|
|
59
|
+
* captured before the spawn, and the client to poll with. Present only
|
|
60
|
+
* when the caller can observe the outcome — absent, `start` reports the
|
|
61
|
+
* spawn as before and never claims to have seen it come online.
|
|
62
|
+
*/
|
|
63
|
+
id?: string | undefined;
|
|
64
|
+
priorLastSeen?: number | undefined;
|
|
65
|
+
client?: DropshClient | undefined;
|
|
46
66
|
}): Promise<ConductorActionResult>;
|
|
47
67
|
stop(input: {
|
|
48
68
|
pid: number | undefined;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn as nodeSpawn } from 'node:child_process';
|
|
2
2
|
import { machineContextPath, readMachineContext, signalVerifiedProcess, } from '@gaia-ai/core';
|
|
3
|
+
import { fetchConductorLastSeen } from './conductors.js';
|
|
3
4
|
/**
|
|
4
5
|
* This machine's host_key, from the same MachineContext the CLI reads.
|
|
5
6
|
*
|
|
@@ -28,9 +29,13 @@ export function createConductorProcessHost(deps = {}) {
|
|
|
28
29
|
const spawn = deps.spawn ?? defaultSpawn;
|
|
29
30
|
const signal = deps.signal ?? signalVerifiedProcess;
|
|
30
31
|
const thisHostKey = deps.thisHostKey ?? defaultThisHostKey;
|
|
32
|
+
const fetchLastSeen = deps.fetchLastSeen ?? fetchConductorLastSeen;
|
|
33
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
34
|
+
const heartbeatPollAttempts = deps.heartbeatPollAttempts ?? 5;
|
|
35
|
+
const heartbeatPollIntervalMs = deps.heartbeatPollIntervalMs ?? 1000;
|
|
31
36
|
return {
|
|
32
37
|
thisHostKey,
|
|
33
|
-
async start({ workspaceRoot, conductorName, hostKey }) {
|
|
38
|
+
async start({ workspaceRoot, conductorName, hostKey, id, priorLastSeen, client, }) {
|
|
34
39
|
// GAIA-450 review B5: the only gate this used to have was `isLocal` — a
|
|
35
40
|
// filesystem check on `workspaceRoot`, which answers "does this path
|
|
36
41
|
// exist on THIS disk", not "is this THIS machine's conductor". Two
|
|
@@ -68,6 +73,30 @@ export function createConductorProcessHost(deps = {}) {
|
|
|
68
73
|
GAIA_CONDUCTOR_CONFIG: '',
|
|
69
74
|
},
|
|
70
75
|
});
|
|
76
|
+
// GAIA-477 AC-5: `spawn` settling only proves the fork happened, not
|
|
77
|
+
// that a foreground conductor is running under it — the herdr pane
|
|
78
|
+
// boundary this ticket's other fix crosses can still leave a row
|
|
79
|
+
// silently offline. Watch for a genuinely NEW heartbeat (never the
|
|
80
|
+
// stale one already on the row) over a bounded budget before telling
|
|
81
|
+
// the operator this succeeded, the same distinction the CLI's own
|
|
82
|
+
// `waitForFreshHeartbeat` makes.
|
|
83
|
+
if (id && client) {
|
|
84
|
+
for (let attempt = 0; attempt < heartbeatPollAttempts; attempt += 1) {
|
|
85
|
+
await sleep(heartbeatPollIntervalMs);
|
|
86
|
+
const lastSeen = await fetchLastSeen(client, id);
|
|
87
|
+
if (lastSeen !== undefined &&
|
|
88
|
+
(priorLastSeen === undefined || lastSeen > priorLastSeen)) {
|
|
89
|
+
return {
|
|
90
|
+
ok: true,
|
|
91
|
+
message: `starting ${conductorName ?? 'conductor'} in ${workspaceRoot}`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
message: `${conductorName ?? 'conductor'} did not come online — spawned in ${workspaceRoot} but no fresh heartbeat arrived`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
71
100
|
return {
|
|
72
101
|
ok: true,
|
|
73
102
|
message: `starting ${conductorName ?? 'conductor'} in ${workspaceRoot}`,
|
|
@@ -303,6 +303,9 @@ export class ConductorsScreen extends ListScreen {
|
|
|
303
303
|
workspaceRoot: row.workspaceRoot,
|
|
304
304
|
conductorName: row.conductorName,
|
|
305
305
|
hostKey: row.hostKey,
|
|
306
|
+
id: row.id,
|
|
307
|
+
priorLastSeen: row.lastSeen || undefined,
|
|
308
|
+
client: context.services?.client,
|
|
306
309
|
})
|
|
307
310
|
: await host.stop({
|
|
308
311
|
pid: row.lastPid,
|
|
@@ -39,18 +39,31 @@ export interface SeriesFigureValue {
|
|
|
39
39
|
days: StatisticDay[];
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
|
-
* A `ratio` figure's value:
|
|
42
|
+
* A `ratio` figure's value: the quotient, and the two factors behind it.
|
|
43
43
|
*
|
|
44
|
-
* The
|
|
45
|
-
* server-computed copy would be a second source of truth
|
|
46
|
-
*
|
|
47
|
-
*
|
|
44
|
+
* The division was CLIENT-SIDE until GAIA-481 D3, on the reasoning that a
|
|
45
|
+
* server-computed copy would be a second source of truth. The web Stats tabs
|
|
46
|
+
* reversed that reasoning: a second SURFACE would have had to re-implement
|
|
47
|
+
* the same rule in PHP, so the division and its zero-denominator case moved
|
|
48
|
+
* to the server instead. `ratio` is therefore READ and rendered, never
|
|
49
|
+
* recomputed here — and it arrives already formatted, because the empty
|
|
50
|
+
* value is part of the rule rather than a presentation choice each surface
|
|
51
|
+
* makes for itself.
|
|
52
|
+
*
|
|
53
|
+
* `numerator` and `denominator` still travel beside it, unchanged.
|
|
48
54
|
*/
|
|
49
55
|
export interface RatioFigureValue {
|
|
50
56
|
generated: string;
|
|
57
|
+
ratio: RatioQuotients;
|
|
51
58
|
numerator: WindowsFigureValue;
|
|
52
59
|
denominator: WindowsFigureValue;
|
|
53
60
|
}
|
|
61
|
+
/** A ratio's derived value per trailing window: two decimals, or `—`. */
|
|
62
|
+
export interface RatioQuotients {
|
|
63
|
+
day: string;
|
|
64
|
+
week: string;
|
|
65
|
+
month: string;
|
|
66
|
+
}
|
|
54
67
|
export type FigureValue = WindowsFigureValue | SeriesFigureValue | RatioFigureValue;
|
|
55
68
|
/** One figure discovery names: which blocks the caller may render (spec D5). */
|
|
56
69
|
export interface StatisticListing {
|
|
@@ -132,11 +145,3 @@ export declare function loadStatisticValue(client: DropshClient, statisticId: st
|
|
|
132
145
|
* its own (spec D5). Figures already arrive weight-ordered from discovery.
|
|
133
146
|
*/
|
|
134
147
|
export declare function loadGroupStatistics(client: DropshClient, scopeId: string): Promise<GroupStatisticsResult>;
|
|
135
|
-
/** Closed-per-run-shaped ratio, for each trailing window, from two totals. */
|
|
136
|
-
export declare function ratioOf(numerator: StatisticsWindows, denominator: StatisticsWindows): {
|
|
137
|
-
day: number | null;
|
|
138
|
-
week: number | null;
|
|
139
|
-
month: number | null;
|
|
140
|
-
};
|
|
141
|
-
/** Format a ratio value for the screen: two decimals, or `—`. */
|
|
142
|
-
export declare const formatRatio: (value: number | null) => string;
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { DrupalJsonApiParams } from 'drupal-jsonapi-params';
|
|
2
|
+
/**
|
|
3
|
+
* What a ratio window reads as when there is nothing to divide by.
|
|
4
|
+
*
|
|
5
|
+
* The server owns this rule and sends the character; this constant exists
|
|
6
|
+
* only so a response that carries no quotient at all degrades to the same
|
|
7
|
+
* thing rather than to an empty cell.
|
|
8
|
+
*/
|
|
9
|
+
const RATIO_EMPTY = '—';
|
|
2
10
|
/** How an `id: null` breakdown row is named on screen. */
|
|
3
11
|
export const UNATTRIBUTED_LABEL = '— unattributed';
|
|
4
12
|
const num = (value) => typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
@@ -43,6 +51,23 @@ function windowsFigureOf(attributes) {
|
|
|
43
51
|
...(rows ? { rows } : {}),
|
|
44
52
|
};
|
|
45
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* A ratio figure's quotients, as the server formatted them (GAIA-481 D3).
|
|
56
|
+
*
|
|
57
|
+
* A control plane that predates the move sends no `ratio` at all, and this
|
|
58
|
+
* reads the empty value for every window rather than throwing or dividing a
|
|
59
|
+
* replacement: an older server is a missing figure, not a wrong one, and the
|
|
60
|
+
* one thing this must not do is quietly reintroduce the division it replaced.
|
|
61
|
+
*/
|
|
62
|
+
function quotientsOf(value) {
|
|
63
|
+
const source = (value ?? {});
|
|
64
|
+
const windowOf = (key) => typeof source[key] === 'string' ? source[key] : RATIO_EMPTY;
|
|
65
|
+
return {
|
|
66
|
+
day: windowOf('day'),
|
|
67
|
+
week: windowOf('week'),
|
|
68
|
+
month: windowOf('month'),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
46
71
|
function figureValueOf(attributes) {
|
|
47
72
|
if (Array.isArray(attributes.days)) {
|
|
48
73
|
return {
|
|
@@ -56,6 +81,7 @@ function figureValueOf(attributes) {
|
|
|
56
81
|
attributes.denominator !== null) {
|
|
57
82
|
return {
|
|
58
83
|
generated: typeof attributes.generated === 'string' ? attributes.generated : '',
|
|
84
|
+
ratio: quotientsOf(attributes.ratio),
|
|
59
85
|
numerator: windowsFigureOf(attributes.numerator),
|
|
60
86
|
denominator: windowsFigureOf(attributes.denominator),
|
|
61
87
|
};
|
|
@@ -198,14 +224,12 @@ export async function loadGroupStatistics(client, scopeId) {
|
|
|
198
224
|
}
|
|
199
225
|
return { kind: 'ok', figures };
|
|
200
226
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
/** Format a ratio value for the screen: two decimals, or `—`. */
|
|
211
|
-
export const formatRatio = (value) => value === null ? '—' : value.toFixed(2);
|
|
227
|
+
// GAIA-481 D3 — `ratioOf` and `formatRatio` were exported from here, and are
|
|
228
|
+
// gone. A `ratio` figure's quotient is the server's now
|
|
229
|
+
// (`RatioFigureValue.ratio`), so this module deliberately offers no ratio
|
|
230
|
+
// arithmetic for a caller to reach for: a shared helper sitting beside the
|
|
231
|
+
// server's own value is exactly how the second implementation would come
|
|
232
|
+
// back. The team Users screen still divides, because it derives a PER-MEMBER
|
|
233
|
+
// quotient out of two breakdown figures' rows and no configured figure
|
|
234
|
+
// produces that one; it owns that arithmetic privately rather than sharing
|
|
235
|
+
// it from here.
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { statsPanelRows, } from '../../Component/stats-panel/index.js';
|
|
2
|
-
import { formatRatio, ratioOf } from './Data/statistics.js';
|
|
3
2
|
// GAIA-456 spec D2/D7 — mapping the four configured display kinds onto the
|
|
4
3
|
// renderers the TUI already had: `windows` is the totals row GAIA-256 drew,
|
|
5
4
|
// `breakdown` is the label-by-window table, `series` is the seven-day strip
|
|
@@ -24,13 +23,19 @@ function seriesLines(days) {
|
|
|
24
23
|
const counts = ` ${'Count'.padEnd(LABEL - 2, ' ')}${days.map((row) => cell(String(row.count))).join('')}`;
|
|
25
24
|
return [weekdays, dates, counts];
|
|
26
25
|
}
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
/**
|
|
27
|
+
* A ratio figure's quotient per trailing window, as the SERVER divided it.
|
|
28
|
+
*
|
|
29
|
+
* The values arrive already formatted (GAIA-481 D3), so this lays them out
|
|
30
|
+
* and does nothing else — no division and no em-dash rule, both of which are
|
|
31
|
+
* the server's now and would otherwise be a terminal-only second opinion on
|
|
32
|
+
* the same figure the browser renders.
|
|
33
|
+
*/
|
|
34
|
+
function ratioLines(ratios) {
|
|
30
35
|
return [
|
|
31
|
-
` ${WINDOWS[0]} ${
|
|
32
|
-
` ${WINDOWS[1]} ${
|
|
33
|
-
` ${WINDOWS[2]} ${
|
|
36
|
+
` ${WINDOWS[0]} ${ratios.day}`,
|
|
37
|
+
` ${WINDOWS[1]} ${ratios.week}`,
|
|
38
|
+
` ${WINDOWS[2]} ${ratios.month}`,
|
|
34
39
|
];
|
|
35
40
|
}
|
|
36
41
|
/** One figure's own section: its label as the heading, its display kind's lines. */
|
|
@@ -41,10 +46,7 @@ export function figureSection({ listing, value }) {
|
|
|
41
46
|
return { heading, lines: seriesLines(value.days) };
|
|
42
47
|
case 'ratio': {
|
|
43
48
|
const ratio = value;
|
|
44
|
-
return {
|
|
45
|
-
heading,
|
|
46
|
-
lines: ratioLines(ratio.numerator.total, ratio.denominator.total),
|
|
47
|
-
};
|
|
49
|
+
return { heading, lines: ratioLines(ratio.ratio) };
|
|
48
50
|
}
|
|
49
51
|
case 'breakdown': {
|
|
50
52
|
const windows = value;
|
|
@@ -4,7 +4,7 @@ import { ListCursor } from '../../../lib/cursor.js';
|
|
|
4
4
|
import { columnNode } from '../../../lib/vnode.js';
|
|
5
5
|
import { AsyncScreen } from '../../../Screen/async-screen.js';
|
|
6
6
|
import { listRowIds, listTableTree } from '../../collection.js';
|
|
7
|
-
import {
|
|
7
|
+
import { loadGroupStatistics, } from '../../statistics/Data/statistics.js';
|
|
8
8
|
import { listUsers, TEAM_MEMBER_TABLE, } from '../../user/Data/users.js';
|
|
9
9
|
import { listTeamMemberIds } from '../Data/members.js';
|
|
10
10
|
import { TeamsScreen } from './teams-screen.js';
|
|
@@ -48,6 +48,20 @@ function rowsById(statistics, statisticId) {
|
|
|
48
48
|
}
|
|
49
49
|
return map;
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* This screen's own closed-per-run, for ONE member over the 30-day window.
|
|
53
|
+
*
|
|
54
|
+
* Deliberately local and unexported (GAIA-481 D3). A `ratio` FIGURE is
|
|
55
|
+
* divided by the server, and this is not one: it is derived per member from
|
|
56
|
+
* two breakdown figures' rows, and no configured figure produces it. Keeping
|
|
57
|
+
* it here rather than in the shared statistics module is what stops it being
|
|
58
|
+
* mistaken for the general "how a ratio is computed" helper and reached for
|
|
59
|
+
* by a surface that should be reading the server's value instead.
|
|
60
|
+
*
|
|
61
|
+
* The rule matches the server's so the two columns agree on screen: two
|
|
62
|
+
* decimals, and an em-dash when there is nothing to divide by.
|
|
63
|
+
*/
|
|
64
|
+
const perMemberRatio = (numerator, denominator) => denominator > 0 ? (numerator / denominator).toFixed(2) : '—';
|
|
51
65
|
/** One member's row: identity from the collection, figures from the two per-user breakdowns. */
|
|
52
66
|
function rowOf(user, ticketsById, runsById) {
|
|
53
67
|
const tickets30d = ticketsById.get(user.id)?.month ?? 0;
|
|
@@ -57,7 +71,7 @@ function rowOf(user, ticketsById, runsById) {
|
|
|
57
71
|
label: user.label,
|
|
58
72
|
tickets30d,
|
|
59
73
|
runs30d,
|
|
60
|
-
closedPerRun30d:
|
|
74
|
+
closedPerRun30d: perMemberRatio(tickets30d, runs30d),
|
|
61
75
|
};
|
|
62
76
|
}
|
|
63
77
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { VNode } from '@opentui/core';
|
|
2
|
-
import type { Section } from '../../../../Component/section-list/index.js';
|
|
3
|
-
import type { PanelSection } from '../../../../Extension/contract.js';
|
|
2
|
+
import type { Section, SectionLine } from '../../../../Component/section-list/index.js';
|
|
3
|
+
import type { PanelColumn, PanelSection } from '../../../../Extension/contract.js';
|
|
4
4
|
import { type StyledLine } from '../../../../lib/vnode.js';
|
|
5
5
|
import type { TicketDetailDocument } from '../../../../types.js';
|
|
6
6
|
import type { CommentItem } from '../comment-item/index.js';
|
|
@@ -142,6 +142,35 @@ export declare function detailLines(detail?: TicketDetailDocument | null, labels
|
|
|
142
142
|
boldRows: Set<number>;
|
|
143
143
|
};
|
|
144
144
|
/** One rendered ticket section: its body, and the selection map into it. */
|
|
145
|
+
/** A row of a contributed table — identity is required, the rest is the addon's. */
|
|
146
|
+
export interface PanelTableRow {
|
|
147
|
+
readonly id: string;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* GAIA-475 — what the screen resolved out of a `UiTable`, ready to lay out.
|
|
151
|
+
*
|
|
152
|
+
* Data only, and deliberately not the `UiTable` itself: by the time it reaches
|
|
153
|
+
* here `columns()` and `rows()` have already been called inside the screen's
|
|
154
|
+
* error boundary, so this component never invokes contributed code and cannot be
|
|
155
|
+
* the thing that throws.
|
|
156
|
+
*/
|
|
157
|
+
export interface PanelTableData {
|
|
158
|
+
/** The id prefix every node of this list carries; derived from the table id. */
|
|
159
|
+
readonly noun: string;
|
|
160
|
+
readonly columns: readonly PanelColumn<PanelTableRow>[];
|
|
161
|
+
readonly rows: readonly PanelTableRow[];
|
|
162
|
+
readonly emptyMessage: string;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* GAIA-475 A4 — a contributed table as the PIPE's text, one serialisation reused.
|
|
166
|
+
*
|
|
167
|
+
* `listTableRows` is the same function the built-in lists print through, so the
|
|
168
|
+
* piped form of a contributed table is the piped form of every other list rather
|
|
169
|
+
* than a second renderer that drifts from it. It is also what keeps the contract
|
|
170
|
+
* honest: a contribution supplied data, so BOTH renderings are derivable, and
|
|
171
|
+
* neither is the addon's problem.
|
|
172
|
+
*/
|
|
173
|
+
export declare const panelTableLines: (data: PanelTableData) => SectionLine[];
|
|
145
174
|
export interface TabView {
|
|
146
175
|
/** GAIA-340 D1 — the tab's rows as the renderable tree the screen hands over. */
|
|
147
176
|
readonly body: VNode;
|
|
@@ -235,7 +264,7 @@ export declare function firstLineSummary(lines: readonly StyledLine[]): StyledLi
|
|
|
235
264
|
* off — the Runs tab, by contrast, is one node per run plus six cells and is
|
|
236
265
|
* bounded by the document's sideloaded runs.
|
|
237
266
|
*/
|
|
238
|
-
export declare function tabViewTree(detail: TicketDetailDocument | null | undefined, tabIndex: number, { expanded, nowSeconds, labels, selectedIndex, panelSections, }?: {
|
|
267
|
+
export declare function tabViewTree(detail: TicketDetailDocument | null | undefined, tabIndex: number, { expanded, nowSeconds, labels, selectedIndex, panelSections, panelTable, }?: {
|
|
239
268
|
expanded?: ReadonlySet<number>;
|
|
240
269
|
nowSeconds?: number;
|
|
241
270
|
labels?: AssignmentLabels;
|
|
@@ -249,4 +278,14 @@ export declare function tabViewTree(detail: TicketDetailDocument | null | undefi
|
|
|
249
278
|
* `PanelSection[]` contract (spec D4): an addon restates none of it.
|
|
250
279
|
*/
|
|
251
280
|
panelSections?: readonly PanelSection[];
|
|
281
|
+
/**
|
|
282
|
+
* GAIA-475 — a contributed table's columns and rows, already resolved by the
|
|
283
|
+
* screen, exactly as `panelSections` is.
|
|
284
|
+
*
|
|
285
|
+
* Present ⇒ this tab is a TABLE and `tabIndex` names no built-in section.
|
|
286
|
+
* Resolved by the screen rather than here for the same reason `panelSections`
|
|
287
|
+
* is: the contribution is the SCREEN's dependency, and this stays a pure
|
|
288
|
+
* function of rows.
|
|
289
|
+
*/
|
|
290
|
+
panelTable?: PanelTableData;
|
|
252
291
|
}): TabView;
|
|
@@ -686,6 +686,37 @@ const EXPANDABLE_HEADINGS = new Set([
|
|
|
686
686
|
* be one somewhere else, while `ticket-detail-description-0` is this screen's.
|
|
687
687
|
*/
|
|
688
688
|
const BODY_ID = 'ticket-detail';
|
|
689
|
+
/**
|
|
690
|
+
* The contributed table as the `ListTable` the host's renderer speaks.
|
|
691
|
+
*
|
|
692
|
+
* A translation and nothing more — `fields` is `[]` because sparse fieldsets are
|
|
693
|
+
* a JSON:API query's business and a contribution issues none, and `get` drops the
|
|
694
|
+
* `Ctx` argument because a contribution's context arrived with `PanelRender`.
|
|
695
|
+
*/
|
|
696
|
+
const listTableOf = (data) => ({
|
|
697
|
+
noun: data.noun,
|
|
698
|
+
emptyMessage: data.emptyMessage,
|
|
699
|
+
columns: data.columns.map((column) => ({
|
|
700
|
+
key: column.key,
|
|
701
|
+
label: column.label,
|
|
702
|
+
width: column.width,
|
|
703
|
+
fields: [],
|
|
704
|
+
...(column.min !== undefined ? { min: column.min } : {}),
|
|
705
|
+
...(column.max !== undefined ? { max: column.max } : {}),
|
|
706
|
+
...(column.grow !== undefined ? { grow: column.grow } : {}),
|
|
707
|
+
get: (row) => column.get(row),
|
|
708
|
+
})),
|
|
709
|
+
});
|
|
710
|
+
/**
|
|
711
|
+
* GAIA-475 A4 — a contributed table as the PIPE's text, one serialisation reused.
|
|
712
|
+
*
|
|
713
|
+
* `listTableRows` is the same function the built-in lists print through, so the
|
|
714
|
+
* piped form of a contributed table is the piped form of every other list rather
|
|
715
|
+
* than a second renderer that drifts from it. It is also what keeps the contract
|
|
716
|
+
* honest: a contribution supplied data, so BOTH renderings are derivable, and
|
|
717
|
+
* neither is the addon's problem.
|
|
718
|
+
*/
|
|
719
|
+
export const panelTableLines = (data) => listTableRows(listTableOf(data), data.rows, undefined);
|
|
689
720
|
/**
|
|
690
721
|
* One expandable entry: the caret header, the D1 extract while collapsed, the
|
|
691
722
|
* whole body while expanded.
|
|
@@ -853,7 +884,7 @@ function sectionTabRows(sections, { expanded, expandableHeadings = EXPANDABLE_HE
|
|
|
853
884
|
* off — the Runs tab, by contrast, is one node per run plus six cells and is
|
|
854
885
|
* bounded by the document's sideloaded runs.
|
|
855
886
|
*/
|
|
856
|
-
export function tabViewTree(detail, tabIndex, { expanded = new Set(), nowSeconds = 0, labels, selectedIndex = -1, panelSections, } = {}) {
|
|
887
|
+
export function tabViewTree(detail, tabIndex, { expanded = new Set(), nowSeconds = 0, labels, selectedIndex = -1, panelSections, panelTable, } = {}) {
|
|
857
888
|
if (panelSections) {
|
|
858
889
|
const sections = panelSections.map((section) => ({
|
|
859
890
|
heading: section.heading,
|
|
@@ -871,6 +902,22 @@ export function tabViewTree(detail, tabIndex, { expanded = new Set(), nowSeconds
|
|
|
871
902
|
keyOf: panelSectionKey,
|
|
872
903
|
}), selectedIndex);
|
|
873
904
|
}
|
|
905
|
+
// GAIA-475 A2 — a contributed TABLE, mapped onto the very machinery the `runs`
|
|
906
|
+
// branch below uses. Same `listTableTree`, same `1 + index` header arithmetic,
|
|
907
|
+
// same `listRowIds`: the contribution supplied columns and rows, and every
|
|
908
|
+
// question after that — layout, the header row, the highlight, the click map —
|
|
909
|
+
// is answered by code that already existed. That is the whole reason the
|
|
910
|
+
// contract carries data instead of a render tree.
|
|
911
|
+
if (panelTable) {
|
|
912
|
+
const { rows } = panelTable;
|
|
913
|
+
const table = listTableOf(panelTable);
|
|
914
|
+
return {
|
|
915
|
+
body: columnNode(BODY_ID, listTableTree(table, rows, undefined, { selectedIndex })),
|
|
916
|
+
headerRows: rows.map((_row, index) => 1 + index),
|
|
917
|
+
selectable: rows.length,
|
|
918
|
+
rowIds: listRowIds(table, rows),
|
|
919
|
+
};
|
|
920
|
+
}
|
|
874
921
|
const id = TICKET_SECTIONS[tabIndex]?.id;
|
|
875
922
|
if (id === 'runs') {
|
|
876
923
|
const runs = ticketRunRows(detail ?? {});
|
|
@@ -57,6 +57,25 @@ export declare class TicketDetailScreen extends AsyncScreen<TicketDetailDocument
|
|
|
57
57
|
*/
|
|
58
58
|
selectTopTab(tabId: string, context: ScreenContext): boolean;
|
|
59
59
|
reload(context: ScreenContext): void;
|
|
60
|
+
/**
|
|
61
|
+
* GAIA-475 criterion 24 — arm or cancel the table's self-refresh.
|
|
62
|
+
*
|
|
63
|
+
* Called wherever the OPEN TAB can change: `enter`, `selectTab`, and after an
|
|
64
|
+
* activation. A contributed table is a view of the machine rather than of the
|
|
65
|
+
* ticket — a Storybook started ten minutes into a run is not in any document
|
|
66
|
+
* the host fetched — so it is the one tab that has to re-read without being
|
|
67
|
+
* asked. The built-in tabs deliberately do not tick: their data comes from the
|
|
68
|
+
* API, and a timer on them would be a polling loop nobody asked for.
|
|
69
|
+
*
|
|
70
|
+
* What a tick does is the cheap half of `reload`: bump the token and redraw.
|
|
71
|
+
* It does NOT call `retry()`, so no HTTP request is made — the contribution
|
|
72
|
+
* re-reads its own source (the socket table, the state file) and everything
|
|
73
|
+
* the screen shows about the ticket is left exactly as it was.
|
|
74
|
+
*/
|
|
75
|
+
private syncTableTick;
|
|
76
|
+
private armTableTick;
|
|
77
|
+
private cancelTableTick;
|
|
78
|
+
leave(): void;
|
|
60
79
|
static readonly route: {
|
|
61
80
|
id: string;
|
|
62
81
|
path: string;
|
|
@@ -86,6 +105,19 @@ export declare class TicketDetailScreen extends AsyncScreen<TicketDetailDocument
|
|
|
86
105
|
*/
|
|
87
106
|
private panelQuery;
|
|
88
107
|
private panelParams;
|
|
108
|
+
/** GAIA-475 — the table rows whose `onActivate` is in flight. */
|
|
109
|
+
private activatingRows;
|
|
110
|
+
/** GAIA-475 A5 — bumped on every reload, handed to a table so it can drop its
|
|
111
|
+
* cache. A counter, not a clock. */
|
|
112
|
+
private reloadToken;
|
|
113
|
+
/**
|
|
114
|
+
* GAIA-475 criterion 24 — the self-refresh, as its cancel function.
|
|
115
|
+
*
|
|
116
|
+
* Set while a contributed TABLE is the open tab, `undefined` otherwise, so the
|
|
117
|
+
* field's presence IS "a tick is armed" and there is no second flag to
|
|
118
|
+
* disagree with it.
|
|
119
|
+
*/
|
|
120
|
+
private tableTick;
|
|
89
121
|
/** GAIA-326 — the last copy's outcome, shown in the footer. Empty until the
|
|
90
122
|
* operator asks for one; see `copyOpenTicket`. */
|
|
91
123
|
private status;
|
|
@@ -98,7 +130,7 @@ export declare class TicketDetailScreen extends AsyncScreen<TicketDetailDocument
|
|
|
98
130
|
setLabels(labels: AssignmentLabels | undefined): void;
|
|
99
131
|
selectTab(index: number, context: ScreenContext): boolean;
|
|
100
132
|
select(index: number, context: ScreenContext): boolean;
|
|
101
|
-
activate(index: number, context: ScreenContext): boolean;
|
|
133
|
+
activate(index: number, context: ScreenContext, intent?: 'activate' | 'deactivate'): boolean;
|
|
102
134
|
get document(): TicketDetailDocument | undefined;
|
|
103
135
|
protected load(context: ScreenContext): Promise<TicketDetailDocument>;
|
|
104
136
|
protected renderLoading(): string[];
|
|
@@ -178,8 +210,8 @@ export declare class TicketDetailScreen extends AsyncScreen<TicketDetailDocument
|
|
|
178
210
|
reset(): void;
|
|
179
211
|
private initialize;
|
|
180
212
|
private currentView;
|
|
181
|
-
/** The
|
|
182
|
-
private
|
|
213
|
+
/** The contribution the active index names, or none while a built-in is open. */
|
|
214
|
+
private activeContribution;
|
|
183
215
|
/**
|
|
184
216
|
* One panel's sections, with its own explicit empty state applied.
|
|
185
217
|
*
|
|
@@ -206,5 +238,62 @@ export declare class TicketDetailScreen extends AsyncScreen<TicketDetailDocument
|
|
|
206
238
|
* untouched, so `emptyMessage` still means "nothing to show", never "it
|
|
207
239
|
* broke".
|
|
208
240
|
*/
|
|
241
|
+
/**
|
|
242
|
+
* GAIA-475 A3 — activate one table row, following the conductor screen's shape.
|
|
243
|
+
*
|
|
244
|
+
* Mark busy, redraw, await, record the outcome as the footer status, clear busy
|
|
245
|
+
* in a `finally`, redraw. The busy set is what makes a second Enter a no-op
|
|
246
|
+
* while the first is in flight (US-12) and what a cell reads through
|
|
247
|
+
* `PanelRender.activating` to show the work.
|
|
248
|
+
*
|
|
249
|
+
* The row is picked from the SAME guarded resolution the render uses
|
|
250
|
+
* (`tableView`), so the row `onActivate` receives is exactly the row the
|
|
251
|
+
* operator saw; a table whose `rows()` throws resolves to the error branch and
|
|
252
|
+
* simply has no row to activate.
|
|
253
|
+
*/
|
|
254
|
+
/**
|
|
255
|
+
* GAIA-500 — `l`/`t`'s shared plumbing: resolve the active table's selected
|
|
256
|
+
* row and ask its `copyLocal`/`copyTunnel`, returning `undefined` when there
|
|
257
|
+
* is no active table, the table does not implement that side, or the row has
|
|
258
|
+
* nothing to offer — in every one of which cases the key is left UNHANDLED
|
|
259
|
+
* (the caller falls through) rather than reporting a copy that did not
|
|
260
|
+
* happen, exactly as `l`/`t` do nothing on every tab that is not a table.
|
|
261
|
+
*/
|
|
262
|
+
private copyActiveTableRow;
|
|
263
|
+
private activateTableRow;
|
|
264
|
+
/** Tests and the busy indicator: the row ids whose activation is in flight. */
|
|
265
|
+
activatingRowIds(): string[];
|
|
266
|
+
/**
|
|
267
|
+
* One contributed table's columns and rows — `panelSections`' twin (GAIA-475).
|
|
268
|
+
*
|
|
269
|
+
* Returns the OPTION BAG `tabViewTree` takes rather than the data directly,
|
|
270
|
+
* because a failure has to change WHICH branch renders: there is no such thing
|
|
271
|
+
* as a table of an error. On a throw this degrades to `panelSections`, so a
|
|
272
|
+
* broken table produces the same named, readable failure row a broken panel
|
|
273
|
+
* does — one rendering of "this addon broke", not two.
|
|
274
|
+
*
|
|
275
|
+
* The same chokepoint argument applies here as there, and more sharply: both
|
|
276
|
+
* `columns()` and `rows()` are contributed code, both run on the draw path, and
|
|
277
|
+
* the draw path is not uniformly guarded (`Kernel/tui-kernel.ts` wires `draw`
|
|
278
|
+
* straight onto the resize and mouse-scroll events). A throw out of either one
|
|
279
|
+
* would freeze or crash the cockpit.
|
|
280
|
+
*
|
|
281
|
+
* `noun` replaces `/` with `-` because the id is org-prefixed by convention
|
|
282
|
+
* (`gaia/ports`) and it becomes a node id prefix here — `rowNodeId(noun, …)` is
|
|
283
|
+
* what a click resolves through, so the separator has to survive that round
|
|
284
|
+
* trip intact.
|
|
285
|
+
*/
|
|
286
|
+
private tableView;
|
|
287
|
+
/**
|
|
288
|
+
* One contributed table as the pipe's lines — the SAME resolution the draw
|
|
289
|
+
* path uses, so the two renderings cannot disagree about what the table holds.
|
|
290
|
+
*
|
|
291
|
+
* Routed through `tableView`, which means the error boundary covers the pipe
|
|
292
|
+
* too: a throwing table prints its named failure row here exactly as it draws
|
|
293
|
+
* one on screen.
|
|
294
|
+
*/
|
|
295
|
+
private tableLines;
|
|
296
|
+
/** The `PanelRender` a contribution is handed — one construction, two kinds. */
|
|
297
|
+
private panelRender;
|
|
209
298
|
private panelSections;
|
|
210
299
|
}
|