@profullstack/hqtui-demo 0.3.0 → 0.5.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 +8 -2
- package/dist/main.js +47 -20
- package/dist/main.js.map +1 -1
- package/dist/screens/components.js +3 -3
- package/dist/screens/components.js.map +1 -1
- package/dist/screens/dashboard.js +2 -1
- package/dist/screens/dashboard.js.map +1 -1
- package/dist/screens/graphics.js +3 -3
- package/dist/screens/graphics.js.map +1 -1
- package/dist/screens/index.js +1 -0
- package/dist/screens/index.js.map +1 -1
- package/dist/screens/network.js +2 -2
- package/dist/screens/network.js.map +1 -1
- package/dist/screens/services.js +2 -2
- package/dist/screens/services.js.map +1 -1
- package/dist/screens/sessions.js +2 -2
- package/dist/screens/sessions.js.map +1 -1
- package/dist/screens/themes.js +3 -1
- package/dist/screens/themes.js.map +1 -1
- package/dist/screens/traffic.js +3 -3
- package/dist/screens/traffic.js.map +1 -1
- package/dist/screens/world.js +97 -0
- package/dist/screens/world.js.map +1 -0
- package/dist/state.js +12 -1
- package/dist/state.js.map +1 -1
- package/package.json +2 -2
- package/src/main.ts +42 -21
- package/src/screens/components.ts +3 -3
- package/src/screens/dashboard.ts +2 -1
- package/src/screens/graphics.ts +3 -3
- package/src/screens/index.ts +1 -0
- package/src/screens/network.ts +2 -2
- package/src/screens/services.ts +2 -2
- package/src/screens/sessions.ts +2 -2
- package/src/screens/themes.ts +3 -2
- package/src/screens/traffic.ts +3 -3
- package/src/screens/world.ts +102 -0
- package/src/state.ts +18 -2
package/src/screens/services.ts
CHANGED
|
@@ -13,7 +13,7 @@ export function servicesScreen(ui: Container, state: DemoState, theme: Theme): v
|
|
|
13
13
|
const t = state.sample.telemetry;
|
|
14
14
|
const failed = t.services.filter((s) => s.active === "failed");
|
|
15
15
|
|
|
16
|
-
ui.row({ size: "1fr", gap
|
|
16
|
+
ui.row({ size: "1fr", gap }, (row) => {
|
|
17
17
|
row.panel({
|
|
18
18
|
title: "Services",
|
|
19
19
|
subtitle: failed.length ? `${failed.length} failed` : `${t.services.length} units`,
|
|
@@ -50,7 +50,7 @@ export function servicesScreen(ui: Container, state: DemoState, theme: Theme): v
|
|
|
50
50
|
});
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
row.column({ width: "0.85fr", gap }, (column) => {
|
|
53
|
+
row.column({ width: "0.85fr", gap, bordered: true }, (column) => {
|
|
54
54
|
column.panel({ title: "Kernel", size: 11, borderColor: theme.accent }, (p) => {
|
|
55
55
|
const k = t.kernel;
|
|
56
56
|
p.keyValues([
|
package/src/screens/sessions.ts
CHANGED
|
@@ -43,7 +43,7 @@ export function sessionsScreen(ui: Container, state: DemoState, theme: Theme): v
|
|
|
43
43
|
});
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
ui.row({ size: "1fr", gap
|
|
46
|
+
ui.row({ size: "1fr", gap }, (row) => {
|
|
47
47
|
row.panel({ title: "Recent Logins", subtitle: `${t.logins.length} from wtmp`, borderColor: theme.accent }, (p) => {
|
|
48
48
|
if (t.logins.length === 0) {
|
|
49
49
|
p.label("No login history available.");
|
|
@@ -75,7 +75,7 @@ export function sessionsScreen(ui: Container, state: DemoState, theme: Theme): v
|
|
|
75
75
|
});
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
-
row.column({ width: "0.8fr", gap }, (column) => {
|
|
78
|
+
row.column({ width: "0.8fr", gap, bordered: true }, (column) => {
|
|
79
79
|
column.panel({ title: "Failed Logins", borderColor: theme.danger }, (p) => {
|
|
80
80
|
if (t.failedLogins.length === 0) {
|
|
81
81
|
p.label("None recorded.");
|
package/src/screens/themes.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { Container, Theme } from "@profullstack/hqtui";
|
|
2
2
|
import { themeList } from "@profullstack/hqtui";
|
|
3
|
-
import type
|
|
3
|
+
import { panelGap, type DemoState } from "../state.ts";
|
|
4
4
|
|
|
5
5
|
/** Live theme switching. Every theme renders the same content side by side. */
|
|
6
6
|
export function themesScreen(ui: Container, state: DemoState, theme: Theme): void {
|
|
7
7
|
const values = state.sample.cpu.history;
|
|
8
|
+
const gap = panelGap(state);
|
|
8
9
|
ui.label(`Theme ${state.themeIndex + 1}/${themeList.length}: ${theme.name} ←/→ or F2 to change`, { size: 1 });
|
|
9
10
|
ui.spacer(1);
|
|
10
|
-
ui.grid({ columns: 3, rows: Math.ceil(themeList.length / 3), gap
|
|
11
|
+
ui.grid({ columns: 3, rows: Math.ceil(themeList.length / 3), gap }, (grid) => {
|
|
11
12
|
themeList.forEach((entry, i) => {
|
|
12
13
|
grid.panel({
|
|
13
14
|
title: entry.name,
|
package/src/screens/traffic.ts
CHANGED
|
@@ -86,8 +86,8 @@ export function trafficScreen(ui: Container, state: DemoState, theme: Theme): vo
|
|
|
86
86
|
});
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
ui.row({ size: "1fr", gap
|
|
90
|
-
row.column({ gap }, (column) => {
|
|
89
|
+
ui.row({ size: "1fr", gap }, (row) => {
|
|
90
|
+
row.column({ gap, bordered: true }, (column) => {
|
|
91
91
|
column.panel({
|
|
92
92
|
title: "HTTP",
|
|
93
93
|
subtitle: http ? `${num(http.requestsPerSecond, 1)} req/s` : "no access log",
|
|
@@ -136,7 +136,7 @@ export function trafficScreen(ui: Container, state: DemoState, theme: Theme): vo
|
|
|
136
136
|
});
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
-
row.column({ width: "0.85fr", gap }, (column) => {
|
|
139
|
+
row.column({ width: "0.85fr", gap, bordered: true }, (column) => {
|
|
140
140
|
column.panel({ title: "SSH Activity", subtitle: String(t.ssh.length), borderColor: theme.warning }, (p) => {
|
|
141
141
|
if (t.ssh.length === 0) {
|
|
142
142
|
p.label("No sshd events in the journal.");
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Container, Theme } from "@profullstack/hqtui";
|
|
2
|
+
import { WORLD_COUNTRIES, WORLD_X, WORLD_Y, countryBounds } from "@profullstack/hqtui";
|
|
3
|
+
import { focusPane, pane, panelGap, scrollPane, type DemoState } from "../state.ts";
|
|
4
|
+
|
|
5
|
+
/** How many coordinates a country's outlines are drawn from. */
|
|
6
|
+
function pointCount(rings: number[][]): number {
|
|
7
|
+
let total = 0;
|
|
8
|
+
for (const ring of rings) total += ring.length / 2;
|
|
9
|
+
return total;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function degrees(value: number, positive: string, negative: string): string {
|
|
13
|
+
return `${Math.abs(value).toFixed(1)}°${value >= 0 ? positive : negative}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The clickable world map.
|
|
18
|
+
*
|
|
19
|
+
* The country list and the map are two views of one selection rather than two
|
|
20
|
+
* selections that have to be kept in step: the list's pane index *is* which
|
|
21
|
+
* country is picked, so the arrows move the highlight on the map for free, and
|
|
22
|
+
* a click on the map only has to move the list.
|
|
23
|
+
*/
|
|
24
|
+
export function worldScreen(ui: Container, state: DemoState, theme: Theme): void {
|
|
25
|
+
const gap = panelGap(state);
|
|
26
|
+
const countries = pane(state, "world.countries", WORLD_COUNTRIES.length);
|
|
27
|
+
const selected = WORLD_COUNTRIES[countries.selected];
|
|
28
|
+
const hovered = state.worldHovered ? WORLD_COUNTRIES.find((c) => c.name === state.worldHovered) : undefined;
|
|
29
|
+
|
|
30
|
+
// Zooming is only meaningful with something to zoom to, and a country whose
|
|
31
|
+
// outlines are a single pixel would otherwise zoom to an empty window.
|
|
32
|
+
const window = state.worldZoom && selected ? countryBounds(selected) : { x: WORLD_X, y: WORLD_Y };
|
|
33
|
+
|
|
34
|
+
ui.row({ size: "1fr", gap }, (row) => {
|
|
35
|
+
row.panel({
|
|
36
|
+
title: "World",
|
|
37
|
+
subtitle: hovered ? hovered.name : selected?.name ?? "click a country",
|
|
38
|
+
subtitleColor: hovered ? theme.accent : theme.muted,
|
|
39
|
+
borderColor: theme.primary,
|
|
40
|
+
footer: state.worldZoom ? "click select · z zoom · r whole globe" : "click select · z zoom to selection",
|
|
41
|
+
}, (p) => {
|
|
42
|
+
p.worldMap({
|
|
43
|
+
...window,
|
|
44
|
+
color: theme.border,
|
|
45
|
+
highlight: [selected?.iso || selected?.name || "", hovered?.iso || hovered?.name || ""],
|
|
46
|
+
highlightColor: theme.accent,
|
|
47
|
+
onSelect: (country) => {
|
|
48
|
+
if (!country) return;
|
|
49
|
+
const index = WORLD_COUNTRIES.findIndex((c) => c.name === country.name);
|
|
50
|
+
if (index >= 0) {
|
|
51
|
+
countries.selected = index;
|
|
52
|
+
focusPane(state, "world.countries");
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
onHover: (country) => {
|
|
56
|
+
state.worldHovered = country?.name ?? "";
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
row.column({ width: 34, gap, bordered: true }, (column) => {
|
|
62
|
+
column.panel({ title: "Selection", size: 9, borderColor: theme.accent }, (p) => {
|
|
63
|
+
if (!selected) {
|
|
64
|
+
p.label("Nothing selected.");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const b = countryBounds(selected);
|
|
68
|
+
p.keyValues([
|
|
69
|
+
{ label: "Country", value: selected.name, color: theme.accent },
|
|
70
|
+
{ label: "ISO", value: selected.iso || "—" },
|
|
71
|
+
{ label: "Longitude", value: `${degrees(b.x.min, "E", "W")} – ${degrees(b.x.max, "E", "W")}`, color: theme.primary },
|
|
72
|
+
{ label: "Latitude", value: `${degrees(b.y.min, "N", "S")} – ${degrees(b.y.max, "N", "S")}`, color: theme.primary },
|
|
73
|
+
{ label: "Outlines", value: String(selected.rings.length), color: theme.muted },
|
|
74
|
+
{ label: "Points", value: String(pointCount(selected.rings)), color: theme.muted },
|
|
75
|
+
], { spread: false });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
column.panel({
|
|
79
|
+
title: "Countries",
|
|
80
|
+
subtitle: String(WORLD_COUNTRIES.length),
|
|
81
|
+
size: "1fr",
|
|
82
|
+
borderColor: theme.secondary,
|
|
83
|
+
}, (p) => {
|
|
84
|
+
p.table({
|
|
85
|
+
rows: WORLD_COUNTRIES as unknown as Array<{ name: string; iso: string }>,
|
|
86
|
+
selected: countries.selected,
|
|
87
|
+
offset: countries.offset,
|
|
88
|
+
followSelection: true,
|
|
89
|
+
onScroll: (delta) => scrollPane(countries, delta),
|
|
90
|
+
onFocus: () => focusPane(state, "world.countries"),
|
|
91
|
+
onSelectRow: (row) => { countries.selected = countries.offset + row; },
|
|
92
|
+
zebra: true,
|
|
93
|
+
scrollbar: true,
|
|
94
|
+
columns: [
|
|
95
|
+
{ key: "name", title: "Country", min: 16, color: theme.foreground },
|
|
96
|
+
{ key: "iso", title: "ISO", width: 4, color: theme.muted },
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
package/src/state.ts
CHANGED
|
@@ -3,13 +3,23 @@ import type { SystemSample } from "./system/index.ts";
|
|
|
3
3
|
|
|
4
4
|
export type ScreenName =
|
|
5
5
|
| "dashboard" | "traffic" | "sessions" | "network" | "services"
|
|
6
|
-
| "components" | "graphics" | "themes" | "input" | "stress";
|
|
6
|
+
| "components" | "graphics" | "themes" | "input" | "stress" | "world";
|
|
7
7
|
|
|
8
8
|
export const SCREENS: ScreenName[] = [
|
|
9
9
|
"dashboard", "traffic", "sessions", "network", "services",
|
|
10
|
-
"components", "graphics", "themes", "input", "stress",
|
|
10
|
+
"components", "graphics", "themes", "input", "stress", "world",
|
|
11
11
|
];
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* The key that jumps straight to each screen. Digits run out at ten, so the
|
|
15
|
+
* eleventh takes a letter rather than a second "1" that would shadow the first.
|
|
16
|
+
*/
|
|
17
|
+
export const SCREEN_KEYS: Record<ScreenName, string> = {
|
|
18
|
+
dashboard: "1", traffic: "2", sessions: "3", network: "4", services: "5",
|
|
19
|
+
components: "6", graphics: "7", themes: "8", input: "9", stress: "0",
|
|
20
|
+
world: "w",
|
|
21
|
+
};
|
|
22
|
+
|
|
13
23
|
export interface DemoState {
|
|
14
24
|
sample: SystemSample;
|
|
15
25
|
screen: ScreenName;
|
|
@@ -42,6 +52,10 @@ export interface DemoState {
|
|
|
42
52
|
paletteIndex: number;
|
|
43
53
|
themeIndex: number;
|
|
44
54
|
/** Component-showcase interactive state. */
|
|
55
|
+
/** The country under the pointer on the world map, by name; "" for open water. */
|
|
56
|
+
worldHovered: string;
|
|
57
|
+
/** Whether the world map is framed on the selection rather than the globe. */
|
|
58
|
+
worldZoom: boolean;
|
|
45
59
|
toggle: boolean;
|
|
46
60
|
checkbox: boolean;
|
|
47
61
|
selectOpen: boolean;
|
|
@@ -142,6 +156,8 @@ export function createState(
|
|
|
142
156
|
focused: {},
|
|
143
157
|
sort: "cpu",
|
|
144
158
|
collapsed: false,
|
|
159
|
+
worldHovered: "",
|
|
160
|
+
worldZoom: false,
|
|
145
161
|
filter: "",
|
|
146
162
|
filtering: false,
|
|
147
163
|
showHelp: false,
|