@gaia-ai/addon-gaia-ui 0.6.4 → 0.7.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/README.md +1 -1
- package/dist/src/Agent/Component/agent-list/index.d.ts +6 -0
- package/dist/src/Agent/Component/agent-list/index.js +14 -0
- package/dist/src/Agent/launch.d.ts +25 -0
- package/dist/src/Agent/launch.js +49 -0
- package/dist/src/Component/section-list/index.d.ts +37 -0
- package/dist/src/Component/section-list/index.js +60 -13
- package/dist/src/Component/stats-panel/index.d.ts +49 -0
- package/dist/src/Component/stats-panel/index.js +114 -0
- package/dist/src/Component/tab-bar/index.d.ts +11 -0
- package/dist/src/Component/tab-bar/index.js +21 -1
- package/dist/src/Component/ticket-detail/index.d.ts +8 -0
- package/dist/src/Component/ticket-detail/index.js +68 -4
- package/dist/src/Component/top-tabs/index.d.ts +118 -3
- package/dist/src/Component/top-tabs/index.js +123 -8
- package/dist/src/Entity/conductor/Data/conductors.d.ts +28 -0
- package/dist/src/Entity/conductor/Data/conductors.js +23 -0
- package/dist/src/Entity/conductor/Data/liveness.d.ts +10 -0
- package/dist/src/Entity/conductor/Data/liveness.js +25 -0
- package/dist/src/Entity/project/Component/dashboard-list/index.d.ts +8 -0
- package/dist/src/Entity/project/Component/dashboard-list/index.js +7 -0
- package/dist/src/Entity/project/Component/project-list/index.d.ts +8 -0
- package/dist/src/Entity/project/Component/project-list/index.js +7 -0
- package/dist/src/Entity/project/Component/project-picker/index.d.ts +15 -0
- package/dist/src/Entity/project/Component/project-picker/index.js +42 -0
- package/dist/src/Entity/project/Data/projects.d.ts +21 -0
- package/dist/src/Entity/project/Data/projects.js +81 -0
- package/dist/src/Entity/project/Data/with-tickets.d.ts +20 -0
- package/dist/src/Entity/project/Data/with-tickets.js +36 -0
- package/dist/src/Entity/run/Component/run-detail/index.d.ts +17 -0
- package/dist/src/Entity/run/Component/run-detail/index.js +145 -0
- package/dist/src/Entity/run/Component/run-table/index.d.ts +10 -0
- package/dist/src/Entity/run/Component/run-table/index.js +20 -0
- package/dist/src/Entity/run/Data/active-run.d.ts +15 -0
- package/dist/src/Entity/run/Data/active-run.js +23 -0
- package/dist/src/Entity/run/Data/runs.d.ts +72 -0
- package/dist/src/Entity/run/Data/runs.js +125 -0
- package/dist/src/Entity/run/Data/status.d.ts +12 -0
- package/dist/src/Entity/run/Data/status.js +40 -0
- package/dist/src/Entity/statistics/Data/statistics.d.ts +87 -0
- package/dist/src/Entity/statistics/Data/statistics.js +144 -0
- package/dist/src/Entity/term/Data/terms.d.ts +16 -0
- package/dist/src/Entity/term/Data/terms.js +14 -0
- package/dist/src/Entity/ticket/Component/comment-item/index.d.ts +21 -0
- package/dist/src/Entity/ticket/Component/comment-item/index.js +31 -0
- package/dist/src/Entity/ticket/Component/create-form/index.d.ts +15 -0
- package/dist/src/Entity/ticket/Component/create-form/index.js +60 -0
- package/dist/src/Entity/ticket/Component/edit-form/index.d.ts +28 -0
- package/dist/src/Entity/ticket/Component/edit-form/index.js +76 -0
- package/dist/src/Entity/ticket/Component/tab-bar/index.d.ts +20 -0
- package/dist/src/Entity/ticket/Component/tab-bar/index.js +65 -0
- package/dist/src/Entity/ticket/Component/ticket-detail/index.d.ts +106 -0
- package/dist/src/Entity/ticket/Component/ticket-detail/index.js +461 -0
- package/dist/src/Entity/ticket/Component/ticket-table/index.d.ts +5 -0
- package/dist/src/Entity/ticket/Component/ticket-table/index.js +14 -0
- package/dist/src/Entity/ticket/Component/ticket-teaser/index.d.ts +7 -0
- package/dist/src/Entity/ticket/Component/ticket-teaser/index.js +76 -0
- package/dist/src/Entity/ticket/Data/detail.d.ts +28 -0
- package/dist/src/Entity/ticket/Data/detail.js +29 -0
- package/dist/src/Entity/ticket/Data/parents.d.ts +27 -0
- package/dist/src/Entity/ticket/Data/parents.js +67 -0
- package/dist/src/Entity/ticket/Data/query.d.ts +20 -0
- package/dist/src/Entity/ticket/Data/query.js +66 -0
- package/dist/src/Entity/ticket/Data/tickets.d.ts +45 -0
- package/dist/src/Entity/ticket/Data/tickets.js +80 -0
- package/dist/src/Entity/ticket/Data/workflow.d.ts +13 -0
- package/dist/src/Entity/ticket/Data/workflow.js +26 -0
- package/dist/src/Entity/ticket/Data/write.d.ts +12 -0
- package/dist/src/Entity/ticket/Data/write.js +18 -0
- package/dist/src/Entity/ticket/Form/create-data.d.ts +64 -0
- package/dist/src/Entity/ticket/Form/create-data.js +178 -0
- package/dist/src/Entity/ticket/Form/create.d.ts +125 -0
- package/dist/src/Entity/ticket/Form/create.js +112 -0
- package/dist/src/Entity/ticket/Form/edit-data.d.ts +9 -0
- package/dist/src/Entity/ticket/Form/edit-data.js +38 -0
- package/dist/src/Entity/ticket/Form/edit.d.ts +65 -0
- package/dist/src/Entity/ticket/Form/edit.js +61 -0
- package/dist/src/Entity/transport.d.ts +50 -0
- package/dist/src/Entity/transport.js +49 -0
- package/dist/src/Entity/user/Data/users.d.ts +29 -0
- package/dist/src/Entity/user/Data/users.js +28 -0
- package/dist/src/Form/field.d.ts +99 -0
- package/dist/src/Form/field.js +27 -0
- package/dist/src/Form/form.d.ts +53 -0
- package/dist/src/Form/form.js +142 -0
- package/dist/src/Form/index.d.ts +3 -0
- package/dist/src/Form/index.js +5 -0
- package/dist/src/Form/keys.d.ts +3 -0
- package/dist/src/Form/keys.js +57 -0
- package/dist/src/Kernel/tui-kernel.d.ts +11 -0
- package/dist/src/Kernel/tui-kernel.js +1566 -320
- package/dist/src/Screen/projects.d.ts +1 -0
- package/dist/src/Screen/projects.js +1 -0
- package/dist/src/Widget/action.d.ts +3 -0
- package/dist/src/Widget/action.js +9 -0
- package/dist/src/Widget/autocomplete.d.ts +3 -0
- package/dist/src/Widget/autocomplete.js +80 -0
- package/dist/src/Widget/chrome.d.ts +38 -0
- package/dist/src/Widget/chrome.js +60 -0
- package/dist/src/Widget/index.d.ts +4 -0
- package/dist/src/Widget/index.js +12 -0
- package/dist/src/Widget/multiselect.d.ts +3 -0
- package/dist/src/Widget/multiselect.js +36 -0
- package/dist/src/Widget/option-source.d.ts +58 -0
- package/dist/src/Widget/option-source.js +98 -0
- package/dist/src/Widget/render.d.ts +42 -0
- package/dist/src/Widget/render.js +82 -0
- package/dist/src/Widget/select.d.ts +3 -0
- package/dist/src/Widget/select.js +26 -0
- package/dist/src/Widget/text.d.ts +3 -0
- package/dist/src/Widget/text.js +23 -0
- package/dist/src/Widget/types.d.ts +59 -0
- package/dist/src/Widget/types.js +21 -0
- package/dist/src/launcher.d.ts +33 -8
- package/dist/src/launcher.js +12 -4
- package/dist/src/lib/ansi.d.ts +15 -0
- package/dist/src/lib/ansi.js +53 -9
- package/dist/src/lib/format.d.ts +21 -0
- package/dist/src/lib/format.js +46 -4
- package/dist/src/lib/hit-test.d.ts +138 -0
- package/dist/src/lib/hit-test.js +138 -0
- package/dist/src/lib/jsonapi-error.d.ts +7 -0
- package/dist/src/lib/jsonapi-error.js +43 -0
- package/dist/src/lib/mouse.d.ts +35 -0
- package/dist/src/lib/mouse.js +89 -0
- package/dist/src/lib/width-table.d.ts +4 -0
- package/dist/src/lib/width-table.js +147 -0
- package/dist/src/lib/width.d.ts +61 -0
- package/dist/src/lib/width.js +277 -0
- package/dist/src/plugin.js +3 -3
- package/dist/src/route.d.ts +11 -0
- package/dist/src/route.js +215 -0
- package/dist/src/types.d.ts +53 -2
- package/package.json +3 -3
|
@@ -3,6 +3,121 @@ export interface TopTab {
|
|
|
3
3
|
id: string;
|
|
4
4
|
label: string;
|
|
5
5
|
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
/** The root level: what there is before a project is chosen. */
|
|
7
|
+
export declare const ROOT_TABS: readonly [{
|
|
8
|
+
readonly id: "dashboard";
|
|
9
|
+
readonly label: "Dashboard";
|
|
10
|
+
}, {
|
|
11
|
+
readonly id: "projects";
|
|
12
|
+
readonly label: "Projects";
|
|
13
|
+
}];
|
|
14
|
+
/** The project level: the tabs of an opened project. */
|
|
15
|
+
export declare const PROJECT_TABS: readonly [{
|
|
16
|
+
readonly id: "dashboard";
|
|
17
|
+
readonly label: "Dashboard";
|
|
18
|
+
}, {
|
|
19
|
+
readonly id: "tickets";
|
|
20
|
+
readonly label: "Tickets";
|
|
21
|
+
}, {
|
|
22
|
+
readonly id: "ticket-agents";
|
|
23
|
+
readonly label: "Ticket-Agents";
|
|
24
|
+
}, {
|
|
25
|
+
readonly id: "runs";
|
|
26
|
+
readonly label: "Runs";
|
|
27
|
+
}];
|
|
28
|
+
/** The tab bars, keyed by level — the one place a level is named. */
|
|
29
|
+
export declare const TAB_LEVELS: {
|
|
30
|
+
readonly root: readonly [{
|
|
31
|
+
readonly id: "dashboard";
|
|
32
|
+
readonly label: "Dashboard";
|
|
33
|
+
}, {
|
|
34
|
+
readonly id: "projects";
|
|
35
|
+
readonly label: "Projects";
|
|
36
|
+
}];
|
|
37
|
+
readonly project: readonly [{
|
|
38
|
+
readonly id: "dashboard";
|
|
39
|
+
readonly label: "Dashboard";
|
|
40
|
+
}, {
|
|
41
|
+
readonly id: "tickets";
|
|
42
|
+
readonly label: "Tickets";
|
|
43
|
+
}, {
|
|
44
|
+
readonly id: "ticket-agents";
|
|
45
|
+
readonly label: "Ticket-Agents";
|
|
46
|
+
}, {
|
|
47
|
+
readonly id: "runs";
|
|
48
|
+
readonly label: "Runs";
|
|
49
|
+
}];
|
|
50
|
+
};
|
|
51
|
+
/** The name of a level that carries a top-level tab bar. */
|
|
52
|
+
export type TabLevel = keyof typeof TAB_LEVELS;
|
|
53
|
+
/** The ids of one level's tabs, derived from {@link TAB_LEVELS}. */
|
|
54
|
+
export type TabIdOf<L extends TabLevel> = (typeof TAB_LEVELS)[L][number]['id'];
|
|
55
|
+
/**
|
|
56
|
+
* The id of any top-level tab, derived — never re-listed. Before GAIA-239 this
|
|
57
|
+
* union was hand-written in `tui-kernel.ts` alongside a third hardcoded copy of
|
|
58
|
+
* the same ids for the Tab cycle order; the three could drift silently.
|
|
59
|
+
*/
|
|
60
|
+
export type TopTabId = {
|
|
61
|
+
[L in TabLevel]: TabIdOf<L>;
|
|
62
|
+
}[TabLevel];
|
|
63
|
+
/**
|
|
64
|
+
* Where a screen sits in the navigation: which bar it draws, and which tab of
|
|
65
|
+
* that bar is its own. A discriminated union over the levels, so a location can
|
|
66
|
+
* never pair one level's name with another level's tab id.
|
|
67
|
+
*/
|
|
68
|
+
export type TabLocation = {
|
|
69
|
+
[L in TabLevel]: {
|
|
70
|
+
level: L;
|
|
71
|
+
tab: TabIdOf<L>;
|
|
72
|
+
};
|
|
73
|
+
}[TabLevel];
|
|
74
|
+
/**
|
|
75
|
+
* One level's tab bar as a single row; the active tab is bracketed.
|
|
76
|
+
*
|
|
77
|
+
* The level is part of the signature on purpose: it is what keeps the id checked
|
|
78
|
+
* at compile time now that there is more than one bar. `topTabBar('root', 'runs')`
|
|
79
|
+
* is a TS2345 naming the root level's valid ids — not a bar that silently
|
|
80
|
+
* brackets nothing.
|
|
81
|
+
*/
|
|
82
|
+
export declare function topTabBar<L extends TabLevel>(level: L, activeId: TabIdOf<L>): string;
|
|
83
|
+
/**
|
|
84
|
+
* The same bar, for a caller holding a whole {@link TabLocation} rather than a
|
|
85
|
+
* literal level. The pairing was already checked when the location was built, so
|
|
86
|
+
* this needs no second check — and unlike `topTabBar(loc.level, loc.tab)` it does
|
|
87
|
+
* not ask the compiler to correlate two independently narrowed properties.
|
|
88
|
+
*/
|
|
89
|
+
export declare function tabBarFor(location: TabLocation): string;
|
|
90
|
+
/** Where one tab sits inside the bar: `[start, end)` in display columns from the
|
|
91
|
+
* bar's own first column. */
|
|
92
|
+
export interface TopTabSpan<Id extends string = TopTabId> {
|
|
93
|
+
id: Id;
|
|
94
|
+
start: number;
|
|
95
|
+
end: number;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* One level's own hit map, built from the SAME `segment`/`SEPARATOR` that level's
|
|
99
|
+
* rendered bar is joined from — so the layout and the click mapping are two
|
|
100
|
+
* readings of one expression and cannot drift apart. That is the argument
|
|
101
|
+
* `lib/hit-test.ts` makes for the body rows, applied to a column axis.
|
|
102
|
+
*
|
|
103
|
+
* Offsets are display columns, which is the unit a mouse report carries. For the
|
|
104
|
+
* current ASCII labels that coincides with string offsets, which is what lets
|
|
105
|
+
* the test slice `topTabBar()` with a span and compare.
|
|
106
|
+
*
|
|
107
|
+
* GAIA-239: the LEVEL is part of the signature, exactly as it is for
|
|
108
|
+
* {@link topTabBar}, and for the same reason twice over. GAIA-241 shipped one
|
|
109
|
+
* `topTabSpans(activeId)` over the single flat bar; read level-blind it would
|
|
110
|
+
* publish the project bar's three spans on the root screen too, so a click at
|
|
111
|
+
* the column where `Ticket-Agents` used to sit would open a tab that screen does
|
|
112
|
+
* not draw — the very defect this ticket removes, arriving through the mouse.
|
|
113
|
+
* A map and a bar of two different levels is now a TS2345, not a silent
|
|
114
|
+
* mis-click.
|
|
115
|
+
*/
|
|
116
|
+
export declare function topTabSpans<L extends TabLevel>(level: L, activeId: TabIdOf<L>): TopTabSpan<TabIdOf<L>>[];
|
|
117
|
+
/**
|
|
118
|
+
* The same hit map, for a caller holding a whole {@link TabLocation} — the
|
|
119
|
+
* companion of {@link tabBarFor}, and the pairing it needs was already checked
|
|
120
|
+
* when the location was built. Keeping the two in step is what makes "the bar you
|
|
121
|
+
* see is the bar you click" true by construction: both read `location`.
|
|
122
|
+
*/
|
|
123
|
+
export declare function tabSpansFor(location: TabLocation): TopTabSpan[];
|
|
@@ -1,13 +1,128 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
1
|
+
import { displayWidth } from '../../lib/width.js';
|
|
2
|
+
// GAIA-239: THE registry — the single place the set, the order and the labels of
|
|
3
|
+
// the top-level tabs are written. Navigation has THREE levels, and every screen
|
|
4
|
+
// shows exactly ONE tab bar: the one of its own level.
|
|
5
|
+
//
|
|
6
|
+
// root dashboard, projects, project-picker → Dashboard · Projects
|
|
7
|
+
// project dashboard, project, ticket-agents, → Dashboard · Tickets ·
|
|
8
|
+
// project-runs Ticket-Agents · Runs
|
|
9
|
+
// ticket ticket → the DETAIL tabs, drawn as the
|
|
10
|
+
// header box's meta row, so no
|
|
11
|
+
// top-level bar at all
|
|
12
|
+
//
|
|
13
|
+
// `tickets` is the project/ticket drill-down; `ticket-agents` is the managed
|
|
14
|
+
// create-agent list (GAIA-190); `runs` is the project-scoped run overview
|
|
15
|
+
// (GAIA-192) — all three project-scoped, which is why they sit on the project
|
|
16
|
+
// level and not on the root one.
|
|
17
|
+
//
|
|
18
|
+
// GAIA-256 spec D2: `dashboard` is declared on BOTH levels, on purpose. There is
|
|
19
|
+
// ONE Dashboard screen — the root one and the project one are the same screen
|
|
20
|
+
// with its project filter unset or set, and its LEVEL follows that same bit, so
|
|
21
|
+
// the bar and the filter can never disagree: they read one value.
|
|
22
|
+
//
|
|
23
|
+
// That is why tab ids are no longer globally unique, and why nothing here
|
|
24
|
+
// requires them to be. The requirement was never a property of the model — every
|
|
25
|
+
// function in this file is already level-parameterised (`topTabBar(level, id)`,
|
|
26
|
+
// `topTabSpans(level, id)`, `tabBarFor(location)`, `TabIdOf<L>`). It was a
|
|
27
|
+
// property of ONE function, the level-blind `locateTab`, which walked the levels
|
|
28
|
+
// in key order and would now resolve the project level's `dashboard` to the ROOT
|
|
29
|
+
// location. `gotoTopTab` was its only caller, and it only ever moves within one
|
|
30
|
+
// level — the bar you Tab through or click IS the current level's bar — so it
|
|
31
|
+
// pairs the target with `here.level` directly and `locateTab` is deleted.
|
|
32
|
+
//
|
|
33
|
+
// Only tabs that are actually navigable are listed. A planned screen (`Users` on
|
|
34
|
+
// the root level, `Conductors` on the project level) arrives with its own ticket
|
|
35
|
+
// and adds its entry here then — a tab drawn before its screen exists would
|
|
36
|
+
// advertise navigation the operator cannot take, which is the very defect
|
|
37
|
+
// GAIA-239 removes.
|
|
38
|
+
//
|
|
39
|
+
// `as const satisfies` is load-bearing on both halves: `satisfies` still
|
|
40
|
+
// shape-checks every entry against `TopTab`, while `as const` keeps the literal
|
|
41
|
+
// ids so the id unions below can be DERIVED from them. A plain `: TopTab[]`
|
|
42
|
+
// annotation would widen `id` to `string` and the derivation would collapse to
|
|
43
|
+
// `string`, taking the compile-time check with it.
|
|
44
|
+
/** The root level: what there is before a project is chosen. */
|
|
45
|
+
export const ROOT_TABS = [
|
|
46
|
+
{ id: 'dashboard', label: 'Dashboard' },
|
|
47
|
+
{ id: 'projects', label: 'Projects' },
|
|
48
|
+
];
|
|
49
|
+
/** The project level: the tabs of an opened project. */
|
|
50
|
+
export const PROJECT_TABS = [
|
|
51
|
+
{ id: 'dashboard', label: 'Dashboard' },
|
|
6
52
|
{ id: 'tickets', label: 'Tickets' },
|
|
7
53
|
{ id: 'ticket-agents', label: 'Ticket-Agents' },
|
|
8
54
|
{ id: 'runs', label: 'Runs' },
|
|
9
55
|
];
|
|
10
|
-
/** The
|
|
11
|
-
export
|
|
12
|
-
|
|
56
|
+
/** The tab bars, keyed by level — the one place a level is named. */
|
|
57
|
+
export const TAB_LEVELS = {
|
|
58
|
+
root: ROOT_TABS,
|
|
59
|
+
project: PROJECT_TABS,
|
|
60
|
+
};
|
|
61
|
+
/** What joins two tabs in the rendered bar — part of neither tab's span. */
|
|
62
|
+
const SEPARATOR = ' ';
|
|
63
|
+
/** One tab's rendered segment: bracketed when active, space-padded otherwise. */
|
|
64
|
+
const segment = (tab, activeId) => tab.id === activeId ? `[${tab.label}]` : ` ${tab.label} `;
|
|
65
|
+
const renderBar = (tabs, activeId) => tabs.map((t) => segment(t, activeId)).join(SEPARATOR);
|
|
66
|
+
/**
|
|
67
|
+
* One level's tab bar as a single row; the active tab is bracketed.
|
|
68
|
+
*
|
|
69
|
+
* The level is part of the signature on purpose: it is what keeps the id checked
|
|
70
|
+
* at compile time now that there is more than one bar. `topTabBar('root', 'runs')`
|
|
71
|
+
* is a TS2345 naming the root level's valid ids — not a bar that silently
|
|
72
|
+
* brackets nothing.
|
|
73
|
+
*/
|
|
74
|
+
export function topTabBar(level, activeId) {
|
|
75
|
+
return renderBar(TAB_LEVELS[level], activeId);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The same bar, for a caller holding a whole {@link TabLocation} rather than a
|
|
79
|
+
* literal level. The pairing was already checked when the location was built, so
|
|
80
|
+
* this needs no second check — and unlike `topTabBar(loc.level, loc.tab)` it does
|
|
81
|
+
* not ask the compiler to correlate two independently narrowed properties.
|
|
82
|
+
*/
|
|
83
|
+
export function tabBarFor(location) {
|
|
84
|
+
return renderBar(TAB_LEVELS[location.level], location.tab);
|
|
85
|
+
}
|
|
86
|
+
const renderSpans = (tabs, activeId) => {
|
|
87
|
+
const spans = [];
|
|
88
|
+
let start = 0;
|
|
89
|
+
tabs.forEach((tab, index) => {
|
|
90
|
+
if (index > 0)
|
|
91
|
+
start += displayWidth(SEPARATOR);
|
|
92
|
+
const width = displayWidth(segment(tab, activeId));
|
|
93
|
+
spans.push({ id: tab.id, start, end: start + width });
|
|
94
|
+
start += width;
|
|
95
|
+
});
|
|
96
|
+
return spans;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* One level's own hit map, built from the SAME `segment`/`SEPARATOR` that level's
|
|
100
|
+
* rendered bar is joined from — so the layout and the click mapping are two
|
|
101
|
+
* readings of one expression and cannot drift apart. That is the argument
|
|
102
|
+
* `lib/hit-test.ts` makes for the body rows, applied to a column axis.
|
|
103
|
+
*
|
|
104
|
+
* Offsets are display columns, which is the unit a mouse report carries. For the
|
|
105
|
+
* current ASCII labels that coincides with string offsets, which is what lets
|
|
106
|
+
* the test slice `topTabBar()` with a span and compare.
|
|
107
|
+
*
|
|
108
|
+
* GAIA-239: the LEVEL is part of the signature, exactly as it is for
|
|
109
|
+
* {@link topTabBar}, and for the same reason twice over. GAIA-241 shipped one
|
|
110
|
+
* `topTabSpans(activeId)` over the single flat bar; read level-blind it would
|
|
111
|
+
* publish the project bar's three spans on the root screen too, so a click at
|
|
112
|
+
* the column where `Ticket-Agents` used to sit would open a tab that screen does
|
|
113
|
+
* not draw — the very defect this ticket removes, arriving through the mouse.
|
|
114
|
+
* A map and a bar of two different levels is now a TS2345, not a silent
|
|
115
|
+
* mis-click.
|
|
116
|
+
*/
|
|
117
|
+
export function topTabSpans(level, activeId) {
|
|
118
|
+
return renderSpans(TAB_LEVELS[level], activeId);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The same hit map, for a caller holding a whole {@link TabLocation} — the
|
|
122
|
+
* companion of {@link tabBarFor}, and the pairing it needs was already checked
|
|
123
|
+
* when the location was built. Keeping the two in step is what makes "the bar you
|
|
124
|
+
* see is the bar you click" true by construction: both read `location`.
|
|
125
|
+
*/
|
|
126
|
+
export function tabSpansFor(location) {
|
|
127
|
+
return renderSpans(TAB_LEVELS[location.level], location.tab);
|
|
13
128
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { DropshClient } from '../../../types.js';
|
|
2
|
+
import { type ListOutcome, type OptionClients } from '../../transport.js';
|
|
3
|
+
/** One conductor of the control plane, as this renderer needs it. */
|
|
4
|
+
export interface Conductor {
|
|
5
|
+
id: string;
|
|
6
|
+
/** Never empty — the uuid is the last fallback, so nothing renders as blank. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** The machine the conductor runs on. */
|
|
9
|
+
machineId: string | undefined;
|
|
10
|
+
/** Liveness, as the control plane reports it. */
|
|
11
|
+
status: string | undefined;
|
|
12
|
+
/** The conductor's project, `null` when unlinked. */
|
|
13
|
+
projectId: string | null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The conductor collection through one client, in server order.
|
|
17
|
+
*
|
|
18
|
+
* Yields the OUTCOME rather than a swallowed array (GAIA-257 D4) — see the same
|
|
19
|
+
* note on `listUsers`. `listConductorsPreferringWrite` keeps the swallowing
|
|
20
|
+
* contract, because it already answers a failure by falling back.
|
|
21
|
+
*/
|
|
22
|
+
export declare const listConductors: (client: DropshClient) => Promise<ListOutcome<Conductor>>;
|
|
23
|
+
/**
|
|
24
|
+
* The same collection read through the WRITE client where there is one — still just
|
|
25
|
+
* reading, but the write scope is the one that decides what a write may reference,
|
|
26
|
+
* and `pm` sees the FULL conductor set where `session` sees a subset.
|
|
27
|
+
*/
|
|
28
|
+
export declare const listConductorsPreferringWrite: (clients: OptionClients) => Promise<Conductor[]>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { listOutcome, listPreferringWrite, } from '../../transport.js';
|
|
2
|
+
const str = (v) => typeof v === 'string' && v.length > 0 ? v : undefined;
|
|
3
|
+
const toConductor = (r) => ({
|
|
4
|
+
id: r.id,
|
|
5
|
+
label: str(r.attr('label')) ?? str(r.attr('machine_id')) ?? r.id,
|
|
6
|
+
machineId: str(r.attr('machine_id')),
|
|
7
|
+
status: str(r.attr('status')),
|
|
8
|
+
projectId: r.rel('project_id'),
|
|
9
|
+
});
|
|
10
|
+
/**
|
|
11
|
+
* The conductor collection through one client, in server order.
|
|
12
|
+
*
|
|
13
|
+
* Yields the OUTCOME rather than a swallowed array (GAIA-257 D4) — see the same
|
|
14
|
+
* note on `listUsers`. `listConductorsPreferringWrite` keeps the swallowing
|
|
15
|
+
* contract, because it already answers a failure by falling back.
|
|
16
|
+
*/
|
|
17
|
+
export const listConductors = (client) => listOutcome(client, 'gaia_conductor', toConductor);
|
|
18
|
+
/**
|
|
19
|
+
* The same collection read through the WRITE client where there is one — still just
|
|
20
|
+
* reading, but the write scope is the one that decides what a write may reference,
|
|
21
|
+
* and `pm` sees the FULL conductor set where `session` sees a subset.
|
|
22
|
+
*/
|
|
23
|
+
export const listConductorsPreferringWrite = (clients) => listPreferringWrite(clients, 'gaia_conductor', toConductor);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** The derived liveness state of a conductor. */
|
|
2
|
+
export type ConductorStatus = 'running' | 'stale' | 'offline' | 'registry-only';
|
|
3
|
+
/** The raw liveness inputs derived from a `gaia_conductor` record. */
|
|
4
|
+
export interface ConductorInput {
|
|
5
|
+
present?: boolean;
|
|
6
|
+
status?: string | null;
|
|
7
|
+
lastSeen?: number | null;
|
|
8
|
+
leaseExpiresAt?: number | null;
|
|
9
|
+
}
|
|
10
|
+
export declare function deriveConductorStatus(conductor: ConductorInput, nowSeconds: number): ConductorStatus;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Conductor liveness derivation. Mirrors the server-side ConductorDeriver
|
|
2
|
+
// (web/modules/custom/gaia_core/modules/gaia_dashboard/src/Dashboard/ConductorDeriver.php)
|
|
3
|
+
// and the CLI `classify()` in gaia-cli/conductor/src/commands/conductor.ts:
|
|
4
|
+
// no gaia_conductor record → registry-only; status 'offline' → offline; else
|
|
5
|
+
// stale when last_seen older than 600s OR the lease has expired; else running.
|
|
6
|
+
const STALE_WINDOW_SECONDS = 600;
|
|
7
|
+
export function deriveConductorStatus(conductor, nowSeconds) {
|
|
8
|
+
if (!conductor?.present)
|
|
9
|
+
return 'registry-only';
|
|
10
|
+
if (conductor.status === 'offline')
|
|
11
|
+
return 'offline';
|
|
12
|
+
if (conductor.lastSeen == null)
|
|
13
|
+
return 'stale';
|
|
14
|
+
if (nowSeconds - conductor.lastSeen > STALE_WINDOW_SECONDS)
|
|
15
|
+
return 'stale';
|
|
16
|
+
if (conductor.leaseExpiresAt != null &&
|
|
17
|
+
conductor.leaseExpiresAt < nowSeconds) {
|
|
18
|
+
return 'stale';
|
|
19
|
+
}
|
|
20
|
+
return 'running';
|
|
21
|
+
}
|
|
22
|
+
// `humanizeAge` moved to `lib/format.ts` (GAIA-236, entity-slice Phase 0): it is a
|
|
23
|
+
// generic relative-time formatter with no conductor vocabulary, and keeping it here
|
|
24
|
+
// is what made `Component/comment-item` and `Deriver/run-status` import a CONDUCTOR
|
|
25
|
+
// module. Its test moved with it, to `tests/lib/format.test.ts`.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The minimum shape a dashboard row needs to render. */
|
|
2
|
+
export interface DashboardRowInput {
|
|
3
|
+
project: string;
|
|
4
|
+
derivedStatus: string;
|
|
5
|
+
ageLabel: string;
|
|
6
|
+
}
|
|
7
|
+
/** One line per project: name, derived status, relative age. */
|
|
8
|
+
export declare function dashboardRows(projects: DashboardRowInput[]): string[];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { pad } from '../../../../lib/format.js';
|
|
2
|
+
/** One line per project: name, derived status, relative age. */
|
|
3
|
+
export function dashboardRows(projects) {
|
|
4
|
+
if (projects.length === 0)
|
|
5
|
+
return ['No projects.'];
|
|
6
|
+
return projects.map((p) => `${pad(p.project, 24)} ${pad(p.derivedStatus, 14)} ${p.ageLabel}`);
|
|
7
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The minimum shape a project row needs to render. */
|
|
2
|
+
export interface ProjectRowInput {
|
|
3
|
+
project: string;
|
|
4
|
+
derivedStatus: string;
|
|
5
|
+
ageLabel: string;
|
|
6
|
+
}
|
|
7
|
+
/** One line per project: name, derived status, relative age. */
|
|
8
|
+
export declare function projectRows(projects: ProjectRowInput[]): string[];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { pad } from '../../../../lib/format.js';
|
|
2
|
+
/** One line per project: name, derived status, relative age. */
|
|
3
|
+
export function projectRows(projects) {
|
|
4
|
+
if (projects.length === 0)
|
|
5
|
+
return ['No projects.'];
|
|
6
|
+
return projects.map((p) => `${pad(p.project, 24)} ${pad(p.derivedStatus, 14)} ${p.ageLabel}`);
|
|
7
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** A project that claims the repo. */
|
|
2
|
+
export interface PickerCandidate {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The picker screen's rows: a heading naming the repo, then one row per
|
|
8
|
+
* candidate with `▸` on the selected one.
|
|
9
|
+
*/
|
|
10
|
+
export declare function projectPickerRows(candidates: PickerCandidate[], repo: string, selected: number): string[];
|
|
11
|
+
/**
|
|
12
|
+
* The non-TTY form (spec Decision 11): the candidate list plus the direct
|
|
13
|
+
* command, printed once so `gaia ui --here | cat` stays scriptable — no prompt.
|
|
14
|
+
*/
|
|
15
|
+
export declare function projectPickerHint(candidates: PickerCandidate[], repo: string): string;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// GAIA-223 AC-8 (spec Decision 14) — the picker `gaia ui --here` shows when the
|
|
2
|
+
// cwd's repo is claimed by MORE THAN ONE gaia project.
|
|
3
|
+
//
|
|
4
|
+
// One repo mapping to several projects is designed for, not an edge case:
|
|
5
|
+
// GAIA-126/137 let `.gaia/` hold one conductor config per project, which is how
|
|
6
|
+
// a Drupal multisite repo maps each site to its own GAIA project. Erroring on
|
|
7
|
+
// N>1 would fail permanently, by design, in exactly the repos the feature is
|
|
8
|
+
// most useful in — so the ambiguity is shown rather than resolved behind the
|
|
9
|
+
// user's back.
|
|
10
|
+
//
|
|
11
|
+
// Pure, like every other Component: candidates in, rows out. The kernel owns
|
|
12
|
+
// selection state, Enter and Esc.
|
|
13
|
+
/** Clamp a selection index into the candidate range (0 when there are none). */
|
|
14
|
+
function clamp(selected, length) {
|
|
15
|
+
if (length <= 0)
|
|
16
|
+
return 0;
|
|
17
|
+
return Math.max(0, Math.min(Math.trunc(selected), length - 1));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The picker screen's rows: a heading naming the repo, then one row per
|
|
21
|
+
* candidate with `▸` on the selected one.
|
|
22
|
+
*/
|
|
23
|
+
export function projectPickerRows(candidates, repo, selected) {
|
|
24
|
+
const heading = [`Several gaia projects claim ${repo} — which one?`, ''];
|
|
25
|
+
if (candidates.length === 0) {
|
|
26
|
+
return [...heading, 'No projects.'];
|
|
27
|
+
}
|
|
28
|
+
const index = clamp(selected, candidates.length);
|
|
29
|
+
return [
|
|
30
|
+
...heading,
|
|
31
|
+
...candidates.map((c, i) => `${i === index ? '▸' : ' '} ${c.name}`),
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The non-TTY form (spec Decision 11): the candidate list plus the direct
|
|
36
|
+
* command, printed once so `gaia ui --here | cat` stays scriptable — no prompt.
|
|
37
|
+
*/
|
|
38
|
+
export function projectPickerHint(candidates, repo) {
|
|
39
|
+
const names = candidates.map((c) => ` ${c.name}`).join('\n');
|
|
40
|
+
return (`Several gaia projects claim ${repo}:\n${names}\n` +
|
|
41
|
+
"Open one directly with 'gaia ui project:<name>'.");
|
|
42
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { DropshClient } from '../../../types.js';
|
|
2
|
+
import { type ConductorStatus } from '../../conductor/Data/liveness.js';
|
|
3
|
+
/** A derived project row: one distinct project + its liveliest conductor. */
|
|
4
|
+
export interface ProjectRow {
|
|
5
|
+
project: string;
|
|
6
|
+
derivedStatus: ConductorStatus;
|
|
7
|
+
lastSeen: number | null;
|
|
8
|
+
ageLabel: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Resolve the projects screen's rows straight from the control plane: list the
|
|
12
|
+
* `gaia_conductor` entities (read is owner-gated, so this is already scoped to
|
|
13
|
+
* the current user's conductors — no local registry needed), sideload their
|
|
14
|
+
* `project_id`, group by project name, and report each project's liveliest
|
|
15
|
+
* conductor (running > stale > offline). Everything is a Drupal entity read
|
|
16
|
+
* through `services.client` (AC-5); the screen itself stays a screen/route.
|
|
17
|
+
*/
|
|
18
|
+
export declare function loadProjectRows({ client, nowSeconds, }: {
|
|
19
|
+
client: DropshClient;
|
|
20
|
+
nowSeconds: () => number;
|
|
21
|
+
}): Promise<ProjectRow[]>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { DrupalJsonApiParams } from 'drupal-jsonapi-params';
|
|
2
|
+
import { humanizeAge } from '../../../lib/format.js';
|
|
3
|
+
import { deriveConductorStatus, } from '../../conductor/Data/liveness.js';
|
|
4
|
+
/** JSON:API timestamps arrive as ISO-8601 strings; liveness works in seconds. */
|
|
5
|
+
function toEpochSeconds(value) {
|
|
6
|
+
if (value == null)
|
|
7
|
+
return null;
|
|
8
|
+
if (typeof value === 'number')
|
|
9
|
+
return value;
|
|
10
|
+
if (typeof value !== 'string')
|
|
11
|
+
return null;
|
|
12
|
+
const ms = Date.parse(value);
|
|
13
|
+
return Number.isNaN(ms) ? null : Math.floor(ms / 1000);
|
|
14
|
+
}
|
|
15
|
+
/** Id of a single-valued relationship linkage (`data` object), or null. */
|
|
16
|
+
function singleRelId(rel) {
|
|
17
|
+
const data = rel?.data;
|
|
18
|
+
if (!data || Array.isArray(data))
|
|
19
|
+
return null;
|
|
20
|
+
return data.id ?? null;
|
|
21
|
+
}
|
|
22
|
+
// Most-alive-first ranking so a project with several conductors reports its
|
|
23
|
+
// liveliest one, not whichever the collection happened to return first.
|
|
24
|
+
const STATUS_RANK = {
|
|
25
|
+
running: 0,
|
|
26
|
+
stale: 1,
|
|
27
|
+
offline: 2,
|
|
28
|
+
'registry-only': 3,
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the projects screen's rows straight from the control plane: list the
|
|
32
|
+
* `gaia_conductor` entities (read is owner-gated, so this is already scoped to
|
|
33
|
+
* the current user's conductors — no local registry needed), sideload their
|
|
34
|
+
* `project_id`, group by project name, and report each project's liveliest
|
|
35
|
+
* conductor (running > stale > offline). Everything is a Drupal entity read
|
|
36
|
+
* through `services.client` (AC-5); the screen itself stays a screen/route.
|
|
37
|
+
*/
|
|
38
|
+
export async function loadProjectRows({ client, nowSeconds, }) {
|
|
39
|
+
const params = new DrupalJsonApiParams().addInclude(['project_id']);
|
|
40
|
+
let doc;
|
|
41
|
+
try {
|
|
42
|
+
doc = (await client.get('gaia_conductor/gaia_conductor', params));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
// Resolve each conductor's project name from the sideloaded gaia_project.
|
|
48
|
+
const projectNameById = new Map();
|
|
49
|
+
for (const inc of doc.included ?? []) {
|
|
50
|
+
if (inc.id && typeof inc.attributes?.name === 'string') {
|
|
51
|
+
projectNameById.set(inc.id, inc.attributes.name);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const now = nowSeconds();
|
|
55
|
+
const best = new Map();
|
|
56
|
+
for (const c of doc.data ?? []) {
|
|
57
|
+
const a = c.attributes ?? {};
|
|
58
|
+
const projectId = singleRelId(c.relationships?.project_id);
|
|
59
|
+
const project = projectId ? projectNameById.get(projectId) : undefined;
|
|
60
|
+
// A conductor whose project cannot be resolved is not a project row.
|
|
61
|
+
if (!project)
|
|
62
|
+
continue;
|
|
63
|
+
const lastSeen = toEpochSeconds(a.last_seen ?? null);
|
|
64
|
+
const derivedStatus = deriveConductorStatus({
|
|
65
|
+
present: true,
|
|
66
|
+
status: typeof a.status === 'string' ? a.status : null,
|
|
67
|
+
lastSeen,
|
|
68
|
+
leaseExpiresAt: toEpochSeconds(a.lease_expires_at ?? null),
|
|
69
|
+
}, now);
|
|
70
|
+
const prev = best.get(project);
|
|
71
|
+
if (!prev || STATUS_RANK[derivedStatus] < STATUS_RANK[prev.derivedStatus]) {
|
|
72
|
+
best.set(project, { derivedStatus, lastSeen });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return [...best.entries()].map(([project, b]) => ({
|
|
76
|
+
project,
|
|
77
|
+
derivedStatus: b.derivedStatus,
|
|
78
|
+
lastSeen: b.lastSeen,
|
|
79
|
+
ageLabel: humanizeAge(b.lastSeen, now),
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DropshClient } from '../../../types.js';
|
|
2
|
+
import { type TicketRow } from '../../ticket/Data/tickets.js';
|
|
3
|
+
/** The resolved project header + its ticket rows. */
|
|
4
|
+
export interface ProjectWithTickets {
|
|
5
|
+
project: {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
slug: string;
|
|
9
|
+
};
|
|
10
|
+
tickets: TicketRow[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Resolve a `gaia_project` by its slug (the registry `project`, which matches
|
|
14
|
+
* the project's `name`) and list its tickets, most-recently-changed first.
|
|
15
|
+
*
|
|
16
|
+
* The ticket listing delegates to {@link loadFilteredTickets} with empty
|
|
17
|
+
* criteria — one include-aware `client.get` path shared with the search/filter
|
|
18
|
+
* load, so both resolve parent/conductor identically (no dual-mapper drift).
|
|
19
|
+
*/
|
|
20
|
+
export declare function loadProjectWithTickets(client: DropshClient, projectSlug: string): Promise<ProjectWithTickets>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Resolving ONE project by slug together with its ticket rows. The project half of
|
|
2
|
+
// the former `Service/project-data.ts` (GAIA-236 phase 3).
|
|
3
|
+
//
|
|
4
|
+
// This is one of exactly two enumerated `project/Data -> ticket/Data` edges the
|
|
5
|
+
// boundary guard allows: the ticket listing deliberately delegates to
|
|
6
|
+
// `loadFilteredTickets` so the search/filter path and this one resolve
|
|
7
|
+
// parent/conductor through the same mapper, with no dual-mapper drift.
|
|
8
|
+
import { loadFilteredTickets, } from '../../ticket/Data/tickets.js';
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a `gaia_project` by its slug (the registry `project`, which matches
|
|
11
|
+
* the project's `name`) and list its tickets, most-recently-changed first.
|
|
12
|
+
*
|
|
13
|
+
* The ticket listing delegates to {@link loadFilteredTickets} with empty
|
|
14
|
+
* criteria — one include-aware `client.get` path shared with the search/filter
|
|
15
|
+
* load, so both resolve parent/conductor identically (no dual-mapper drift).
|
|
16
|
+
*/
|
|
17
|
+
export async function loadProjectWithTickets(client, projectSlug) {
|
|
18
|
+
const projectRes = await client
|
|
19
|
+
.collection('gaia_project')
|
|
20
|
+
.where('name', '=', projectSlug)
|
|
21
|
+
.first();
|
|
22
|
+
const projectId = projectRes?.id ?? projectSlug;
|
|
23
|
+
const tickets = await loadFilteredTickets(client, projectId, {
|
|
24
|
+
query: '',
|
|
25
|
+
state: null,
|
|
26
|
+
type: null,
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
project: {
|
|
30
|
+
id: projectId,
|
|
31
|
+
name: projectRes?.attr('name') ?? projectSlug,
|
|
32
|
+
slug: projectSlug,
|
|
33
|
+
},
|
|
34
|
+
tickets,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Section } from '../../../../Component/section-list/index.js';
|
|
2
|
+
import type { JsonApiResource, TicketDetailDocument } from '../../../../types.js';
|
|
3
|
+
/** The sideloaded `gaia_run` with this id, or undefined. */
|
|
4
|
+
export declare function runResource(document: TicketDetailDocument, runId: string | null | undefined): JsonApiResource | undefined;
|
|
5
|
+
/** `#<id> · <ticket> · <state>` — the run screen's header-box title row. */
|
|
6
|
+
export declare function runDetailTitle(document: TicketDetailDocument, runId: string | null | undefined): string;
|
|
7
|
+
/**
|
|
8
|
+
* The run's sections, in the order the screen draws them. An unresolvable run
|
|
9
|
+
* yields ONE section saying so, rather than an empty screen that looks like a
|
|
10
|
+
* failed render.
|
|
11
|
+
*/
|
|
12
|
+
export declare function runDetailSections(document: TicketDetailDocument, runId: string | null | undefined): Section[];
|
|
13
|
+
/** The run block flattened — the screen's body, and the piped render (D7). */
|
|
14
|
+
export declare function runDetailLines(document: TicketDetailDocument, runId: string | null | undefined): {
|
|
15
|
+
lines: string[];
|
|
16
|
+
boldRows: Set<number>;
|
|
17
|
+
};
|