querysub 0.509.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/package.json
CHANGED
|
@@ -8,7 +8,7 @@ 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";
|
|
@@ -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
|
|
|
@@ -44,7 +45,7 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
44
45
|
}),
|
|
45
46
|
// Milliseconds; 0 means no scheduled time picked yet (use Deploy Now instead)
|
|
46
47
|
switchTime: t.number(0),
|
|
47
|
-
//
|
|
48
|
+
// Seconds; 0 means use the service's configured overlapTime
|
|
48
49
|
overlapOverride: t.number(0),
|
|
49
50
|
});
|
|
50
51
|
|
|
@@ -153,6 +154,29 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
153
154
|
});
|
|
154
155
|
}
|
|
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
|
+
|
|
156
180
|
render() {
|
|
157
181
|
const selectedServiceId = selectedServiceIdParam.value;
|
|
158
182
|
if (!selectedServiceId) return <div>No service selected</div>;
|
|
@@ -200,7 +224,7 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
200
224
|
};
|
|
201
225
|
});
|
|
202
226
|
sort(machineStatuses, x => (x.isDisabled ? 3 : x.hasError ? 0 : x.isMachineDead ? 1 : 2) * 1000000 + x.heartbeat);
|
|
203
|
-
let now =
|
|
227
|
+
let now = Querysub.nowDelayed(timeInSecond);
|
|
204
228
|
|
|
205
229
|
let machines = (controller.getMachineList() || []).map(x => controller.getMachineInfo(x)).filter(isDefined);
|
|
206
230
|
sort(machines, x => -x.heartbeat);
|
|
@@ -506,7 +530,7 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
506
530
|
}
|
|
507
531
|
|
|
508
532
|
{this.state.isSaving && <div className={css.pad2(12, 8).bord2(0, 0, 20) + " rainbow-flash"}>
|
|
509
|
-
|
|
533
|
+
Saving pending changes
|
|
510
534
|
</div>}
|
|
511
535
|
|
|
512
536
|
<div className={css.marginAuto}></div>
|
|
@@ -568,13 +592,15 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
568
592
|
</div>
|
|
569
593
|
|
|
570
594
|
{(hasPendingConfig || switchover) && (() => {
|
|
571
|
-
let
|
|
595
|
+
let overlapSeconds = this.state.overlapOverride || Math.round((config.parameters.overlapTime || DEFAULT_OVERLAP_TIME) / timeInSecond);
|
|
596
|
+
let overlap = overlapSeconds * timeInSecond;
|
|
572
597
|
let scheduledTime = this.state.switchTime;
|
|
573
|
-
let notEnoughTime = !!scheduledTime && scheduledTime - now < overlap;
|
|
598
|
+
let notEnoughTime = !!scheduledTime && scheduledTime > now && scheduledTime - now < overlap;
|
|
574
599
|
return <div className={css.vbox(10).pad2(12).fillWidth.bord2(210, 60, 50).hsl(210, 40, 95)}>
|
|
575
600
|
<div className={css.boldStyle.fontSize(15)}>
|
|
576
601
|
{switchover && "🕐 Switchover scheduled" || "📝 Pending changes saved (not deployed)"}
|
|
577
602
|
</div>
|
|
603
|
+
{hasPendingConfig && <ConfigDiff base={originalConfig} next={config} />}
|
|
578
604
|
{switchover && <div className={css.vbox(4)}>
|
|
579
605
|
<div>
|
|
580
606
|
Old instances shut down at <b>{formatVeryNiceDateTime(switchover.switchTime)}</b> (in {formatTime(switchover.switchTime - now)}), overlap {formatTime(switchover.overlapTime)}
|
|
@@ -592,69 +618,75 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
592
618
|
</button>
|
|
593
619
|
</div>
|
|
594
620
|
</div>}
|
|
595
|
-
{hasPendingConfig && !switchover &&
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
|
630
668
|
</span>}
|
|
631
669
|
{notEnoughTime && <span className={css.colorhsl(35, 85, 40).fontWeight("bold")}>
|
|
632
|
-
⚠️
|
|
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
|
|
633
671
|
</span>}
|
|
634
|
-
<
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
this.runControllerAction(() => controller.discardPendingServiceConfig.promise(selectedServiceId));
|
|
653
|
-
}}>
|
|
654
|
-
Discard Pending Config
|
|
655
|
-
</button>
|
|
656
|
-
</div>
|
|
657
|
-
</div>}
|
|
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
|
+
})()}
|
|
658
690
|
</div>;
|
|
659
691
|
})()}
|
|
660
692
|
|
|
@@ -675,4 +707,47 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
675
707
|
/>
|
|
676
708
|
</div>;
|
|
677
709
|
}
|
|
678
|
-
}
|
|
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
|
+
}
|