querysub 0.508.0 → 0.510.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/bin/function-public.js +2 -0
- package/bin/server.js +0 -2
- package/package.json +3 -4
- package/spec.txt +5 -0
- package/src/-f-node-discovery/NodeDiscovery.ts +1 -1
- package/src/-g-core-values/NodeCapabilities.ts +4 -0
- package/src/-g-core-values/scheduledShutdown.ts +48 -0
- package/src/0-path-value-core/AuthorityLookup.ts +50 -9
- package/src/0-path-value-core/PathRouter.ts +10 -31
- package/src/0-path-value-core/PathRouterRouteOverride.ts +14 -98
- package/src/0-path-value-core/PathRouterServerAuthoritySpec.tsx +1 -4
- package/src/0-path-value-core/PathValueCommitter.ts +1 -6
- package/src/0-path-value-core/PathValueController.ts +5 -7
- package/src/0-path-value-core/PathWatcher.ts +8 -11
- package/src/0-path-value-core/hackedPackedPathParentFiltering.ts +8 -22
- package/src/0-path-value-core/pathValueCore.ts +4 -5
- package/src/1-path-client/RemoteWatcher.ts +47 -34
- package/src/2-proxy/archiveMoveHarness.ts +2 -1
- package/src/3-path-functions/PathFunctionHelpers.ts +11 -10
- package/src/3-path-functions/PathFunctionRunner.ts +165 -24
- package/src/3-path-functions/functionReplay.ts +2 -0
- package/src/3-path-functions/syncSchema.ts +0 -2
- package/src/4-deploy/edgeBootstrap.ts +10 -0
- package/src/4-deploy/edgeClientWatcher.tsx +4 -6
- package/src/4-deploy/edgeNodes.ts +52 -12
- package/src/4-querysub/FunctionRunnerTracking.ts +290 -0
- package/src/4-querysub/Querysub.ts +11 -16
- package/src/4-querysub/QuerysubController.ts +13 -11
- package/src/4-querysub/permissions.ts +3 -0
- package/src/4-querysub/querysubPrediction.ts +2 -2
- package/src/config.ts +1 -3
- package/src/deployManager/components/ServiceDetailPage.tsx +206 -77
- package/src/deployManager/components/ServicesListPage.tsx +3 -3
- package/src/deployManager/machineApplyMainCode.ts +139 -153
- package/src/deployManager/machineController.ts +12 -61
- package/src/deployManager/machineSchema.ts +84 -3
- package/src/deployManager/setupMachineMain.ts +2 -1
- package/src/diagnostics/grossStats/GrossStatsController.ts +3 -2
- package/src/diagnostics/logs/errorTickets/autoFixer.ts +3 -2
- package/src/diagnostics/logs/logGitHashes.ts +1 -1
- package/src/diagnostics/managementPages.tsx +2 -0
- package/src/diagnostics/misc-pages/AuthoritySpecPage.tsx +36 -28
- package/src/diagnostics/misc-pages/FunctionCaptureAnalyzePage.tsx +1 -1
- package/src/library-components/NetworkSelector.tsx +77 -0
- package/src/misc.ts +19 -0
- package/src/user-implementation/userData.ts +1 -1
- package/src/zipThreaded.ts +3 -2
- package/bin/server-dev.js +0 -11
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
2
2
|
import { qreact } from "../../4-dom/qreact";
|
|
3
|
-
import { MACHINE_RESYNC_INTERVAL, MachineServiceController, ServiceConfig } from "../machineSchema";
|
|
3
|
+
import { DEFAULT_OVERLAP_TIME, MACHINE_RESYNC_INTERVAL, MachineServiceController, ServiceConfig } from "../machineSchema";
|
|
4
4
|
import { css } from "typesafecss";
|
|
5
5
|
import { t } from "../../2-proxy/schema2";
|
|
6
6
|
import { Querysub } from "../../4-querysub/Querysub";
|
|
@@ -8,11 +8,11 @@ import { currentViewParam, selectedServiceIdParam, selectedMachineIdParam } from
|
|
|
8
8
|
import { formatTime, formatVeryNiceDateTime } from "socket-function/src/formatting/format";
|
|
9
9
|
import { InputPicker } from "../../library-components/InputPicker";
|
|
10
10
|
import { MachinePicker } from "./MachinePicker";
|
|
11
|
-
import { deepCloneJSON, nextId, sort } from "socket-function/src/misc";
|
|
11
|
+
import { deepCloneJSON, nextId, sort, timeInSecond } from "socket-function/src/misc";
|
|
12
12
|
import { InputLabel } from "../../library-components/InputLabel";
|
|
13
13
|
import { Button } from "../../library-components/Button";
|
|
14
14
|
import { isDefined } from "../../misc";
|
|
15
|
-
import { watchScreenOutput, stopWatchingScreenOutput
|
|
15
|
+
import { watchScreenOutput, stopWatchingScreenOutput } from "../machineController";
|
|
16
16
|
import { getPathStr2 } from "../../path";
|
|
17
17
|
import { ATag, Anchor } from "../../library-components/ATag";
|
|
18
18
|
import { ScrollOnMount } from "../../library-components/ScrollOnMount";
|
|
@@ -26,6 +26,7 @@ import { getLogViewerParams } from "../../diagnostics/logs/IndexedLogs/LogViewer
|
|
|
26
26
|
import { getScreenName } from "../machineApplyMainCode";
|
|
27
27
|
import { getOwnThreadId } from "../../-f-node-discovery/NodeDiscovery";
|
|
28
28
|
import { decodeNodeId } from "sliftutils/misc/https/certs";
|
|
29
|
+
import { showModal } from "../../5-diagnostics/Modal";
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
|
|
@@ -42,7 +43,10 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
42
43
|
data: t.type(""),
|
|
43
44
|
callbackId: t.type(""),
|
|
44
45
|
}),
|
|
45
|
-
|
|
46
|
+
// Milliseconds; 0 means no scheduled time picked yet (use Deploy Now instead)
|
|
47
|
+
switchTime: t.number(0),
|
|
48
|
+
// Seconds; 0 means use the service's configured overlapTime
|
|
49
|
+
overlapOverride: t.number(0),
|
|
46
50
|
});
|
|
47
51
|
|
|
48
52
|
|
|
@@ -109,37 +113,7 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
109
113
|
});
|
|
110
114
|
}
|
|
111
115
|
|
|
112
|
-
|
|
113
|
-
Querysub.commit(() => {
|
|
114
|
-
this.state.isKillingRolling = true;
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
Querysub.onCommitFinished(async () => {
|
|
118
|
-
try {
|
|
119
|
-
let uniqueMachineIds = Array.from(new Set(config.machineIds));
|
|
120
|
-
|
|
121
|
-
for (let machineId of uniqueMachineIds) {
|
|
122
|
-
let machineInfo = await MachineServiceController(SocketFunction.browserNodeId()).getMachineInfo.promise(machineId);
|
|
123
|
-
if (!machineInfo) continue;
|
|
124
|
-
|
|
125
|
-
let applyNodeId = machineInfo.applyNodeId;
|
|
126
|
-
await MachineController(SocketFunction.browserNodeId()).killRollingServicesFromBrowser.promise({
|
|
127
|
-
machineNodeId: applyNodeId,
|
|
128
|
-
serviceKey: config.parameters.key,
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
} catch (e: any) {
|
|
132
|
-
Querysub.localCommit(() => {
|
|
133
|
-
this.state.saveError = `Error killing rolling services: ${e.message}`;
|
|
134
|
-
});
|
|
135
|
-
} finally {
|
|
136
|
-
Querysub.localCommit(() => {
|
|
137
|
-
this.state.isKillingRolling = false;
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
116
|
+
// Saves to the PENDING config, which only takes effect once a switchover is deployed / scheduled.
|
|
143
117
|
private async updateConfig(updatedConfig: ServiceConfig): Promise<void> {
|
|
144
118
|
const selectedServiceId = selectedServiceIdParam.value;
|
|
145
119
|
if (!selectedServiceId) return;
|
|
@@ -153,7 +127,7 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
153
127
|
|
|
154
128
|
Querysub.onCommitFinished(async () => {
|
|
155
129
|
try {
|
|
156
|
-
await MachineServiceController(SocketFunction.browserNodeId()).
|
|
130
|
+
await MachineServiceController(SocketFunction.browserNodeId()).setPendingServiceConfigs.promise([updatedConfig]);
|
|
157
131
|
} catch (error) {
|
|
158
132
|
Querysub.localCommit(() => {
|
|
159
133
|
this.state.saveError = error instanceof Error ? error.message : String(error);
|
|
@@ -168,6 +142,41 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
168
142
|
});
|
|
169
143
|
}
|
|
170
144
|
|
|
145
|
+
private runControllerAction(action: () => Promise<void>) {
|
|
146
|
+
Querysub.onCommitFinished(async () => {
|
|
147
|
+
try {
|
|
148
|
+
await action();
|
|
149
|
+
} catch (error) {
|
|
150
|
+
Querysub.localCommit(() => {
|
|
151
|
+
this.state.saveError = error instanceof Error ? error.message : String(error);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private confirmForceDeployNow(serviceId: string, overlapTime: number) {
|
|
158
|
+
let modal: { close: () => void } | undefined;
|
|
159
|
+
modal = showModal({
|
|
160
|
+
content: <div className={css.fixed.pos(0, 0).size("100vw", "100vh").hsla(0, 0, 0, 0.5).display("flex").alignItems("center").justifyContent("center")}>
|
|
161
|
+
<div className={css.vbox(12).pad2(20).hsl(0, 0, 98).bord2(0, 0, 40) + " keepModalsOpen"}>
|
|
162
|
+
<div className={css.boldStyle.fontSize(16)}>⚡ Force deploy now?</div>
|
|
163
|
+
<div>The pending config becomes live immediately. The old instances are shut down right away, with no overlap.</div>
|
|
164
|
+
<div className={css.hbox(10)}>
|
|
165
|
+
<Button hue={0} onClick={() => {
|
|
166
|
+
modal && modal.close();
|
|
167
|
+
this.runControllerAction(() => MachineServiceController(SocketFunction.browserNodeId()).scheduleSwitchover.promise({
|
|
168
|
+
serviceId,
|
|
169
|
+
switchTime: Date.now(),
|
|
170
|
+
overlapTime,
|
|
171
|
+
}));
|
|
172
|
+
}}>⚡ Force Deploy Now</Button>
|
|
173
|
+
<Button onClick={() => modal && modal.close()}>Cancel</Button>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
171
180
|
render() {
|
|
172
181
|
const selectedServiceId = selectedServiceIdParam.value;
|
|
173
182
|
if (!selectedServiceId) return <div>No service selected</div>;
|
|
@@ -175,40 +184,21 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
175
184
|
const gitInfo = MachineServiceController(SocketFunction.browserNodeId()).getGitInfo();
|
|
176
185
|
|
|
177
186
|
let controller = MachineServiceController(SocketFunction.browserNodeId());
|
|
178
|
-
|
|
187
|
+
const originalConfig = controller.getServiceConfig(selectedServiceId);
|
|
179
188
|
let serviceConfigType = controller.getServiceConfigType();
|
|
189
|
+
let pendingConfig = controller.getPendingServiceConfig(selectedServiceId);
|
|
190
|
+
let switchover = controller.getServiceSwitchover(selectedServiceId);
|
|
180
191
|
|
|
181
192
|
if (!originalConfig || !serviceConfigType) {
|
|
182
193
|
if (controller.isAnyLoading()) return <div>Loading service...</div>;
|
|
183
194
|
return <div>Service not found</div>;
|
|
184
195
|
}
|
|
185
196
|
|
|
186
|
-
//
|
|
187
|
-
const config = this.state.unsavedChanges
|
|
197
|
+
// The editor shows unsaved edits, else the saved-but-not-deployed pending config, else the live config
|
|
198
|
+
const config = this.state.unsavedChanges || pendingConfig || originalConfig;
|
|
188
199
|
const configT = config;
|
|
189
200
|
const hasUnsavedChanges = !!this.state.unsavedChanges;
|
|
190
|
-
|
|
191
|
-
let rollingServiceCount = 0;
|
|
192
|
-
if (config.parameters.rollingWindow) {
|
|
193
|
-
let uniqueMachineIds = Array.from(new Set(config.machineIds));
|
|
194
|
-
for (let machineId of uniqueMachineIds) {
|
|
195
|
-
try {
|
|
196
|
-
let machineInfo = controller.getMachineInfo(machineId);
|
|
197
|
-
if (!machineInfo) continue;
|
|
198
|
-
|
|
199
|
-
let applyNodeId = machineInfo.applyNodeId;
|
|
200
|
-
let rollingInfo = MachineController(SocketFunction.browserNodeId()).getRollingServiceInfoFromBrowser({
|
|
201
|
-
machineNodeId: applyNodeId,
|
|
202
|
-
serviceKey: config.parameters.key,
|
|
203
|
-
});
|
|
204
|
-
if (rollingInfo) {
|
|
205
|
-
rollingServiceCount += rollingInfo.length;
|
|
206
|
-
}
|
|
207
|
-
} catch (e: any) {
|
|
208
|
-
console.error(`Error getting rolling service info for machine ${machineId}: ${e.message}`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
201
|
+
const hasPendingConfig = !!pendingConfig;
|
|
212
202
|
|
|
213
203
|
// Sort machines by status and heartbeat
|
|
214
204
|
let nextIndexes = new Map<string, number>();
|
|
@@ -234,7 +224,7 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
234
224
|
};
|
|
235
225
|
});
|
|
236
226
|
sort(machineStatuses, x => (x.isDisabled ? 3 : x.hasError ? 0 : x.isMachineDead ? 1 : 2) * 1000000 + x.heartbeat);
|
|
237
|
-
let now =
|
|
227
|
+
let now = Querysub.nowDelayed(timeInSecond);
|
|
238
228
|
|
|
239
229
|
let machines = (controller.getMachineList() || []).map(x => controller.getMachineInfo(x)).filter(isDefined);
|
|
240
230
|
sort(machines, x => -x.heartbeat);
|
|
@@ -358,6 +348,11 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
358
348
|
<div title={formatVeryNiceDateTime(machineInfo.heartbeat)}>
|
|
359
349
|
Heartbeat: {formatTime(now - machineInfo.heartbeat)}
|
|
360
350
|
</div>
|
|
351
|
+
{switchover && (
|
|
352
|
+
<div className={css.colorhsl(35, 80, 40).fontWeight("bold")} title={formatVeryNiceDateTime(switchover.switchTime)}>
|
|
353
|
+
⏻ Shuts down in {formatTime(switchover.switchTime - now)}
|
|
354
|
+
</div>
|
|
355
|
+
)}
|
|
361
356
|
{serviceInfo && (
|
|
362
357
|
<>
|
|
363
358
|
{!!serviceInfo.lastLaunchedTime && <div title={formatVeryNiceDateTime(serviceInfo.lastLaunchedTime)}>
|
|
@@ -482,22 +477,14 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
482
477
|
|
|
483
478
|
<button className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(0, 0, 80)}
|
|
484
479
|
onClick={() => {
|
|
485
|
-
|
|
486
|
-
|
|
480
|
+
// Poke bypasses the pending flow, as it exists to force an immediate restart of the LIVE config
|
|
481
|
+
let live = deepCloneJSON(originalConfig);
|
|
482
|
+
live.parameters.poke = (live.parameters.poke || 0) + 1;
|
|
483
|
+
this.runControllerAction(() => controller.setServiceConfigs.promise([live]));
|
|
487
484
|
}}>
|
|
488
485
|
Poke
|
|
489
486
|
</button>
|
|
490
487
|
|
|
491
|
-
{!!configT.parameters.rollingWindow && rollingServiceCount > 0 && <button
|
|
492
|
-
className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(0, 70, 90)}
|
|
493
|
-
onClick={() => {
|
|
494
|
-
void this.killAllRollingServices(config);
|
|
495
|
-
}}
|
|
496
|
-
disabled={this.state.isKillingRolling}
|
|
497
|
-
>
|
|
498
|
-
{this.state.isKillingRolling ? "Killing..." : `💀 Kill ${rollingServiceCount} Rolling Service${rollingServiceCount === 1 ? "" : "s"}`}
|
|
499
|
-
</button>}
|
|
500
|
-
|
|
501
488
|
{hasUnsavedChanges && <>
|
|
502
489
|
<Button
|
|
503
490
|
flavor="noui"
|
|
@@ -511,7 +498,7 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
511
498
|
this.updateConfigSynced(this.state.unsavedChanges);
|
|
512
499
|
}
|
|
513
500
|
}>
|
|
514
|
-
{this.state.isSaving ? "Saving..." : "Save"}
|
|
501
|
+
{this.state.isSaving ? "Saving..." : "Save Pending"}
|
|
515
502
|
</Button>
|
|
516
503
|
<button
|
|
517
504
|
className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(50, 80, 50)}
|
|
@@ -543,7 +530,7 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
543
530
|
}
|
|
544
531
|
|
|
545
532
|
{this.state.isSaving && <div className={css.pad2(12, 8).bord2(0, 0, 20) + " rainbow-flash"}>
|
|
546
|
-
|
|
533
|
+
Saving pending changes
|
|
547
534
|
</div>}
|
|
548
535
|
|
|
549
536
|
<div className={css.marginAuto}></div>
|
|
@@ -604,6 +591,105 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
604
591
|
</button>
|
|
605
592
|
</div>
|
|
606
593
|
|
|
594
|
+
{(hasPendingConfig || switchover) && (() => {
|
|
595
|
+
let overlapSeconds = this.state.overlapOverride || Math.round((config.parameters.overlapTime || DEFAULT_OVERLAP_TIME) / timeInSecond);
|
|
596
|
+
let overlap = overlapSeconds * timeInSecond;
|
|
597
|
+
let scheduledTime = this.state.switchTime;
|
|
598
|
+
let notEnoughTime = !!scheduledTime && scheduledTime > now && scheduledTime - now < overlap;
|
|
599
|
+
return <div className={css.vbox(10).pad2(12).fillWidth.bord2(210, 60, 50).hsl(210, 40, 95)}>
|
|
600
|
+
<div className={css.boldStyle.fontSize(15)}>
|
|
601
|
+
{switchover && "🕐 Switchover scheduled" || "📝 Pending changes saved (not deployed)"}
|
|
602
|
+
</div>
|
|
603
|
+
{hasPendingConfig && <ConfigDiff base={originalConfig} next={config} />}
|
|
604
|
+
{switchover && <div className={css.vbox(4)}>
|
|
605
|
+
<div>
|
|
606
|
+
Old instances shut down at <b>{formatVeryNiceDateTime(switchover.switchTime)}</b> (in {formatTime(switchover.switchTime - now)}), overlap {formatTime(switchover.overlapTime)}
|
|
607
|
+
</div>
|
|
608
|
+
<div>
|
|
609
|
+
New instances start at <b>{formatVeryNiceDateTime(switchover.switchTime - switchover.overlapTime)}</b> {switchover.switchTime - switchover.overlapTime <= now && "(already running)" || `(in ${formatTime(switchover.switchTime - switchover.overlapTime - now)})`}
|
|
610
|
+
</div>
|
|
611
|
+
<div className={css.hbox(8)}>
|
|
612
|
+
<button
|
|
613
|
+
className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(0, 70, 90)}
|
|
614
|
+
onClick={() => {
|
|
615
|
+
this.runControllerAction(() => controller.cancelSwitchover.promise(selectedServiceId));
|
|
616
|
+
}}>
|
|
617
|
+
Cancel Switchover
|
|
618
|
+
</button>
|
|
619
|
+
</div>
|
|
620
|
+
</div>}
|
|
621
|
+
{hasPendingConfig && !switchover && (() => {
|
|
622
|
+
// The switch time is inferred from the overlap, unless one is explicitly set (in which case the overlap only controls when the new instances start)
|
|
623
|
+
let effectiveSwitchTime = scheduledTime || now + overlap;
|
|
624
|
+
return <div className={css.vbox(8)}>
|
|
625
|
+
<div className={css.hbox(12).wrap}>
|
|
626
|
+
<button
|
|
627
|
+
className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(120, 70, 85).fontWeight("bold")}
|
|
628
|
+
onClick={() => {
|
|
629
|
+
this.runControllerAction(() => controller.scheduleSwitchover.promise({
|
|
630
|
+
serviceId: selectedServiceId,
|
|
631
|
+
switchTime: scheduledTime || Date.now() + overlap,
|
|
632
|
+
overlapTime: overlap,
|
|
633
|
+
}));
|
|
634
|
+
}}>
|
|
635
|
+
🚀 Deploy
|
|
636
|
+
</button>
|
|
637
|
+
<div className={css.opacity(scheduledTime && 1 || 0.5)}>
|
|
638
|
+
<InputLabel
|
|
639
|
+
label="Switch at"
|
|
640
|
+
isDatetime
|
|
641
|
+
value={effectiveSwitchTime}
|
|
642
|
+
onChangeValue={value => {
|
|
643
|
+
this.state.switchTime = Number(value) || 0;
|
|
644
|
+
}}
|
|
645
|
+
/>
|
|
646
|
+
</div>
|
|
647
|
+
<span>(in {formatTime(effectiveSwitchTime - now)})</span>
|
|
648
|
+
{!!scheduledTime && <button
|
|
649
|
+
className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(0, 0, 90)}
|
|
650
|
+
onClick={() => {
|
|
651
|
+
this.state.switchTime = 0;
|
|
652
|
+
}}>
|
|
653
|
+
↺ Reset to overlap-based time
|
|
654
|
+
</button>}
|
|
655
|
+
<div className={css.opacity(scheduledTime && 0.5 || 1)}>
|
|
656
|
+
<InputLabel
|
|
657
|
+
label="Overlap (seconds)"
|
|
658
|
+
integer
|
|
659
|
+
value={overlapSeconds}
|
|
660
|
+
onChangeValue={value => {
|
|
661
|
+
this.state.overlapOverride = Number(value) || 0;
|
|
662
|
+
}}
|
|
663
|
+
/>
|
|
664
|
+
</div>
|
|
665
|
+
</div>
|
|
666
|
+
{!!scheduledTime && scheduledTime <= now && <span className={css.colorhsl(0, 85, 45).fontWeight("bold")}>
|
|
667
|
+
⚠️ The switch time is in the past — deploying switches immediately
|
|
668
|
+
</span>}
|
|
669
|
+
{notEnoughTime && <span className={css.colorhsl(35, 85, 40).fontWeight("bold")}>
|
|
670
|
+
⚠️ The switch time is sooner than the overlap ({formatTime(overlap)}) — the new instances won't be fully started before the old ones shut down
|
|
671
|
+
</span>}
|
|
672
|
+
<div className={css.hbox(8)}>
|
|
673
|
+
<button
|
|
674
|
+
className={css.pad2(12, 8).button.bord2(0, 70, 40).hsl(0, 70, 90)}
|
|
675
|
+
onClick={() => {
|
|
676
|
+
this.confirmForceDeployNow(selectedServiceId, overlap);
|
|
677
|
+
}}>
|
|
678
|
+
⚡ Force Deploy Now
|
|
679
|
+
</button>
|
|
680
|
+
<button
|
|
681
|
+
className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(50, 80, 60)}
|
|
682
|
+
onClick={() => {
|
|
683
|
+
this.runControllerAction(() => controller.discardPendingServiceConfig.promise(selectedServiceId));
|
|
684
|
+
}}>
|
|
685
|
+
Discard Pending Config
|
|
686
|
+
</button>
|
|
687
|
+
</div>
|
|
688
|
+
</div>;
|
|
689
|
+
})()}
|
|
690
|
+
</div>;
|
|
691
|
+
})()}
|
|
692
|
+
|
|
607
693
|
{this.state.saveError && (
|
|
608
694
|
<div className={css.pad2(12).bord2(0, 80, 50).hsl(0, 80, 95).colorhsl(0, 80, 30)}>
|
|
609
695
|
⚠️ Error: {this.state.saveError}
|
|
@@ -621,4 +707,47 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
621
707
|
/>
|
|
622
708
|
</div>;
|
|
623
709
|
}
|
|
624
|
-
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// An inline diff of the live config vs the config being deployed, so it's clear exactly what a switchover will change.
|
|
713
|
+
class ConfigDiff extends qreact.Component<{ base: ServiceConfig; next: ServiceConfig }> {
|
|
714
|
+
render() {
|
|
715
|
+
let baseValues = new Map<string, unknown>();
|
|
716
|
+
let nextValues = new Map<string, unknown>();
|
|
717
|
+
flattenConfigValues(this.props.base, "", baseValues);
|
|
718
|
+
flattenConfigValues(this.props.next, "", nextValues);
|
|
719
|
+
let keys = Array.from(new Set([...baseValues.keys(), ...nextValues.keys()]));
|
|
720
|
+
sort(keys, x => x);
|
|
721
|
+
let changed = keys.filter(key =>
|
|
722
|
+
// The save time always changes on save, so it would just be noise
|
|
723
|
+
key !== "info.lastUpdatedTime"
|
|
724
|
+
&& JSON.stringify(baseValues.get(key)) !== JSON.stringify(nextValues.get(key))
|
|
725
|
+
);
|
|
726
|
+
if (changed.length === 0) {
|
|
727
|
+
return <div className={css.colorhsl(0, 0, 50)}>(no changes from the live config)</div>;
|
|
728
|
+
}
|
|
729
|
+
return <div className={css.vbox(2).fontFamily("monospace").fontSize(13)}>
|
|
730
|
+
{changed.map(key => <div className={css.hbox(6).wrap}>
|
|
731
|
+
<span className={css.boldStyle}>{key}:</span>
|
|
732
|
+
{baseValues.has(key) && <span className={css.colorhsl(0, 70, 40).textDecoration("line-through")}>{formatDiffValue(baseValues.get(key))}</span>}
|
|
733
|
+
{nextValues.has(key) && <span className={css.colorhsl(120, 60, 30)}>{formatDiffValue(nextValues.get(key))}</span> || <span className={css.colorhsl(0, 70, 40)}>(removed)</span>}
|
|
734
|
+
</div>)}
|
|
735
|
+
</div>;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
function flattenConfigValues(obj: unknown, prefix: string, out: Map<string, unknown>) {
|
|
739
|
+
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
|
|
740
|
+
for (let [key, value] of Object.entries(obj as { [key: string]: unknown })) {
|
|
741
|
+
flattenConfigValues(value, prefix && prefix + "." + key || key, out);
|
|
742
|
+
}
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
out.set(prefix, obj);
|
|
746
|
+
}
|
|
747
|
+
function formatDiffValue(value: unknown): string {
|
|
748
|
+
let result = JSON.stringify(value);
|
|
749
|
+
if (result === undefined) {
|
|
750
|
+
return "undefined";
|
|
751
|
+
}
|
|
752
|
+
return result;
|
|
753
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
2
2
|
import { qreact } from "../../4-dom/qreact";
|
|
3
|
-
import { MachineServiceController, ServiceConfig } from "../machineSchema";
|
|
3
|
+
import { DEFAULT_OVERLAP_TIME, MachineServiceController, ServiceConfig } from "../machineSchema";
|
|
4
4
|
import { css } from "typesafecss";
|
|
5
5
|
import { t } from "../../2-proxy/schema2";
|
|
6
6
|
import { Querysub } from "../../4-querysub/Querysub";
|
|
@@ -55,7 +55,7 @@ export class ServicesListPage extends qreact.Component {
|
|
|
55
55
|
gitRef: gitInfo.latestRef,
|
|
56
56
|
command: "ping 1.1.1.1",
|
|
57
57
|
deploy: false,
|
|
58
|
-
|
|
58
|
+
overlapTime: DEFAULT_OVERLAP_TIME,
|
|
59
59
|
},
|
|
60
60
|
info: {
|
|
61
61
|
title: `New Service created at ${formatNiceDateTime(Date.now())}`,
|
|
@@ -138,7 +138,7 @@ export class ServicesListPage extends qreact.Component {
|
|
|
138
138
|
</div>
|
|
139
139
|
<div className={css.vbox(4)}>
|
|
140
140
|
<div>Updated {formatDateJSX(config.info.lastUpdatedTime)} AGO</div>
|
|
141
|
-
{config.parameters.
|
|
141
|
+
{config.parameters.overlapTime && <div className={css.colorhsl(210, 50, 50).fontSize(14).fontWeight("bold")}>Switchover overlap: {formatTime(config.parameters.overlapTime)}</div> || undefined}
|
|
142
142
|
</div>
|
|
143
143
|
</div>
|
|
144
144
|
</Anchor>
|