@gaia-ai/addon-gaia-ui 0.10.0 → 0.11.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/Component/top-tabs/index.d.ts +49 -5
- package/dist/src/Component/top-tabs/index.js +62 -6
- package/dist/src/Entity/conductor/Data/conductors.d.ts +4 -3
- package/dist/src/Entity/conductor/Data/conductors.js +76 -10
- package/dist/src/Entity/conductor/Data/own-conductor.js +7 -1
- package/dist/src/Entity/statistics/Data/statistics.d.ts +83 -6
- package/dist/src/Entity/statistics/Data/statistics.js +85 -6
- package/dist/src/Entity/statistics/Screen/dashboard.js +17 -3
- package/dist/src/Entity/team/Data/members.d.ts +30 -0
- package/dist/src/Entity/team/Data/members.js +83 -0
- package/dist/src/Entity/team/Data/team.d.ts +22 -0
- package/dist/src/Entity/team/Data/team.js +38 -0
- package/dist/src/Entity/team/Data/teams.d.ts +223 -0
- package/dist/src/Entity/team/Data/teams.js +267 -0
- package/dist/src/Entity/team/Screen/team-users.d.ts +76 -0
- package/dist/src/Entity/team/Screen/team-users.js +215 -0
- package/dist/src/Entity/team/Screen/teams-screen.d.ts +116 -0
- package/dist/src/Entity/team/Screen/teams-screen.js +349 -0
- package/dist/src/Entity/ticket/Component/edit-form/index.d.ts +8 -3
- package/dist/src/Entity/ticket/Component/edit-form/index.js +4 -6
- package/dist/src/Entity/ticket/Data/detail.d.ts +7 -5
- package/dist/src/Entity/ticket/Data/detail.js +7 -5
- package/dist/src/Entity/ticket/Screen/edit-assignment-form.js +0 -1
- package/dist/src/Entity/transport.d.ts +14 -4
- package/dist/src/Entity/transport.js +2 -1
- package/dist/src/Entity/user/Data/users.d.ts +38 -0
- package/dist/src/Entity/user/Data/users.js +14 -0
- package/dist/src/Kernel/tui-kernel.js +0 -1
- package/dist/src/Routing/registry.js +19 -2
- package/dist/src/Routing/routes.js +13 -2
- package/dist/src/launcher.d.ts +18 -26
- package/dist/src/launcher.js +9 -11
- package/dist/src/types.d.ts +15 -12
- package/package.json +2 -2
|
@@ -14,8 +14,8 @@ export declare const ROOT_TABS: readonly [{
|
|
|
14
14
|
readonly id: "conductors";
|
|
15
15
|
readonly label: "Conductors";
|
|
16
16
|
}, {
|
|
17
|
-
readonly id: "
|
|
18
|
-
readonly label: "
|
|
17
|
+
readonly id: "teams";
|
|
18
|
+
readonly label: "Teams";
|
|
19
19
|
}];
|
|
20
20
|
/** The project level: the tabs of an opened project. */
|
|
21
21
|
export declare const PROJECT_TABS: readonly [{
|
|
@@ -65,11 +65,36 @@ export declare const TICKET_TABS: readonly [{
|
|
|
65
65
|
readonly id: "runs";
|
|
66
66
|
readonly label: "Runs";
|
|
67
67
|
}];
|
|
68
|
+
/**
|
|
69
|
+
* The team level: Overview · Users for one crew — GAIA-391 AC4, by operator
|
|
70
|
+
* ruling.
|
|
71
|
+
*
|
|
72
|
+
* The level the `users` root tab BECAME. It sits between root and user, so the
|
|
73
|
+
* navigation reads root → team → user: which crews are there, who is in this
|
|
74
|
+
* one, what has that person done. Destinations, not modes — each tab is its own
|
|
75
|
+
* screen, so Tab runs a real `Router.navigate` and `teamId` is preserved
|
|
76
|
+
* (`Routing/registry.ts` keeps the params for this scope for the same reason it
|
|
77
|
+
* keeps them for `project` and `user`).
|
|
78
|
+
*
|
|
79
|
+
* `overview` is also declared on the user level, which GAIA-256 D2 already made
|
|
80
|
+
* legal: ids are not globally unique, every function here is level-parameterised,
|
|
81
|
+
* and one bar is on screen at a time.
|
|
82
|
+
*/
|
|
83
|
+
export declare const TEAM_TABS: readonly [{
|
|
84
|
+
readonly id: "overview";
|
|
85
|
+
readonly label: "Overview";
|
|
86
|
+
}, {
|
|
87
|
+
readonly id: "users";
|
|
88
|
+
readonly label: "Users";
|
|
89
|
+
}];
|
|
68
90
|
/**
|
|
69
91
|
* The user level: Overview · Tickets · Runs for one person (GAIA-377 D1).
|
|
70
92
|
*
|
|
71
93
|
* Destinations, not modes — each tab is its own screen in the slice that owns
|
|
72
94
|
* its query (D2), so Tab runs a real `Router.navigate` and preserves `userId`.
|
|
95
|
+
*
|
|
96
|
+
* UNCHANGED by GAIA-391: the ruling moved this level's PARENT (it hangs off
|
|
97
|
+
* {@link TEAM_TABS} now instead of off a root `users` tab), not its contents.
|
|
73
98
|
*/
|
|
74
99
|
export declare const USER_TABS: readonly [{
|
|
75
100
|
readonly id: "overview";
|
|
@@ -81,7 +106,13 @@ export declare const USER_TABS: readonly [{
|
|
|
81
106
|
readonly id: "runs";
|
|
82
107
|
readonly label: "Runs";
|
|
83
108
|
}];
|
|
84
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* The tab bars, keyed by level — the one place a level is named.
|
|
111
|
+
*
|
|
112
|
+
* Declared in DEPTH order (root → project → ticket, then team → user), which is
|
|
113
|
+
* documentation rather than mechanism: nothing reads this object in key order
|
|
114
|
+
* since GAIA-256 D2 deleted `locateTab`, the one function that did.
|
|
115
|
+
*/
|
|
85
116
|
export declare const TAB_LEVELS: {
|
|
86
117
|
readonly root: readonly [{
|
|
87
118
|
readonly id: "dashboard";
|
|
@@ -93,8 +124,8 @@ export declare const TAB_LEVELS: {
|
|
|
93
124
|
readonly id: "conductors";
|
|
94
125
|
readonly label: "Conductors";
|
|
95
126
|
}, {
|
|
96
|
-
readonly id: "
|
|
97
|
-
readonly label: "
|
|
127
|
+
readonly id: "teams";
|
|
128
|
+
readonly label: "Teams";
|
|
98
129
|
}];
|
|
99
130
|
readonly project: readonly [{
|
|
100
131
|
readonly id: "dashboard";
|
|
@@ -122,6 +153,13 @@ export declare const TAB_LEVELS: {
|
|
|
122
153
|
readonly id: "runs";
|
|
123
154
|
readonly label: "Runs";
|
|
124
155
|
}];
|
|
156
|
+
readonly team: readonly [{
|
|
157
|
+
readonly id: "overview";
|
|
158
|
+
readonly label: "Overview";
|
|
159
|
+
}, {
|
|
160
|
+
readonly id: "users";
|
|
161
|
+
readonly label: "Users";
|
|
162
|
+
}];
|
|
125
163
|
readonly user: readonly [{
|
|
126
164
|
readonly id: "overview";
|
|
127
165
|
readonly label: "Overview";
|
|
@@ -174,6 +212,12 @@ export type TabLocation = {
|
|
|
174
212
|
level: 'ticket';
|
|
175
213
|
tab: string;
|
|
176
214
|
tabs?: readonly TopTab[];
|
|
215
|
+
} | {
|
|
216
|
+
level: 'team';
|
|
217
|
+
tab: TabIdOf<'team'>;
|
|
218
|
+
} | {
|
|
219
|
+
level: 'user';
|
|
220
|
+
tab: TabIdOf<'user'>;
|
|
177
221
|
};
|
|
178
222
|
/** The entries a location draws: its own, else its level's registry. */
|
|
179
223
|
export declare const tabsOfLocation: (location: TabLocation) => readonly TopTab[];
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
// GAIA-239: THE registry — the single place the set, the order and the labels of
|
|
2
|
-
// the top-level tabs are written. Navigation has
|
|
2
|
+
// the top-level tabs are written. Navigation has FIVE levels, and every screen
|
|
3
3
|
// shows exactly ONE tab bar: the one of its own level.
|
|
4
4
|
//
|
|
5
|
-
// root dashboard, projects, project-picker → Dashboard · Projects
|
|
5
|
+
// root dashboard, projects, project-picker → Dashboard · Projects ·
|
|
6
|
+
// Conductors · Teams
|
|
6
7
|
// project dashboard, project, ticket-agents, → Dashboard · Tickets ·
|
|
7
8
|
// project-runs Ticket-Agents · Runs
|
|
8
9
|
// ticket overview, comments, related, runs → the sections of one open
|
|
9
10
|
// ticket, in the bar row every
|
|
10
11
|
// other level uses (GAIA-346)
|
|
12
|
+
// team one crew → Overview · Users (GAIA-391)
|
|
13
|
+
// user one person → Overview · Tickets · Runs
|
|
14
|
+
// (GAIA-377)
|
|
11
15
|
//
|
|
12
16
|
// `tickets` is the project/ticket drill-down; `ticket-agents` is the managed
|
|
13
17
|
// create-agent list (GAIA-190); `runs` is the project-scoped run overview
|
|
@@ -31,7 +35,10 @@
|
|
|
31
35
|
//
|
|
32
36
|
// Only tabs that are actually navigable are listed. GAIA-377 added `Users` with
|
|
33
37
|
// its screens; a tab drawn before its screen exists would advertise navigation
|
|
34
|
-
// the operator cannot take, which is the very defect GAIA-239 removes.
|
|
38
|
+
// the operator cannot take, which is the very defect GAIA-239 removes. The same
|
|
39
|
+
// rule runs the other way, and GAIA-391 is where it did: a LEVEL is declared
|
|
40
|
+
// with the screens that occupy it, so `team` arrives with the crew overview and
|
|
41
|
+
// the crew's Users list, not before them.
|
|
35
42
|
//
|
|
36
43
|
// GAIA-232 added `conductors`, and put it on the ROOT level although the note
|
|
37
44
|
// above once reserved it for the project level. A conductor is an operator
|
|
@@ -52,8 +59,28 @@ export const ROOT_TABS = [
|
|
|
52
59
|
{ id: 'dashboard', label: 'Dashboard' },
|
|
53
60
|
{ id: 'projects', label: 'Projects' },
|
|
54
61
|
{ id: 'conductors', label: 'Conductors' },
|
|
55
|
-
// GAIA-
|
|
56
|
-
|
|
62
|
+
// GAIA-391 AC4 — `teams` is the LAST root tab, and there is deliberately no
|
|
63
|
+
// `users` beside it any more.
|
|
64
|
+
//
|
|
65
|
+
// ROOT level for teams, by GAIA-232's argument for Conductors: a team is the
|
|
66
|
+
// level that cuts ACROSS customer projects (GAIA-321), so "which crews are
|
|
67
|
+
// there" has to be answerable without first choosing a project.
|
|
68
|
+
//
|
|
69
|
+
// THIS REVISES AN INFORMATION-ARCHITECTURE DECISION THAT BELONGS TO GAIA-377,
|
|
70
|
+
// NOT TO GAIA-391. GAIA-377 D1 shipped `users` as a root tab and declared
|
|
71
|
+
// {@link USER_TABS} as the level below it; GAIA-391 moved that whole level
|
|
72
|
+
// under the team level by OPERATOR RULING, so the root bar reads
|
|
73
|
+
// `Dashboard · Projects · Conductors · Teams` and a person is reached through
|
|
74
|
+
// the crew they are in: Teams → one crew → Users → one person. Recorded here
|
|
75
|
+
// because a silent removal would later read as a regression against 377, and
|
|
76
|
+
// because the previous comment on this list argued the OPPOSITE order
|
|
77
|
+
// ("people, then the crews they are in") — an argument this ruling overturns
|
|
78
|
+
// rather than an accident.
|
|
79
|
+
//
|
|
80
|
+
// Users did NOT become unreachable, which is what makes the move a move: the
|
|
81
|
+
// team level carries the tab ({@link TEAM_TABS}), and the Dashboard's PER USER
|
|
82
|
+
// rows still open one person directly.
|
|
83
|
+
{ id: 'teams', label: 'Teams' },
|
|
57
84
|
];
|
|
58
85
|
/** The project level: the tabs of an opened project. */
|
|
59
86
|
export const PROJECT_TABS = [
|
|
@@ -89,22 +116,51 @@ export const TICKET_TABS = [
|
|
|
89
116
|
{ id: 'related', label: 'Related' },
|
|
90
117
|
{ id: 'runs', label: 'Runs' },
|
|
91
118
|
];
|
|
119
|
+
/**
|
|
120
|
+
* The team level: Overview · Users for one crew — GAIA-391 AC4, by operator
|
|
121
|
+
* ruling.
|
|
122
|
+
*
|
|
123
|
+
* The level the `users` root tab BECAME. It sits between root and user, so the
|
|
124
|
+
* navigation reads root → team → user: which crews are there, who is in this
|
|
125
|
+
* one, what has that person done. Destinations, not modes — each tab is its own
|
|
126
|
+
* screen, so Tab runs a real `Router.navigate` and `teamId` is preserved
|
|
127
|
+
* (`Routing/registry.ts` keeps the params for this scope for the same reason it
|
|
128
|
+
* keeps them for `project` and `user`).
|
|
129
|
+
*
|
|
130
|
+
* `overview` is also declared on the user level, which GAIA-256 D2 already made
|
|
131
|
+
* legal: ids are not globally unique, every function here is level-parameterised,
|
|
132
|
+
* and one bar is on screen at a time.
|
|
133
|
+
*/
|
|
134
|
+
export const TEAM_TABS = [
|
|
135
|
+
{ id: 'overview', label: 'Overview' },
|
|
136
|
+
{ id: 'users', label: 'Users' },
|
|
137
|
+
];
|
|
92
138
|
/**
|
|
93
139
|
* The user level: Overview · Tickets · Runs for one person (GAIA-377 D1).
|
|
94
140
|
*
|
|
95
141
|
* Destinations, not modes — each tab is its own screen in the slice that owns
|
|
96
142
|
* its query (D2), so Tab runs a real `Router.navigate` and preserves `userId`.
|
|
143
|
+
*
|
|
144
|
+
* UNCHANGED by GAIA-391: the ruling moved this level's PARENT (it hangs off
|
|
145
|
+
* {@link TEAM_TABS} now instead of off a root `users` tab), not its contents.
|
|
97
146
|
*/
|
|
98
147
|
export const USER_TABS = [
|
|
99
148
|
{ id: 'overview', label: 'Overview' },
|
|
100
149
|
{ id: 'tickets', label: 'Tickets' },
|
|
101
150
|
{ id: 'runs', label: 'Runs' },
|
|
102
151
|
];
|
|
103
|
-
/**
|
|
152
|
+
/**
|
|
153
|
+
* The tab bars, keyed by level — the one place a level is named.
|
|
154
|
+
*
|
|
155
|
+
* Declared in DEPTH order (root → project → ticket, then team → user), which is
|
|
156
|
+
* documentation rather than mechanism: nothing reads this object in key order
|
|
157
|
+
* since GAIA-256 D2 deleted `locateTab`, the one function that did.
|
|
158
|
+
*/
|
|
104
159
|
export const TAB_LEVELS = {
|
|
105
160
|
root: ROOT_TABS,
|
|
106
161
|
project: PROJECT_TABS,
|
|
107
162
|
ticket: TICKET_TABS,
|
|
163
|
+
team: TEAM_TABS,
|
|
108
164
|
user: USER_TABS,
|
|
109
165
|
};
|
|
110
166
|
/** The entries a location draws: its own, else its level's registry. */
|
|
@@ -223,9 +223,10 @@ export interface ConductorRowContext {
|
|
|
223
223
|
/**
|
|
224
224
|
* The conductor collection as cockpit rows, in server order.
|
|
225
225
|
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
226
|
+
* Sideloads `machine` so capacity (GAIA-353 D5/D10) comes from the host budget,
|
|
227
|
+
* not removed conductor fields. Swallows a read failure into an empty list on
|
|
228
|
+
* purpose: the Conductors screen re-reads every 5 s, so one failed poll must
|
|
229
|
+
* leave the screen navigable rather than replace it with an error.
|
|
229
230
|
*/
|
|
230
231
|
export declare const listConductorRows: (client: DropshClient, ctx: ConductorRowContext) => Promise<ConductorRow[]>;
|
|
231
232
|
/**
|
|
@@ -4,14 +4,19 @@
|
|
|
4
4
|
// service that applied them would be unusable for any other purpose. The two read
|
|
5
5
|
// strategies are both just reading, so both live here.
|
|
6
6
|
import { toUnixSeconds } from '@gaia-ai/core';
|
|
7
|
+
import { DrupalJsonApiParams } from 'drupal-jsonapi-params';
|
|
7
8
|
import { humanizeAge } from '../../../lib/format.js';
|
|
8
9
|
import { listOutcome, listPreferringWrite, } from '../../transport.js';
|
|
9
10
|
import { deriveConductorStatus } from './liveness.js';
|
|
10
11
|
const str = (v) => typeof v === 'string' && v.length > 0 ? v : undefined;
|
|
11
12
|
const toConductor = (r) => ({
|
|
12
13
|
id: r.id,
|
|
13
|
-
label: str(r.attr('label')) ??
|
|
14
|
-
|
|
14
|
+
label: str(r.attr('label')) ??
|
|
15
|
+
str(r.attr('conductor_id')) ??
|
|
16
|
+
str(r.attr('machine_id')) ??
|
|
17
|
+
r.id,
|
|
18
|
+
// Registration key (GAIA-353 renamed machine_id → conductor_id).
|
|
19
|
+
machineId: str(r.attr('conductor_id')) ?? str(r.attr('machine_id')),
|
|
15
20
|
status: str(r.attr('status')),
|
|
16
21
|
projectId: r.rel('project_id'),
|
|
17
22
|
});
|
|
@@ -242,19 +247,68 @@ export const CONDUCTOR_TABLE = {
|
|
|
242
247
|
],
|
|
243
248
|
};
|
|
244
249
|
const num = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
245
|
-
|
|
250
|
+
/** Id of a single-valued relationship linkage (`data` object), or null. */
|
|
251
|
+
function singleRelId(rel) {
|
|
252
|
+
const data = rel?.data;
|
|
253
|
+
if (!data || Array.isArray(data))
|
|
254
|
+
return null;
|
|
255
|
+
return data.id ?? null;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Dropsh's collection path hands Resource accessors; the cockpit row path uses
|
|
259
|
+
* client.get so it can sideload `machine`. Map the raw JSON:API resource into
|
|
260
|
+
* the same attr/rel surface toConductor already speaks.
|
|
261
|
+
*/
|
|
262
|
+
function asResource(raw) {
|
|
263
|
+
return {
|
|
264
|
+
id: raw.id ?? '',
|
|
265
|
+
type: raw.type ?? '',
|
|
266
|
+
attr: (key) => raw.attributes?.[key],
|
|
267
|
+
rel: (key) => singleRelId(raw.relationships?.[key]),
|
|
268
|
+
rels: (key) => {
|
|
269
|
+
const data = raw.relationships?.[key]?.data;
|
|
270
|
+
if (!data)
|
|
271
|
+
return [];
|
|
272
|
+
if (Array.isArray(data)) {
|
|
273
|
+
return data
|
|
274
|
+
.map((d) => d.id)
|
|
275
|
+
.filter((id) => typeof id === 'string');
|
|
276
|
+
}
|
|
277
|
+
return data.id ? [data.id] : [];
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
/** Host capacity from the sideloaded machine, with cutover fallbacks (GAIA-353). */
|
|
282
|
+
function capacityOf(raw, machinesById) {
|
|
283
|
+
const machineId = singleRelId(raw.relationships?.machine);
|
|
284
|
+
const machine = machineId ? machinesById.get(machineId) : undefined;
|
|
285
|
+
const attrs = raw.attributes ?? {};
|
|
286
|
+
return {
|
|
287
|
+
// Prefer machine.current_load; process telemetry / legacy conductor attrs
|
|
288
|
+
// only when the host row is missing during cutover.
|
|
289
|
+
currentLoad: num(machine?.attributes?.current_load) ??
|
|
290
|
+
num(attrs.reported_load) ??
|
|
291
|
+
num(attrs.current_load) ??
|
|
292
|
+
0,
|
|
293
|
+
// max_parallel is machine-owned SoT; legacy conductor attr is last resort.
|
|
294
|
+
maxParallel: num(machine?.attributes?.max_parallel) ?? num(attrs.max_parallel) ?? 0,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const toConductorRow = (raw, ctx, machinesById) => {
|
|
298
|
+
const r = asResource(raw);
|
|
246
299
|
const workspaceRoot = str(r.attr('workspace_root')) ?? '';
|
|
247
300
|
// ISO-8601 over the wire, not the unix integer the field name implies — a
|
|
248
301
|
// `typeof === 'number'` guard here is what made a live conductor read `stale`.
|
|
249
302
|
const lastSeen = toUnixSeconds(r.attr('last_seen')) ?? 0;
|
|
250
303
|
const leaseExpiresAt = toUnixSeconds(r.attr('lease_expires_at')) ?? 0;
|
|
304
|
+
const { currentLoad, maxParallel } = capacityOf(raw, machinesById);
|
|
251
305
|
return {
|
|
252
306
|
...toConductor(r),
|
|
253
307
|
workspaceRoot,
|
|
254
308
|
lastSeen,
|
|
255
309
|
leaseExpiresAt,
|
|
256
|
-
currentLoad
|
|
257
|
-
maxParallel
|
|
310
|
+
currentLoad,
|
|
311
|
+
maxParallel,
|
|
258
312
|
lastPid: num(r.attr('last_pid')),
|
|
259
313
|
conductorName: str(r.attr('conductor_name')),
|
|
260
314
|
// `present: true` because the row IS a control-plane record — which is why
|
|
@@ -273,13 +327,25 @@ const toConductorRow = (r, ctx) => {
|
|
|
273
327
|
/**
|
|
274
328
|
* The conductor collection as cockpit rows, in server order.
|
|
275
329
|
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
330
|
+
* Sideloads `machine` so capacity (GAIA-353 D5/D10) comes from the host budget,
|
|
331
|
+
* not removed conductor fields. Swallows a read failure into an empty list on
|
|
332
|
+
* purpose: the Conductors screen re-reads every 5 s, so one failed poll must
|
|
333
|
+
* leave the screen navigable rather than replace it with an error.
|
|
279
334
|
*/
|
|
280
335
|
export const listConductorRows = async (client, ctx) => {
|
|
281
|
-
const
|
|
282
|
-
|
|
336
|
+
const params = new DrupalJsonApiParams().addInclude(['machine']);
|
|
337
|
+
try {
|
|
338
|
+
const doc = (await client.get('gaia_conductor/gaia_conductor', params));
|
|
339
|
+
const machinesById = new Map();
|
|
340
|
+
for (const inc of doc.included ?? []) {
|
|
341
|
+
if (inc.id)
|
|
342
|
+
machinesById.set(inc.id, inc);
|
|
343
|
+
}
|
|
344
|
+
return (doc.data ?? []).map((raw) => toConductorRow(raw, ctx, machinesById));
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
283
349
|
};
|
|
284
350
|
/**
|
|
285
351
|
* Project uuid → name, for the Conductors screen's project column.
|
|
@@ -49,7 +49,13 @@ export function selectOwnConductor(doc, gaiaDir) {
|
|
|
49
49
|
? str((doc.included ?? []).find((inc) => inc.id === projectId)?.attributes
|
|
50
50
|
?.name)
|
|
51
51
|
: undefined;
|
|
52
|
-
|
|
52
|
+
// GAIA-353 renamed the registration key machine_id → conductor_id; accept
|
|
53
|
+
// the legacy attribute during cutover so a pre-upgrade control plane still
|
|
54
|
+
// preselects this checkout's conductor.
|
|
55
|
+
return {
|
|
56
|
+
machineId: str(own.attributes?.conductor_id) ?? str(own.attributes?.machine_id),
|
|
57
|
+
project,
|
|
58
|
+
};
|
|
53
59
|
}
|
|
54
60
|
/**
|
|
55
61
|
* Read the conductor collection and resolve this checkout's own record.
|
|
@@ -51,6 +51,24 @@ export interface StatisticsBlock {
|
|
|
51
51
|
total: StatisticsWindows;
|
|
52
52
|
byProject?: StatisticsRow[];
|
|
53
53
|
byUser?: StatisticsRow[];
|
|
54
|
+
/**
|
|
55
|
+
* GAIA-391 (AC4): the crew axis the server has served since GAIA-321.
|
|
56
|
+
*
|
|
57
|
+
* A team row is expanded from the per-USER counts onto every active team the
|
|
58
|
+
* assignee belongs to, so a dual-member's ticket is counted once per team and
|
|
59
|
+
* `sum(byTeam.*.window)` MAY EXCEED `total.window`. That is the server's own
|
|
60
|
+
* documented intent, not drift — which is why the summing invariant `byUser`
|
|
61
|
+
* carries (spec D8) is deliberately not claimed for this axis anywhere.
|
|
62
|
+
*
|
|
63
|
+
* Absent, never empty, when the read was team-scoped: the server omits it, the
|
|
64
|
+
* same way it omits `byProject` under `?project_id=` (spec D5).
|
|
65
|
+
*
|
|
66
|
+
* GAIA-319 D7 — a crew whose workspace the CALLER does not belong to is
|
|
67
|
+
* DROPPED, not folded, so this axis never carries an `id: null` row and the
|
|
68
|
+
* rows present may be fewer than the instance's crews. That is a disclosure
|
|
69
|
+
* boundary rather than a count: the axis reports what the caller may see.
|
|
70
|
+
*/
|
|
71
|
+
byTeam?: StatisticsRow[];
|
|
54
72
|
byDay?: StatisticsDayRow[];
|
|
55
73
|
}
|
|
56
74
|
/** The whole document, as the Dashboard consumes it. */
|
|
@@ -80,6 +98,41 @@ export type StatisticsResult = {
|
|
|
80
98
|
};
|
|
81
99
|
/** How an `id: null` breakdown row is named on screen (spec D8/D11). */
|
|
82
100
|
export declare const UNATTRIBUTED_LABEL = "\u2014 unattributed";
|
|
101
|
+
/**
|
|
102
|
+
* `byTeam` with the part of the total no crew accounts for, as a real row.
|
|
103
|
+
*
|
|
104
|
+
* GAIA-391 round 4. The dashboard drew three breakdown axes identically while
|
|
105
|
+
* only two of them added up, and only `byUser` had a NAME for what it could not
|
|
106
|
+
* attribute — so a crew axis reporting 1 against a total of 2 was, at the
|
|
107
|
+
* surface, indistinguishable from a broken expansion. Measured on this branch's
|
|
108
|
+
* control plane as uid 4: `runs.total` 2, `byProject` 2, `byUser` 1 +
|
|
109
|
+
* unattributed 1, `byTeam` 1, because the second run's ticket carried no
|
|
110
|
+
* assignee. An operator read it as a defect within minutes.
|
|
111
|
+
*
|
|
112
|
+
* THE SERVER IS NOT THE THING BEING CHANGED. `by_team` still carries no
|
|
113
|
+
* `id: null` row and {@link rowsOf} still does not invent one — that is
|
|
114
|
+
* GAIA-321 D6, and the parse is asserted to leave it alone. This is the
|
|
115
|
+
* RENDERING's own row, derived here so both the interactive and the piped
|
|
116
|
+
* surface get it from one place, and labelled with the very same
|
|
117
|
+
* {@link UNATTRIBUTED_LABEL} `byUser` uses: the problem was three axes drawn
|
|
118
|
+
* alike, so a fourth visual language would not be a fix.
|
|
119
|
+
*
|
|
120
|
+
* The shortfall is computed PER WINDOW, because the axis can be exact in one
|
|
121
|
+
* window and short in another, and it is clamped at zero for the one documented
|
|
122
|
+
* case where the axis legitimately OVER-reports: a dual member's row is counted
|
|
123
|
+
* once per crew (GAIA-321 D6), and a negative shortfall is not a shortfall.
|
|
124
|
+
*
|
|
125
|
+
* `undefined` in, `undefined` out. A team-scoped read omits the axis (spec D5)
|
|
126
|
+
* and deriving a row there would draw a `PER TEAM` heading whose single row is
|
|
127
|
+
* the whole total, on the one screen already scoped to a single crew. An axis
|
|
128
|
+
* that is present but EMPTY is a different thing and does get the row: the
|
|
129
|
+
* server computed it and nothing landed in it.
|
|
130
|
+
*
|
|
131
|
+
* @param block One entity type's figures, as {@link loadStatistics} parsed them.
|
|
132
|
+
*
|
|
133
|
+
* @returns The crew rows to draw, or `undefined` when there is no crew axis.
|
|
134
|
+
*/
|
|
135
|
+
export declare function teamRowsWithUnattributed(block: StatisticsBlock): StatisticsRow[] | undefined;
|
|
83
136
|
/**
|
|
84
137
|
* Closed tickets per run for each trailing window (GAIA-377 D4).
|
|
85
138
|
*
|
|
@@ -98,15 +151,39 @@ export declare const formatClosedPerRun: (value: number | null) => string;
|
|
|
98
151
|
* Read the control plane's ticket and run statistics — the ONE source of every
|
|
99
152
|
* figure the Dashboard renders (AC-8).
|
|
100
153
|
*
|
|
154
|
+
* NOT INSTANCE-WIDE since GAIA-319: the endpoint scopes every count to the
|
|
155
|
+
* requesting account's workspace memberships (`StatisticsCounter`,
|
|
156
|
+
* `TeamStatistics`, `StatisticsResource`), unless that account holds
|
|
157
|
+
* `administer gaia` and bypasses the scope. The document's SHAPE is unchanged —
|
|
158
|
+
* the same `total` plus `by_project` / `by_user` / `by_team` / `by_day`, with the
|
|
159
|
+
* same rows — so nothing in this parser moved, and that is exactly what the
|
|
160
|
+
* GAIA-319 cases in `tests/Entity/statistics/Data/statistics.test.ts` pin.
|
|
161
|
+
*
|
|
162
|
+
* Two of that scoping's effects are worth knowing HERE, because they look like
|
|
163
|
+
* client bugs and are not:
|
|
164
|
+
*
|
|
165
|
+
* - a person the caller may not name is FOLDED into the `by_user` row with
|
|
166
|
+
* `id: null` (D6), which this file renders as {@link UNATTRIBUTED_LABEL};
|
|
167
|
+
* - a crew outside the caller's workspaces is DROPPED from `by_team` (D7), so
|
|
168
|
+
* `sum(byTeam)` under-reports the instance rather than carrying a residual row.
|
|
169
|
+
*
|
|
170
|
+
* And one this client cannot fix: an account with no workspace memberships gets a
|
|
171
|
+
* document full of ZEROES, which the Dashboard reads as "no activity". Nothing in
|
|
172
|
+
* the payload distinguishes that from a quiet month, so the CLI does not claim to.
|
|
173
|
+
*
|
|
101
174
|
* With `projectId` the read is scoped and the server drops `by_project` from
|
|
102
175
|
* both blocks (spec D5). With `userId` (GAIA-377 D3) it drops `by_user` and
|
|
103
|
-
* keeps `by_project`.
|
|
104
|
-
*
|
|
176
|
+
* keeps `by_project`. With `teamId` (GAIA-391 AC4, over the `?team_id=` GAIA-321
|
|
177
|
+
* already serves) it drops `by_team` and narrows the counts to the team's ACTIVE
|
|
178
|
+
* members. All three compose with AND, as the server's own resource documents.
|
|
179
|
+
* The parameter names are `project_id` / `user_id` / `team_id` and NOT bare
|
|
180
|
+
* letters: JSON:API requires an implementation-specific query parameter to
|
|
105
181
|
* contain at least one non-`a-z` character, and Drupal enforces it.
|
|
106
182
|
*
|
|
107
183
|
* The client is the caller's, singular and used once: the TUI reads everything
|
|
108
|
-
* through `services.client
|
|
109
|
-
*
|
|
110
|
-
*
|
|
184
|
+
* through `services.client`. Reaching for a second client would render zero runs
|
|
185
|
+
* as a real measurement (spec R3) — the reason has changed with GAIA-391 (there
|
|
186
|
+
* is one identity now, not a read profile that can see runs and a write profile
|
|
187
|
+
* that cannot), but the rule has not: one figure, one source.
|
|
111
188
|
*/
|
|
112
|
-
export declare function loadStatistics(client: DropshClient, projectId?: string, userId?: string): Promise<StatisticsResult>;
|
|
189
|
+
export declare function loadStatistics(client: DropshClient, projectId?: string, userId?: string, teamId?: string): Promise<StatisticsResult>;
|
|
@@ -41,11 +41,13 @@ function blockOf(value) {
|
|
|
41
41
|
const b = (value ?? {});
|
|
42
42
|
const byProject = rowsOf(b.by_project);
|
|
43
43
|
const byUser = rowsOf(b.by_user);
|
|
44
|
+
const byTeam = rowsOf(b.by_team);
|
|
44
45
|
const byDay = dayRowsOf(b.by_day);
|
|
45
46
|
return {
|
|
46
47
|
total: windowsOf(b.total),
|
|
47
48
|
...(byProject ? { byProject } : {}),
|
|
48
49
|
...(byUser ? { byUser } : {}),
|
|
50
|
+
...(byTeam ? { byTeam } : {}),
|
|
49
51
|
...(byDay ? { byDay } : {}),
|
|
50
52
|
};
|
|
51
53
|
}
|
|
@@ -100,6 +102,56 @@ function jsonApiErrorDetail(caught) {
|
|
|
100
102
|
// fall back to a non-empty marker rather than to `undefined`.
|
|
101
103
|
return typeof detail === 'string' && detail !== '' ? detail : 'HTTP 404';
|
|
102
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* `byTeam` with the part of the total no crew accounts for, as a real row.
|
|
107
|
+
*
|
|
108
|
+
* GAIA-391 round 4. The dashboard drew three breakdown axes identically while
|
|
109
|
+
* only two of them added up, and only `byUser` had a NAME for what it could not
|
|
110
|
+
* attribute — so a crew axis reporting 1 against a total of 2 was, at the
|
|
111
|
+
* surface, indistinguishable from a broken expansion. Measured on this branch's
|
|
112
|
+
* control plane as uid 4: `runs.total` 2, `byProject` 2, `byUser` 1 +
|
|
113
|
+
* unattributed 1, `byTeam` 1, because the second run's ticket carried no
|
|
114
|
+
* assignee. An operator read it as a defect within minutes.
|
|
115
|
+
*
|
|
116
|
+
* THE SERVER IS NOT THE THING BEING CHANGED. `by_team` still carries no
|
|
117
|
+
* `id: null` row and {@link rowsOf} still does not invent one — that is
|
|
118
|
+
* GAIA-321 D6, and the parse is asserted to leave it alone. This is the
|
|
119
|
+
* RENDERING's own row, derived here so both the interactive and the piped
|
|
120
|
+
* surface get it from one place, and labelled with the very same
|
|
121
|
+
* {@link UNATTRIBUTED_LABEL} `byUser` uses: the problem was three axes drawn
|
|
122
|
+
* alike, so a fourth visual language would not be a fix.
|
|
123
|
+
*
|
|
124
|
+
* The shortfall is computed PER WINDOW, because the axis can be exact in one
|
|
125
|
+
* window and short in another, and it is clamped at zero for the one documented
|
|
126
|
+
* case where the axis legitimately OVER-reports: a dual member's row is counted
|
|
127
|
+
* once per crew (GAIA-321 D6), and a negative shortfall is not a shortfall.
|
|
128
|
+
*
|
|
129
|
+
* `undefined` in, `undefined` out. A team-scoped read omits the axis (spec D5)
|
|
130
|
+
* and deriving a row there would draw a `PER TEAM` heading whose single row is
|
|
131
|
+
* the whole total, on the one screen already scoped to a single crew. An axis
|
|
132
|
+
* that is present but EMPTY is a different thing and does get the row: the
|
|
133
|
+
* server computed it and nothing landed in it.
|
|
134
|
+
*
|
|
135
|
+
* @param block One entity type's figures, as {@link loadStatistics} parsed them.
|
|
136
|
+
*
|
|
137
|
+
* @returns The crew rows to draw, or `undefined` when there is no crew axis.
|
|
138
|
+
*/
|
|
139
|
+
export function teamRowsWithUnattributed(block) {
|
|
140
|
+
const rows = block.byTeam;
|
|
141
|
+
if (rows === undefined)
|
|
142
|
+
return undefined;
|
|
143
|
+
const shortfall = (window) => Math.max(0, block.total[window] - rows.reduce((sum, row) => sum + row[window], 0));
|
|
144
|
+
const residual = {
|
|
145
|
+
id: null,
|
|
146
|
+
label: UNATTRIBUTED_LABEL,
|
|
147
|
+
day: shortfall('day'),
|
|
148
|
+
week: shortfall('week'),
|
|
149
|
+
month: shortfall('month'),
|
|
150
|
+
};
|
|
151
|
+
if (residual.day === 0 && residual.week === 0 && residual.month === 0)
|
|
152
|
+
return rows;
|
|
153
|
+
return [...rows, residual];
|
|
154
|
+
}
|
|
103
155
|
/**
|
|
104
156
|
* Closed tickets per run for each trailing window (GAIA-377 D4).
|
|
105
157
|
*
|
|
@@ -121,18 +173,42 @@ export const formatClosedPerRun = (value) => value === null ? '—' : value.toFi
|
|
|
121
173
|
* Read the control plane's ticket and run statistics — the ONE source of every
|
|
122
174
|
* figure the Dashboard renders (AC-8).
|
|
123
175
|
*
|
|
176
|
+
* NOT INSTANCE-WIDE since GAIA-319: the endpoint scopes every count to the
|
|
177
|
+
* requesting account's workspace memberships (`StatisticsCounter`,
|
|
178
|
+
* `TeamStatistics`, `StatisticsResource`), unless that account holds
|
|
179
|
+
* `administer gaia` and bypasses the scope. The document's SHAPE is unchanged —
|
|
180
|
+
* the same `total` plus `by_project` / `by_user` / `by_team` / `by_day`, with the
|
|
181
|
+
* same rows — so nothing in this parser moved, and that is exactly what the
|
|
182
|
+
* GAIA-319 cases in `tests/Entity/statistics/Data/statistics.test.ts` pin.
|
|
183
|
+
*
|
|
184
|
+
* Two of that scoping's effects are worth knowing HERE, because they look like
|
|
185
|
+
* client bugs and are not:
|
|
186
|
+
*
|
|
187
|
+
* - a person the caller may not name is FOLDED into the `by_user` row with
|
|
188
|
+
* `id: null` (D6), which this file renders as {@link UNATTRIBUTED_LABEL};
|
|
189
|
+
* - a crew outside the caller's workspaces is DROPPED from `by_team` (D7), so
|
|
190
|
+
* `sum(byTeam)` under-reports the instance rather than carrying a residual row.
|
|
191
|
+
*
|
|
192
|
+
* And one this client cannot fix: an account with no workspace memberships gets a
|
|
193
|
+
* document full of ZEROES, which the Dashboard reads as "no activity". Nothing in
|
|
194
|
+
* the payload distinguishes that from a quiet month, so the CLI does not claim to.
|
|
195
|
+
*
|
|
124
196
|
* With `projectId` the read is scoped and the server drops `by_project` from
|
|
125
197
|
* both blocks (spec D5). With `userId` (GAIA-377 D3) it drops `by_user` and
|
|
126
|
-
* keeps `by_project`.
|
|
127
|
-
*
|
|
198
|
+
* keeps `by_project`. With `teamId` (GAIA-391 AC4, over the `?team_id=` GAIA-321
|
|
199
|
+
* already serves) it drops `by_team` and narrows the counts to the team's ACTIVE
|
|
200
|
+
* members. All three compose with AND, as the server's own resource documents.
|
|
201
|
+
* The parameter names are `project_id` / `user_id` / `team_id` and NOT bare
|
|
202
|
+
* letters: JSON:API requires an implementation-specific query parameter to
|
|
128
203
|
* contain at least one non-`a-z` character, and Drupal enforces it.
|
|
129
204
|
*
|
|
130
205
|
* The client is the caller's, singular and used once: the TUI reads everything
|
|
131
|
-
* through `services.client
|
|
132
|
-
*
|
|
133
|
-
*
|
|
206
|
+
* through `services.client`. Reaching for a second client would render zero runs
|
|
207
|
+
* as a real measurement (spec R3) — the reason has changed with GAIA-391 (there
|
|
208
|
+
* is one identity now, not a read profile that can see runs and a write profile
|
|
209
|
+
* that cannot), but the rule has not: one figure, one source.
|
|
134
210
|
*/
|
|
135
|
-
export async function loadStatistics(client, projectId, userId) {
|
|
211
|
+
export async function loadStatistics(client, projectId, userId, teamId) {
|
|
136
212
|
const params = new DrupalJsonApiParams();
|
|
137
213
|
if (projectId !== undefined) {
|
|
138
214
|
params.addCustomParam({ project_id: projectId });
|
|
@@ -140,6 +216,9 @@ export async function loadStatistics(client, projectId, userId) {
|
|
|
140
216
|
if (userId !== undefined) {
|
|
141
217
|
params.addCustomParam({ user_id: userId });
|
|
142
218
|
}
|
|
219
|
+
if (teamId !== undefined) {
|
|
220
|
+
params.addCustomParam({ team_id: teamId });
|
|
221
|
+
}
|
|
143
222
|
let doc;
|
|
144
223
|
try {
|
|
145
224
|
doc = (await client.get('gaia/statistics', params));
|
|
@@ -3,7 +3,7 @@ import { lineText, sectionRowNode, sectionRows, } from '../../../Component/secti
|
|
|
3
3
|
import { statsPanelRows, } from '../../../Component/stats-panel/index.js';
|
|
4
4
|
import { columnNode } from '../../../lib/vnode.js';
|
|
5
5
|
import { AsyncScreen } from '../../../Screen/async-screen.js';
|
|
6
|
-
import { closedPerRun, formatClosedPerRun, loadStatistics, } from '../Data/statistics.js';
|
|
6
|
+
import { closedPerRun, formatClosedPerRun, loadStatistics, teamRowsWithUnattributed, } from '../Data/statistics.js';
|
|
7
7
|
/** Rows that are plain text, keyed under one name: a failure or empty state. */
|
|
8
8
|
const plainRows = (key, lines) => lines.map((text, index) => ({ key: `${key}-${index}`, text, bold: false }));
|
|
9
9
|
/** The Tab hint is a TERMINAL affordance; a pipe has no tabs (spec D9). */
|
|
@@ -219,10 +219,11 @@ const totalRows = (block) => [
|
|
|
219
219
|
{ label: WINDOWS[2] ?? '', numbers: [block.total.month] },
|
|
220
220
|
];
|
|
221
221
|
/**
|
|
222
|
-
* The breakdown groups of one axis (`by_project` or `
|
|
222
|
+
* The breakdown groups of one axis (`by_project`, `by_user` or `by_team`), titled.
|
|
223
223
|
*
|
|
224
224
|
* The title and the rows come from the SAME literal pair, so a third entity type
|
|
225
|
-
* is added in one place and cannot be named one way and grouped another
|
|
225
|
+
* is added in one place and cannot be named one way and grouped another — which
|
|
226
|
+
* is what made GAIA-391's `by_team` axis a one-line addition to the list below.
|
|
226
227
|
*/
|
|
227
228
|
const groups = (statistics, pick) => [
|
|
228
229
|
['Tickets', pick(statistics.tickets)],
|
|
@@ -267,6 +268,19 @@ function statisticsSections(statistics) {
|
|
|
267
268
|
for (const [heading, pick] of [
|
|
268
269
|
['PER PROJECT', (block) => block.byProject],
|
|
269
270
|
['PER USER', (block) => block.byUser],
|
|
271
|
+
// GAIA-391 (AC4): the crew axis, drawn LAST because it is the narrowest
|
|
272
|
+
// reading of the same figures — and because `PER USER` owns the selection
|
|
273
|
+
// cursor below, which keys its row ids off its own header offset. A team row
|
|
274
|
+
// is deliberately not selectable: the team detail is reached from the Teams
|
|
275
|
+
// list, its own route, and a second entrance here would put one destination
|
|
276
|
+
// behind two cursors on one screen.
|
|
277
|
+
//
|
|
278
|
+
// Round 4 — picked through `teamRowsWithUnattributed` rather than straight
|
|
279
|
+
// off the block, so the axis that carries no sum invariant SAYS SO on
|
|
280
|
+
// screen, in the row `PER USER` already uses for the same thing. Three axes
|
|
281
|
+
// drawn identically while one silently under-reported is what an operator
|
|
282
|
+
// read as a defect; the derivation is documented where it lives.
|
|
283
|
+
['PER TEAM', (block) => teamRowsWithUnattributed(block)],
|
|
270
284
|
]) {
|
|
271
285
|
const rowGroups = groups(statistics, pick);
|
|
272
286
|
if (rowGroups.length === 0)
|