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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.509.0",
3
+ "version": "0.510.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
@@ -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
- // Milliseconds; 0 means use the service's configured overlapTime
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 = Date.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
- Deploying changes
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 overlap = this.state.overlapOverride || config.parameters.overlapTime || DEFAULT_OVERLAP_TIME;
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 && <div className={css.vbox(8)}>
596
- <div className={css.hbox(12).wrap}>
597
- <InputLabel
598
- label={`Overlap (ms, default ${formatTime(config.parameters.overlapTime || DEFAULT_OVERLAP_TIME)})`}
599
- number
600
- value={this.state.overlapOverride || ""}
601
- onChangeValue={value => {
602
- this.state.overlapOverride = Number(value) || 0;
603
- }}
604
- />
605
- <span>Effective overlap: {formatTime(overlap)}</span>
606
- </div>
607
- <div className={css.hbox(12).wrap}>
608
- <button
609
- className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(120, 70, 85).fontWeight("bold")}
610
- onClick={() => {
611
- let switchTime = Date.now() + overlap;
612
- this.runControllerAction(() => controller.scheduleSwitchover.promise({
613
- serviceId: selectedServiceId,
614
- switchTime,
615
- overlapTime: overlap,
616
- }));
617
- }}>
618
- 🚀 Deploy Now (switch in {formatTime(overlap)})
619
- </button>
620
- <InputLabel
621
- label="Switch at"
622
- isDatetime
623
- value={this.state.switchTime}
624
- onChangeValue={value => {
625
- this.state.switchTime = Number(value) || 0;
626
- }}
627
- />
628
- {!!scheduledTime && <span>
629
- {scheduledTime > now && `in ${formatTime(scheduledTime - now)}` || "in the past!"}
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
- ⚠️ Not enough time for the full overlap ({formatTime(overlap)}) — the new instances won't be fully started before the old ones shut down
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
- <button
635
- className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(120, 70, 90)}
636
- disabled={!scheduledTime}
637
- onClick={() => {
638
- if (!scheduledTime) return;
639
- this.runControllerAction(() => controller.scheduleSwitchover.promise({
640
- serviceId: selectedServiceId,
641
- switchTime: scheduledTime,
642
- overlapTime: overlap,
643
- }));
644
- }}>
645
- 🕐 Schedule Switchover
646
- </button>
647
- </div>
648
- <div className={css.hbox(8)}>
649
- <button
650
- className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(50, 80, 60)}
651
- onClick={() => {
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
+ }
@@ -96,7 +96,7 @@ export type ServiceConfig = {
96
96
  };
97
97
  };
98
98
 
99
- export const DEFAULT_OVERLAP_TIME = timeInMinute;
99
+ export const DEFAULT_OVERLAP_TIME = timeInMinute * 10;
100
100
 
101
101
  export type ServiceSwitchover = {
102
102
  serviceId: string;