querysub 0.652.0 → 0.653.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/package.json
CHANGED
|
@@ -1,14 +1,48 @@
|
|
|
1
1
|
import preact from "preact";
|
|
2
2
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
3
3
|
import { qreact } from "../../4-dom/qreact";
|
|
4
|
-
import { sort } from "socket-function/src/misc";
|
|
4
|
+
import { sort, timeInMinute } from "socket-function/src/misc";
|
|
5
5
|
import { isDefined } from "../../misc";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { formatTime } from "socket-function/src/formatting/format";
|
|
7
|
+
import { css } from "typesafecss";
|
|
8
|
+
import { Querysub } from "../../4-querysub/Querysub";
|
|
9
|
+
import { MachineServiceController, getLiveServiceParameters, getMachineTargets, applyCommandTemplate } from "../machineSchema";
|
|
10
|
+
import { FUNCTION_RUNNER_COMMAND_PREFIX } from "../serviceCategories";
|
|
9
11
|
|
|
10
12
|
module.hotreload = true;
|
|
11
13
|
|
|
14
|
+
const DEAD_HEARTBEAT_THRESHOLD = 10 * timeInMinute;
|
|
15
|
+
|
|
16
|
+
const NETWORK_ARG_REGEX = /--network[= ]+("([^"]*)"|'([^']*)'|\S+)/g;
|
|
17
|
+
|
|
18
|
+
/** The networks each machine's deployed function runner services listen on, from the `--network` args of their commands (templated per machine entry). */
|
|
19
|
+
export function getMachineNetworks(controller: ReturnType<typeof MachineServiceController>): Map<string, string[]> {
|
|
20
|
+
let result = new Map<string, string[]>();
|
|
21
|
+
for (let serviceId of controller.getServiceList() || []) {
|
|
22
|
+
let config = controller.getServiceConfig(serviceId);
|
|
23
|
+
if (!config) continue;
|
|
24
|
+
let parameters = getLiveServiceParameters(config);
|
|
25
|
+
if (!parameters.deploy) continue;
|
|
26
|
+
if (!parameters.command.trimStart().startsWith(FUNCTION_RUNNER_COMMAND_PREFIX)) continue;
|
|
27
|
+
for (let target of getMachineTargets(parameters)) {
|
|
28
|
+
let command = applyCommandTemplate(parameters.command, target.variables);
|
|
29
|
+
for (let match of command.matchAll(NETWORK_ARG_REGEX)) {
|
|
30
|
+
let network = match[2] ?? match[3] ?? match[1];
|
|
31
|
+
if (!network) continue;
|
|
32
|
+
let list = result.get(target.machineId);
|
|
33
|
+
if (!list) {
|
|
34
|
+
list = [];
|
|
35
|
+
result.set(target.machineId, list);
|
|
36
|
+
}
|
|
37
|
+
if (!list.includes(network)) {
|
|
38
|
+
list.push(network);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
12
46
|
export class MachinePicker extends qreact.Component<{
|
|
13
47
|
label?: preact.ComponentChild;
|
|
14
48
|
picked: string[];
|
|
@@ -22,19 +56,51 @@ export class MachinePicker extends qreact.Component<{
|
|
|
22
56
|
let machines = (controller.getMachineList() || []).map(x => controller.getMachineInfo(x)).filter(isDefined);
|
|
23
57
|
sort(machines, x => -x.heartbeat);
|
|
24
58
|
|
|
25
|
-
let
|
|
26
|
-
|
|
27
|
-
label: `${machineObj.machineId} ${machineObj.info["getExternalIP"]} (${Object.keys(machineObj.services || {}).length} services, last heartbeat ${formatVeryNiceDateTime(machineObj.heartbeat)})`,
|
|
28
|
-
}));
|
|
59
|
+
let networksPerMachine = getMachineNetworks(controller);
|
|
60
|
+
let now = Querysub.nowDelayed(timeInMinute);
|
|
29
61
|
|
|
30
|
-
return <
|
|
31
|
-
label
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
62
|
+
return <div className={css.vbox(6) + (this.props.fillWidth && css.fillWidth || "")}>
|
|
63
|
+
{this.props.label && <b>{this.props.label}</b>}
|
|
64
|
+
<div className={css.hbox(8).wrap}>
|
|
65
|
+
{machines.map(machine => {
|
|
66
|
+
let machineId = machine.machineId;
|
|
67
|
+
let isPicked = this.props.picked.includes(machineId);
|
|
68
|
+
let ip = machine.info["getExternalIP"];
|
|
69
|
+
if (typeof ip !== "string") {
|
|
70
|
+
ip = "";
|
|
71
|
+
}
|
|
72
|
+
let sinceHeartbeat = now - machine.heartbeat;
|
|
73
|
+
let isLikelyDead = sinceHeartbeat > DEAD_HEARTBEAT_THRESHOLD;
|
|
74
|
+
let networks = networksPerMachine.get(machineId) || [];
|
|
75
|
+
return <button
|
|
76
|
+
key={machineId}
|
|
77
|
+
className={
|
|
78
|
+
css.button.pad2(10, 6).textAlign("left").vbox(2)
|
|
79
|
+
+ (isPicked && css.hsl(100, 50, 85).bord2(100, 50, 40, 2) || css.hsl(0, 0, 100).bord2(0, 0, 70))
|
|
80
|
+
}
|
|
81
|
+
onClick={() => {
|
|
82
|
+
if (isPicked) {
|
|
83
|
+
this.props.removePicked(machineId);
|
|
84
|
+
} else {
|
|
85
|
+
this.props.addPicked(machineId);
|
|
86
|
+
}
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
<div className={css.fontSize(17).fontWeight("bold")}>
|
|
90
|
+
{ip || machineId}
|
|
91
|
+
</div>
|
|
92
|
+
{ip && <div className={css.fontSize(11).colorhsl(0, 0, 40)}>
|
|
93
|
+
{machineId}
|
|
94
|
+
</div>}
|
|
95
|
+
{networks.length > 0 && <div className={css.fontSize(11).colorhsl(220, 60, 40)}>
|
|
96
|
+
{networks.join(", ")}
|
|
97
|
+
</div>}
|
|
98
|
+
{isLikelyDead && <div className={css.fontSize(11).colorhsl(0, 80, 45)}>
|
|
99
|
+
⚠️ Likely dead ({formatTime(sinceHeartbeat)} since heartbeat)
|
|
100
|
+
</div>}
|
|
101
|
+
</button>;
|
|
102
|
+
})}
|
|
103
|
+
</div>
|
|
104
|
+
</div>;
|
|
39
105
|
}
|
|
40
106
|
}
|
|
@@ -73,10 +73,10 @@ class ProcessOutput extends qreact.Component<{ record: ProcessRecord; machineNod
|
|
|
73
73
|
private outputDiv: HTMLDivElement | undefined;
|
|
74
74
|
// The pane opens showing the newest output and follows it as it streams; scrolling up to read releases the pin, scrolling back down re-engages it
|
|
75
75
|
private pinnedToBottom = true;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
private scrollToEndIfPinned() {
|
|
77
|
+
let div = this.outputDiv;
|
|
78
|
+
if (!this.pinnedToBottom || !div) return;
|
|
79
|
+
div.scrollTop = div.scrollHeight;
|
|
80
80
|
}
|
|
81
81
|
render() {
|
|
82
82
|
return <div
|
|
@@ -96,6 +96,11 @@ class ProcessOutput extends qreact.Component<{ record: ProcessRecord; machineNod
|
|
|
96
96
|
// The pane is dark, so the ansi colour only sets the hue - the lightness has to stay readable against it
|
|
97
97
|
return <span className={css.colorhsl(rgbToHsl(color).h, ANSI_SATURATION, ANSI_LIGHTNESS)}>{text}</span>;
|
|
98
98
|
})}
|
|
99
|
+
{/* Keyed by the data length so it is recreated on every append, and its ref fires after the new content is in the DOM - which is when scrolling to the end works */}
|
|
100
|
+
{this.state.data && <span
|
|
101
|
+
key={`end-${this.state.data.length}`}
|
|
102
|
+
ref={element => element && this.scrollToEndIfPinned()}
|
|
103
|
+
/>}
|
|
99
104
|
</div>;
|
|
100
105
|
}
|
|
101
106
|
}
|
|
@@ -7,7 +7,7 @@ import { Querysub } from "../../4-querysub/Querysub";
|
|
|
7
7
|
import { currentViewParam, selectedServiceIdParam, selectedMachineIdParam } from "../urlParams";
|
|
8
8
|
import { formatDateTime, formatDateTimeDetailed, formatTime, formatVeryNiceDateTime } from "socket-function/src/formatting/format";
|
|
9
9
|
import { InputPicker } from "../../library-components/InputPicker";
|
|
10
|
-
import { MachinePicker } from "./MachinePicker";
|
|
10
|
+
import { MachinePicker, getMachineNetworks } from "./MachinePicker";
|
|
11
11
|
import { deepCloneJSON, sort, timeInSecond } from "socket-function/src/misc";
|
|
12
12
|
import { InputLabel } from "../../library-components/InputLabel";
|
|
13
13
|
import { Button } from "../../library-components/Button";
|
|
@@ -105,43 +105,72 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
105
105
|
});
|
|
106
106
|
};
|
|
107
107
|
|
|
108
|
-
let
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}}>
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
108
|
+
let controller = MachineServiceController(SocketFunction.browserNodeId());
|
|
109
|
+
let networksPerMachine = getMachineNetworks(controller);
|
|
110
|
+
|
|
111
|
+
// Grouped by machine, keeping each entry's index into the targets array (edits go by that index) and its per-machine occurrence index (the screen name index).
|
|
112
|
+
let machineGroups = new Map<string, { entryIndex: number; index: number; target: (typeof targets)[number] }[]>();
|
|
113
|
+
targets.forEach((target, entryIndex) => {
|
|
114
|
+
let group = machineGroups.get(target.machineId);
|
|
115
|
+
if (!group) {
|
|
116
|
+
group = [];
|
|
117
|
+
machineGroups.set(target.machineId, group);
|
|
118
|
+
}
|
|
119
|
+
group.push({ entryIndex, index: group.length, target });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return <div className={css.vbox(8).fillWidth}>
|
|
123
|
+
{[...machineGroups.entries()].map(([machineId, group]) => {
|
|
124
|
+
let ip = controller.getMachineInfo(machineId)?.info["getExternalIP"];
|
|
125
|
+
if (typeof ip !== "string") {
|
|
126
|
+
ip = "";
|
|
127
|
+
}
|
|
128
|
+
let networks = networksPerMachine.get(machineId) || [];
|
|
129
|
+
return <div key={machineId} className={css.vbox(6).fillWidth.pad2(8, 6).hsl(0, 0, 94)}>
|
|
130
|
+
<div className={css.hbox(10).alignItems("baseline")}>
|
|
131
|
+
<div className={css.boldStyle.fontSize(15)}>{ip || machineId}</div>
|
|
132
|
+
{ip && <div className={css.fontSize(11).colorhsl(0, 0, 40)}>{machineId}</div>}
|
|
133
|
+
{networks.length > 0 && <div className={css.fontSize(11).colorhsl(220, 60, 40)}>
|
|
134
|
+
{networks.join(", ")}
|
|
135
|
+
</div>}
|
|
136
|
+
</div>
|
|
137
|
+
<div className={css.hbox(6).wrap}>
|
|
138
|
+
{group.map(({ entryIndex, index, target }) => {
|
|
139
|
+
// Variables set on the entry but no longer in the command are still shown (flagged), so stale values can be cleared.
|
|
140
|
+
let variableNames = [...new Set([...templateVariables, ...Object.keys(target.variables)])];
|
|
141
|
+
return <div key={entryIndex} className={css.hbox(8).alignItems("center").pad2(8, 4).hsl(0, 0, 100)}>
|
|
142
|
+
<div className={css.fontSize(11).colorhsl(0, 0, 40)}>{getScreenName({ serviceKey: config.parameters.key, index })}</div>
|
|
143
|
+
{variableNames.map(name => {
|
|
144
|
+
let inCommand = templateVariables.includes(name);
|
|
145
|
+
return <InputLabel
|
|
146
|
+
label={inCommand && name || `${name} (not in command)`}
|
|
147
|
+
value={target.variables[name] || ""}
|
|
148
|
+
onChangeValue={value => setVariable(entryIndex, name, value)}
|
|
149
|
+
/>;
|
|
150
|
+
})}
|
|
151
|
+
<button
|
|
152
|
+
className={css.pad2(10, 4).button.bord2(0, 0, 20).hsl(120, 70, 90)}
|
|
153
|
+
title="Duplicate this entry (same machine and variables)"
|
|
154
|
+
onClick={() => {
|
|
155
|
+
updateTargets(targets => {
|
|
156
|
+
targets.splice(entryIndex + 1, 0, deepCloneJSON(targets[entryIndex]));
|
|
157
|
+
});
|
|
158
|
+
}}>
|
|
159
|
+
+
|
|
160
|
+
</button>
|
|
161
|
+
<button
|
|
162
|
+
className={css.pad2(10, 4).button.bord2(0, 0, 20).hsl(0, 70, 90)}
|
|
163
|
+
title="Remove this entry"
|
|
164
|
+
onClick={() => {
|
|
165
|
+
updateTargets(targets => {
|
|
166
|
+
targets.splice(entryIndex, 1);
|
|
167
|
+
});
|
|
168
|
+
}}>
|
|
169
|
+
−
|
|
170
|
+
</button>
|
|
171
|
+
</div>;
|
|
172
|
+
})}
|
|
173
|
+
</div>
|
|
145
174
|
</div>;
|
|
146
175
|
})}
|
|
147
176
|
</div>;
|
|
@@ -306,7 +335,7 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
306
335
|
this.updateEditorState(updated);
|
|
307
336
|
}}
|
|
308
337
|
/>
|
|
309
|
-
<div className={css.hbox(12)}>
|
|
338
|
+
<div className={css.hbox(12).alignItems("center")}>
|
|
310
339
|
{(() => {
|
|
311
340
|
let lastDeployTime = getLiveServiceParameters(originalConfig).releaseTime;
|
|
312
341
|
if (!lastDeployTime || lastDeployTime > now) return undefined;
|
|
@@ -314,6 +343,9 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
314
343
|
Last deploy at {formatDateTime(lastDeployTime)} ({formatTime(now - lastDeployTime)} ago)
|
|
315
344
|
</span>;
|
|
316
345
|
})()}
|
|
346
|
+
{!config.parameters.deploy && <div className={css.fontSize(24).boldStyle.colorhsl(0, 80, 45).hsl(0, 80, 95).pad2(12, 6)}>
|
|
347
|
+
NOT DEPLOYED, ENABLE IN ORDER TO DEPLOY
|
|
348
|
+
</div>}
|
|
317
349
|
</div>
|
|
318
350
|
{/* Machine Status */}
|
|
319
351
|
{config.parameters.deploy && <div className={css.vbox(8).fillWidth}>
|
|
@@ -5,7 +5,7 @@ import { DISK_PATHVALUE_COLOR, PATHVALUE_COLOR, FUNCTION_RUNNER_COLOR } from "..
|
|
|
5
5
|
// What a service IS, worked out from the command it runs. The commands are the only signal we have - nothing in the config says "this is a storage server" - so the prefixes live here rather than in whichever page happened to need one first.
|
|
6
6
|
export const STORAGE_COMMAND_PREFIX = "yarn storageserve";
|
|
7
7
|
const PATHVALUE_COMMAND_PREFIX = "yarn server-public";
|
|
8
|
-
const FUNCTION_RUNNER_COMMAND_PREFIX = "yarn function-public";
|
|
8
|
+
export const FUNCTION_RUNNER_COMMAND_PREFIX = "yarn function-public";
|
|
9
9
|
|
|
10
10
|
export type ServiceCategory = {
|
|
11
11
|
label: string;
|