@gaia-ai/addon-gaia-ui 0.9.2 → 0.10.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/footer-actions/index.d.ts +58 -0
- package/dist/src/Component/footer-actions/index.js +111 -0
- package/dist/src/Component/list-chrome/index.d.ts +19 -0
- package/dist/src/Component/list-chrome/index.js +61 -0
- package/dist/src/Component/markdown/index.d.ts +68 -0
- package/dist/src/Component/markdown/index.js +133 -0
- package/dist/src/Component/section-list/index.d.ts +115 -23
- package/dist/src/Component/section-list/index.js +120 -34
- package/dist/src/Component/top-tabs/index.d.ts +65 -6
- package/dist/src/Component/top-tabs/index.js +29 -6
- package/dist/src/Entity/collection.js +1 -1
- package/dist/src/Entity/conductor/Screen/conductors.d.ts +26 -41
- package/dist/src/Entity/conductor/Screen/conductors.js +65 -123
- package/dist/src/Entity/project/Screen/project-picker.d.ts +20 -31
- package/dist/src/Entity/project/Screen/project-picker.js +48 -58
- package/dist/src/Entity/project/Screen/projects-screen.d.ts +4 -2
- package/dist/src/Entity/project/Screen/projects-screen.js +11 -8
- package/dist/src/Entity/run/Component/run-detail/index.js +15 -7
- package/dist/src/Entity/run/Data/query.d.ts +10 -0
- package/dist/src/Entity/run/Data/query.js +16 -1
- package/dist/src/Entity/run/Data/runs.d.ts +2 -0
- package/dist/src/Entity/run/Data/runs.js +19 -1
- package/dist/src/Entity/run/Screen/run-detail.d.ts +16 -23
- package/dist/src/Entity/run/Screen/run-detail.js +27 -34
- package/dist/src/Entity/run/Screen/runs.d.ts +7 -3
- package/dist/src/Entity/run/Screen/runs.js +31 -48
- package/dist/src/Entity/run/Screen/user-runs.d.ts +79 -0
- package/dist/src/Entity/run/Screen/user-runs.js +98 -0
- package/dist/src/Entity/statistics/Data/statistics.d.ts +31 -6
- package/dist/src/Entity/statistics/Data/statistics.js +39 -6
- package/dist/src/Entity/statistics/Screen/dashboard.d.ts +10 -2
- package/dist/src/Entity/statistics/Screen/dashboard.js +111 -34
- package/dist/src/Entity/ticket/Component/comment-item/index.d.ts +14 -1
- package/dist/src/Entity/ticket/Component/comment-item/index.js +15 -1
- package/dist/src/Entity/ticket/Component/sections/index.js +16 -1
- package/dist/src/Entity/ticket/Component/ticket-detail/index.d.ts +102 -13
- package/dist/src/Entity/ticket/Component/ticket-detail/index.js +440 -72
- package/dist/src/Entity/ticket/Data/detail.d.ts +14 -0
- package/dist/src/Entity/ticket/Data/detail.js +15 -1
- package/dist/src/Entity/ticket/Data/query.d.ts +7 -0
- package/dist/src/Entity/ticket/Data/query.js +18 -2
- package/dist/src/Entity/ticket/Data/tickets.d.ts +24 -0
- package/dist/src/Entity/ticket/Data/tickets.js +41 -1
- package/dist/src/Entity/ticket/Screen/create-ticket-form.d.ts +10 -4
- package/dist/src/Entity/ticket/Screen/create-ticket-form.js +37 -16
- package/dist/src/Entity/ticket/Screen/edit-assignment-form.d.ts +6 -4
- package/dist/src/Entity/ticket/Screen/edit-assignment-form.js +16 -15
- package/dist/src/Entity/ticket/Screen/parent-filter-form.d.ts +6 -4
- package/dist/src/Entity/ticket/Screen/parent-filter-form.js +14 -11
- package/dist/src/Entity/ticket/Screen/ticket-agents.d.ts +5 -2
- package/dist/src/Entity/ticket/Screen/ticket-agents.js +14 -14
- package/dist/src/Entity/ticket/Screen/ticket-detail.d.ts +89 -4
- package/dist/src/Entity/ticket/Screen/ticket-detail.js +295 -74
- package/dist/src/Entity/ticket/Screen/tickets.d.ts +66 -3
- package/dist/src/Entity/ticket/Screen/tickets.js +140 -76
- package/dist/src/Entity/ticket/Screen/user-tickets.d.ts +91 -0
- package/dist/src/Entity/ticket/Screen/user-tickets.js +94 -0
- package/dist/src/Entity/ticket/Screen/who-filter-form.d.ts +6 -4
- package/dist/src/Entity/ticket/Screen/who-filter-form.js +14 -11
- package/dist/src/Entity/user/Data/users.d.ts +49 -0
- package/dist/src/Entity/user/Data/users.js +46 -0
- package/dist/src/Entity/user/Screen/user-overview.d.ts +33 -0
- package/dist/src/Entity/user/Screen/user-overview.js +159 -0
- package/dist/src/Entity/user/Screen/users.d.ts +39 -0
- package/dist/src/Entity/user/Screen/users.js +136 -0
- package/dist/src/Extension/contract.d.ts +58 -0
- package/dist/src/Extension/contract.js +1 -0
- package/dist/src/Extension/load.d.ts +10 -0
- package/dist/src/Extension/load.js +21 -0
- package/dist/src/Extension/store.d.ts +11 -0
- package/dist/src/Extension/store.js +44 -0
- package/dist/src/Kernel/chrome.d.ts +49 -12
- package/dist/src/Kernel/chrome.js +150 -57
- package/dist/src/Kernel/key-input.js +1 -1
- package/dist/src/Kernel/mount.d.ts +1 -1
- package/dist/src/Kernel/mount.js +1 -1
- package/dist/src/Kernel/regions.d.ts +64 -0
- package/dist/src/Kernel/regions.js +328 -0
- package/dist/src/Kernel/renderer.d.ts +25 -0
- package/dist/src/Kernel/renderer.js +142 -0
- package/dist/src/Kernel/splash.d.ts +27 -0
- package/dist/src/Kernel/splash.js +72 -0
- package/dist/src/Kernel/tui-kernel.js +139 -434
- package/dist/src/Routing/context.d.ts +5 -0
- package/dist/src/Routing/context.js +6 -0
- package/dist/src/Routing/registry.js +4 -2
- package/dist/src/Routing/router.js +5 -1
- package/dist/src/Routing/routes.js +8 -0
- package/dist/src/Screen/async-modal-screen.d.ts +26 -0
- package/dist/src/Screen/async-modal-screen.js +44 -0
- package/dist/src/Screen/async-screen.d.ts +32 -40
- package/dist/src/Screen/async-screen.js +45 -41
- package/dist/src/Screen/full-screen.d.ts +1 -0
- package/dist/src/Screen/full-screen.js +3 -0
- package/dist/src/Screen/list-screen.d.ts +7 -1
- package/dist/src/Screen/list-screen.js +17 -6
- package/dist/src/Screen/modal-screen.d.ts +6 -0
- package/dist/src/Screen/modal-screen.js +2 -0
- package/dist/src/Screen/screen.d.ts +42 -6
- package/dist/src/Screen/screen.js +2 -2
- package/dist/src/Widget/textarea.js +10 -14
- package/dist/src/Widget/types.d.ts +1 -1
- package/dist/src/launcher.d.ts +6 -0
- package/dist/src/launcher.js +2 -0
- package/dist/src/lib/format.js +1 -1
- package/dist/src/lib/markdown.d.ts +53 -0
- package/dist/src/lib/markdown.js +522 -0
- package/dist/src/lib/refresh-signal.d.ts +56 -0
- package/dist/src/lib/refresh-signal.js +85 -0
- package/dist/src/lib/theme.d.ts +36 -0
- package/dist/src/lib/theme.js +40 -0
- package/dist/src/lib/version.d.ts +5 -0
- package/dist/src/lib/version.js +46 -0
- package/dist/src/lib/vnode.d.ts +86 -63
- package/dist/src/lib/vnode.js +112 -85
- package/dist/src/types.d.ts +32 -0
- package/dist/src/ui.d.ts +2 -0
- package/dist/src/ui.js +1 -0
- package/package.json +4 -3
|
@@ -219,6 +219,8 @@ export type RunPage = Page<RunRow>;
|
|
|
219
219
|
* `links.next`.
|
|
220
220
|
*/
|
|
221
221
|
export declare function loadProjectRuns(client: DropshClient, projectId: string, criteria: RunCriteria, page?: number): Promise<RunPage>;
|
|
222
|
+
/** One page of runs attributed to a user via ticket assignee (GAIA-377 D2). */
|
|
223
|
+
export declare function loadUserRuns(client: DropshClient, userId: string, criteria: RunCriteria, page?: number): Promise<RunPage>;
|
|
222
224
|
/**
|
|
223
225
|
* Newest-STARTED first, and TOTAL (GAIA-255 D5).
|
|
224
226
|
*
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// no-request read of the ticket document (spec D7).
|
|
8
8
|
import { loadPage, } from '../../collection.js';
|
|
9
9
|
import { LIST_PAGE_SIZE } from '../../cycle.js';
|
|
10
|
-
import { buildRunQueryParams } from './query.js';
|
|
10
|
+
import { buildRunQueryParams, buildUserRunQueryParams, } from './query.js';
|
|
11
11
|
import { deriveRunStatus, runStartedAge } from './status.js';
|
|
12
12
|
/** Read a value as a string, or `undefined` when it is not one. */
|
|
13
13
|
function str(value) {
|
|
@@ -217,6 +217,24 @@ export async function loadProjectRuns(client, projectId, criteria, page = 0) {
|
|
|
217
217
|
return data.map((item) => mapRunData(item, ticketsById));
|
|
218
218
|
}, page, window);
|
|
219
219
|
}
|
|
220
|
+
/** One page of runs attributed to a user via ticket assignee (GAIA-377 D2). */
|
|
221
|
+
export async function loadUserRuns(client, userId, criteria, page = 0) {
|
|
222
|
+
const window = {
|
|
223
|
+
offset: page * LIST_PAGE_SIZE,
|
|
224
|
+
limit: LIST_PAGE_SIZE,
|
|
225
|
+
};
|
|
226
|
+
const params = buildUserRunQueryParams(userId, criteria, {
|
|
227
|
+
size: window.limit,
|
|
228
|
+
index: page,
|
|
229
|
+
});
|
|
230
|
+
return loadPage(client, 'gaia_run/gaia_run', params, (data, included) => {
|
|
231
|
+
const ticketsById = new Map();
|
|
232
|
+
for (const inc of included)
|
|
233
|
+
if (inc.id)
|
|
234
|
+
ticketsById.set(inc.id, inc);
|
|
235
|
+
return data.map((item) => mapRunData(item, ticketsById));
|
|
236
|
+
}, page, window);
|
|
237
|
+
}
|
|
220
238
|
/**
|
|
221
239
|
* Newest-STARTED first, and TOTAL (GAIA-255 D5).
|
|
222
240
|
*
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { VNode } from '@opentui/core';
|
|
2
|
+
import { AsyncScreen } from '../../../Screen/async-screen.js';
|
|
3
|
+
import type { FullScreenChrome } from '../../../Screen/full-screen.js';
|
|
4
|
+
import type { ScreenContext } from '../../../Screen/screen.js';
|
|
2
5
|
import type { TicketDetailDocument } from '../../../types.js';
|
|
3
6
|
export interface RunDetailInput {
|
|
4
7
|
readonly document: TicketDetailDocument;
|
|
5
8
|
}
|
|
6
9
|
export declare const runDetailInputContext: import("../../../Routing/context.js").ContextKey<RunDetailInput>;
|
|
7
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Run detail — normal {@link AsyncScreen}. The document arrives via navigation
|
|
12
|
+
* context (or empty); refresh is the shared R / ui-refresh path.
|
|
13
|
+
*/
|
|
14
|
+
export declare class RunDetailScreen extends AsyncScreen<TicketDetailDocument> {
|
|
8
15
|
readonly frameId = "run";
|
|
9
16
|
topTab(): null;
|
|
10
17
|
static readonly route: {
|
|
@@ -17,20 +24,13 @@ export declare class RunDetailScreen extends BaseScreen {
|
|
|
17
24
|
context: (_params: Readonly<Record<string, string>>, state: unknown, base: import("../../../Routing/context.js").ContextBag) => import("../../../Routing/context.js").ContextBag;
|
|
18
25
|
};
|
|
19
26
|
readonly id: string;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
* `render(context): string[]` any more, because there is no second
|
|
28
|
-
* description of the screen to keep in step with this one — the bold section
|
|
29
|
-
* headings that were `boldRows` indices into a row surface are props on the
|
|
30
|
-
* rows' own nodes now (D3), and the surface those indices pointed into is
|
|
31
|
-
* gone.
|
|
32
|
-
*/
|
|
33
|
-
present(context: ScreenContext): import("../../../Screen/screen.js").ScreenPresentation;
|
|
27
|
+
reload(context: ScreenContext): void;
|
|
28
|
+
protected load(context: ScreenContext): Promise<TicketDetailDocument>;
|
|
29
|
+
protected renderLoading(): string[];
|
|
30
|
+
protected renderReady(document: TicketDetailDocument, context: ScreenContext): VNode;
|
|
31
|
+
protected chrome(context: ScreenContext): FullScreenChrome;
|
|
32
|
+
/** Read-only — kernel basics cover ↑↓ / Esc / q; no extra chips. */
|
|
33
|
+
protected actions(): never[];
|
|
34
34
|
/**
|
|
35
35
|
* F5 — the pipe's own output, and it is `string[]`.
|
|
36
36
|
*
|
|
@@ -41,13 +41,6 @@ export declare class RunDetailScreen extends BaseScreen {
|
|
|
41
41
|
* path lays the body out on a cell grid of a fixed width, and a pipe has no
|
|
42
42
|
* width — on run #4928 it clipped 420 transcript rows at 80 columns, losing
|
|
43
43
|
* 97.7 % of a 1.34 MB captured agent transcript.
|
|
44
|
-
*
|
|
45
|
-
* This is NOT a `ScreenPresentation.lines` coming back (R3): `present()` still
|
|
46
|
-
* hands over a tree and nothing else. Both shapes are projections of the block's
|
|
47
|
-
* ONE keyed traversal (`flatRows`), so there is no second description of the
|
|
48
|
-
* screen to keep in step — see `tests/Screen/run-detail-non-tty.test.ts`, which
|
|
49
|
-
* asserts the two row for row.
|
|
50
44
|
*/
|
|
51
45
|
renderNonInteractive(context: ScreenContext): string[];
|
|
52
|
-
reset(): void;
|
|
53
46
|
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { defineContext } from '../../../Routing/context.js';
|
|
2
|
-
import {
|
|
2
|
+
import { AsyncScreen } from '../../../Screen/async-screen.js';
|
|
3
3
|
import { runDetailRows, runDetailTitle, runDetailTree, } from '../Component/run-detail/index.js';
|
|
4
4
|
export const runDetailInputContext = defineContext('run-detail-input', 'input');
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Run detail — normal {@link AsyncScreen}. The document arrives via navigation
|
|
7
|
+
* context (or empty); refresh is the shared R / ui-refresh path.
|
|
8
|
+
*/
|
|
9
|
+
export class RunDetailScreen extends AsyncScreen {
|
|
6
10
|
frameId = 'run';
|
|
7
11
|
topTab() {
|
|
8
12
|
return null;
|
|
@@ -19,30 +23,28 @@ export class RunDetailScreen extends BaseScreen {
|
|
|
19
23
|
: base,
|
|
20
24
|
};
|
|
21
25
|
id = RunDetailScreen.route.id;
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
reload(context) {
|
|
27
|
+
this.retry(context);
|
|
28
|
+
}
|
|
29
|
+
load(context) {
|
|
24
30
|
if (context.contexts?.has(runDetailInputContext))
|
|
25
|
-
|
|
26
|
-
|
|
31
|
+
return Promise.resolve(context.contexts.get(runDetailInputContext).document);
|
|
32
|
+
return Promise.resolve({});
|
|
27
33
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return
|
|
42
|
-
title: runDetailTitle(this.document, runId),
|
|
43
|
-
meta: '↑↓ scroll · Esc back · q quit',
|
|
44
|
-
status: ' ↑↓ scroll · Esc back · q quit ',
|
|
45
|
-
});
|
|
34
|
+
renderLoading() {
|
|
35
|
+
return ['Loading run…'];
|
|
36
|
+
}
|
|
37
|
+
renderReady(document, context) {
|
|
38
|
+
return runDetailTree(document, context.params?.runId ?? '');
|
|
39
|
+
}
|
|
40
|
+
chrome(context) {
|
|
41
|
+
return {
|
|
42
|
+
title: runDetailTitle(this.readyValue ?? {}, context.params?.runId ?? ''),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Read-only — kernel basics cover ↑↓ / Esc / q; no extra chips. */
|
|
46
|
+
actions() {
|
|
47
|
+
return [];
|
|
46
48
|
}
|
|
47
49
|
/**
|
|
48
50
|
* F5 — the pipe's own output, and it is `string[]`.
|
|
@@ -54,17 +56,8 @@ export class RunDetailScreen extends BaseScreen {
|
|
|
54
56
|
* path lays the body out on a cell grid of a fixed width, and a pipe has no
|
|
55
57
|
* width — on run #4928 it clipped 420 transcript rows at 80 columns, losing
|
|
56
58
|
* 97.7 % of a 1.34 MB captured agent transcript.
|
|
57
|
-
*
|
|
58
|
-
* This is NOT a `ScreenPresentation.lines` coming back (R3): `present()` still
|
|
59
|
-
* hands over a tree and nothing else. Both shapes are projections of the block's
|
|
60
|
-
* ONE keyed traversal (`flatRows`), so there is no second description of the
|
|
61
|
-
* screen to keep in step — see `tests/Screen/run-detail-non-tty.test.ts`, which
|
|
62
|
-
* asserts the two row for row.
|
|
63
59
|
*/
|
|
64
60
|
renderNonInteractive(context) {
|
|
65
|
-
return runDetailRows(this.
|
|
66
|
-
}
|
|
67
|
-
reset() {
|
|
68
|
-
this.document = {};
|
|
61
|
+
return runDetailRows(this.readyValue ?? {}, context.params?.runId ?? '');
|
|
69
62
|
}
|
|
70
63
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import type { FullScreenChrome, ListFilterBadge } from '../../../Screen/full-screen.js';
|
|
1
2
|
import { ListScreen } from '../../../Screen/list-screen.js';
|
|
2
|
-
import {
|
|
3
|
+
import type { ScreenContext } from '../../../Screen/screen.js';
|
|
3
4
|
import { type RunCriteria } from '../Data/query.js';
|
|
4
5
|
import { type RunPage, type RunRow } from '../Data/runs.js';
|
|
5
6
|
export declare class RunsScreen extends ListScreen<RunRow, number> {
|
|
6
|
-
readonly frameId = "
|
|
7
|
+
readonly frameId = "runs";
|
|
7
8
|
readonly topTabOverlay = true;
|
|
8
9
|
protected readonly table: {
|
|
9
10
|
readonly noun: "run";
|
|
@@ -78,7 +79,10 @@ export declare class RunsScreen extends ListScreen<RunRow, number> {
|
|
|
78
79
|
* clients/test doubles, as on the ticket list. */
|
|
79
80
|
protected filterRow(row: RunRow): boolean;
|
|
80
81
|
protected renderLoading(): string[];
|
|
81
|
-
|
|
82
|
+
/** Runs expose only the closed cycle — one badge, always shown (GAIA-375 A′). */
|
|
83
|
+
protected badges(): readonly ListFilterBadge[];
|
|
84
|
+
protected chrome(context: ScreenContext): FullScreenChrome;
|
|
85
|
+
protected actions(): import("../../../Component/footer-actions/index.js").ActionItem[];
|
|
82
86
|
activate(index: number, context: ScreenContext): boolean;
|
|
83
87
|
select(index: number, context: ScreenContext): boolean;
|
|
84
88
|
handleInput(input: string, context: ScreenContext): boolean;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import { actionItem } from '../../../Component/footer-actions/index.js';
|
|
1
2
|
import { ListScreen } from '../../../Screen/list-screen.js';
|
|
2
|
-
import { presentation, } from '../../../Screen/screen.js';
|
|
3
3
|
import { CLOSED_FILTER_VALUES, cycleValue } from '../../cycle.js';
|
|
4
4
|
import { DEFAULT_RUN_CRITERIA } from '../Data/query.js';
|
|
5
5
|
import { loadProjectRuns, RUN_TABLE, selectedRunTicketId, } from '../Data/runs.js';
|
|
6
6
|
export class RunsScreen extends ListScreen {
|
|
7
|
-
frameId = '
|
|
7
|
+
frameId = 'runs';
|
|
8
8
|
topTabOverlay = true;
|
|
9
9
|
table = RUN_TABLE;
|
|
10
10
|
topTab() {
|
|
@@ -63,55 +63,38 @@ export class RunsScreen extends ListScreen {
|
|
|
63
63
|
renderLoading() {
|
|
64
64
|
return ['Loading runs…'];
|
|
65
65
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
: 'closed
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
// Built by `ListScreen` since GAIA-294, which is what makes "must not drift
|
|
88
|
-
// from the ticket list" a mechanism rather than a promise.
|
|
89
|
-
const paging = this.pagingSegments();
|
|
90
|
-
// The rows ON SCREEN and the size of the FILTERED COLLECTION, side by side:
|
|
91
|
-
// `7 of 1340 run(s)` when the server reported the total, `7 of ? run(s)`
|
|
92
|
-
// when it reported none (GAIA-292 D6c/D6d). Neither quantity replaces the
|
|
93
|
-
// other, so the row count never jumps; the total is read off the value the
|
|
94
|
-
// rows came with (D4/D4a) and from no second screen member — which is now
|
|
95
|
-
// structural rather than careful, because the server answered both from one
|
|
96
|
-
// query. Empty while the page loads (D6c), which is why the segments are
|
|
97
|
-
// joined rather than concatenated — an omitted count must not leave the line
|
|
98
|
-
// opening with a separator.
|
|
99
|
-
const count = this.countLabel();
|
|
100
|
-
return presentation(this.surface(context), {
|
|
101
|
-
selectable: rows.length,
|
|
66
|
+
/** Runs expose only the closed cycle — one badge, always shown (GAIA-375 A′). */
|
|
67
|
+
badges() {
|
|
68
|
+
if (this.criteria.closed === null) {
|
|
69
|
+
return [{ id: 'closed', label: 'x closed' }];
|
|
70
|
+
}
|
|
71
|
+
return [
|
|
72
|
+
{
|
|
73
|
+
id: 'closed',
|
|
74
|
+
label: 'x closed',
|
|
75
|
+
value: this.criteria.closed ? 'closed: only' : 'closed: hidden',
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
chrome(context) {
|
|
80
|
+
// Count + page in header meta; ←/→ in footer actions (GAIA-375 / GAIA-292).
|
|
81
|
+
const pagingLine = [this.countLabel(), ...this.pagingSegments()]
|
|
82
|
+
.filter(Boolean)
|
|
83
|
+
.join(' · ');
|
|
84
|
+
return {
|
|
85
|
+
selectable: this.rows.length,
|
|
102
86
|
selectedIndex: this.selectedIndex,
|
|
103
|
-
// The node a click on each row lands on (GAIA-340 Task 11).
|
|
104
87
|
rowIds: this.rowNodeIds,
|
|
105
88
|
title: `${context.project?.name ?? context.project?.slug ?? ''} · runs`,
|
|
106
|
-
meta:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
89
|
+
meta: pagingLine,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
actions() {
|
|
93
|
+
return [
|
|
94
|
+
actionItem('runs-enter', 'Enter', 'ticket'),
|
|
95
|
+
actionItem('runs-tab', 'Tab', 'tabs'),
|
|
96
|
+
...this.pagingActionItems(),
|
|
97
|
+
];
|
|
115
98
|
}
|
|
116
99
|
activate(index, context) {
|
|
117
100
|
const id = this.readyValue
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { FullScreenChrome, ListFilterBadge } from '../../../Screen/full-screen.js';
|
|
2
|
+
import { ListScreen } from '../../../Screen/list-screen.js';
|
|
3
|
+
import type { ScreenContext } from '../../../Screen/screen.js';
|
|
4
|
+
import { type RunPage, type RunRow } from '../Data/runs.js';
|
|
5
|
+
/** Runs attributed to one user via ticket assignee (GAIA-377 D2). */
|
|
6
|
+
export declare class UserRunsScreen extends ListScreen<RunRow, number> {
|
|
7
|
+
readonly frameId = "user-runs";
|
|
8
|
+
readonly topTabOverlay = true;
|
|
9
|
+
protected readonly table: {
|
|
10
|
+
readonly noun: "run";
|
|
11
|
+
readonly columns: readonly [{
|
|
12
|
+
readonly key: "run";
|
|
13
|
+
readonly label: "RUN";
|
|
14
|
+
readonly width: 8;
|
|
15
|
+
readonly fields: readonly ["drupal_internal__id"];
|
|
16
|
+
readonly get: (r: RunRow) => string;
|
|
17
|
+
}, {
|
|
18
|
+
readonly key: "ticket";
|
|
19
|
+
readonly label: "TICKET";
|
|
20
|
+
readonly width: 10;
|
|
21
|
+
readonly fields: readonly ["ticket_id"];
|
|
22
|
+
readonly get: (r: RunRow) => string;
|
|
23
|
+
}, {
|
|
24
|
+
readonly key: "state";
|
|
25
|
+
readonly label: "STATE";
|
|
26
|
+
readonly width: 8;
|
|
27
|
+
readonly fields: readonly ["state", "heartbeat", "claim_expires_at"];
|
|
28
|
+
readonly get: (r: RunRow, nowSeconds: number) => string;
|
|
29
|
+
}, {
|
|
30
|
+
readonly key: "phase";
|
|
31
|
+
readonly label: "PHASE";
|
|
32
|
+
readonly width: 13;
|
|
33
|
+
readonly fields: readonly ["state_at_start"];
|
|
34
|
+
readonly get: (r: RunRow) => string;
|
|
35
|
+
}, {
|
|
36
|
+
readonly key: "agent";
|
|
37
|
+
readonly label: "AGENT";
|
|
38
|
+
readonly width: 8;
|
|
39
|
+
readonly fields: readonly ["agent"];
|
|
40
|
+
readonly get: (r: RunRow) => string;
|
|
41
|
+
}, {
|
|
42
|
+
readonly key: "started";
|
|
43
|
+
readonly label: "STARTED";
|
|
44
|
+
readonly width: 0;
|
|
45
|
+
readonly grow: 1;
|
|
46
|
+
readonly fields: readonly ["started_at", "created"];
|
|
47
|
+
readonly get: (r: RunRow, nowSeconds: number) => string;
|
|
48
|
+
}];
|
|
49
|
+
readonly emptyMessage: "No runs for this project.";
|
|
50
|
+
};
|
|
51
|
+
topTab(): {
|
|
52
|
+
readonly level: "user";
|
|
53
|
+
readonly tab: "runs";
|
|
54
|
+
};
|
|
55
|
+
static readonly route: {
|
|
56
|
+
id: string;
|
|
57
|
+
path: string;
|
|
58
|
+
fallback: {
|
|
59
|
+
routeId: string;
|
|
60
|
+
};
|
|
61
|
+
tab: {
|
|
62
|
+
id: string;
|
|
63
|
+
label: string;
|
|
64
|
+
scope: string;
|
|
65
|
+
order: number;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
readonly id: string;
|
|
69
|
+
private criteria;
|
|
70
|
+
protected tableContext(context: ScreenContext): number;
|
|
71
|
+
protected loadRows(context: ScreenContext, page: number): Promise<RunPage>;
|
|
72
|
+
protected filterRow(row: RunRow): boolean;
|
|
73
|
+
protected renderLoading(): string[];
|
|
74
|
+
protected badges(): readonly ListFilterBadge[];
|
|
75
|
+
protected chrome(context: ScreenContext): FullScreenChrome;
|
|
76
|
+
protected actions(): import("../../../Component/footer-actions/index.js").ActionItem[];
|
|
77
|
+
activate(index: number, context: ScreenContext): boolean;
|
|
78
|
+
handleInput(input: string, context: ScreenContext): boolean;
|
|
79
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { actionItem } from '../../../Component/footer-actions/index.js';
|
|
2
|
+
import { ListScreen } from '../../../Screen/list-screen.js';
|
|
3
|
+
import { CLOSED_FILTER_VALUES, cycleValue } from '../../cycle.js';
|
|
4
|
+
import { DEFAULT_RUN_CRITERIA } from '../Data/query.js';
|
|
5
|
+
import { loadUserRuns, RUN_TABLE, selectedRunTicketId, } from '../Data/runs.js';
|
|
6
|
+
/** Runs attributed to one user via ticket assignee (GAIA-377 D2). */
|
|
7
|
+
export class UserRunsScreen extends ListScreen {
|
|
8
|
+
frameId = 'user-runs';
|
|
9
|
+
topTabOverlay = true;
|
|
10
|
+
table = RUN_TABLE;
|
|
11
|
+
topTab() {
|
|
12
|
+
return { level: 'user', tab: 'runs' };
|
|
13
|
+
}
|
|
14
|
+
static route = {
|
|
15
|
+
id: 'user.runs',
|
|
16
|
+
path: '/users/:userId/runs',
|
|
17
|
+
fallback: { routeId: 'users' },
|
|
18
|
+
tab: { id: 'runs', label: 'Runs', scope: 'user', order: 30 },
|
|
19
|
+
};
|
|
20
|
+
id = UserRunsScreen.route.id;
|
|
21
|
+
criteria = { ...DEFAULT_RUN_CRITERIA };
|
|
22
|
+
tableContext(context) {
|
|
23
|
+
return context.nowSeconds?.() ?? Math.floor(Date.now() / 1000);
|
|
24
|
+
}
|
|
25
|
+
loadRows(context, page) {
|
|
26
|
+
if (!context.services)
|
|
27
|
+
throw new Error('User runs requires services');
|
|
28
|
+
const userId = context.user?.id ?? context.params?.userId;
|
|
29
|
+
if (!userId)
|
|
30
|
+
throw new Error('User runs requires userId');
|
|
31
|
+
return loadUserRuns(context.services.client, userId, this.criteria, page);
|
|
32
|
+
}
|
|
33
|
+
filterRow(row) {
|
|
34
|
+
return this.criteria.closed === null || row.closed === this.criteria.closed;
|
|
35
|
+
}
|
|
36
|
+
renderLoading() {
|
|
37
|
+
return ['Loading runs…'];
|
|
38
|
+
}
|
|
39
|
+
badges() {
|
|
40
|
+
if (this.criteria.closed === null) {
|
|
41
|
+
return [{ id: 'closed', label: 'x closed' }];
|
|
42
|
+
}
|
|
43
|
+
return [
|
|
44
|
+
{
|
|
45
|
+
id: 'closed',
|
|
46
|
+
label: 'x closed',
|
|
47
|
+
value: this.criteria.closed ? 'closed: only' : 'closed: hidden',
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
chrome(context) {
|
|
52
|
+
const name = context.user?.name ?? context.params?.userName ?? 'user';
|
|
53
|
+
const pagingLine = [this.countLabel(), ...this.pagingSegments()]
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
.join(' · ');
|
|
56
|
+
return {
|
|
57
|
+
selectable: this.rows.length,
|
|
58
|
+
selectedIndex: this.selectedIndex,
|
|
59
|
+
rowIds: this.rowNodeIds,
|
|
60
|
+
title: `${name} · runs`,
|
|
61
|
+
meta: pagingLine,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
actions() {
|
|
65
|
+
return [
|
|
66
|
+
actionItem('user-runs-enter', 'Enter', 'ticket'),
|
|
67
|
+
actionItem('user-runs-tab', 'Tab', 'tabs'),
|
|
68
|
+
...this.pagingActionItems(),
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
activate(index, context) {
|
|
72
|
+
const id = this.readyValue
|
|
73
|
+
? selectedRunTicketId(this.readyValue.rows, index)
|
|
74
|
+
: null;
|
|
75
|
+
if (!id || !context.navigate)
|
|
76
|
+
return false;
|
|
77
|
+
context.navigate('ticket.detail', { ticketId: id, tab: 'runs' });
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
handleInput(input, context) {
|
|
81
|
+
if (input === 'x') {
|
|
82
|
+
this.criteria = {
|
|
83
|
+
...this.criteria,
|
|
84
|
+
closed: cycleValue(CLOSED_FILTER_VALUES, this.criteria.closed),
|
|
85
|
+
};
|
|
86
|
+
this.page = 0;
|
|
87
|
+
this.retry(context);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
if (this.handlePagingInput(input, context))
|
|
91
|
+
return true;
|
|
92
|
+
if (this.handleCursorInput(input, context))
|
|
93
|
+
return true;
|
|
94
|
+
if (input === 'enter')
|
|
95
|
+
return this.activate(this.selectedIndex, context);
|
|
96
|
+
return super.handleInput(input, context);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -36,10 +36,22 @@ export interface StatisticsRow {
|
|
|
36
36
|
* the blocks the document carries — an empty array would render as an empty
|
|
37
37
|
* `PER PROJECT` heading.
|
|
38
38
|
*/
|
|
39
|
+
/**
|
|
40
|
+
* One UTC calendar day in a user-scoped `by_day` series (GAIA-377).
|
|
41
|
+
*
|
|
42
|
+
* Present only under `?user_id=`; the user overview draws the last seven days
|
|
43
|
+
* as weekday rows. `label` is the English short weekday the server computed.
|
|
44
|
+
*/
|
|
45
|
+
export interface StatisticsDayRow {
|
|
46
|
+
date: string;
|
|
47
|
+
label: string;
|
|
48
|
+
count: number;
|
|
49
|
+
}
|
|
39
50
|
export interface StatisticsBlock {
|
|
40
51
|
total: StatisticsWindows;
|
|
41
52
|
byProject?: StatisticsRow[];
|
|
42
53
|
byUser?: StatisticsRow[];
|
|
54
|
+
byDay?: StatisticsDayRow[];
|
|
43
55
|
}
|
|
44
56
|
/** The whole document, as the Dashboard consumes it. */
|
|
45
57
|
export interface Statistics {
|
|
@@ -68,20 +80,33 @@ export type StatisticsResult = {
|
|
|
68
80
|
};
|
|
69
81
|
/** How an `id: null` breakdown row is named on screen (spec D8/D11). */
|
|
70
82
|
export declare const UNATTRIBUTED_LABEL = "\u2014 unattributed";
|
|
83
|
+
/**
|
|
84
|
+
* Closed tickets per run for each trailing window (GAIA-377 D4).
|
|
85
|
+
*
|
|
86
|
+
* Derived on the client from totals that already travel together — a
|
|
87
|
+
* server-computed copy would be a second source of truth that can disagree.
|
|
88
|
+
* `null` when the window has zero runs: never `0`, never `Infinity`.
|
|
89
|
+
*/
|
|
90
|
+
export declare function closedPerRun(tickets: StatisticsWindows, runs: StatisticsWindows): {
|
|
91
|
+
day: number | null;
|
|
92
|
+
week: number | null;
|
|
93
|
+
month: number | null;
|
|
94
|
+
};
|
|
95
|
+
/** Format a closed-per-run value for the screen: two decimals, or `—`. */
|
|
96
|
+
export declare const formatClosedPerRun: (value: number | null) => string;
|
|
71
97
|
/**
|
|
72
98
|
* Read the control plane's ticket and run statistics — the ONE source of every
|
|
73
99
|
* figure the Dashboard renders (AC-8).
|
|
74
100
|
*
|
|
75
101
|
* With `projectId` the read is scoped and the server drops `by_project` from
|
|
76
|
-
* both blocks (spec D5).
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* reached, so it cannot even be accepted as an alias.
|
|
102
|
+
* both blocks (spec D5). With `userId` (GAIA-377 D3) it drops `by_user` and
|
|
103
|
+
* keeps `by_project`. The parameter names are `project_id` / `user_id` and NOT
|
|
104
|
+
* bare letters: JSON:API requires an implementation-specific query parameter to
|
|
105
|
+
* contain at least one non-`a-z` character, and Drupal enforces it.
|
|
81
106
|
*
|
|
82
107
|
* The client is the caller's, singular and used once: the TUI reads everything
|
|
83
108
|
* through `services.client` (the `session` profile), and `pm` cannot read
|
|
84
109
|
* `gaia_run` — a loader that reached for another client would render zero runs
|
|
85
110
|
* as a real measurement (spec R3).
|
|
86
111
|
*/
|
|
87
|
-
export declare function loadStatistics(client: DropshClient, projectId?: string): Promise<StatisticsResult>;
|
|
112
|
+
export declare function loadStatistics(client: DropshClient, projectId?: string, userId?: string): Promise<StatisticsResult>;
|
|
@@ -25,14 +25,28 @@ function rowsOf(value) {
|
|
|
25
25
|
};
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
|
+
function dayRowsOf(value) {
|
|
29
|
+
if (!Array.isArray(value))
|
|
30
|
+
return undefined;
|
|
31
|
+
return value.map((entry) => {
|
|
32
|
+
const r = (entry ?? {});
|
|
33
|
+
return {
|
|
34
|
+
date: typeof r.date === 'string' ? r.date : '',
|
|
35
|
+
label: typeof r.label === 'string' ? r.label : '',
|
|
36
|
+
count: num(r.count),
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
}
|
|
28
40
|
function blockOf(value) {
|
|
29
41
|
const b = (value ?? {});
|
|
30
42
|
const byProject = rowsOf(b.by_project);
|
|
31
43
|
const byUser = rowsOf(b.by_user);
|
|
44
|
+
const byDay = dayRowsOf(b.by_day);
|
|
32
45
|
return {
|
|
33
46
|
total: windowsOf(b.total),
|
|
34
47
|
...(byProject ? { byProject } : {}),
|
|
35
48
|
...(byUser ? { byUser } : {}),
|
|
49
|
+
...(byDay ? { byDay } : {}),
|
|
36
50
|
};
|
|
37
51
|
}
|
|
38
52
|
/** The HTTP status of an unknown throw — dropsh's `HttpError` carries one. */
|
|
@@ -86,27 +100,46 @@ function jsonApiErrorDetail(caught) {
|
|
|
86
100
|
// fall back to a non-empty marker rather than to `undefined`.
|
|
87
101
|
return typeof detail === 'string' && detail !== '' ? detail : 'HTTP 404';
|
|
88
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Closed tickets per run for each trailing window (GAIA-377 D4).
|
|
105
|
+
*
|
|
106
|
+
* Derived on the client from totals that already travel together — a
|
|
107
|
+
* server-computed copy would be a second source of truth that can disagree.
|
|
108
|
+
* `null` when the window has zero runs: never `0`, never `Infinity`.
|
|
109
|
+
*/
|
|
110
|
+
export function closedPerRun(tickets, runs) {
|
|
111
|
+
const ratio = (t, r) => (r > 0 ? t / r : null);
|
|
112
|
+
return {
|
|
113
|
+
day: ratio(tickets.day, runs.day),
|
|
114
|
+
week: ratio(tickets.week, runs.week),
|
|
115
|
+
month: ratio(tickets.month, runs.month),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** Format a closed-per-run value for the screen: two decimals, or `—`. */
|
|
119
|
+
export const formatClosedPerRun = (value) => value === null ? '—' : value.toFixed(2);
|
|
89
120
|
/**
|
|
90
121
|
* Read the control plane's ticket and run statistics — the ONE source of every
|
|
91
122
|
* figure the Dashboard renders (AC-8).
|
|
92
123
|
*
|
|
93
124
|
* With `projectId` the read is scoped and the server drops `by_project` from
|
|
94
|
-
* both blocks (spec D5).
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* reached, so it cannot even be accepted as an alias.
|
|
125
|
+
* both blocks (spec D5). With `userId` (GAIA-377 D3) it drops `by_user` and
|
|
126
|
+
* keeps `by_project`. The parameter names are `project_id` / `user_id` and NOT
|
|
127
|
+
* bare letters: JSON:API requires an implementation-specific query parameter to
|
|
128
|
+
* contain at least one non-`a-z` character, and Drupal enforces it.
|
|
99
129
|
*
|
|
100
130
|
* The client is the caller's, singular and used once: the TUI reads everything
|
|
101
131
|
* through `services.client` (the `session` profile), and `pm` cannot read
|
|
102
132
|
* `gaia_run` — a loader that reached for another client would render zero runs
|
|
103
133
|
* as a real measurement (spec R3).
|
|
104
134
|
*/
|
|
105
|
-
export async function loadStatistics(client, projectId) {
|
|
135
|
+
export async function loadStatistics(client, projectId, userId) {
|
|
106
136
|
const params = new DrupalJsonApiParams();
|
|
107
137
|
if (projectId !== undefined) {
|
|
108
138
|
params.addCustomParam({ project_id: projectId });
|
|
109
139
|
}
|
|
140
|
+
if (userId !== undefined) {
|
|
141
|
+
params.addCustomParam({ user_id: userId });
|
|
142
|
+
}
|
|
110
143
|
let doc;
|
|
111
144
|
try {
|
|
112
145
|
doc = (await client.get('gaia/statistics', params));
|