@gaia-ai/addon-gaia-ui 0.7.0 → 0.8.0
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/Agent/launch.d.ts +4 -4
- package/dist/src/Agent/launch.js +14 -7
- package/dist/src/Component/stats-panel/index.d.ts +13 -2
- package/dist/src/Component/stats-panel/index.js +28 -18
- package/dist/src/Component/top-tabs/index.d.ts +6 -0
- package/dist/src/Component/top-tabs/index.js +15 -6
- package/dist/src/Entity/collection.d.ts +98 -0
- package/dist/src/Entity/collection.js +46 -0
- package/dist/src/Entity/conductor/Component/conductor-list/index.d.ts +25 -0
- package/dist/src/Entity/conductor/Component/conductor-list/index.js +60 -0
- package/dist/src/Entity/conductor/Data/conductors.d.ts +57 -0
- package/dist/src/Entity/conductor/Data/conductors.js +67 -0
- package/dist/src/Entity/conductor/Data/liveness.d.ts +2 -1
- package/dist/src/Entity/conductor/Data/liveness.js +12 -16
- package/dist/src/Entity/conductor/Data/log-tail.d.ts +25 -0
- package/dist/src/Entity/conductor/Data/log-tail.js +133 -0
- package/dist/src/Entity/conductor/Data/own-conductor.d.ts +30 -0
- package/dist/src/Entity/conductor/Data/own-conductor.js +73 -0
- package/dist/src/Entity/conductor/Data/process.d.ts +78 -0
- package/dist/src/Entity/conductor/Data/process.js +101 -0
- package/dist/src/Entity/conductor/Screen/conductors.d.ts +42 -0
- package/dist/src/Entity/conductor/Screen/conductors.js +200 -0
- package/dist/src/Entity/cycle.d.ts +18 -0
- package/dist/src/Entity/cycle.js +30 -0
- package/dist/src/Entity/project/Data/project.d.ts +8 -0
- package/dist/src/Entity/project/Data/project.js +12 -0
- package/dist/src/Entity/project/Data/projects.d.ts +3 -0
- package/dist/src/Entity/project/Data/projects.js +3 -0
- package/dist/src/Entity/project/Data/with-tickets.d.ts +5 -0
- package/dist/src/Entity/project/Data/with-tickets.js +15 -13
- package/dist/src/Entity/project/Screen/project-picker.d.ts +36 -0
- package/dist/src/Entity/project/Screen/project-picker.js +90 -0
- package/dist/src/Entity/project/Screen/projects-screen.d.ts +40 -0
- package/dist/src/Entity/project/Screen/projects-screen.js +103 -0
- package/dist/src/Entity/run/Component/run-table/index.d.ts +5 -7
- package/dist/src/Entity/run/Component/run-table/index.js +9 -18
- package/dist/src/Entity/run/Data/query.d.ts +22 -0
- package/dist/src/Entity/run/Data/query.js +41 -0
- package/dist/src/Entity/run/Data/runs.d.ts +85 -4
- package/dist/src/Entity/run/Data/runs.js +143 -27
- package/dist/src/Entity/run/Screen/run-detail.d.ts +25 -0
- package/dist/src/Entity/run/Screen/run-detail.js +51 -0
- package/dist/src/Entity/run/Screen/runs.d.ts +82 -0
- package/dist/src/Entity/run/Screen/runs.js +169 -0
- package/dist/src/Entity/statistics/Screen/dashboard.d.ts +42 -0
- package/dist/src/Entity/statistics/Screen/dashboard.js +177 -0
- package/dist/src/Entity/ticket/Component/create-form/index.d.ts +10 -0
- package/dist/src/Entity/ticket/Component/create-form/index.js +4 -0
- package/dist/src/Entity/ticket/Component/edit-form/index.d.ts +3 -2
- package/dist/src/Entity/ticket/Component/edit-form/index.js +30 -8
- package/dist/src/Entity/ticket/Component/tab-bar/index.js +6 -0
- package/dist/src/Entity/ticket/Component/ticket-detail/index.d.ts +1 -1
- package/dist/src/Entity/ticket/Component/ticket-detail/index.js +33 -2
- package/dist/src/Entity/ticket/Component/ticket-table/index.d.ts +4 -2
- package/dist/src/Entity/ticket/Component/ticket-table/index.js +8 -12
- package/dist/src/Entity/ticket/Data/parents.d.ts +13 -1
- package/dist/src/Entity/ticket/Data/parents.js +64 -12
- package/dist/src/Entity/ticket/Data/query.d.ts +44 -7
- package/dist/src/Entity/ticket/Data/query.js +60 -26
- package/dist/src/Entity/ticket/Data/tickets.d.ts +105 -4
- package/dist/src/Entity/ticket/Data/tickets.js +164 -12
- package/dist/src/Entity/ticket/Data/workflow.d.ts +1 -5
- package/dist/src/Entity/ticket/Data/workflow.js +5 -13
- package/dist/src/Entity/ticket/Data/write.d.ts +32 -0
- package/dist/src/Entity/ticket/Data/write.js +80 -0
- package/dist/src/Entity/ticket/Form/create-data.d.ts +1 -11
- package/dist/src/Entity/ticket/Form/create-data.js +7 -36
- package/dist/src/Entity/ticket/Form/create.d.ts +45 -5
- package/dist/src/Entity/ticket/Form/create.js +45 -8
- package/dist/src/Entity/ticket/Form/edit-data.d.ts +1 -1
- package/dist/src/Entity/ticket/Form/edit-data.js +4 -0
- package/dist/src/Entity/ticket/Form/edit.d.ts +79 -12
- package/dist/src/Entity/ticket/Form/edit.js +165 -18
- package/dist/src/Entity/ticket/Form/keys.d.ts +7 -0
- package/dist/src/Entity/ticket/Form/keys.js +7 -0
- package/dist/src/Entity/ticket/Form/parent-filter.d.ts +36 -0
- package/dist/src/Entity/ticket/Form/parent-filter.js +37 -0
- package/dist/src/Entity/ticket/Form/parent-source.d.ts +27 -0
- package/dist/src/Entity/ticket/Form/parent-source.js +56 -0
- package/dist/src/Entity/ticket/Form/who-filter.d.ts +33 -0
- package/dist/src/Entity/ticket/Form/who-filter.js +33 -0
- package/dist/src/Entity/ticket/Form/who-source.d.ts +18 -0
- package/dist/src/Entity/ticket/Form/who-source.js +38 -0
- package/dist/src/Entity/ticket/Screen/create-ticket-form.d.ts +68 -0
- package/dist/src/Entity/ticket/Screen/create-ticket-form.js +336 -0
- package/dist/src/Entity/ticket/Screen/edit-assignment-form.d.ts +81 -0
- package/dist/src/Entity/ticket/Screen/edit-assignment-form.js +200 -0
- package/dist/src/Entity/ticket/Screen/parent-filter-form.d.ts +27 -0
- package/dist/src/Entity/ticket/Screen/parent-filter-form.js +177 -0
- package/dist/src/Entity/ticket/Screen/ticket-agents.d.ts +45 -0
- package/dist/src/Entity/ticket/Screen/ticket-agents.js +135 -0
- package/dist/src/Entity/ticket/Screen/ticket-detail.d.ts +61 -0
- package/dist/src/Entity/ticket/Screen/ticket-detail.js +300 -0
- package/dist/src/Entity/ticket/Screen/tickets.d.ts +129 -0
- package/dist/src/Entity/ticket/Screen/tickets.js +507 -0
- package/dist/src/Entity/ticket/Screen/who-filter-form.d.ts +27 -0
- package/dist/src/Entity/ticket/Screen/who-filter-form.js +154 -0
- package/dist/src/Entity/user/Data/users.d.ts +17 -0
- package/dist/src/Entity/user/Data/users.js +66 -6
- package/dist/src/Form/field.d.ts +3 -1
- package/dist/src/Form/field.js +1 -0
- package/dist/src/Form/form.d.ts +1 -1
- package/dist/src/Form/form.js +61 -9
- package/dist/src/Form/keys.d.ts +5 -1
- package/dist/src/Form/keys.js +38 -5
- package/dist/src/Kernel/tui-kernel.d.ts +0 -10
- package/dist/src/Kernel/tui-kernel.js +250 -2384
- package/dist/src/Routing/context.d.ts +31 -0
- package/dist/src/Routing/context.js +52 -0
- package/dist/src/Routing/registry.d.ts +19 -0
- package/dist/src/Routing/registry.js +97 -0
- package/dist/src/Routing/route.d.ts +39 -0
- package/dist/src/Routing/route.js +1 -0
- package/dist/src/Routing/router.d.ts +33 -0
- package/dist/src/Routing/router.js +102 -0
- package/dist/src/Routing/routes.d.ts +17 -0
- package/dist/src/Routing/routes.js +57 -0
- package/dist/src/Screen/async-screen.d.ts +50 -0
- package/dist/src/Screen/async-screen.js +164 -0
- package/dist/src/Screen/list-screen.d.ts +158 -0
- package/dist/src/Screen/list-screen.js +237 -0
- package/dist/src/Screen/screen.d.ts +106 -0
- package/dist/src/Screen/screen.js +18 -0
- package/dist/src/Tui/controller.d.ts +13 -0
- package/dist/src/Tui/controller.js +1794 -0
- package/dist/src/Widget/text.js +49 -4
- package/dist/src/Widget/types.d.ts +2 -1
- package/dist/src/launcher.d.ts +9 -2
- package/dist/src/launcher.js +2 -1
- package/dist/src/lib/ansi.js +13 -5
- package/dist/src/lib/cursor.d.ts +31 -0
- package/dist/src/lib/cursor.js +45 -0
- package/dist/src/lib/format.d.ts +21 -0
- package/dist/src/lib/format.js +59 -0
- package/dist/src/lib/table.d.ts +37 -0
- package/dist/src/lib/table.js +20 -0
- package/dist/src/lib/width.d.ts +33 -1
- package/dist/src/lib/width.js +107 -12
- package/dist/src/plugin.js +19 -0
- package/dist/src/types.d.ts +26 -0
- package/dist/src/types.js +0 -4
- package/package.json +2 -2
|
@@ -3,58 +3,174 @@
|
|
|
3
3
|
// (AC-5). Runs are scoped to the selected project via the nested filter
|
|
4
4
|
// `ticket_id.project_id.id` (a project may span several conductors, so the
|
|
5
5
|
// ticket→project relationship is the canonical scope, not `conductor_id`).
|
|
6
|
-
|
|
6
|
+
// The query itself lives in `./query.ts` (GAIA-282); `ticketRunRows` stays a
|
|
7
|
+
// no-request read of the ticket document (spec D7).
|
|
8
|
+
import { loadPage } from '../../collection.js';
|
|
9
|
+
import { LIST_PAGE_SIZE } from '../../cycle.js';
|
|
10
|
+
import { buildRunQueryParams } from './query.js';
|
|
11
|
+
import { deriveRunStatus, runStartedAge } from './status.js';
|
|
7
12
|
/** Read a value as a string, or `undefined` when it is not one. */
|
|
8
13
|
function str(value) {
|
|
9
14
|
return typeof value === 'string' ? value : undefined;
|
|
10
15
|
}
|
|
16
|
+
/** In the fieldset, or the sideloaded ticket stops resolving (spec fact 5). */
|
|
17
|
+
export const RUN_LIST_RELATIONSHIPS = ['ticket_id'];
|
|
18
|
+
/** Requested, rendered by no column: `closed` feeds the closed predicate in
|
|
19
|
+
* `RunsScreen.load`. */
|
|
20
|
+
const RUN_PREDICATE_ATTRIBUTES = ['closed'];
|
|
21
|
+
/**
|
|
22
|
+
* The run list's columns — RUN · TICKET · STATE · PHASE · AGENT · STARTED, each
|
|
23
|
+
* carrying its own fieldset entry (GAIA-294 AC 4).
|
|
24
|
+
*
|
|
25
|
+
* `Ctx` is `nowSeconds`: STATE is the derived liveness status and STARTED the
|
|
26
|
+
* relative start age, and both need a clock the row does not carry.
|
|
27
|
+
*/
|
|
28
|
+
export const RUN_COLUMNS = [
|
|
29
|
+
{
|
|
30
|
+
key: 'run',
|
|
31
|
+
label: 'RUN',
|
|
32
|
+
width: 8,
|
|
33
|
+
fields: ['drupal_internal__id'],
|
|
34
|
+
get: (r) => `#${r.drupalId ?? r.id.slice(0, 6)}`,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: 'ticket',
|
|
38
|
+
label: 'TICKET',
|
|
39
|
+
width: 10,
|
|
40
|
+
fields: ['ticket_id'],
|
|
41
|
+
get: (r) => r.ticketIdentifier,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
// Three fields for one column: `deriveRunStatus` reads the run's own state
|
|
45
|
+
// plus its heartbeat and claim window.
|
|
46
|
+
key: 'state',
|
|
47
|
+
label: 'STATE',
|
|
48
|
+
width: 8,
|
|
49
|
+
fields: ['state', 'heartbeat', 'claim_expires_at'],
|
|
50
|
+
get: (r, nowSeconds) => deriveRunStatus(r, nowSeconds),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
key: 'phase',
|
|
54
|
+
label: 'PHASE',
|
|
55
|
+
width: 10,
|
|
56
|
+
fields: ['state_at_start'],
|
|
57
|
+
get: (r) => r.phase,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
key: 'agent',
|
|
61
|
+
label: 'AGENT',
|
|
62
|
+
width: 8,
|
|
63
|
+
fields: ['agent'],
|
|
64
|
+
get: (r) => r.agent,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
// The trailing column, unpadded. `created` is `started_at`'s documented
|
|
68
|
+
// fallback for a run that never started.
|
|
69
|
+
key: 'started',
|
|
70
|
+
label: 'STARTED',
|
|
71
|
+
width: 0,
|
|
72
|
+
fields: ['started_at', 'created'],
|
|
73
|
+
get: (r, nowSeconds) => runStartedAge(r, nowSeconds),
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
/**
|
|
77
|
+
* The attributes a run row needs (GAIA-292 D2), in the order the query string
|
|
78
|
+
* states them. `created` is a column field as `started_at`'s documented fallback
|
|
79
|
+
* for a run that never started, and `heartbeat` + `claim_expires_at` ride the STATE
|
|
80
|
+
* column because `deriveRunStatus` reads them.
|
|
81
|
+
*
|
|
82
|
+
* Membership is owned by the columns and proved exhaustive below; only the ORDER
|
|
83
|
+
* is declared, because `tests/Entity/run/Data/query.test.ts` pins the emitted
|
|
84
|
+
* `fields[gaia_run--gaia_run]=…` sequence and column order would hoist
|
|
85
|
+
* `heartbeat`/`claim_expires_at` to third and fourth (GAIA-294, behaviour-neutral
|
|
86
|
+
* to the bytes on the wire).
|
|
87
|
+
*/
|
|
88
|
+
export const RUN_LIST_ATTRIBUTES = [
|
|
89
|
+
'drupal_internal__id',
|
|
90
|
+
'state',
|
|
91
|
+
'state_at_start',
|
|
92
|
+
'agent',
|
|
93
|
+
'started_at',
|
|
94
|
+
'created',
|
|
95
|
+
'heartbeat',
|
|
96
|
+
'claim_expires_at',
|
|
97
|
+
'closed',
|
|
98
|
+
];
|
|
99
|
+
/** What `addFields('gaia_run--gaia_run', …)` takes. */
|
|
100
|
+
export const RUN_LIST_FIELDS = [
|
|
101
|
+
...RUN_LIST_ATTRIBUTES,
|
|
102
|
+
...RUN_LIST_RELATIONSHIPS,
|
|
103
|
+
];
|
|
104
|
+
/**
|
|
105
|
+
* The sideloaded ticket renders ONE column: its identifier.
|
|
106
|
+
*
|
|
107
|
+
* Measured (GAIA-292 spec fact 4): without this, a 37-row run page shipped
|
|
108
|
+
* 191.4 KB of `included` — 12 whole ticket resources with their descriptions —
|
|
109
|
+
* out of 254.7 KB total, and took 4732 ms.
|
|
110
|
+
*/
|
|
111
|
+
export const RUN_TICKET_INCLUDE_FIELDS = ['identifier'];
|
|
11
112
|
/**
|
|
12
113
|
* Map a RAW JSON:API `data[]` entry (from `client.get`) to a run row, resolving
|
|
13
114
|
* the ticket identifier from the sideloaded includes indexed by id. Mirrors
|
|
14
115
|
* `mapTicketData` — the only I/O is upstream in {@link loadProjectRuns}.
|
|
116
|
+
*
|
|
117
|
+
* Every attribute and relationship is read through an accessor keyed on the
|
|
118
|
+
* fieldset, so reading one the list does not request is a compile error (AC-5).
|
|
15
119
|
*/
|
|
16
120
|
export function mapRunData(item, ticketsById) {
|
|
17
121
|
const a = item.attributes ?? {};
|
|
18
|
-
const
|
|
122
|
+
const text = (key) => typeof a[key] === 'string' ? a[key] : undefined;
|
|
123
|
+
const raw = (key) => a[key];
|
|
124
|
+
const rel = (key) => item.relationships?.[key]?.data;
|
|
125
|
+
const ticketRel = rel('ticket_id');
|
|
19
126
|
const ticketId = Array.isArray(ticketRel)
|
|
20
127
|
? (ticketRel[0]?.id ?? null)
|
|
21
128
|
: (ticketRel?.id ?? null);
|
|
22
129
|
const ticket = ticketId ? ticketsById.get(ticketId) : undefined;
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
?
|
|
130
|
+
const internal = raw('drupal_internal__id');
|
|
131
|
+
const drupalId = typeof internal === 'number' || typeof internal === 'string'
|
|
132
|
+
? internal
|
|
26
133
|
: null;
|
|
27
134
|
return {
|
|
28
135
|
id: item.id ?? '',
|
|
29
136
|
drupalId,
|
|
30
137
|
ticketId,
|
|
31
138
|
ticketIdentifier: str(ticket?.attributes?.identifier) ?? (ticketId ? ticketId : '—'),
|
|
32
|
-
state:
|
|
33
|
-
phase:
|
|
34
|
-
agent:
|
|
35
|
-
startedAt:
|
|
36
|
-
heartbeat:
|
|
37
|
-
claimExpiresAt:
|
|
139
|
+
state: text('state') ?? 'unknown',
|
|
140
|
+
phase: text('state_at_start') ?? '—',
|
|
141
|
+
agent: text('agent') ?? '—',
|
|
142
|
+
startedAt: text('started_at') ?? text('created') ?? null,
|
|
143
|
+
heartbeat: text('heartbeat') ?? null,
|
|
144
|
+
claimExpiresAt: text('claim_expires_at') ?? null,
|
|
145
|
+
closed: typeof raw('closed') === 'boolean' ? raw('closed') : false,
|
|
38
146
|
};
|
|
39
147
|
}
|
|
40
148
|
/**
|
|
41
|
-
* List the `gaia_run` entities of a project
|
|
42
|
-
*
|
|
43
|
-
*
|
|
149
|
+
* List the `gaia_run` entities of a project under the given criteria,
|
|
150
|
+
* most-recently-started first — ONE page of them. The query (scope, include,
|
|
151
|
+
* fieldsets, sort, closed filter) is built by {@link buildRunQueryParams}; the
|
|
152
|
+
* page (size + index) is the caller's, and `hasNext` comes from the document's
|
|
153
|
+
* `links.next`.
|
|
44
154
|
*/
|
|
45
|
-
export async function loadProjectRuns(client, projectId) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
return (
|
|
155
|
+
export async function loadProjectRuns(client, projectId, criteria, page = 0) {
|
|
156
|
+
// The window this read asks for, stated once and handed BOTH to the query
|
|
157
|
+
// builder and to the page it produces, so the offset the screen positions with
|
|
158
|
+
// is the offset the server was sent (GAIA-312 D3).
|
|
159
|
+
const window = {
|
|
160
|
+
offset: page * LIST_PAGE_SIZE,
|
|
161
|
+
limit: LIST_PAGE_SIZE,
|
|
162
|
+
};
|
|
163
|
+
const params = buildRunQueryParams(projectId, criteria, {
|
|
164
|
+
size: window.limit,
|
|
165
|
+
index: page,
|
|
166
|
+
});
|
|
167
|
+
return loadPage(client, 'gaia_run/gaia_run', params, (data, included) => {
|
|
168
|
+
const ticketsById = new Map();
|
|
169
|
+
for (const inc of included)
|
|
170
|
+
if (inc.id)
|
|
171
|
+
ticketsById.set(inc.id, inc);
|
|
172
|
+
return data.map((item) => mapRunData(item, ticketsById));
|
|
173
|
+
}, page, window);
|
|
58
174
|
}
|
|
59
175
|
/**
|
|
60
176
|
* Newest-STARTED first, and TOTAL (GAIA-255 D5).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { BaseScreen, type ScreenContext } from '../../../Screen/screen.js';
|
|
2
|
+
import type { TicketDetailDocument } from '../../../types.js';
|
|
3
|
+
export interface RunDetailInput {
|
|
4
|
+
readonly document: TicketDetailDocument;
|
|
5
|
+
}
|
|
6
|
+
export declare const runDetailInputContext: import("../../../Routing/context.js").ContextKey<RunDetailInput>;
|
|
7
|
+
export declare class RunDetailScreen extends BaseScreen {
|
|
8
|
+
readonly frameId = "run";
|
|
9
|
+
topTab(): null;
|
|
10
|
+
static readonly route: {
|
|
11
|
+
id: string;
|
|
12
|
+
path: string;
|
|
13
|
+
parent: string;
|
|
14
|
+
fallback: {
|
|
15
|
+
routeId: string;
|
|
16
|
+
};
|
|
17
|
+
context: (_params: Readonly<Record<string, string>>, state: unknown, base: import("../../../Routing/context.js").ContextBag) => import("../../../Routing/context.js").ContextBag;
|
|
18
|
+
};
|
|
19
|
+
readonly id: string;
|
|
20
|
+
private document;
|
|
21
|
+
enter(context: ScreenContext): void;
|
|
22
|
+
render(context: ScreenContext): string[];
|
|
23
|
+
present(context: ScreenContext): import("../../../Screen/screen.js").ScreenPresentation;
|
|
24
|
+
reset(): void;
|
|
25
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { defineContext } from '../../../Routing/context.js';
|
|
2
|
+
import { BaseScreen, presentation, } from '../../../Screen/screen.js';
|
|
3
|
+
import { runDetailLines, runDetailTitle, } from '../Component/run-detail/index.js';
|
|
4
|
+
export const runDetailInputContext = defineContext('run-detail-input', 'input');
|
|
5
|
+
export class RunDetailScreen extends BaseScreen {
|
|
6
|
+
frameId = 'run';
|
|
7
|
+
topTab() {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
static route = {
|
|
11
|
+
id: 'run.detail',
|
|
12
|
+
path: '/tickets/:ticketId/runs/:runId',
|
|
13
|
+
parent: 'ticket.detail',
|
|
14
|
+
fallback: { routeId: 'project.dashboard' },
|
|
15
|
+
context: (_params, state, base) => state
|
|
16
|
+
? base.set(runDetailInputContext, {
|
|
17
|
+
document: state,
|
|
18
|
+
})
|
|
19
|
+
: base,
|
|
20
|
+
};
|
|
21
|
+
id = RunDetailScreen.route.id;
|
|
22
|
+
document = {};
|
|
23
|
+
enter(context) {
|
|
24
|
+
if (context.contexts?.has(runDetailInputContext))
|
|
25
|
+
this.document = context.contexts.get(runDetailInputContext).document;
|
|
26
|
+
context.requestDraw();
|
|
27
|
+
}
|
|
28
|
+
render(context) {
|
|
29
|
+
const runId = context.params?.runId;
|
|
30
|
+
if (!runId)
|
|
31
|
+
return ['Run detail requires runId'];
|
|
32
|
+
return [
|
|
33
|
+
runDetailTitle(this.document, runId),
|
|
34
|
+
'',
|
|
35
|
+
...runDetailLines(this.document, runId).lines,
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
present(context) {
|
|
39
|
+
const runId = context.params?.runId ?? '';
|
|
40
|
+
const rendered = runDetailLines(this.document, runId);
|
|
41
|
+
return presentation(rendered.lines, {
|
|
42
|
+
boldRows: rendered.boldRows,
|
|
43
|
+
title: runDetailTitle(this.document, runId),
|
|
44
|
+
meta: '↑↓ scroll · Esc back · q quit',
|
|
45
|
+
status: ' ↑↓ scroll · Esc back · q quit ',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
reset() {
|
|
49
|
+
this.document = {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { ListScreen } from '../../../Screen/list-screen.js';
|
|
2
|
+
import { type ScreenContext, type ScreenPresentation } from '../../../Screen/screen.js';
|
|
3
|
+
import { type RunCriteria } from '../Data/query.js';
|
|
4
|
+
import { type RunPage, type RunRow } from '../Data/runs.js';
|
|
5
|
+
export declare class RunsScreen extends ListScreen<RunRow, number> {
|
|
6
|
+
readonly frameId = "project-runs";
|
|
7
|
+
readonly topTabOverlay = true;
|
|
8
|
+
protected readonly columns: readonly [{
|
|
9
|
+
readonly key: "run";
|
|
10
|
+
readonly label: "RUN";
|
|
11
|
+
readonly width: 8;
|
|
12
|
+
readonly fields: readonly ["drupal_internal__id"];
|
|
13
|
+
readonly get: (r: RunRow) => string;
|
|
14
|
+
}, {
|
|
15
|
+
readonly key: "ticket";
|
|
16
|
+
readonly label: "TICKET";
|
|
17
|
+
readonly width: 10;
|
|
18
|
+
readonly fields: readonly ["ticket_id"];
|
|
19
|
+
readonly get: (r: RunRow) => string;
|
|
20
|
+
}, {
|
|
21
|
+
readonly key: "state";
|
|
22
|
+
readonly label: "STATE";
|
|
23
|
+
readonly width: 8;
|
|
24
|
+
readonly fields: readonly ["state", "heartbeat", "claim_expires_at"];
|
|
25
|
+
readonly get: (r: RunRow, nowSeconds: number) => string;
|
|
26
|
+
}, {
|
|
27
|
+
readonly key: "phase";
|
|
28
|
+
readonly label: "PHASE";
|
|
29
|
+
readonly width: 10;
|
|
30
|
+
readonly fields: readonly ["state_at_start"];
|
|
31
|
+
readonly get: (r: RunRow) => string;
|
|
32
|
+
}, {
|
|
33
|
+
readonly key: "agent";
|
|
34
|
+
readonly label: "AGENT";
|
|
35
|
+
readonly width: 8;
|
|
36
|
+
readonly fields: readonly ["agent"];
|
|
37
|
+
readonly get: (r: RunRow) => string;
|
|
38
|
+
}, {
|
|
39
|
+
readonly key: "started";
|
|
40
|
+
readonly label: "STARTED";
|
|
41
|
+
readonly width: 0;
|
|
42
|
+
readonly fields: readonly ["started_at", "created"];
|
|
43
|
+
readonly get: (r: RunRow, nowSeconds: number) => string;
|
|
44
|
+
}];
|
|
45
|
+
protected readonly emptyMessage = "No runs for this project.";
|
|
46
|
+
topTab(): {
|
|
47
|
+
readonly level: "project";
|
|
48
|
+
readonly tab: "runs";
|
|
49
|
+
};
|
|
50
|
+
static readonly route: {
|
|
51
|
+
id: string;
|
|
52
|
+
path: string;
|
|
53
|
+
parent: string;
|
|
54
|
+
tab: {
|
|
55
|
+
id: string;
|
|
56
|
+
label: string;
|
|
57
|
+
scope: string;
|
|
58
|
+
order: number;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
readonly id: string;
|
|
62
|
+
private criteria;
|
|
63
|
+
saveState(): {
|
|
64
|
+
criteria: RunCriteria;
|
|
65
|
+
page: number;
|
|
66
|
+
selectedRunId: string | null;
|
|
67
|
+
};
|
|
68
|
+
restoreState(token: unknown): void;
|
|
69
|
+
/** STATE is the derived liveness status and STARTED the relative start age, so
|
|
70
|
+
* the columns need a clock the row does not carry. */
|
|
71
|
+
protected tableContext(context: ScreenContext): number;
|
|
72
|
+
protected loadRows(context: ScreenContext, page: number): Promise<RunPage>;
|
|
73
|
+
/** Server is authoritative; the double-check protects against param-ignoring
|
|
74
|
+
* clients/test doubles, as on the ticket list. */
|
|
75
|
+
protected filterRow(row: RunRow): boolean;
|
|
76
|
+
protected renderLoading(): string[];
|
|
77
|
+
present(context: ScreenContext): ScreenPresentation;
|
|
78
|
+
activate(index: number, context: ScreenContext): boolean;
|
|
79
|
+
select(index: number, context: ScreenContext): boolean;
|
|
80
|
+
handleInput(input: string, context: ScreenContext): boolean;
|
|
81
|
+
reset(): void;
|
|
82
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { ListScreen } from '../../../Screen/list-screen.js';
|
|
2
|
+
import { presentation, } from '../../../Screen/screen.js';
|
|
3
|
+
import { CLOSED_FILTER_VALUES, cycleValue } from '../../cycle.js';
|
|
4
|
+
import { DEFAULT_RUN_CRITERIA } from '../Data/query.js';
|
|
5
|
+
import { loadProjectRuns, RUN_COLUMNS, selectedRunTicketId, } from '../Data/runs.js';
|
|
6
|
+
export class RunsScreen extends ListScreen {
|
|
7
|
+
frameId = 'project-runs';
|
|
8
|
+
topTabOverlay = true;
|
|
9
|
+
columns = RUN_COLUMNS;
|
|
10
|
+
emptyMessage = 'No runs for this project.';
|
|
11
|
+
topTab() {
|
|
12
|
+
return { level: 'project', tab: 'runs' };
|
|
13
|
+
}
|
|
14
|
+
static route = {
|
|
15
|
+
id: 'project.runs',
|
|
16
|
+
path: '/projects/:projectId/runs',
|
|
17
|
+
parent: 'project.dashboard',
|
|
18
|
+
tab: { id: 'runs', label: 'Runs', scope: 'project', order: 40 },
|
|
19
|
+
};
|
|
20
|
+
id = RunsScreen.route.id;
|
|
21
|
+
criteria = { ...DEFAULT_RUN_CRITERIA };
|
|
22
|
+
saveState() {
|
|
23
|
+
return {
|
|
24
|
+
criteria: { ...this.criteria },
|
|
25
|
+
page: this.page,
|
|
26
|
+
selectedRunId: this.rows[this.selectedIndex]?.id ?? null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
restoreState(token) {
|
|
30
|
+
if (!token || typeof token !== 'object')
|
|
31
|
+
return;
|
|
32
|
+
const saved = token;
|
|
33
|
+
if (saved.criteria) {
|
|
34
|
+
this.criteria = { ...saved.criteria };
|
|
35
|
+
// Tokens saved before GAIA-282 carry no closed; normalize so a stale
|
|
36
|
+
// token cannot smuggle undefined into a predicate testing !== null.
|
|
37
|
+
this.criteria.closed =
|
|
38
|
+
this.criteria.closed ?? DEFAULT_RUN_CRITERIA.closed;
|
|
39
|
+
}
|
|
40
|
+
// Clamped for the same reason `closed` is normalised above: a stale token
|
|
41
|
+
// must not smuggle a negative offset into the query.
|
|
42
|
+
this.restorePage(saved);
|
|
43
|
+
if (typeof saved.selectedRunId !== 'string')
|
|
44
|
+
return;
|
|
45
|
+
const index = this.rows.findIndex((row) => row.id === saved.selectedRunId);
|
|
46
|
+
if (index >= 0)
|
|
47
|
+
this.selectedIndex = index;
|
|
48
|
+
}
|
|
49
|
+
/** STATE is the derived liveness status and STARTED the relative start age, so
|
|
50
|
+
* the columns need a clock the row does not carry. */
|
|
51
|
+
tableContext(context) {
|
|
52
|
+
return context.nowSeconds?.() ?? Math.floor(Date.now() / 1000);
|
|
53
|
+
}
|
|
54
|
+
loadRows(context, page) {
|
|
55
|
+
if (!context.services || !context.project)
|
|
56
|
+
throw new Error('Runs requires project context');
|
|
57
|
+
return loadProjectRuns(context.services.client, context.project.id, this.criteria, page);
|
|
58
|
+
}
|
|
59
|
+
/** Server is authoritative; the double-check protects against param-ignoring
|
|
60
|
+
* clients/test doubles, as on the ticket list. */
|
|
61
|
+
filterRow(row) {
|
|
62
|
+
return this.criteria.closed === null || row.closed === this.criteria.closed;
|
|
63
|
+
}
|
|
64
|
+
renderLoading() {
|
|
65
|
+
return ['Loading runs…'];
|
|
66
|
+
}
|
|
67
|
+
present(context) {
|
|
68
|
+
const rows = this.rows;
|
|
69
|
+
// Standing segment: stated whenever rows are hidden, including the very
|
|
70
|
+
// first render (AC-4) — unconditional, as on the ticket list.
|
|
71
|
+
const closedSegment = this.criteria.closed === null
|
|
72
|
+
? ''
|
|
73
|
+
: this.criteria.closed
|
|
74
|
+
? 'closed: only'
|
|
75
|
+
: 'closed: hidden';
|
|
76
|
+
// The PAGE number, and only that; the collection's size is a different
|
|
77
|
+
// quantity, stated in its own segment by `countLabel` off the `meta.count`
|
|
78
|
+
// the page response carries (D6e). A page that offers a next page while
|
|
79
|
+
// showing few rows is correct rather than a bug: `links.next` is the
|
|
80
|
+
// end-of-collection signal, and a page can be full without being the last.
|
|
81
|
+
// The whole segment describes the LOADED page, not the requested index —
|
|
82
|
+
// that is what the `page` the value carries is for (D4); same reasoning,
|
|
83
|
+
// same shape and the same bottom-line home as the ticket list, which must
|
|
84
|
+
// not drift from this one. It is the same `loadedPage` the ARROW keys act
|
|
85
|
+
// on, so the number the operator reads and the number the keys move from
|
|
86
|
+
// are one value (R1).
|
|
87
|
+
//
|
|
88
|
+
// Built by `ListScreen` since GAIA-294, which is what makes "must not drift
|
|
89
|
+
// from the ticket list" a mechanism rather than a promise.
|
|
90
|
+
const paging = this.pagingSegments();
|
|
91
|
+
// The rows ON SCREEN and the size of the FILTERED COLLECTION, side by side:
|
|
92
|
+
// `7 of 1340 run(s)` when the server reported the total, `7 of ? run(s)`
|
|
93
|
+
// when it reported none (GAIA-292 D6c/D6d). Neither quantity replaces the
|
|
94
|
+
// other, so the row count never jumps; the total is read off the value the
|
|
95
|
+
// rows came with (D4/D4a) and from no second screen member — which is now
|
|
96
|
+
// structural rather than careful, because the server answered both from one
|
|
97
|
+
// query. Empty while the page loads (D6c), which is why the segments are
|
|
98
|
+
// joined rather than concatenated — an omitted count must not leave the line
|
|
99
|
+
// opening with a separator.
|
|
100
|
+
const count = this.countLabel('run');
|
|
101
|
+
return presentation(this.render(context), {
|
|
102
|
+
selectable: rows.length,
|
|
103
|
+
selectedIndex: this.selectedIndex,
|
|
104
|
+
selectedRow: rows.length ? 1 + this.selectedIndex : -1,
|
|
105
|
+
title: `${context.project?.name ?? context.project?.slug ?? ''} · runs`,
|
|
106
|
+
meta: [
|
|
107
|
+
count,
|
|
108
|
+
closedSegment,
|
|
109
|
+
'Enter ticket · Tab tabs · x closed · Esc back',
|
|
110
|
+
]
|
|
111
|
+
.filter(Boolean)
|
|
112
|
+
.join(' · '),
|
|
113
|
+
status: ` ${paging.join(' · ')} · ↑↓ select · Enter ticket · Tab tabs · x closed · Esc back · q quit `,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
activate(index, context) {
|
|
117
|
+
const id = this.readyValue
|
|
118
|
+
? selectedRunTicketId(this.readyValue.rows, index)
|
|
119
|
+
: null;
|
|
120
|
+
if (!id || !context.navigate)
|
|
121
|
+
return false;
|
|
122
|
+
context.navigate('ticket.detail', { ticketId: id, tab: 'runs' });
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
select(index, context) {
|
|
126
|
+
if (index < 0 || index >= this.rows.length)
|
|
127
|
+
return false;
|
|
128
|
+
this.selectedIndex = index;
|
|
129
|
+
context.requestDraw();
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
handleInput(input, context) {
|
|
133
|
+
if (super.handleInput(input, context))
|
|
134
|
+
return true;
|
|
135
|
+
if (input === 'x') {
|
|
136
|
+
this.criteria = {
|
|
137
|
+
...this.criteria,
|
|
138
|
+
closed: cycleValue(CLOSED_FILTER_VALUES, this.criteria.closed),
|
|
139
|
+
};
|
|
140
|
+
// A criteria change starts over at page one, or narrowing on page 4 lands
|
|
141
|
+
// on an empty page with no rows and no explanation (D5).
|
|
142
|
+
this.page = 0;
|
|
143
|
+
this.selectedIndex = 0;
|
|
144
|
+
this.retry(context);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
// The ARROW keys since the operator's live review (D6 chose `]` / `[`); the
|
|
148
|
+
// kernel handles neither sequence itself, so the screen claims them. This
|
|
149
|
+
// screen has no search field, so there is nothing they could shadow.
|
|
150
|
+
// Both move from the page ON SCREEN, never from the requested index (R1).
|
|
151
|
+
if (this.handlePagingInput(input, context))
|
|
152
|
+
return true;
|
|
153
|
+
const rows = this.readyValue?.rows;
|
|
154
|
+
if (!rows)
|
|
155
|
+
return false;
|
|
156
|
+
if (this.handleCursorInput(input, context))
|
|
157
|
+
return true;
|
|
158
|
+
if (input === '\r' || input === '\n')
|
|
159
|
+
return this.activate(this.selectedIndex, context);
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
reset() {
|
|
163
|
+
// The page, the cursor and the counter — a reset means a new project or
|
|
164
|
+
// session, hence a different collection (GAIA-294).
|
|
165
|
+
this.resetList();
|
|
166
|
+
this.criteria = { ...DEFAULT_RUN_CRITERIA };
|
|
167
|
+
super.reset();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { AsyncScreen } from '../../../Screen/async-screen.js';
|
|
2
|
+
import { type ScreenAction, type ScreenContext, type ScreenPresentation } from '../../../Screen/screen.js';
|
|
3
|
+
import { type StatisticsResult } from '../Data/statistics.js';
|
|
4
|
+
export declare class DashboardScreen extends AsyncScreen<StatisticsResult> {
|
|
5
|
+
readonly frameId = "dashboard";
|
|
6
|
+
topTab(context: ScreenContext): {
|
|
7
|
+
readonly level: "project";
|
|
8
|
+
readonly tab: "dashboard";
|
|
9
|
+
} | {
|
|
10
|
+
readonly level: "root";
|
|
11
|
+
readonly tab: "dashboard";
|
|
12
|
+
};
|
|
13
|
+
reload(context: ScreenContext): void;
|
|
14
|
+
static readonly route: {
|
|
15
|
+
id: string;
|
|
16
|
+
path: string;
|
|
17
|
+
fallback: {
|
|
18
|
+
routeId: string;
|
|
19
|
+
};
|
|
20
|
+
tab: {
|
|
21
|
+
id: string;
|
|
22
|
+
label: string;
|
|
23
|
+
scope: string;
|
|
24
|
+
scopes: string[];
|
|
25
|
+
order: number;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
readonly id: string;
|
|
29
|
+
private status;
|
|
30
|
+
actionForInput(input: string, context: ScreenContext): ScreenAction | undefined;
|
|
31
|
+
protected load(context: ScreenContext): Promise<StatisticsResult>;
|
|
32
|
+
protected renderLoading(): string[];
|
|
33
|
+
protected renderReady(result: StatisticsResult, context: ScreenContext): string[];
|
|
34
|
+
renderNonInteractive(context: ScreenContext): string[];
|
|
35
|
+
nonInteractive(context: ScreenContext): {
|
|
36
|
+
stream?: "stderr";
|
|
37
|
+
exitCode?: number;
|
|
38
|
+
lines: string[];
|
|
39
|
+
};
|
|
40
|
+
present(context: ScreenContext): ScreenPresentation;
|
|
41
|
+
reset(): void;
|
|
42
|
+
}
|