@gaia-ai/addon-gaia-ui 0.11.5 → 0.12.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/Entity/conductor/Data/conductors.d.ts +34 -9
- package/dist/src/Entity/conductor/Data/conductors.js +65 -18
- package/dist/src/Entity/conductor/Data/own-conductor.d.ts +5 -1
- package/dist/src/Entity/conductor/Data/own-conductor.js +5 -1
- package/dist/src/Entity/conductor/Data/own-rows.d.ts +59 -0
- package/dist/src/Entity/conductor/Data/own-rows.js +87 -0
- package/dist/src/Entity/conductor/Screen/conductors.d.ts +92 -16
- package/dist/src/Entity/conductor/Screen/conductors.js +103 -38
- package/dist/src/Entity/conductor/Screen/project-conductors.d.ts +99 -21
- package/dist/src/Entity/conductor/Screen/project-conductors.js +122 -51
- package/dist/src/Entity/project/Data/project.d.ts +8 -0
- package/dist/src/Entity/project/Data/project.js +18 -2
- package/dist/src/Entity/run/Data/query.d.ts +3 -1
- package/dist/src/Entity/run/Data/query.js +3 -1
- package/dist/src/Entity/statistics/Data/statistics.d.ts +99 -146
- package/dist/src/Entity/statistics/Data/statistics.js +118 -163
- package/dist/src/Entity/statistics/Screen/dashboard.d.ts +24 -40
- package/dist/src/Entity/statistics/Screen/dashboard.js +111 -214
- package/dist/src/Entity/statistics/render.d.ts +6 -0
- package/dist/src/Entity/statistics/render.js +69 -0
- package/dist/src/Entity/team/Screen/team-users.js +28 -10
- package/dist/src/Entity/team/Screen/teams-screen.d.ts +4 -4
- package/dist/src/Entity/team/Screen/teams-screen.js +29 -80
- package/dist/src/Entity/ticket/Form/create-data.d.ts +8 -0
- package/dist/src/Entity/ticket/Form/create-data.js +11 -4
- package/dist/src/Entity/ticket/Form/edit-data.d.ts +2 -0
- package/dist/src/Entity/ticket/Form/edit-data.js +6 -4
- package/dist/src/Entity/ticket/Screen/create-ticket-form.d.ts +7 -0
- package/dist/src/Entity/ticket/Screen/create-ticket-form.js +18 -0
- package/dist/src/Entity/ticket/Screen/edit-assignment-form.d.ts +7 -0
- package/dist/src/Entity/ticket/Screen/edit-assignment-form.js +21 -1
- package/dist/src/Entity/user/Screen/user-overview.d.ts +4 -4
- package/dist/src/Entity/user/Screen/user-overview.js +20 -99
- package/dist/src/Entity/workspace/Data/workspaces.d.ts +20 -0
- package/dist/src/Entity/workspace/Data/workspaces.js +20 -0
- package/dist/src/Entity/workspace/Screen/workspace-picker.d.ts +34 -0
- package/dist/src/Entity/workspace/Screen/workspace-picker.js +167 -0
- package/dist/src/Kernel/chrome.d.ts +10 -0
- package/dist/src/Kernel/chrome.js +26 -5
- package/dist/src/Kernel/tui-kernel.js +22 -1
- package/dist/src/Routing/routes.d.ts +6 -0
- package/dist/src/Routing/routes.js +8 -0
- package/dist/src/launcher.d.ts +9 -0
- package/dist/src/launcher.js +7 -0
- package/dist/src/plugin.js +13 -0
- package/dist/src/types.d.ts +36 -0
- package/package.json +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DropshClient } from '../../../types.js';
|
|
2
|
+
import { type Page } from '../../collection.js';
|
|
2
3
|
import { type ListOutcome, type OptionClients } from '../../transport.js';
|
|
3
4
|
import { type ConductorStatus } from './liveness.js';
|
|
4
5
|
/** One conductor of the control plane, as this renderer needs it. */
|
|
@@ -12,6 +13,8 @@ export interface Conductor {
|
|
|
12
13
|
status: string | undefined;
|
|
13
14
|
/** The conductor's project, `null` when unlinked. */
|
|
14
15
|
projectId: string | null;
|
|
16
|
+
/** The user who owns this record, `null` when unowned (GAIA-460 D8/D9). */
|
|
17
|
+
ownerUserId: string | null;
|
|
15
18
|
}
|
|
16
19
|
/**
|
|
17
20
|
* The conductor collection through one client, in server order.
|
|
@@ -375,19 +378,41 @@ export interface ConductorRowContext {
|
|
|
375
378
|
*/
|
|
376
379
|
thisHostKey: string;
|
|
377
380
|
}
|
|
381
|
+
/** What narrows a conductor read — both filters AND together in one query. */
|
|
382
|
+
export interface ConductorQuery {
|
|
383
|
+
/** The project's own conductors (GAIA-450 D13). */
|
|
384
|
+
projectId?: string | undefined;
|
|
385
|
+
/** The conductors this user OWNS (GAIA-460 D3) — by owner, never by machine. */
|
|
386
|
+
ownerUserId?: string | undefined;
|
|
387
|
+
}
|
|
378
388
|
/**
|
|
379
|
-
*
|
|
389
|
+
* One PAGE of the conductor collection as cockpit rows (GAIA-460 D6).
|
|
390
|
+
*
|
|
391
|
+
* Sideloads `machine` so capacity (GAIA-353 D5/D10) comes from the host budget.
|
|
392
|
+
* The owner filter rides this same query rather than the returned rows (D3):
|
|
393
|
+
* applied to an already-bounded page it would hide GAIA-458's truncation instead
|
|
394
|
+
* of fixing it.
|
|
380
395
|
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
396
|
+
* A FAILED READ PROPAGATES, and that is the change D6 buys rather than an
|
|
397
|
+
* oversight. The read this replaced swallowed a failure into an empty list, so
|
|
398
|
+
* one failed 5 s poll left the screen navigable; on the shared `ListScreen`
|
|
399
|
+
* base a rejected load is the error state every other GAIA list already shows,
|
|
400
|
+
* and one collection may not keep a second spelling of what a failure looks
|
|
401
|
+
* like. The cost is real — a single bad poll now replaces the screen — and it
|
|
402
|
+
* is the convention's cost, not this read's decision to make.
|
|
403
|
+
*/
|
|
404
|
+
export declare const listConductorRowPage: (client: DropshClient, ctx: ConductorRowContext, query: ConductorQuery, page?: number) => Promise<Page<ConductorRow>>;
|
|
405
|
+
/**
|
|
406
|
+
* How large the collection under `query` is, or `null` when that is not
|
|
407
|
+
* established (GAIA-460 D4).
|
|
385
408
|
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
409
|
+
* One row is requested because no row is wanted: the answer is `meta.count`, and
|
|
410
|
+
* asking for a page of them to count them is the walk GAIA-292 removed. `null`
|
|
411
|
+
* covers both a plane that reports no count (R1) and a failed probe — an
|
|
412
|
+
* explanation is never a precondition, so its failure may not disturb the list
|
|
413
|
+
* it was meant to explain.
|
|
389
414
|
*/
|
|
390
|
-
export declare const
|
|
415
|
+
export declare const countConductors: (client: DropshClient, query: ConductorQuery) => Promise<number | null>;
|
|
391
416
|
/**
|
|
392
417
|
* Project uuid → name, for the Conductors screen's project column.
|
|
393
418
|
*
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import { toUnixSeconds } from '@gaia-ai/core';
|
|
7
7
|
import { DrupalJsonApiParams } from 'drupal-jsonapi-params';
|
|
8
8
|
import { humanizeAge } from '../../../lib/format.js';
|
|
9
|
+
import { loadPage, } from '../../collection.js';
|
|
10
|
+
import { LIST_PAGE_SIZE } from '../../cycle.js';
|
|
9
11
|
import { listOutcome, listPreferringWrite, } from '../../transport.js';
|
|
10
12
|
import { deriveConductorStatus } from './liveness.js';
|
|
11
13
|
const str = (v) => typeof v === 'string' && v.length > 0 ? v : undefined;
|
|
@@ -19,6 +21,7 @@ const toConductor = (r) => ({
|
|
|
19
21
|
machineId: str(r.attr('conductor_id')) ?? str(r.attr('machine_id')),
|
|
20
22
|
status: str(r.attr('status')),
|
|
21
23
|
projectId: r.rel('project_id'),
|
|
24
|
+
ownerUserId: r.rel('owner_user_id'),
|
|
22
25
|
});
|
|
23
26
|
/**
|
|
24
27
|
* The conductor collection through one client, in server order.
|
|
@@ -377,32 +380,76 @@ const toConductorRow = (raw, ctx, machinesById) => {
|
|
|
377
380
|
};
|
|
378
381
|
};
|
|
379
382
|
/**
|
|
380
|
-
* The
|
|
383
|
+
* The query both reads share: the narrowing, the declared order, the sideload.
|
|
381
384
|
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
* GAIA-450 D13: an optional `projectId` narrows the read to the project's own
|
|
388
|
-
* conductors — the project-scoped Conductors tab's whole filter, applied at
|
|
389
|
-
* the query rather than client-side so the read stays one round trip.
|
|
385
|
+
* `sort=-last_seen` is not cosmetic (GAIA-460 D7, GAIA-458 AC-2). Paging over a
|
|
386
|
+
* collection with no declared order lets a row repeat on two pages while another
|
|
387
|
+
* is skipped entirely, so the order is a correctness property of the paging
|
|
388
|
+
* before it is a convenience for the reader.
|
|
390
389
|
*/
|
|
391
|
-
|
|
390
|
+
const conductorParams = (query) => {
|
|
392
391
|
const params = new DrupalJsonApiParams().addInclude(['machine']);
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
392
|
+
params.addSort('last_seen', 'DESC');
|
|
393
|
+
if (query.projectId)
|
|
394
|
+
params.addFilter('project_id.id', query.projectId);
|
|
395
|
+
if (query.ownerUserId)
|
|
396
|
+
params.addFilter('owner_user_id.id', query.ownerUserId);
|
|
397
|
+
return params;
|
|
398
|
+
};
|
|
399
|
+
/**
|
|
400
|
+
* One PAGE of the conductor collection as cockpit rows (GAIA-460 D6).
|
|
401
|
+
*
|
|
402
|
+
* Sideloads `machine` so capacity (GAIA-353 D5/D10) comes from the host budget.
|
|
403
|
+
* The owner filter rides this same query rather than the returned rows (D3):
|
|
404
|
+
* applied to an already-bounded page it would hide GAIA-458's truncation instead
|
|
405
|
+
* of fixing it.
|
|
406
|
+
*
|
|
407
|
+
* A FAILED READ PROPAGATES, and that is the change D6 buys rather than an
|
|
408
|
+
* oversight. The read this replaced swallowed a failure into an empty list, so
|
|
409
|
+
* one failed 5 s poll left the screen navigable; on the shared `ListScreen`
|
|
410
|
+
* base a rejected load is the error state every other GAIA list already shows,
|
|
411
|
+
* and one collection may not keep a second spelling of what a failure looks
|
|
412
|
+
* like. The cost is real — a single bad poll now replaces the screen — and it
|
|
413
|
+
* is the convention's cost, not this read's decision to make.
|
|
414
|
+
*/
|
|
415
|
+
export const listConductorRowPage = async (client, ctx, query, page = 0) => {
|
|
416
|
+
const window = {
|
|
417
|
+
offset: page * LIST_PAGE_SIZE,
|
|
418
|
+
limit: LIST_PAGE_SIZE,
|
|
419
|
+
};
|
|
420
|
+
const params = conductorParams(query);
|
|
421
|
+
params.addPageLimit(window.limit);
|
|
422
|
+
params.addPageOffset(window.offset);
|
|
423
|
+
return loadPage(client, 'gaia_conductor/gaia_conductor', params, (data, included) => {
|
|
397
424
|
const machinesById = new Map();
|
|
398
|
-
for (const inc of
|
|
425
|
+
for (const inc of included)
|
|
399
426
|
if (inc.id)
|
|
400
427
|
machinesById.set(inc.id, inc);
|
|
401
|
-
|
|
402
|
-
|
|
428
|
+
return data.map((raw) => toConductorRow(raw, ctx, machinesById));
|
|
429
|
+
}, page, window);
|
|
430
|
+
};
|
|
431
|
+
/**
|
|
432
|
+
* How large the collection under `query` is, or `null` when that is not
|
|
433
|
+
* established (GAIA-460 D4).
|
|
434
|
+
*
|
|
435
|
+
* One row is requested because no row is wanted: the answer is `meta.count`, and
|
|
436
|
+
* asking for a page of them to count them is the walk GAIA-292 removed. `null`
|
|
437
|
+
* covers both a plane that reports no count (R1) and a failed probe — an
|
|
438
|
+
* explanation is never a precondition, so its failure may not disturb the list
|
|
439
|
+
* it was meant to explain.
|
|
440
|
+
*/
|
|
441
|
+
export const countConductors = async (client, query) => {
|
|
442
|
+
const params = conductorParams(query);
|
|
443
|
+
params.addPageLimit(1);
|
|
444
|
+
try {
|
|
445
|
+
const doc = (await client.get('gaia_conductor/gaia_conductor', params));
|
|
446
|
+
const count = doc?.meta?.count;
|
|
447
|
+
return typeof count === 'number' && Number.isFinite(count) && count >= 0
|
|
448
|
+
? count
|
|
449
|
+
: null;
|
|
403
450
|
}
|
|
404
451
|
catch {
|
|
405
|
-
return
|
|
452
|
+
return null;
|
|
406
453
|
}
|
|
407
454
|
};
|
|
408
455
|
/**
|
|
@@ -21,7 +21,11 @@ export declare function selectOwnConductor(doc: JsonApiCollectionDoc, gaiaDir: s
|
|
|
21
21
|
* Read the conductor collection and resolve this checkout's own record.
|
|
22
22
|
*
|
|
23
23
|
* One request with the project sideloaded, so the project NAME costs no second
|
|
24
|
-
* round trip. The read is owner-gated
|
|
24
|
+
* round trip. The read is NOT owner-gated: GAIA-391 granted `view gaia_conductor` to every
|
|
25
|
+
* `gaia:session` token, so this reads the whole collection. That is harmless
|
|
26
|
+
* here — the match is on `workspace_root`, a path on THIS machine, which no
|
|
27
|
+
* other user's record can hold. GAIA-460 filters the conductor LIST by owner;
|
|
28
|
+
* this lookup needs no filter because its predicate is already stricter.
|
|
25
29
|
*
|
|
26
30
|
* A failure yields nothing rather than rejecting: this runs on the boot path,
|
|
27
31
|
* and an unreachable control plane must leave the cockpit openable — the two
|
|
@@ -75,7 +75,11 @@ export function selectOwnConductor(doc, gaiaDir) {
|
|
|
75
75
|
* Read the conductor collection and resolve this checkout's own record.
|
|
76
76
|
*
|
|
77
77
|
* One request with the project sideloaded, so the project NAME costs no second
|
|
78
|
-
* round trip. The read is owner-gated
|
|
78
|
+
* round trip. The read is NOT owner-gated: GAIA-391 granted `view gaia_conductor` to every
|
|
79
|
+
* `gaia:session` token, so this reads the whole collection. That is harmless
|
|
80
|
+
* here — the match is on `workspace_root`, a path on THIS machine, which no
|
|
81
|
+
* other user's record can hold. GAIA-460 filters the conductor LIST by owner;
|
|
82
|
+
* this lookup needs no filter because its predicate is already stricter.
|
|
79
83
|
*
|
|
80
84
|
* A failure yields nothing rather than rejecting: this runs on the boot path,
|
|
81
85
|
* and an unreachable control plane must leave the cockpit openable — the two
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ListFilterBadge } from '../../../Component/list-chrome/index.js';
|
|
2
|
+
import type { DropshClient } from '../../../types.js';
|
|
3
|
+
/** Who the operator is, or why the screen could not establish it. */
|
|
4
|
+
export type OwnerOutcome = {
|
|
5
|
+
kind: 'resolved';
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'unavailable';
|
|
10
|
+
reason: string;
|
|
11
|
+
};
|
|
12
|
+
/** The read this identity needs — `resolveWhoCandidate`'s shape, injected. */
|
|
13
|
+
export type OwnerResolver = (client: DropshClient, name: string) => Promise<{
|
|
14
|
+
rows: {
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
}[];
|
|
18
|
+
error?: string;
|
|
19
|
+
}>;
|
|
20
|
+
/**
|
|
21
|
+
* The operator's user uuid, resolved at most once per screen lifetime.
|
|
22
|
+
*
|
|
23
|
+
* WHAT IS CACHED, AND WHAT IS NOT. A success is cached, and so is a definitive
|
|
24
|
+
* miss — a name the collection does not hold will not start holding it inside one
|
|
25
|
+
* session, and re-asking on every refresh would spend a request per poll to learn
|
|
26
|
+
* the same "no". A READ ERROR is cached by neither: it says nothing about the
|
|
27
|
+
* collection, only that this attempt failed, so the next load tries again
|
|
28
|
+
* (GAIA-287 D5).
|
|
29
|
+
*/
|
|
30
|
+
export declare class OwnerIdentity {
|
|
31
|
+
private readonly resolveOwner;
|
|
32
|
+
private cached;
|
|
33
|
+
constructor(resolveOwner: OwnerResolver);
|
|
34
|
+
resolve(client: DropshClient, currentUser: string | undefined): Promise<OwnerOutcome>;
|
|
35
|
+
/** A new project or session means a new context; forget the answer. */
|
|
36
|
+
reset(): void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The shipped default (GAIA-460 D1) — the list opens showing MINE.
|
|
40
|
+
*
|
|
41
|
+
* A product decision, and deliberately not an inheritance from the access layer:
|
|
42
|
+
* GAIA-391 granted `view gaia_conductor` to every session token on purpose, and
|
|
43
|
+
* this default restores what operators had before it without touching that grant.
|
|
44
|
+
*/
|
|
45
|
+
export declare const DEFAULT_CONDUCTOR_MINE = true;
|
|
46
|
+
/** The filter strip both conductor screens show, so neither can word it its own way. */
|
|
47
|
+
export declare const mineBadges: (mine: boolean) => ListFilterBadge[];
|
|
48
|
+
/**
|
|
49
|
+
* `5 hidden by mine — press m`, or '' when there is nothing to say (AC-2).
|
|
50
|
+
*
|
|
51
|
+
* Three cases collapse to the empty string and each for its own reason. `null` is
|
|
52
|
+
* a total nobody established — a plane that reports no `meta.count` (R1) or a
|
|
53
|
+
* failed probe — and a number nobody measured must not be printed. `0` is a
|
|
54
|
+
* measured nothing: an operator sent to press `m` for rows that do not exist
|
|
55
|
+
* learns only that the screen is guessing. Anything else is a fact, and it names
|
|
56
|
+
* the key that releases it, because a count without a remedy is just a smaller
|
|
57
|
+
* mystery.
|
|
58
|
+
*/
|
|
59
|
+
export declare const hiddenNotice: (hidden: number | null) => string;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// GAIA-460 — "which conductors are MINE?", resolved once per screen.
|
|
2
|
+
//
|
|
3
|
+
// The Conductors list opens FILTERED (D1), so the operator's uuid must be known
|
|
4
|
+
// before the first read rather than on the first keypress the way GAIA-287's
|
|
5
|
+
// ticket list resolves it. That is the whole reason this is a small object with
|
|
6
|
+
// a cache instead of a function: the resolution happens on the load path, may
|
|
7
|
+
// fail there, and must not be retried on every 5 s refresh once it has been
|
|
8
|
+
// definitively answered.
|
|
9
|
+
//
|
|
10
|
+
// It takes the RESOLVER as an argument rather than importing it.
|
|
11
|
+
// `tests/boundary.test.ts` G2 forbids a `Data/` module reaching another slice,
|
|
12
|
+
// and `resolveWhoCandidate` lives in the user slice; the screens inject it, as
|
|
13
|
+
// they already inject `exists` and `thisHostKey` into `ConductorRowContext`.
|
|
14
|
+
const messageOf = (caught) => caught instanceof Error ? caught.message : String(caught);
|
|
15
|
+
/**
|
|
16
|
+
* The operator's user uuid, resolved at most once per screen lifetime.
|
|
17
|
+
*
|
|
18
|
+
* WHAT IS CACHED, AND WHAT IS NOT. A success is cached, and so is a definitive
|
|
19
|
+
* miss — a name the collection does not hold will not start holding it inside one
|
|
20
|
+
* session, and re-asking on every refresh would spend a request per poll to learn
|
|
21
|
+
* the same "no". A READ ERROR is cached by neither: it says nothing about the
|
|
22
|
+
* collection, only that this attempt failed, so the next load tries again
|
|
23
|
+
* (GAIA-287 D5).
|
|
24
|
+
*/
|
|
25
|
+
export class OwnerIdentity {
|
|
26
|
+
resolveOwner;
|
|
27
|
+
cached = null;
|
|
28
|
+
constructor(resolveOwner) {
|
|
29
|
+
this.resolveOwner = resolveOwner;
|
|
30
|
+
}
|
|
31
|
+
async resolve(client, currentUser) {
|
|
32
|
+
if (this.cached)
|
|
33
|
+
return this.cached;
|
|
34
|
+
if (currentUser === undefined) {
|
|
35
|
+
// Not cached: the launcher supplies this, and a screen re-entered under a
|
|
36
|
+
// different context may well have one.
|
|
37
|
+
return { kind: 'unavailable', reason: 'mine: no current user' };
|
|
38
|
+
}
|
|
39
|
+
let outcome;
|
|
40
|
+
try {
|
|
41
|
+
outcome = await this.resolveOwner(client, currentUser);
|
|
42
|
+
}
|
|
43
|
+
catch (caught) {
|
|
44
|
+
return { kind: 'unavailable', reason: `mine: ${messageOf(caught)}` };
|
|
45
|
+
}
|
|
46
|
+
if (outcome.error !== undefined) {
|
|
47
|
+
return { kind: 'unavailable', reason: `mine: ${outcome.error}` };
|
|
48
|
+
}
|
|
49
|
+
const row = outcome.rows[0];
|
|
50
|
+
this.cached =
|
|
51
|
+
row === undefined
|
|
52
|
+
? { kind: 'unavailable', reason: `mine: '${currentUser}' not found` }
|
|
53
|
+
: { kind: 'resolved', id: row.id, label: row.label };
|
|
54
|
+
return this.cached;
|
|
55
|
+
}
|
|
56
|
+
/** A new project or session means a new context; forget the answer. */
|
|
57
|
+
reset() {
|
|
58
|
+
this.cached = null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The shipped default (GAIA-460 D1) — the list opens showing MINE.
|
|
63
|
+
*
|
|
64
|
+
* A product decision, and deliberately not an inheritance from the access layer:
|
|
65
|
+
* GAIA-391 granted `view gaia_conductor` to every session token on purpose, and
|
|
66
|
+
* this default restores what operators had before it without touching that grant.
|
|
67
|
+
*/
|
|
68
|
+
export const DEFAULT_CONDUCTOR_MINE = true;
|
|
69
|
+
/** The filter strip both conductor screens show, so neither can word it its own way. */
|
|
70
|
+
export const mineBadges = (mine) => [
|
|
71
|
+
mine
|
|
72
|
+
? { id: 'mine', label: 'm mine', value: 'mine' }
|
|
73
|
+
: { id: 'mine', label: 'm mine' },
|
|
74
|
+
{ id: 'clear', label: 'c clear' },
|
|
75
|
+
];
|
|
76
|
+
/**
|
|
77
|
+
* `5 hidden by mine — press m`, or '' when there is nothing to say (AC-2).
|
|
78
|
+
*
|
|
79
|
+
* Three cases collapse to the empty string and each for its own reason. `null` is
|
|
80
|
+
* a total nobody established — a plane that reports no `meta.count` (R1) or a
|
|
81
|
+
* failed probe — and a number nobody measured must not be printed. `0` is a
|
|
82
|
+
* measured nothing: an operator sent to press `m` for rows that do not exist
|
|
83
|
+
* learns only that the screen is guessing. Anything else is a fact, and it names
|
|
84
|
+
* the key that releases it, because a count without a remedy is just a smaller
|
|
85
|
+
* mystery.
|
|
86
|
+
*/
|
|
87
|
+
export const hiddenNotice = (hidden) => hidden === null || hidden <= 0 ? '' : `${hidden} hidden by mine — press m`;
|
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
import type { VNode } from '@opentui/core';
|
|
2
|
-
import {
|
|
2
|
+
import type { ListFilterBadge } from '../../../Component/list-chrome/index.js';
|
|
3
3
|
import type { FullScreenChrome } from '../../../Screen/full-screen.js';
|
|
4
|
+
import { ListScreen } from '../../../Screen/list-screen.js';
|
|
4
5
|
import type { ScreenContext } from '../../../Screen/screen.js';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
interface ConductorsData {
|
|
8
|
-
readonly rows: readonly ConductorRow[];
|
|
9
|
-
readonly projectNames: ReadonlyMap<string, string>;
|
|
10
|
-
}
|
|
6
|
+
import type { Page } from '../../collection.js';
|
|
7
|
+
import { type ConductorListContext, type ConductorRow } from '../Data/conductors.js';
|
|
11
8
|
/**
|
|
12
|
-
* Conductors list —
|
|
9
|
+
* Conductors list — an ordinary {@link ListScreen}, paged and filtered to the
|
|
10
|
+
* operator's own conductors by default (GAIA-460 D1).
|
|
13
11
|
*
|
|
14
12
|
* Refresh is the shared mechanism (GAIA-376 `ui-refresh` file + `R`), not a
|
|
15
13
|
* screen-private timer. Enter loads; navigation away cancels in-flight reads
|
|
16
14
|
* via the async lifecycle.
|
|
17
15
|
*/
|
|
18
|
-
export declare class ConductorsScreen extends
|
|
16
|
+
export declare class ConductorsScreen extends ListScreen<ConductorRow, ConductorListContext> {
|
|
19
17
|
static readonly route: {
|
|
20
18
|
id: string;
|
|
21
19
|
path: string;
|
|
@@ -28,7 +26,68 @@ export declare class ConductorsScreen extends AsyncScreen<ConductorsData> {
|
|
|
28
26
|
};
|
|
29
27
|
readonly id: string;
|
|
30
28
|
readonly frameId = "conductors";
|
|
31
|
-
|
|
29
|
+
protected readonly table: {
|
|
30
|
+
readonly noun: "conductor";
|
|
31
|
+
readonly emptyMessage: "No conductors on this control plane.";
|
|
32
|
+
readonly columns: readonly [{
|
|
33
|
+
readonly key: "name";
|
|
34
|
+
readonly label: "NAME";
|
|
35
|
+
readonly width: 0;
|
|
36
|
+
readonly min: 20;
|
|
37
|
+
readonly grow: 2;
|
|
38
|
+
readonly fields: readonly ["label", "machine_id"];
|
|
39
|
+
readonly get: (row: ConductorRow) => string;
|
|
40
|
+
}, {
|
|
41
|
+
readonly key: "machine";
|
|
42
|
+
readonly label: "MACHINE";
|
|
43
|
+
readonly width: 20;
|
|
44
|
+
readonly fields: readonly ["machine_id"];
|
|
45
|
+
readonly get: (row: ConductorRow) => string;
|
|
46
|
+
}, {
|
|
47
|
+
readonly key: "project";
|
|
48
|
+
readonly label: "PROJECT";
|
|
49
|
+
readonly width: 14;
|
|
50
|
+
readonly grow: 1;
|
|
51
|
+
readonly fields: readonly ["project_id"];
|
|
52
|
+
readonly get: (row: ConductorRow, ctx: ConductorListContext) => string;
|
|
53
|
+
}, {
|
|
54
|
+
readonly key: "status";
|
|
55
|
+
readonly label: "STATUS";
|
|
56
|
+
readonly width: 9;
|
|
57
|
+
readonly fields: readonly ["status", "lease_expires_at", "last_seen"];
|
|
58
|
+
readonly get: (row: ConductorRow) => import("../Data/liveness.js").ConductorStatus;
|
|
59
|
+
}, {
|
|
60
|
+
readonly key: "age";
|
|
61
|
+
readonly label: "AGE";
|
|
62
|
+
readonly width: 8;
|
|
63
|
+
readonly fields: readonly ["last_seen"];
|
|
64
|
+
readonly get: (row: ConductorRow, ctx: ConductorListContext) => string;
|
|
65
|
+
}, {
|
|
66
|
+
readonly key: "workspace";
|
|
67
|
+
readonly label: "WORKSPACE";
|
|
68
|
+
readonly width: 18;
|
|
69
|
+
readonly fields: readonly ["workspace_root"];
|
|
70
|
+
readonly get: (row: ConductorRow) => string;
|
|
71
|
+
}, {
|
|
72
|
+
readonly key: "pid";
|
|
73
|
+
readonly label: "PID";
|
|
74
|
+
readonly width: 7;
|
|
75
|
+
readonly fields: readonly ["last_pid"];
|
|
76
|
+
readonly get: (row: ConductorRow) => string;
|
|
77
|
+
}, {
|
|
78
|
+
readonly key: "marks";
|
|
79
|
+
readonly label: "MARKS";
|
|
80
|
+
readonly width: 11;
|
|
81
|
+
readonly fields: readonly ["workspace_root"];
|
|
82
|
+
readonly get: (row: ConductorRow, ctx: ConductorListContext) => string;
|
|
83
|
+
}, {
|
|
84
|
+
readonly key: "load";
|
|
85
|
+
readonly label: "LOAD";
|
|
86
|
+
readonly width: 5;
|
|
87
|
+
readonly fields: readonly ["current_load", "max_parallel"];
|
|
88
|
+
readonly get: (row: ConductorRow) => string;
|
|
89
|
+
}];
|
|
90
|
+
};
|
|
32
91
|
private logFor;
|
|
33
92
|
private readonly busyIds;
|
|
34
93
|
/**
|
|
@@ -39,16 +98,34 @@ export declare class ConductorsScreen extends AsyncScreen<ConductorsData> {
|
|
|
39
98
|
* cannot act on is marked BEFORE a key is pressed, not only after.
|
|
40
99
|
*/
|
|
41
100
|
private status;
|
|
101
|
+
/**
|
|
102
|
+
* GAIA-460 review — separate from {@link status}: the identity reason is
|
|
103
|
+
* how `mine` failed to resolve, not an action's outcome. Sharing one field
|
|
104
|
+
* let `loadRows()` blank a start/stop result on every refresh and let a
|
|
105
|
+
* standing identity reason permanently block {@link viewOnlyNotice}; kept
|
|
106
|
+
* apart, each notice source has its own lifecycle.
|
|
107
|
+
*/
|
|
108
|
+
private identityReason;
|
|
109
|
+
private mine;
|
|
110
|
+
private readonly identity;
|
|
111
|
+
private ownerId;
|
|
112
|
+
private projectNames;
|
|
113
|
+
private hidden;
|
|
42
114
|
topTab(): {
|
|
43
115
|
readonly level: "root";
|
|
44
116
|
readonly tab: "conductors";
|
|
45
117
|
};
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
118
|
+
protected tableContext(context: ScreenContext): ConductorListContext;
|
|
119
|
+
/**
|
|
120
|
+
* GAIA-460 D5 — the identity is resolved BEFORE the first read, because the
|
|
121
|
+
* filter is on by default. Every way of failing to establish it loads the
|
|
122
|
+
* FULL list and states the reason: a cockpit that cannot prove who you are
|
|
123
|
+
* shows you everything, never an empty list it cannot explain.
|
|
124
|
+
*/
|
|
125
|
+
protected loadRows(context: ScreenContext, page: number): Promise<Page<ConductorRow>>;
|
|
126
|
+
protected badges(): readonly ListFilterBadge[];
|
|
49
127
|
protected renderLoading(): string[];
|
|
50
|
-
protected renderReady(
|
|
51
|
-
protected didLoad(data: ConductorsData): void;
|
|
128
|
+
protected renderReady(value: Page<ConductorRow>, context: ScreenContext): VNode;
|
|
52
129
|
protected chrome(_context: ScreenContext): FullScreenChrome;
|
|
53
130
|
/**
|
|
54
131
|
* GAIA-450 review B9 — `s`/`x` are advertised only when the SELECTED row can
|
|
@@ -92,4 +169,3 @@ export declare class ConductorsScreen extends AsyncScreen<ConductorsData> {
|
|
|
92
169
|
*/
|
|
93
170
|
private act;
|
|
94
171
|
}
|
|
95
|
-
export {};
|