@gaia-ai/addon-gaia-ui 0.15.0 → 0.15.2
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/list-chrome/index.d.ts +6 -3
- package/dist/src/Component/list-chrome/index.js +10 -1
- package/dist/src/Entity/conductor/Data/conductors.d.ts +16 -8
- package/dist/src/Entity/conductor/Data/conductors.js +15 -8
- package/dist/src/Entity/conductor/Screen/conductors.d.ts +2 -2
- package/dist/src/Entity/conductor/Screen/project-conductors.d.ts +2 -2
- package/dist/src/Entity/project/Data/with-tickets.js +1 -0
- package/dist/src/Entity/project/Data/workload.js +3 -10
- package/dist/src/Entity/run/Component/run-detail/index.d.ts +2 -2
- package/dist/src/Entity/run/Component/run-detail/index.js +37 -35
- package/dist/src/Entity/run/Data/query.js +2 -0
- package/dist/src/Entity/run/Screen/run-detail.js +2 -2
- package/dist/src/Entity/team/Data/projects.js +1 -0
- package/dist/src/Entity/team/Data/teams.js +1 -0
- package/dist/src/Entity/ticket/Component/ticket-content/model.js +0 -3
- package/dist/src/Entity/ticket/Data/parents-all.d.ts +14 -0
- package/dist/src/Entity/ticket/Data/parents-all.js +44 -0
- package/dist/src/Entity/ticket/Data/parents.js +1 -0
- package/dist/src/Entity/ticket/Data/projects-search.d.ts +35 -0
- package/dist/src/Entity/ticket/Data/projects-search.js +61 -0
- package/dist/src/Entity/ticket/Data/query.d.ts +12 -0
- package/dist/src/Entity/ticket/Data/query.js +14 -2
- package/dist/src/Entity/ticket/Data/tickets.d.ts +12 -1
- package/dist/src/Entity/ticket/Data/tickets.js +25 -1
- package/dist/src/Entity/ticket/Form/create-data.js +3 -2
- package/dist/src/Entity/ticket/Form/edit-data.js +1 -1
- package/dist/src/Entity/ticket/Form/my-work-parent-source.d.ts +11 -0
- package/dist/src/Entity/ticket/Form/my-work-parent-source.js +41 -0
- package/dist/src/Entity/ticket/Form/project-filter.d.ts +36 -0
- package/dist/src/Entity/ticket/Form/project-filter.js +36 -0
- package/dist/src/Entity/ticket/Form/project-source.d.ts +20 -0
- package/dist/src/Entity/ticket/Form/project-source.js +40 -0
- package/dist/src/Entity/ticket/Screen/my-work-parent-filter-form.d.ts +30 -0
- package/dist/src/Entity/ticket/Screen/my-work-parent-filter-form.js +161 -0
- package/dist/src/Entity/ticket/Screen/my-work.d.ts +21 -2
- package/dist/src/Entity/ticket/Screen/my-work.js +63 -4
- package/dist/src/Entity/ticket/Screen/project-filter-form.d.ts +30 -0
- package/dist/src/Entity/ticket/Screen/project-filter-form.js +173 -0
- package/dist/src/Entity/ticket/Screen/ticket-filter-bar.d.ts +29 -0
- package/dist/src/Entity/ticket/Screen/ticket-filter-bar.js +129 -0
- package/dist/src/Entity/ticket/Screen/tickets.d.ts +10 -5
- package/dist/src/Entity/ticket/Screen/tickets.js +35 -18
- package/dist/src/Entity/user/Data/member-context.js +1 -0
- package/dist/src/Entity/user/Data/users.js +4 -1
- package/dist/src/Entity/workspace/Data/members.js +1 -0
- package/dist/src/Routing/routes.js +4 -0
- package/dist/src/Statistics/comparison-chart.d.ts +5 -0
- package/dist/src/Statistics/comparison-chart.js +517 -0
- package/dist/src/Statistics/figures.js +2 -20
- package/dist/src/Statistics/member-stats.js +1 -0
- package/dist/src/lib/markdown.d.ts +1 -1
- package/dist/src/lib/markdown.js +2 -2
- package/dist/src/lib/repo-url.js +1 -1
- package/dist/src/lib/theme.d.ts +18 -0
- package/dist/src/lib/theme.js +18 -0
- package/dist/src/route.js +21 -24
- package/dist/src/types.d.ts +1 -1
- package/package.json +2 -2
|
@@ -3,11 +3,14 @@ import { type VNode } from '@opentui/core';
|
|
|
3
3
|
export interface ListFilterBadge {
|
|
4
4
|
/** Stable id suffix — node id is `${stripId}-${id}`. */
|
|
5
5
|
id: string;
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Shortcut plus a space plus the idle caption, e.g. `/ search` or `x closed`.
|
|
8
|
+
* The first space-delimited token is the shortcut rendered before an active value.
|
|
9
|
+
*/
|
|
7
10
|
label: string;
|
|
8
11
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
12
|
+
* Active caption rendered after the shortcut (e.g. `closed: hidden`, `who: cw`).
|
|
13
|
+
* Undefined or empty keeps the badge idle: {@link label} only, idle palette.
|
|
11
14
|
*/
|
|
12
15
|
value?: string;
|
|
13
16
|
}
|
|
@@ -32,7 +32,16 @@ export const filterBadgeStrip = (id, badges) => {
|
|
|
32
32
|
paddingLeft: 2,
|
|
33
33
|
paddingRight: 2,
|
|
34
34
|
flexDirection: 'row',
|
|
35
|
-
},
|
|
35
|
+
}, ...(active
|
|
36
|
+
? [
|
|
37
|
+
textNode({
|
|
38
|
+
id: `${id}-${badge.id}-key`,
|
|
39
|
+
content: `${badge.label.split(' ', 1)[0]} `,
|
|
40
|
+
bg: DEFAULT_BACKGROUND,
|
|
41
|
+
fg: FOREGROUND,
|
|
42
|
+
}),
|
|
43
|
+
]
|
|
44
|
+
: []), textNode({
|
|
36
45
|
id: `${id}-${badge.id}`,
|
|
37
46
|
content: label,
|
|
38
47
|
bg: active ? BACKGROUND_ACTIVE : DEFAULT_BACKGROUND,
|
|
@@ -11,6 +11,8 @@ export interface Conductor {
|
|
|
11
11
|
machineId: string | undefined;
|
|
12
12
|
/** Liveness, as the control plane reports it. */
|
|
13
13
|
status: string | undefined;
|
|
14
|
+
/** Operator-set lifecycle flag; inactive conductors are never assignable. */
|
|
15
|
+
active: boolean;
|
|
14
16
|
/** The conductor's project, `null` when unlinked. */
|
|
15
17
|
projectId: string | null;
|
|
16
18
|
/** The user who owns this record, `null` when unowned (GAIA-460 D8/D9). */
|
|
@@ -188,13 +190,16 @@ export declare const CONDUCTOR_TABLE: {
|
|
|
188
190
|
}, {
|
|
189
191
|
/** A CLOSED VOCABULARY. `ConductorStatus`'s reachable members here are
|
|
190
192
|
* `online | stale | offline` — this screen can never produce
|
|
191
|
-
* `registry-only`, because every row IS a control-plane record —
|
|
192
|
-
*
|
|
193
|
+
* `registry-only`, because every row IS a control-plane record — and an
|
|
194
|
+
* inactive row appends a trailing `!`, whose longest, `offline!`, is 8,
|
|
195
|
+
* so 9 still shows each of them whole (GAIA-542 N1: `(inactive)` does
|
|
196
|
+
* not fit this budget without disturbing every other column's exact
|
|
197
|
+
* width, so the marker is a glyph rather than a word). */
|
|
193
198
|
readonly key: "status";
|
|
194
199
|
readonly label: "STATUS";
|
|
195
200
|
readonly width: 9;
|
|
196
|
-
readonly fields: readonly ["status", "lease_expires_at", "last_seen"];
|
|
197
|
-
readonly get: (row: ConductorRow) =>
|
|
201
|
+
readonly fields: readonly ["status", "active", "lease_expires_at", "last_seen"];
|
|
202
|
+
readonly get: (row: ConductorRow) => string;
|
|
198
203
|
}, {
|
|
199
204
|
/** `humanizeAge`'s own vocabulary, exactly as on the projects list: `—`,
|
|
200
205
|
* `just now`, `<n>m|h|d ago`. 8 is the longest of them. */
|
|
@@ -296,13 +301,16 @@ export declare const PROJECT_CONDUCTOR_TABLE: {
|
|
|
296
301
|
} | {
|
|
297
302
|
/** A CLOSED VOCABULARY. `ConductorStatus`'s reachable members here are
|
|
298
303
|
* `online | stale | offline` — this screen can never produce
|
|
299
|
-
* `registry-only`, because every row IS a control-plane record —
|
|
300
|
-
*
|
|
304
|
+
* `registry-only`, because every row IS a control-plane record — and an
|
|
305
|
+
* inactive row appends a trailing `!`, whose longest, `offline!`, is 8,
|
|
306
|
+
* so 9 still shows each of them whole (GAIA-542 N1: `(inactive)` does
|
|
307
|
+
* not fit this budget without disturbing every other column's exact
|
|
308
|
+
* width, so the marker is a glyph rather than a word). */
|
|
301
309
|
readonly key: "status";
|
|
302
310
|
readonly label: "STATUS";
|
|
303
311
|
readonly width: 9;
|
|
304
|
-
readonly fields: readonly ["status", "lease_expires_at", "last_seen"];
|
|
305
|
-
readonly get: (row: ConductorRow) =>
|
|
312
|
+
readonly fields: readonly ["status", "active", "lease_expires_at", "last_seen"];
|
|
313
|
+
readonly get: (row: ConductorRow) => string;
|
|
306
314
|
} | {
|
|
307
315
|
/** `humanizeAge`'s own vocabulary, exactly as on the projects list: `—`,
|
|
308
316
|
* `just now`, `<n>m|h|d ago`. 8 is the longest of them. */
|
|
@@ -20,6 +20,7 @@ const toConductor = (r) => ({
|
|
|
20
20
|
// Registration key (GAIA-353 renamed machine_id → conductor_id).
|
|
21
21
|
machineId: str(r.attr('conductor_id')) ?? str(r.attr('machine_id')),
|
|
22
22
|
status: str(r.attr('status')),
|
|
23
|
+
active: r.attr('active') !== false,
|
|
23
24
|
projectId: r.rel('project_id'),
|
|
24
25
|
ownerUserId: r.rel('owner_user_id'),
|
|
25
26
|
});
|
|
@@ -199,13 +200,16 @@ export const CONDUCTOR_TABLE = {
|
|
|
199
200
|
{
|
|
200
201
|
/** A CLOSED VOCABULARY. `ConductorStatus`'s reachable members here are
|
|
201
202
|
* `online | stale | offline` — this screen can never produce
|
|
202
|
-
* `registry-only`, because every row IS a control-plane record —
|
|
203
|
-
*
|
|
203
|
+
* `registry-only`, because every row IS a control-plane record — and an
|
|
204
|
+
* inactive row appends a trailing `!`, whose longest, `offline!`, is 8,
|
|
205
|
+
* so 9 still shows each of them whole (GAIA-542 N1: `(inactive)` does
|
|
206
|
+
* not fit this budget without disturbing every other column's exact
|
|
207
|
+
* width, so the marker is a glyph rather than a word). */
|
|
204
208
|
key: 'status',
|
|
205
209
|
label: 'STATUS',
|
|
206
210
|
width: 9,
|
|
207
|
-
fields: ['status', 'lease_expires_at', 'last_seen'],
|
|
208
|
-
get: (row) => row.derivedStatus
|
|
211
|
+
fields: ['status', 'active', 'lease_expires_at', 'last_seen'],
|
|
212
|
+
get: (row) => row.active ? row.derivedStatus : `${row.derivedStatus}!`,
|
|
209
213
|
},
|
|
210
214
|
{
|
|
211
215
|
/** `humanizeAge`'s own vocabulary, exactly as on the projects list: `—`,
|
|
@@ -382,14 +386,17 @@ const toConductorRow = (raw, ctx, machinesById) => {
|
|
|
382
386
|
/**
|
|
383
387
|
* The query both reads share: the narrowing, the declared order, the sideload.
|
|
384
388
|
*
|
|
385
|
-
* `sort=-last_seen` is not cosmetic (GAIA-460 D7,
|
|
386
|
-
* collection with no
|
|
387
|
-
* is skipped entirely, so the order is a
|
|
388
|
-
* before it is a convenience for the reader.
|
|
389
|
+
* `sort=-last_seen,-drupal_internal__id` is not cosmetic (GAIA-460 D7,
|
|
390
|
+
* GAIA-458 AC-2). Paging over a collection with no total order lets a row
|
|
391
|
+
* repeat on two pages while another is skipped entirely, so the order is a
|
|
392
|
+
* correctness property of the paging before it is a convenience for the reader.
|
|
393
|
+
* `last_seen` is nullable and two conductors can share a heartbeat; the serial
|
|
394
|
+
* id is the unique second key that makes offset paging well defined.
|
|
389
395
|
*/
|
|
390
396
|
const conductorParams = (query) => {
|
|
391
397
|
const params = new DrupalJsonApiParams().addInclude(['machine']);
|
|
392
398
|
params.addSort('last_seen', 'DESC');
|
|
399
|
+
params.addSort('drupal_internal__id', 'DESC');
|
|
393
400
|
if (query.projectId)
|
|
394
401
|
params.addFilter('project_id.id', query.projectId);
|
|
395
402
|
if (query.ownerUserId)
|
|
@@ -53,8 +53,8 @@ export declare class ConductorsScreen extends ListScreen<ConductorRow, Conductor
|
|
|
53
53
|
readonly key: "status";
|
|
54
54
|
readonly label: "STATUS";
|
|
55
55
|
readonly width: 9;
|
|
56
|
-
readonly fields: readonly ["status", "lease_expires_at", "last_seen"];
|
|
57
|
-
readonly get: (row: ConductorRow) =>
|
|
56
|
+
readonly fields: readonly ["status", "active", "lease_expires_at", "last_seen"];
|
|
57
|
+
readonly get: (row: ConductorRow) => string;
|
|
58
58
|
}, {
|
|
59
59
|
readonly key: "age";
|
|
60
60
|
readonly label: "AGE";
|
|
@@ -46,8 +46,8 @@ export declare class ProjectConductorsScreen extends ListScreen<ConductorRow, Co
|
|
|
46
46
|
readonly key: "status";
|
|
47
47
|
readonly label: "STATUS";
|
|
48
48
|
readonly width: 9;
|
|
49
|
-
readonly fields: readonly ["status", "lease_expires_at", "last_seen"];
|
|
50
|
-
readonly get: (row: ConductorRow) =>
|
|
49
|
+
readonly fields: readonly ["status", "active", "lease_expires_at", "last_seen"];
|
|
50
|
+
readonly get: (row: ConductorRow) => string;
|
|
51
51
|
} | {
|
|
52
52
|
readonly key: "age";
|
|
53
53
|
readonly label: "AGE";
|
|
@@ -10,16 +10,9 @@ const relationshipIds = (relationship) => {
|
|
|
10
10
|
return (Array.isArray(data) ? data : [data]).map(({ id }) => id);
|
|
11
11
|
};
|
|
12
12
|
const repositoryUrl = (value) => {
|
|
13
|
-
if (!
|
|
13
|
+
if (!value || typeof value !== 'object')
|
|
14
14
|
return undefined;
|
|
15
|
-
|
|
16
|
-
if (!repository || typeof repository !== 'object')
|
|
17
|
-
continue;
|
|
18
|
-
const url = asString(repository.url);
|
|
19
|
-
if (url)
|
|
20
|
-
return url;
|
|
21
|
-
}
|
|
22
|
-
return undefined;
|
|
15
|
+
return asString(value.url);
|
|
23
16
|
};
|
|
24
17
|
async function loadProjectContext(client, projectId) {
|
|
25
18
|
const params = new DrupalJsonApiParams()
|
|
@@ -37,7 +30,7 @@ async function loadProjectContext(client, projectId) {
|
|
|
37
30
|
return name ? [name] : [];
|
|
38
31
|
});
|
|
39
32
|
const key = asString(project.attributes?.key);
|
|
40
|
-
const repository = repositoryUrl(project.attributes?.
|
|
33
|
+
const repository = repositoryUrl(project.attributes?.repository);
|
|
41
34
|
return {
|
|
42
35
|
id: project.id ?? projectId,
|
|
43
36
|
name: asString(project.attributes?.name) ?? projectId,
|
|
@@ -14,7 +14,7 @@ export declare function runDetailTitle(document: TicketDetailDocument, runId: st
|
|
|
14
14
|
* `presentation()` puts the kernel's body-window root over it, because the id of
|
|
15
15
|
* that slot is the kernel's to know and a component may not import it.
|
|
16
16
|
*/
|
|
17
|
-
export declare function runDetailTree(document: TicketDetailDocument, runId: string | null | undefined): VNode;
|
|
17
|
+
export declare function runDetailTree(document: TicketDetailDocument, runId: string | null | undefined, baseUrl?: string | undefined): VNode;
|
|
18
18
|
/**
|
|
19
19
|
* The run block as PLAIN TEXT — the pipe's own output (spec F5).
|
|
20
20
|
*
|
|
@@ -30,4 +30,4 @@ export declare function runDetailTree(document: TicketDetailDocument, runId: str
|
|
|
30
30
|
* Nothing here measures a string (R5): a pipe's text is unpadded by definition,
|
|
31
31
|
* so this is the same rows the tree carries and no column arithmetic at all.
|
|
32
32
|
*/
|
|
33
|
-
export declare function runDetailRows(document: TicketDetailDocument, runId: string | null | undefined): string[];
|
|
33
|
+
export declare function runDetailRows(document: TicketDetailDocument, runId: string | null | undefined, baseUrl?: string | undefined): string[];
|
|
@@ -37,19 +37,6 @@ function relId(resource, name) {
|
|
|
37
37
|
return null;
|
|
38
38
|
return Array.isArray(data) ? (data[0]?.id ?? null) : data.id;
|
|
39
39
|
}
|
|
40
|
-
/**
|
|
41
|
-
* A `text_long` value as a plain string. `log`/`error_log` arrive as
|
|
42
|
-
* `{value, format, processed}` when captured and as `null` when they never were
|
|
43
|
-
* (measured null on GAIA-255's own runs) — the same shape the ticket description
|
|
44
|
-
* has, unwrapped by the same rule.
|
|
45
|
-
*/
|
|
46
|
-
function longText(value) {
|
|
47
|
-
if (value && typeof value === 'object' && 'value' in value) {
|
|
48
|
-
const inner = value.value;
|
|
49
|
-
return typeof inner === 'string' ? inner : '';
|
|
50
|
-
}
|
|
51
|
-
return typeof value === 'string' ? value : '';
|
|
52
|
-
}
|
|
53
40
|
/** A scalar field as a display row value: the value, or an explicit dash. */
|
|
54
41
|
function scalar(value) {
|
|
55
42
|
if (typeof value === 'string' && value !== '')
|
|
@@ -103,7 +90,7 @@ export function runDetailTitle(document, runId) {
|
|
|
103
90
|
* it once the screen handed over nodes, and a second description of a screen
|
|
104
91
|
* that nobody draws is exactly what drifts.
|
|
105
92
|
*/
|
|
106
|
-
function keyedSections(document, runId) {
|
|
93
|
+
function keyedSections(document, runId, baseUrl) {
|
|
107
94
|
const run = runResource(document, runId);
|
|
108
95
|
if (!run) {
|
|
109
96
|
return [
|
|
@@ -115,20 +102,23 @@ function keyedSections(document, runId) {
|
|
|
115
102
|
];
|
|
116
103
|
}
|
|
117
104
|
const a = run.attributes ?? {};
|
|
118
|
-
|
|
119
|
-
|
|
105
|
+
// The transcript/error payload no longer lives on the entity (GAIA-556) —
|
|
106
|
+
// preSave() writes it to a private file and clears the field, so what the
|
|
107
|
+
// API returns is the byte count, not the text. `bytes > 0` and "unset"
|
|
108
|
+
// read differently: the first is a payload retrievable through the run
|
|
109
|
+
// log download route, the second is either a true empty log or, for the
|
|
110
|
+
// transcript, a `capture_logs: false` conductor — the operator cannot
|
|
111
|
+
// tell those apart from a truncated body, so say "not captured" instead
|
|
112
|
+
// of implying failure.
|
|
113
|
+
const logSizeSection = (key, heading, bytes, notCapturedMessage, kind) => {
|
|
114
|
+
const captured = typeof bytes === 'number';
|
|
115
|
+
const text = captured
|
|
116
|
+
? `Captured (${bytes} bytes). ${runLogUrl(baseUrl, run.id, kind) ?? 'Retrieve via the run log download route.'}`
|
|
117
|
+
: notCapturedMessage;
|
|
120
118
|
return {
|
|
121
119
|
key,
|
|
122
120
|
heading,
|
|
123
|
-
|
|
124
|
-
// whatever the fields above it do. A transcript only ever grows at the
|
|
125
|
-
// end, which is the one shape a positional key is honest about.
|
|
126
|
-
rows: text
|
|
127
|
-
? text.split('\n').map((line, index) => ({
|
|
128
|
-
key: `${key}-${index}`,
|
|
129
|
-
text: line,
|
|
130
|
-
}))
|
|
131
|
-
: [{ key: `${key}-empty`, text: emptyMessage }],
|
|
121
|
+
rows: [{ key: `${key}-empty`, text }],
|
|
132
122
|
};
|
|
133
123
|
};
|
|
134
124
|
return [
|
|
@@ -186,12 +176,24 @@ function keyedSections(document, runId) {
|
|
|
186
176
|
},
|
|
187
177
|
],
|
|
188
178
|
},
|
|
189
|
-
// R5
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
179
|
+
// R5 no longer applies to what's rendered here: the payload moved to a
|
|
180
|
+
// private file (GAIA-556), so the section shows the size, not the text.
|
|
181
|
+
logSizeSection('log', 'Log', a.log_bytes, 'Transcript not captured.', 'log'),
|
|
182
|
+
logSizeSection('error-log', 'Error log', a.error_log_bytes, 'No errors logged.', 'error_log'),
|
|
193
183
|
];
|
|
194
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* The run-log download route's resolved URL (`_gaia-api/run/{uuid}/log/{kind}`,
|
|
187
|
+
* @see gaia-cli/addons/remote-drupal/src/index.ts createRunLogFetcher), so an
|
|
188
|
+
* operator reading the run detail screen has something to open rather than a
|
|
189
|
+
* sentence naming a route with no address. `undefined` when the control plane's
|
|
190
|
+
* base URL is not known, which the caller falls back on.
|
|
191
|
+
*/
|
|
192
|
+
function runLogUrl(baseUrl, runId, kind) {
|
|
193
|
+
if (!baseUrl || !runId)
|
|
194
|
+
return undefined;
|
|
195
|
+
return `${baseUrl.replace(/\/+$/, '')}/_gaia-api/run/${runId}/log/${kind}`;
|
|
196
|
+
}
|
|
195
197
|
/**
|
|
196
198
|
* One body row as a renderable. Bold is a PROP here, never an index (D3), and
|
|
197
199
|
* it is stated on every row rather than only on the bold ones — a log line that
|
|
@@ -211,8 +213,8 @@ const textRow = (key, text, bold = false) => textNode({ id: `run-detail-${key}`,
|
|
|
211
213
|
* asserts the two projections row for row, so a section reachable from only one
|
|
212
214
|
* of them fails rather than diverges quietly.
|
|
213
215
|
*/
|
|
214
|
-
function flatRows(document, runId) {
|
|
215
|
-
return keyedSections(document, runId).flatMap((section) => [
|
|
216
|
+
function flatRows(document, runId, baseUrl) {
|
|
217
|
+
return keyedSections(document, runId, baseUrl).flatMap((section) => [
|
|
216
218
|
{ key: `section-${section.key}`, text: section.heading, bold: true },
|
|
217
219
|
...section.rows.map((entry) => ({
|
|
218
220
|
key: entry.key,
|
|
@@ -238,8 +240,8 @@ function flatRows(document, runId) {
|
|
|
238
240
|
* `presentation()` puts the kernel's body-window root over it, because the id of
|
|
239
241
|
* that slot is the kernel's to know and a component may not import it.
|
|
240
242
|
*/
|
|
241
|
-
export function runDetailTree(document, runId) {
|
|
242
|
-
return columnNode('run-detail', ...flatRows(document, runId).map((entry) => textRow(entry.key, entry.text, entry.bold)));
|
|
243
|
+
export function runDetailTree(document, runId, baseUrl) {
|
|
244
|
+
return columnNode('run-detail', ...flatRows(document, runId, baseUrl).map((entry) => textRow(entry.key, entry.text, entry.bold)));
|
|
243
245
|
}
|
|
244
246
|
/**
|
|
245
247
|
* The run block as PLAIN TEXT — the pipe's own output (spec F5).
|
|
@@ -256,6 +258,6 @@ export function runDetailTree(document, runId) {
|
|
|
256
258
|
* Nothing here measures a string (R5): a pipe's text is unpadded by definition,
|
|
257
259
|
* so this is the same rows the tree carries and no column arithmetic at all.
|
|
258
260
|
*/
|
|
259
|
-
export function runDetailRows(document, runId) {
|
|
260
|
-
return flatRows(document, runId).map((entry) => entry.text);
|
|
261
|
+
export function runDetailRows(document, runId, baseUrl) {
|
|
262
|
+
return flatRows(document, runId, baseUrl).map((entry) => entry.text);
|
|
261
263
|
}
|
|
@@ -65,6 +65,7 @@ export function buildConductorRunQueryParams(conductorId, page = { size: LIST_PA
|
|
|
65
65
|
params.addFields('gaia_ticket--gaia_ticket', CONDUCTOR_RUN_TICKET_INCLUDE_FIELDS);
|
|
66
66
|
params.addFields('gaia_run--gaia_run', RUN_LIST_FIELDS);
|
|
67
67
|
params.addSort('started_at', 'DESC');
|
|
68
|
+
params.addSort('drupal_internal__id', 'DESC');
|
|
68
69
|
params.addPageLimit(page.size);
|
|
69
70
|
if (page.index > 0)
|
|
70
71
|
params.addPageOffset(page.index * page.size);
|
|
@@ -79,6 +80,7 @@ function applyRunCriteria(params, criteria, page) {
|
|
|
79
80
|
params.addFields('gaia_ticket--gaia_ticket', RUN_TICKET_INCLUDE_FIELDS);
|
|
80
81
|
params.addFields('gaia_run--gaia_run', RUN_LIST_FIELDS);
|
|
81
82
|
params.addSort('started_at', 'DESC');
|
|
83
|
+
params.addSort('drupal_internal__id', 'DESC');
|
|
82
84
|
params.addPageLimit(page.size);
|
|
83
85
|
// Page one carries no offset, so its query is the shortest it can be.
|
|
84
86
|
if (page.index > 0)
|
|
@@ -42,7 +42,7 @@ export class RunDetailScreen extends AsyncScreen {
|
|
|
42
42
|
return ['Loading run…'];
|
|
43
43
|
}
|
|
44
44
|
renderReady(document, context) {
|
|
45
|
-
return runDetailTree(document, context.params?.runId ?? '');
|
|
45
|
+
return runDetailTree(document, context.params?.runId ?? '', context.services?.baseUrl);
|
|
46
46
|
}
|
|
47
47
|
chrome(context) {
|
|
48
48
|
return {
|
|
@@ -107,6 +107,6 @@ export class RunDetailScreen extends AsyncScreen {
|
|
|
107
107
|
* 97.7 % of a 1.34 MB captured agent transcript.
|
|
108
108
|
*/
|
|
109
109
|
renderNonInteractive(context) {
|
|
110
|
-
return runDetailRows(this.readyValue ?? {}, context.params?.runId ?? '');
|
|
110
|
+
return runDetailRows(this.readyValue ?? {}, context.params?.runId ?? '', context.services?.baseUrl);
|
|
111
111
|
}
|
|
112
112
|
}
|
|
@@ -7,6 +7,7 @@ export async function listLinkedProjectIds(client, teamId) {
|
|
|
7
7
|
while (true) {
|
|
8
8
|
const params = new DrupalJsonApiParams()
|
|
9
9
|
.addFilter('teams.id', teamId)
|
|
10
|
+
.addSort('drupal_internal__id', 'ASC')
|
|
10
11
|
.addPageLimit(LIST_PAGE_SIZE);
|
|
11
12
|
if (offset > 0)
|
|
12
13
|
params.addPageOffset(offset);
|
|
@@ -160,6 +160,7 @@ export const MEMBERSHIP_RESOURCE = 'og_membership/default';
|
|
|
160
160
|
export async function readMembershipPage(client, filters, offset) {
|
|
161
161
|
const params = new DrupalJsonApiParams()
|
|
162
162
|
.addFilter('state', MEMBERSHIP_STATE_ACTIVE)
|
|
163
|
+
.addSort('drupal_internal__id', 'ASC')
|
|
163
164
|
.addPageLimit(MEMBERSHIP_PAGE_SIZE);
|
|
164
165
|
for (const [field, value] of Object.entries(filters)) {
|
|
165
166
|
params.addFilter(field, value);
|
|
@@ -100,9 +100,6 @@ const lifecycleProperties = (document) => {
|
|
|
100
100
|
const changed = asString(attributes.changed);
|
|
101
101
|
if (changed !== undefined)
|
|
102
102
|
extras.push(property('changed', changed));
|
|
103
|
-
const gitlab = asString(attributes.gitlab_assignee_username);
|
|
104
|
-
if (gitlab !== undefined)
|
|
105
|
-
extras.push(property('gitlab assignee', gitlab));
|
|
106
103
|
return extras;
|
|
107
104
|
};
|
|
108
105
|
const technicalProperties = (byHeading, document, labels) => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DropshClient } from '../../../types.js';
|
|
2
|
+
import { type ListOutcome } from '../../transport.js';
|
|
3
|
+
import type { ParentCandidate } from './parents.js';
|
|
4
|
+
/** A page of candidates for the My work parent picker. */
|
|
5
|
+
export declare const MY_WORK_PARENT_PAGE_SIZE = 50;
|
|
6
|
+
/**
|
|
7
|
+
* One page of tickets that have children, across every project.
|
|
8
|
+
*
|
|
9
|
+
* The `filter[identifier][operator]=CONTAINS` shape is the same one the
|
|
10
|
+
* project-scoped parent picker uses for `match === 'identifier'` (GAIA-284,
|
|
11
|
+
* PM steer). `has_children = 1` filters on the server side via
|
|
12
|
+
* `HasChildrenFilterSubscriber`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function loadAllParentCandidates(client: DropshClient, query: string): Promise<ListOutcome<ParentCandidate>>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// GAIA-540 — the My work PARENT CANDIDATE read. Mirrors `loadParentCandidates`
|
|
2
|
+
// from `parents.ts` but does NOT scope the read by `filter[project_id.id]`:
|
|
3
|
+
// My work spans every project the operator is assigned to, so a parent picker
|
|
4
|
+
// they reach from My work must offer parents from all of them.
|
|
5
|
+
//
|
|
6
|
+
// The remaining policy bits stay: `parent: null` and `who: null` criteria
|
|
7
|
+
// (F7), so an active parent filter can never narrow the picker to that
|
|
8
|
+
// parent's own children, and `has_children = 1` so the picker offers only
|
|
9
|
+
// tickets that CAN be picked.
|
|
10
|
+
import { DrupalJsonApiParams } from 'drupal-jsonapi-params';
|
|
11
|
+
import { messageOf } from '../../transport.js';
|
|
12
|
+
/** A page of candidates for the My work parent picker. */
|
|
13
|
+
export const MY_WORK_PARENT_PAGE_SIZE = 50;
|
|
14
|
+
const toCandidate = (item) => {
|
|
15
|
+
const id = item.id ?? '';
|
|
16
|
+
return {
|
|
17
|
+
id,
|
|
18
|
+
identifier: item.attributes?.identifier ?? id,
|
|
19
|
+
title: item.attributes?.title ?? 'Untitled',
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* One page of tickets that have children, across every project.
|
|
24
|
+
*
|
|
25
|
+
* The `filter[identifier][operator]=CONTAINS` shape is the same one the
|
|
26
|
+
* project-scoped parent picker uses for `match === 'identifier'` (GAIA-284,
|
|
27
|
+
* PM steer). `has_children = 1` filters on the server side via
|
|
28
|
+
* `HasChildrenFilterSubscriber`.
|
|
29
|
+
*/
|
|
30
|
+
export async function loadAllParentCandidates(client, query) {
|
|
31
|
+
const params = new DrupalJsonApiParams()
|
|
32
|
+
.addPageLimit(MY_WORK_PARENT_PAGE_SIZE)
|
|
33
|
+
.addFields('gaia_ticket--gaia_ticket', ['identifier', 'title']);
|
|
34
|
+
if (query)
|
|
35
|
+
params.addFilter('identifier', query, 'CONTAINS');
|
|
36
|
+
params.addFilter('has_children', '1');
|
|
37
|
+
try {
|
|
38
|
+
const doc = (await client.get('gaia_ticket/gaia_ticket', params));
|
|
39
|
+
return { rows: (doc?.data ?? []).map(toCandidate) };
|
|
40
|
+
}
|
|
41
|
+
catch (caught) {
|
|
42
|
+
return { rows: [], error: messageOf(caught) };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -85,6 +85,7 @@ export async function loadParentCandidates(client, projectId, query, match = 'id
|
|
|
85
85
|
who: null,
|
|
86
86
|
// Same rule for the owner filter (GAIA-416).
|
|
87
87
|
owner: null,
|
|
88
|
+
project: null,
|
|
88
89
|
},
|
|
89
90
|
// Its own page, stated explicitly: the picker discloses truncation at
|
|
90
91
|
// PARENT_PAGE_SIZE, so inheriting the list's LIST_PAGE_SIZE (20 since
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { DropshClient } from '../../../types.js';
|
|
2
|
+
import { type ListOutcome } from '../../transport.js';
|
|
3
|
+
/** One project offered as a possible filter, as the picker needs it. */
|
|
4
|
+
export interface ProjectCandidate {
|
|
5
|
+
id: string;
|
|
6
|
+
/** The project's `name`; falls back to the uuid, so nothing renders blank. */
|
|
7
|
+
name: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The most rows one read can return: Drupal's `OffsetPage::SIZE_MAX`, verified
|
|
11
|
+
* against the control plane.
|
|
12
|
+
*
|
|
13
|
+
* Exported because a bag of exactly this length may be a TRUNCATED answer, and
|
|
14
|
+
* only the module that performs the read knows the number. The picker does
|
|
15
|
+
* not currently disclose truncation here (project names are short and a small
|
|
16
|
+
* bag is rare), but the constant is the same shape `parents` and `users`
|
|
17
|
+
* already publish so a future disclosure can find it.
|
|
18
|
+
*/
|
|
19
|
+
export declare const PROJECT_PAGE_SIZE = 50;
|
|
20
|
+
/**
|
|
21
|
+
* One page of projects whose `name` contains the typed fragment.
|
|
22
|
+
*
|
|
23
|
+
* `name` is a plain string field on `gaia_project` (no `display_name`
|
|
24
|
+
* computed field), so `filter[name][operator]=CONTAINS` is a real server-side
|
|
25
|
+
* filter — matching is name-only, the same restriction the Who picker's
|
|
26
|
+
* matching carries for the same reason (R4 of GAIA-299). The two-character
|
|
27
|
+
* gate (AC-3 of GAIA-540) is THIS field's policy, encoded in the source that
|
|
28
|
+
* calls this read, NOT here — the data layer returns whatever it was asked
|
|
29
|
+
* for, including an empty array for the empty query.
|
|
30
|
+
*
|
|
31
|
+
* Yields the OUTCOME rather than a swallowed array, so the adapter can record
|
|
32
|
+
* a `projects` error like every other option bag (D4) instead of presenting an
|
|
33
|
+
* empty candidate list as "no projects exist".
|
|
34
|
+
*/
|
|
35
|
+
export declare function loadProjectCandidates(client: DropshClient, query: string): Promise<ListOutcome<ProjectCandidate>>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// GAIA-540 — the PROJECT CANDIDATE read for the project-filter picker (the
|
|
2
|
+
// My work screen's `r` key on the bar). A data module like its neighbours:
|
|
3
|
+
// it returns projects and knows nothing about forms, `Option`s or popups; the
|
|
4
|
+
// label, the form declaration and the screen all sit above this file (boundary
|
|
5
|
+
// guard G2, the `Data/` leaf rule).
|
|
6
|
+
//
|
|
7
|
+
// The query is built directly against `gaia_project` (not through
|
|
8
|
+
// `buildTicketQueryParams`, since this read is NOT a ticket read): every
|
|
9
|
+
// gaia_project the operator can name is fair game, and the
|
|
10
|
+
// `filter[name][operator]=CONTAINS&filter[name][value]=<query>` form is
|
|
11
|
+
// exactly what Drupal JSON:API expects. `display_name`-style computed fields
|
|
12
|
+
// would not be filterable; `name` is the canonical label.
|
|
13
|
+
import { DrupalJsonApiParams } from 'drupal-jsonapi-params';
|
|
14
|
+
import { asString } from '../../../lib/format.js';
|
|
15
|
+
import { messageOf } from '../../transport.js';
|
|
16
|
+
/**
|
|
17
|
+
* The most rows one read can return: Drupal's `OffsetPage::SIZE_MAX`, verified
|
|
18
|
+
* against the control plane.
|
|
19
|
+
*
|
|
20
|
+
* Exported because a bag of exactly this length may be a TRUNCATED answer, and
|
|
21
|
+
* only the module that performs the read knows the number. The picker does
|
|
22
|
+
* not currently disclose truncation here (project names are short and a small
|
|
23
|
+
* bag is rare), but the constant is the same shape `parents` and `users`
|
|
24
|
+
* already publish so a future disclosure can find it.
|
|
25
|
+
*/
|
|
26
|
+
export const PROJECT_PAGE_SIZE = 50;
|
|
27
|
+
const toCandidate = (item) => {
|
|
28
|
+
const id = item.id ?? '';
|
|
29
|
+
return {
|
|
30
|
+
id,
|
|
31
|
+
name: asString(item.attributes?.name) ?? id,
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* One page of projects whose `name` contains the typed fragment.
|
|
36
|
+
*
|
|
37
|
+
* `name` is a plain string field on `gaia_project` (no `display_name`
|
|
38
|
+
* computed field), so `filter[name][operator]=CONTAINS` is a real server-side
|
|
39
|
+
* filter — matching is name-only, the same restriction the Who picker's
|
|
40
|
+
* matching carries for the same reason (R4 of GAIA-299). The two-character
|
|
41
|
+
* gate (AC-3 of GAIA-540) is THIS field's policy, encoded in the source that
|
|
42
|
+
* calls this read, NOT here — the data layer returns whatever it was asked
|
|
43
|
+
* for, including an empty array for the empty query.
|
|
44
|
+
*
|
|
45
|
+
* Yields the OUTCOME rather than a swallowed array, so the adapter can record
|
|
46
|
+
* a `projects` error like every other option bag (D4) instead of presenting an
|
|
47
|
+
* empty candidate list as "no projects exist".
|
|
48
|
+
*/
|
|
49
|
+
export async function loadProjectCandidates(client, query) {
|
|
50
|
+
const params = new DrupalJsonApiParams();
|
|
51
|
+
if (query)
|
|
52
|
+
params.addFilter('name', query, 'CONTAINS');
|
|
53
|
+
params.addPageLimit(PROJECT_PAGE_SIZE);
|
|
54
|
+
try {
|
|
55
|
+
const doc = (await client.get('gaia_project/gaia_project', params));
|
|
56
|
+
return { rows: (doc?.data ?? []).map(toCandidate) };
|
|
57
|
+
}
|
|
58
|
+
catch (caught) {
|
|
59
|
+
return { rows: [], error: messageOf(caught) };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -13,6 +13,13 @@ export interface WhoCriterion {
|
|
|
13
13
|
/** The display name — what the summary names, carried so no lookup is needed. */
|
|
14
14
|
label: string;
|
|
15
15
|
}
|
|
16
|
+
/** A picked project: the uuid the query filters by, plus the name the summary names. */
|
|
17
|
+
export interface ProjectCriterion {
|
|
18
|
+
/** The project's uuid — what the query filters by. */
|
|
19
|
+
id: string;
|
|
20
|
+
/** The project name — what the summary names, carried so no lookup is needed. */
|
|
21
|
+
label: string;
|
|
22
|
+
}
|
|
16
23
|
/** The active search + filter criteria for the project ticket list. */
|
|
17
24
|
export interface TicketCriteria {
|
|
18
25
|
query: string;
|
|
@@ -35,6 +42,8 @@ export interface TicketCriteria {
|
|
|
35
42
|
* exposed-Views facet, which the TUI never queries through.
|
|
36
43
|
*/
|
|
37
44
|
owner: WhoCriterion | null;
|
|
45
|
+
/** Tickets in this project only (GAIA-540); `null` means "all". */
|
|
46
|
+
project: ProjectCriterion | null;
|
|
38
47
|
}
|
|
39
48
|
/** The shipped default: everything unset, closed rows hidden (GAIA-282 D1). */
|
|
40
49
|
export declare const DEFAULT_TICKET_CRITERIA: TicketCriteria;
|
|
@@ -61,6 +70,9 @@ export interface TicketQueryOptions {
|
|
|
61
70
|
/** Sideload parent_id + conductor_id for the PARENT/COND columns. Default true;
|
|
62
71
|
* the parent picker passes false because it renders neither (D8). */
|
|
63
72
|
includes?: boolean;
|
|
73
|
+
/** Apply criteria.project as a filter. Default false; set true in
|
|
74
|
+
* buildUserTicketQueryParams so My work can narrow by project (GAIA-540). */
|
|
75
|
+
projectFromCriteria?: boolean;
|
|
64
76
|
}
|
|
65
77
|
export declare function buildTicketQueryParams(projectId: string, criteria: TicketCriteria, options?: TicketQueryOptions): DrupalJsonApiParams;
|
|
66
78
|
/**
|