querysub 0.564.0 → 0.566.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/appSecrets.ts +1 -1
- package/package.json +2 -2
- package/src/-a-archives/archives2.ts +11 -8
- package/src/deployManager/components/ProcessesView.tsx +7 -1
- package/src/deployManager/components/RouteConfigView.tsx +1 -16
- package/src/deployManager/components/ServiceDetailPage.tsx +14 -74
- package/src/deployManager/components/StoragePage.tsx +22 -12
- package/src/deployManager/components/Tag.tsx +19 -2
- package/src/deployManager/components/deployButtons.tsx +12 -28
package/appSecrets.ts
CHANGED
|
@@ -24,7 +24,7 @@ export const getCloudflareCreds = lazy(async (): Promise<{ key: string; email: s
|
|
|
24
24
|
/** Serves the secret keys sliftutils' getSecret expects, reading them the way querysub already does: backblaze creds off disk (getBackblazePath), cloudflare creds from the keys archives bucket (with its local file fallback). */
|
|
25
25
|
export async function getAppSecret(key: string): Promise<string | undefined> {
|
|
26
26
|
// Import query sub so our logs go to our actual log server. Otherwise we won't know if we get any errors.
|
|
27
|
-
Querysub;
|
|
27
|
+
//Querysub;
|
|
28
28
|
if (key.startsWith("backblaze.json.")) {
|
|
29
29
|
let creds = JSON.parse(fs.readFileSync(getBackblazePath(), "utf8")) as { applicationKeyId?: string; applicationKey?: string };
|
|
30
30
|
if (key === "backblaze.json.applicationKeyId") return creds.applicationKeyId;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "querysub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.566.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",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
|
|
72
72
|
"pako": "^2.1.0",
|
|
73
73
|
"peggy": "^5.0.6",
|
|
74
|
-
"sliftutils": "^1.7.
|
|
74
|
+
"sliftutils": "^1.7.43",
|
|
75
75
|
"socket-function": "^1.2.26",
|
|
76
76
|
"terser": "^5.31.0",
|
|
77
77
|
"typenode": "^6.6.1",
|
|
@@ -19,6 +19,9 @@ function createSourceWindows(
|
|
|
19
19
|
let timeRanges: [number, number][] = [];
|
|
20
20
|
for (let windowIndex = 0; windowIndex < sourceWindows.length; windowIndex++) {
|
|
21
21
|
let startTime = sourceWindows[windowIndex].startTime;
|
|
22
|
+
if (windowIndex === 0) {
|
|
23
|
+
startTime = 0;
|
|
24
|
+
}
|
|
22
25
|
let endTime = sourceWindows[windowIndex + 1]?.startTime ?? Number.MAX_SAFE_INTEGER;
|
|
23
26
|
if (startTime > endTime) throw new Error(`Source window ${formatDateTime(startTime)} starts after end of ${formatDateTime(endTime)}`);
|
|
24
27
|
timeRanges.push([startTime, endTime]);
|
|
@@ -40,29 +43,29 @@ function archiveBuilder(bucket: string, overrides: Partial<RemoteConfigBase>) {
|
|
|
40
43
|
const BACKBLAZE = `https://f002.backblazeb2.com/file/${bucket}/storage/storagerouting.json`;
|
|
41
44
|
let sources = createSourceWindows(overrides, [
|
|
42
45
|
{
|
|
43
|
-
startTime:
|
|
46
|
+
startTime: +new Date("2026-07-20 18:00:00-04:00"),
|
|
44
47
|
sources: [
|
|
45
|
-
{ type: "remote", url: ONTARIO, route: [0,
|
|
46
|
-
{ type: "remote", url: HETZNER, route: [0.5
|
|
48
|
+
{ type: "remote", url: ONTARIO, route: [0.5, 1], },
|
|
49
|
+
{ type: "remote", url: HETZNER, route: [0, 0.5], },
|
|
47
50
|
{ type: "remote", url: ONTARIO, },
|
|
48
51
|
{ type: "remote", url: HETZNER, },
|
|
49
52
|
{ type: "backblaze", url: BACKBLAZE, },
|
|
50
53
|
]
|
|
51
54
|
},
|
|
52
55
|
{
|
|
53
|
-
startTime: +new Date("2026-07-
|
|
56
|
+
startTime: +new Date("2026-07-21 02:59:00-04:00"),
|
|
54
57
|
sources: [
|
|
55
|
-
{ type: "remote", url: ONTARIO, route: [0.5
|
|
56
|
-
{ type: "remote", url: HETZNER, route: [0,
|
|
58
|
+
{ type: "remote", url: ONTARIO, route: [0, 0.5], },
|
|
59
|
+
{ type: "remote", url: HETZNER, route: [0.5, 1], },
|
|
57
60
|
{ type: "remote", url: ONTARIO, },
|
|
58
61
|
{ type: "remote", url: HETZNER, },
|
|
59
62
|
{ type: "backblaze", url: BACKBLAZE, },
|
|
60
63
|
]
|
|
61
|
-
}
|
|
64
|
+
},
|
|
62
65
|
]);
|
|
63
66
|
|
|
64
67
|
return createArchives({
|
|
65
|
-
version:
|
|
68
|
+
version: 15,
|
|
66
69
|
sources,
|
|
67
70
|
});
|
|
68
71
|
}
|
|
@@ -167,6 +167,12 @@ export class ProcessesView extends qreact.Component<{
|
|
|
167
167
|
serviceId?: string;
|
|
168
168
|
}> {
|
|
169
169
|
render() {
|
|
170
|
+
// Callers hand us one entry per service instance, so a machine running several instances arrives several times. Each machine's processes are listed once, whatever it is running.
|
|
171
|
+
let machines = new Map<string, { machineId: string; applyNodeId: string }>();
|
|
172
|
+
for (let machine of this.props.machines) {
|
|
173
|
+
if (machines.has(machine.machineId)) continue;
|
|
174
|
+
machines.set(machine.machineId, machine);
|
|
175
|
+
}
|
|
170
176
|
return <div className={css.vbox(14).fillWidth}>
|
|
171
177
|
<div className={css.hbox(10).alignItems("center")}>
|
|
172
178
|
<h3 className={css.flexGrow(1)}>Processes</h3>
|
|
@@ -176,7 +182,7 @@ export class ProcessesView extends qreact.Component<{
|
|
|
176
182
|
Invalidate
|
|
177
183
|
</Button>
|
|
178
184
|
</div>
|
|
179
|
-
{
|
|
185
|
+
{[...machines.values()].map(machine => <MachineProcesses
|
|
180
186
|
key={machine.machineId}
|
|
181
187
|
machineId={machine.machineId}
|
|
182
188
|
applyNodeId={machine.applyNodeId}
|
|
@@ -12,7 +12,7 @@ import type { RemoteConfig, RemoteConfigBase, HostedConfig, BackblazeConfig } fr
|
|
|
12
12
|
import { showModal } from "../../5-diagnostics/Modal";
|
|
13
13
|
import { FullscreenModal } from "../../5-diagnostics/FullscreenModal";
|
|
14
14
|
import { Button } from "../../library-components/Button";
|
|
15
|
-
import { Tag, TAG_FONT_SIZE, TAG_WARNING_COLOR,
|
|
15
|
+
import { Tag, TAG_FONT_SIZE, TAG_WARNING_COLOR, getSourceTags } from "./Tag";
|
|
16
16
|
import { StorageSynced, type StorageServerBuckets } from "./StoragePage";
|
|
17
17
|
|
|
18
18
|
module.hotreload = true;
|
|
@@ -343,21 +343,6 @@ function showConfigModal(bucketName: string, rawConfig: RemoteConfig): void {
|
|
|
343
343
|
});
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
-
function getSourceTags(source: Source): { icon: string; text: string }[] {
|
|
347
|
-
let tags: { icon: string; text: string }[] = [];
|
|
348
|
-
if (source.public) tags.push({ icon: "🌐", text: "public" });
|
|
349
|
-
if (source.immutable) tags.push({ icon: "🔒", text: "immutable" });
|
|
350
|
-
if (source.noFullSync) tags.push(NO_FULL_SYNC_TAG);
|
|
351
|
-
if (source.intermediate) tags.push({ icon: "⏳", text: "intermediate" });
|
|
352
|
-
if (source.type === "remote") {
|
|
353
|
-
if (source.fast) tags.push({ icon: "⚡", text: "fast" });
|
|
354
|
-
if (source.rawDisk) tags.push({ icon: "💽", text: "rawDisk" });
|
|
355
|
-
if (source.writeDelay !== undefined) tags.push({ icon: "⏱", text: `writeDelay ${formatTime(source.writeDelay)}` });
|
|
356
|
-
}
|
|
357
|
-
if (source.readerDiskLimit !== undefined) tags.push({ icon: "📦", text: `readCache ${formatNumber(source.readerDiskLimit)}B` });
|
|
358
|
-
return tags;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
346
|
/** Watching a source pins that server's own live view of the bucket to the top of the page, so disagreements between servers are visible directly. */
|
|
362
347
|
class WatchButton extends qreact.Component<{ source: Source; bucketName: string }> {
|
|
363
348
|
render() {
|
|
@@ -35,10 +35,6 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
35
35
|
expandedErrors: t.lookup({
|
|
36
36
|
expanded: t.type(false)
|
|
37
37
|
}),
|
|
38
|
-
// Milliseconds; 0 means no scheduled time picked yet (use Deploy Now instead)
|
|
39
|
-
switchTime: t.number(0),
|
|
40
|
-
// Seconds until the release goes live; 0 means use the default (DEFAULT_OVERLAP_TIME)
|
|
41
|
-
releaseDelayOverride: t.number(0),
|
|
42
38
|
// Seconds the old instances outlive the release; 0 means use the default (DEFAULT_OVERLAP_TIME). The no-overlap deploy buttons are how you deploy with an actual 0.
|
|
43
39
|
overlapOverride: t.number(0),
|
|
44
40
|
});
|
|
@@ -64,8 +60,6 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
64
60
|
await MachineServiceController(SocketFunction.browserNodeId()).setServiceConfigs.promise([deployConfig]);
|
|
65
61
|
Querysub.commit(() => {
|
|
66
62
|
this.state.editorState = stripReleaseState(deployConfig);
|
|
67
|
-
this.state.switchTime = 0;
|
|
68
|
-
this.state.releaseDelayOverride = 0;
|
|
69
63
|
this.state.overlapOverride = 0;
|
|
70
64
|
});
|
|
71
65
|
} finally {
|
|
@@ -93,16 +87,16 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
93
87
|
modal = showModal({
|
|
94
88
|
content: <div className={css.fixed.pos(0, 0).size("100vw", "100vh").hsla(0, 0, 0, 0.5).display("flex").alignItems("center").justifyContent("center")}>
|
|
95
89
|
<div className={css.vbox(12).pad2(20).hsl(0, 0, 12).colorhsl(0, 0, 90).bord2(0, 0, 40) + " keepModalsOpen"}>
|
|
96
|
-
<div className={css.boldStyle.fontSize(16)}
|
|
90
|
+
<div className={css.boldStyle.fontSize(16)}>🚀 Deploy?</div>
|
|
97
91
|
<div>{deployConfig.parameters.overlapTime
|
|
98
|
-
? `This config
|
|
99
|
-
: "This config
|
|
92
|
+
? `This config goes live now. The old instances keep running for ${formatTime(deployConfig.parameters.overlapTime)} before shutting down.`
|
|
93
|
+
: "This config goes live now. The old instances are shut down as soon as the new ones are up, so anything still connected to them is dropped."}</div>
|
|
100
94
|
<div className={css.hbox(10)}>
|
|
101
95
|
<Button hue={0} onClick={() => {
|
|
102
96
|
modal && modal.close();
|
|
103
97
|
deployConfig.parameters.releaseTime = Date.now();
|
|
104
98
|
this.deployConfig(deployConfig);
|
|
105
|
-
}}
|
|
99
|
+
}}>🚀 Deploy</Button>
|
|
106
100
|
<Button onClick={() => modal && modal.close()}>Cancel</Button>
|
|
107
101
|
</div>
|
|
108
102
|
</div>
|
|
@@ -642,70 +636,39 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
642
636
|
<span title={`${configCompareKey(config)} !== ${configCompareKey(deployedConfig)}`}>Only the title / notes changed, so this saves immediately</span>
|
|
643
637
|
</div>;
|
|
644
638
|
}
|
|
645
|
-
let releaseDelaySeconds = this.state.releaseDelayOverride || Math.round(DEFAULT_OVERLAP_TIME / timeInSecond);
|
|
646
|
-
let releaseDelay = releaseDelaySeconds * timeInSecond;
|
|
647
639
|
let overlapSeconds = this.state.overlapOverride || Math.round(DEFAULT_OVERLAP_TIME / timeInSecond);
|
|
648
640
|
let overlap = overlapSeconds * timeInSecond;
|
|
649
|
-
|
|
650
|
-
const makeDeployConfig = (
|
|
641
|
+
// A deploy always goes live now. The overlap is what keeps the service up: the old instances keep running for it, so connections have somewhere to go while clients move across.
|
|
642
|
+
const makeDeployConfig = (overlapTime = overlap) => {
|
|
651
643
|
let deployConfig = deepCloneJSON(config);
|
|
652
|
-
deployConfig.parameters.releaseTime =
|
|
644
|
+
deployConfig.parameters.releaseTime = Date.now();
|
|
653
645
|
deployConfig.parameters.overlapTime = overlapTime;
|
|
654
646
|
return deployConfig;
|
|
655
647
|
};
|
|
656
|
-
// The switch time is inferred from the release delay, unless one is explicitly set
|
|
657
|
-
let effectiveSwitchTime = scheduledTime || now + releaseDelay;
|
|
658
648
|
return <div className={css.vbox(10).pad2(12).fillWidth.bord2(45, 70, 45).hsl(45, 60, 95)}>
|
|
659
649
|
<div className={css.boldStyle.fontSize(15)}>
|
|
660
650
|
✏️ Edited (only in this browser, until deployed)
|
|
661
651
|
</div>
|
|
662
652
|
<ParametersDiff base={liveParameters} next={config.parameters} />
|
|
663
|
-
<div className={css.hbox(12).wrap}>
|
|
653
|
+
<div className={css.hbox(12).wrap.alignItems("center")}>
|
|
664
654
|
<button
|
|
665
655
|
className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(120, 70, 85).fontWeight("bold")}
|
|
666
656
|
disabled={this.state.isDeploying}
|
|
657
|
+
title="Deploys now; the old instances keep running for the overlap before shutting down"
|
|
667
658
|
onClick={() => {
|
|
668
|
-
this.
|
|
659
|
+
this.confirmForceDeployNow(makeDeployConfig());
|
|
669
660
|
}}>
|
|
670
|
-
{this.state.isDeploying && "⏳
|
|
661
|
+
{this.state.isDeploying && "⏳ Deploying..." || "🚀 Deploy"}
|
|
671
662
|
</button>
|
|
672
663
|
<button
|
|
673
|
-
className={css.pad2(12, 8).button.bord2(0,
|
|
664
|
+
className={css.pad2(12, 8).button.bord2(0, 90, 30).hsl(0, 85, 65).colorhsl(0, 0, 100).fontWeight("bold")}
|
|
674
665
|
disabled={this.state.isDeploying}
|
|
675
|
-
title="Deploys
|
|
666
|
+
title="Deploys now AND shuts the old instances down as soon as the new ones are up (overlap of 0)"
|
|
676
667
|
onClick={() => {
|
|
677
|
-
this.
|
|
668
|
+
this.confirmForceDeployNow(makeDeployConfig(0));
|
|
678
669
|
}}>
|
|
679
670
|
Deploy, No Overlap
|
|
680
671
|
</button>
|
|
681
|
-
<div className={css.opacity(scheduledTime && 1 || 0.5)}>
|
|
682
|
-
<InputLabel
|
|
683
|
-
label="Switch at"
|
|
684
|
-
isDatetime
|
|
685
|
-
value={effectiveSwitchTime}
|
|
686
|
-
onChangeValue={value => {
|
|
687
|
-
this.state.switchTime = Number(value) || 0;
|
|
688
|
-
}}
|
|
689
|
-
/>
|
|
690
|
-
</div>
|
|
691
|
-
<span>(in {formatTime(effectiveSwitchTime - now)})</span>
|
|
692
|
-
{!!scheduledTime && <button
|
|
693
|
-
className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(0, 0, 90)}
|
|
694
|
-
onClick={() => {
|
|
695
|
-
this.state.switchTime = 0;
|
|
696
|
-
}}>
|
|
697
|
-
↺ Reset to delay-based time
|
|
698
|
-
</button>}
|
|
699
|
-
<div className={css.opacity(scheduledTime && 0.5 || 1)}>
|
|
700
|
-
<InputLabel
|
|
701
|
-
label="Release in (seconds)"
|
|
702
|
-
integer
|
|
703
|
-
value={releaseDelaySeconds}
|
|
704
|
-
onChangeValue={value => {
|
|
705
|
-
this.state.releaseDelayOverride = Number(value) || 0;
|
|
706
|
-
}}
|
|
707
|
-
/>
|
|
708
|
-
</div>
|
|
709
672
|
<InputLabel
|
|
710
673
|
label="Overlap (seconds)"
|
|
711
674
|
integer
|
|
@@ -715,29 +678,6 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
715
678
|
}}
|
|
716
679
|
/>
|
|
717
680
|
</div>
|
|
718
|
-
{!!scheduledTime && scheduledTime <= now && <span className={css.colorhsl(0, 85, 45).fontWeight("bold")}>
|
|
719
|
-
⚠️ The switch time is in the past — deploying switches immediately
|
|
720
|
-
</span>}
|
|
721
|
-
<div className={css.hbox(8)}>
|
|
722
|
-
<button
|
|
723
|
-
className={css.pad2(12, 8).button.bord2(45, 80, 35).hsl(45, 85, 82)}
|
|
724
|
-
disabled={this.state.isDeploying}
|
|
725
|
-
title="Deploys immediately; the old instances keep running for their configured overlap before shutting down"
|
|
726
|
-
onClick={() => {
|
|
727
|
-
this.confirmForceDeployNow(makeDeployConfig(Date.now()));
|
|
728
|
-
}}>
|
|
729
|
-
⚡ Force Deploy Now
|
|
730
|
-
</button>
|
|
731
|
-
<button
|
|
732
|
-
className={css.pad2(12, 8).button.bord2(0, 90, 30).hsl(0, 85, 65).colorhsl(0, 0, 100).fontWeight("bold")}
|
|
733
|
-
disabled={this.state.isDeploying}
|
|
734
|
-
title="Deploys immediately AND shuts down the old instances immediately (overlap of 0)"
|
|
735
|
-
onClick={() => {
|
|
736
|
-
this.confirmForceDeployNow(makeDeployConfig(Date.now(), 0));
|
|
737
|
-
}}>
|
|
738
|
-
⚡ Force Deploy Now, No Overlap
|
|
739
|
-
</button>
|
|
740
|
-
</div>
|
|
741
681
|
</div>;
|
|
742
682
|
})()}
|
|
743
683
|
|
|
@@ -17,7 +17,7 @@ import { isDefined } from "../../misc";
|
|
|
17
17
|
import { STORAGE_ACCOUNT } from "../../-a-archives/archives2";
|
|
18
18
|
import { Table } from "../../5-diagnostics/Table";
|
|
19
19
|
import { RouteConfigView } from "./RouteConfigView";
|
|
20
|
-
import { Tag,
|
|
20
|
+
import { Tag, SourceTag, getSourceTags } from "./Tag";
|
|
21
21
|
|
|
22
22
|
module.hotreload = true;
|
|
23
23
|
|
|
@@ -134,7 +134,7 @@ type BucketRow = {
|
|
|
134
134
|
state: string;
|
|
135
135
|
files: string;
|
|
136
136
|
bytes: string;
|
|
137
|
-
|
|
137
|
+
flags: SourceTag[];
|
|
138
138
|
indexSources: ArchivesConfig["indexSources"];
|
|
139
139
|
readerDiskLimit: string;
|
|
140
140
|
writes: string;
|
|
@@ -152,13 +152,13 @@ const DISK_BAR_TYPE = "DISK";
|
|
|
152
152
|
const ACTIVE_STATE = "active";
|
|
153
153
|
const INACTIVE_STATE = "inactive";
|
|
154
154
|
|
|
155
|
-
/**
|
|
156
|
-
function
|
|
155
|
+
/** The flags this server's own entries in the bucket's routing config set - how it holds the bucket, which is not something the bucket's own state reports. */
|
|
156
|
+
function getBucketFlags(serverUrl: string, bucketName: string, remoteConfig: RemoteConfig | undefined): SourceTag[] {
|
|
157
157
|
let host: URL;
|
|
158
158
|
try {
|
|
159
159
|
host = new URL(serverUrl);
|
|
160
160
|
} catch {
|
|
161
|
-
return
|
|
161
|
+
return [];
|
|
162
162
|
}
|
|
163
163
|
let selfEntries: HostedConfig[] = [];
|
|
164
164
|
for (let source of remoteConfig?.sources || []) {
|
|
@@ -173,7 +173,15 @@ function getNoFullSync(serverUrl: string, bucketName: string, remoteConfig: Remo
|
|
|
173
173
|
// A malformed url in the config is not this column's problem
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
|
-
|
|
176
|
+
// A server can hold several entries for one bucket (different windows or route shards), so the flags are the union of theirs, deduped by what they say
|
|
177
|
+
let flags = new Map<string, SourceTag>();
|
|
178
|
+
for (let entry of selfEntries) {
|
|
179
|
+
for (let tag of getSourceTags(entry)) {
|
|
180
|
+
if (flags.has(tag.text)) continue;
|
|
181
|
+
flags.set(tag.text, tag);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return [...flags.values()];
|
|
177
185
|
}
|
|
178
186
|
|
|
179
187
|
/** How much fast-mode coalescing saved: every accepted write over the ones that actually reached a source. */
|
|
@@ -195,7 +203,7 @@ function getBucketRows(servers: StorageServerBuckets[]): BucketRow[] {
|
|
|
195
203
|
state: "",
|
|
196
204
|
files: "",
|
|
197
205
|
bytes: "",
|
|
198
|
-
|
|
206
|
+
flags: [],
|
|
199
207
|
indexSources: undefined,
|
|
200
208
|
readerDiskLimit: "",
|
|
201
209
|
writes: "",
|
|
@@ -225,7 +233,7 @@ function getBucketRows(servers: StorageServerBuckets[]): BucketRow[] {
|
|
|
225
233
|
state: bucket.active && ACTIVE_STATE || INACTIVE_STATE,
|
|
226
234
|
files: config?.index && formatNumber(config.index.fileCount) || "",
|
|
227
235
|
bytes: config?.index && formatNumber(config.index.byteCount) + "B" || "",
|
|
228
|
-
|
|
236
|
+
flags: getBucketFlags(server.url, bucket.bucketName, config?.remoteConfig),
|
|
229
237
|
indexSources: config?.indexSources,
|
|
230
238
|
readerDiskLimit: config?.readerDiskLimit && formatNumber(config.readerDiskLimit) + "B" || "",
|
|
231
239
|
writes: bucket.writeStats && formatNumber(bucket.writeStats.originalWrites) || "",
|
|
@@ -581,10 +589,12 @@ export class StoragePage extends qreact.Component {
|
|
|
581
589
|
},
|
|
582
590
|
files: { title: "Files" },
|
|
583
591
|
bytes: { title: "Bytes" },
|
|
584
|
-
|
|
585
|
-
title: "
|
|
586
|
-
//
|
|
587
|
-
formatter:
|
|
592
|
+
flags: {
|
|
593
|
+
title: "Flags",
|
|
594
|
+
// Only flags that are set produce a tag, so an empty cell means every default
|
|
595
|
+
formatter: flags => <div className={css.hbox(4).wrap}>
|
|
596
|
+
{(flags || []).map(tag => <Tag key={tag.text} icon={tag.icon} text={tag.text} />)}
|
|
597
|
+
</div>
|
|
588
598
|
},
|
|
589
599
|
indexSources: {
|
|
590
600
|
title: "Index sources",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { qreact } from "../../4-dom/qreact";
|
|
2
2
|
import { css } from "typesafecss";
|
|
3
|
+
import { formatNumber, formatTime } from "socket-function/src/formatting/format";
|
|
4
|
+
import type { HostedConfig, BackblazeConfig } from "sliftutils/storage/IArchives";
|
|
3
5
|
|
|
4
6
|
module.hotreload = true;
|
|
5
7
|
|
|
@@ -9,8 +11,23 @@ export const TAG_COLOR: TagColor = { h: 210, s: 45, l: 88 };
|
|
|
9
11
|
export const TAG_WARNING_COLOR: TagColor = { h: 35, s: 90, l: 85 };
|
|
10
12
|
export const TAG_FONT_SIZE = 11;
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
export type SourceTag = { icon: string; text: string };
|
|
15
|
+
|
|
16
|
+
/** Every flag a source has set. Only flags that are on produce a tag - an absent tag is the default, so there is nothing to show for it. */
|
|
17
|
+
export function getSourceTags(source: HostedConfig | BackblazeConfig): SourceTag[] {
|
|
18
|
+
let tags: SourceTag[] = [];
|
|
19
|
+
if (source.public) tags.push({ icon: "🌐", text: "public" });
|
|
20
|
+
if (source.immutable) tags.push({ icon: "🔒", text: "immutable" });
|
|
21
|
+
if (source.noFullSync) tags.push({ icon: "⇣", text: "noFullSync" });
|
|
22
|
+
if (source.intermediate) tags.push({ icon: "⏳", text: "intermediate" });
|
|
23
|
+
if (source.type === "remote") {
|
|
24
|
+
if (source.fast) tags.push({ icon: "⚡", text: "fast" });
|
|
25
|
+
if (source.rawDisk) tags.push({ icon: "💽", text: "rawDisk" });
|
|
26
|
+
if (source.writeDelay !== undefined) tags.push({ icon: "⏱", text: `writeDelay ${formatTime(source.writeDelay)}` });
|
|
27
|
+
}
|
|
28
|
+
if (source.readerDiskLimit !== undefined) tags.push({ icon: "📦", text: `readCache ${formatNumber(source.readerDiskLimit)}B` });
|
|
29
|
+
return tags;
|
|
30
|
+
}
|
|
14
31
|
|
|
15
32
|
export class Tag extends qreact.Component<{ icon: string; text: string; warning?: boolean; color?: TagColor; title?: string; }> {
|
|
16
33
|
render() {
|
|
@@ -77,21 +77,16 @@ export class UpdateButtons extends qreact.Component<{
|
|
|
77
77
|
state = t.state({
|
|
78
78
|
isDeploying: t.type(false),
|
|
79
79
|
});
|
|
80
|
-
// Deploys all outdated services to latestRef
|
|
81
|
-
private deployAll(outdatedServices: ServiceConfig[], latestRef: string,
|
|
80
|
+
// Deploys all outdated services to latestRef, always immediately. The overlap is what keeps the service up - the old instances keep running for it, so connections have somewhere to go while clients move across. noOverlap kills them at once instead.
|
|
81
|
+
private deployAll(outdatedServices: ServiceConfig[], latestRef: string, noOverlap: boolean) {
|
|
82
82
|
this.state.isDeploying = true;
|
|
83
83
|
// Props are synchronized state, so everything the async work needs is cloned into plain locals HERE, in the synced part
|
|
84
84
|
let toDeploy = outdatedServices.map(service => {
|
|
85
85
|
let updated = deepCloneJSON(service);
|
|
86
86
|
updated.parameters.gitRef = latestRef;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
updated.parameters.
|
|
90
|
-
} else {
|
|
91
|
-
updated.parameters.releaseTime = Date.now();
|
|
92
|
-
if (mode === "nowNoOverlap") {
|
|
93
|
-
updated.parameters.overlapTime = 0;
|
|
94
|
-
}
|
|
87
|
+
updated.parameters.releaseTime = Date.now();
|
|
88
|
+
if (noOverlap) {
|
|
89
|
+
updated.parameters.overlapTime = 0;
|
|
95
90
|
}
|
|
96
91
|
return updated;
|
|
97
92
|
});
|
|
@@ -112,14 +107,14 @@ export class UpdateButtons extends qreact.Component<{
|
|
|
112
107
|
content: <div className={css.vbox(12).pad2(20)}>
|
|
113
108
|
<div className={css.boldStyle.fontSize(16)}>⚡ Deploy all {outdatedServices.length} services now?</div>
|
|
114
109
|
<div>{noOverlap
|
|
115
|
-
? "
|
|
116
|
-
: "
|
|
110
|
+
? "The old instances are shut down as soon as the new ones are up (overlap of 0), so anything still connected to them is dropped."
|
|
111
|
+
: "The old instances keep running for their configured overlap before shutting down, so connections have somewhere to go while clients move across."}</div>
|
|
117
112
|
<div className={css.hbox(10)}>
|
|
118
113
|
<button
|
|
119
114
|
className={css.pad2(12, 8).button + (noOverlap && css.bord2(0, 90, 30).hsl(0, 85, 65).colorhsl(0, 0, 100).fontWeight("bold") || css.bord2(45, 80, 35).hsl(45, 85, 82))}
|
|
120
115
|
onClick={() => {
|
|
121
116
|
modal && modal.close();
|
|
122
|
-
this.deployAll(outdatedServices, latestRef, noOverlap
|
|
117
|
+
this.deployAll(outdatedServices, latestRef, noOverlap);
|
|
123
118
|
}}>
|
|
124
119
|
⚡ Deploy Now
|
|
125
120
|
</button>
|
|
@@ -188,8 +183,9 @@ export class UpdateButtons extends qreact.Component<{
|
|
|
188
183
|
<button
|
|
189
184
|
className={buttonStyle.hsl(120, 70, 90)}
|
|
190
185
|
disabled={this.state.isDeploying}
|
|
186
|
+
title="Deploys now; the old instances keep running for their configured overlap before shutting down"
|
|
191
187
|
onClick={() => {
|
|
192
|
-
this.
|
|
188
|
+
this.confirmDeployAllNow(outdatedServices, gitInfo.latestRef, false);
|
|
193
189
|
}}
|
|
194
190
|
>
|
|
195
191
|
<div>
|
|
@@ -204,28 +200,16 @@ export class UpdateButtons extends qreact.Component<{
|
|
|
204
200
|
<RenderGitRefInfo gitRef={ref} />
|
|
205
201
|
</div>)}
|
|
206
202
|
</button>
|
|
207
|
-
<button
|
|
208
|
-
className={buttonStyle.hsl(45, 85, 82)}
|
|
209
|
-
disabled={this.state.isDeploying}
|
|
210
|
-
title="Deploys immediately; the old instances keep running for their configured overlap before shutting down"
|
|
211
|
-
onClick={() => {
|
|
212
|
-
this.confirmDeployAllNow(outdatedServices, gitInfo.latestRef, false);
|
|
213
|
-
}}
|
|
214
|
-
>
|
|
215
|
-
<div>
|
|
216
|
-
{bigEmoji("⚡")} <span>{this.state.isDeploying && "⏳ Deploying..." || `Deploy All (${outdatedServices.length}) Now (no delay)`}</span>
|
|
217
|
-
</div>
|
|
218
|
-
</button>
|
|
219
203
|
<button
|
|
220
204
|
className={buttonStyle.hsl(0, 85, 70)}
|
|
221
205
|
disabled={this.state.isDeploying}
|
|
222
|
-
title="Deploys
|
|
206
|
+
title="Deploys now AND shuts the old instances down as soon as the new ones are up (overlap of 0)"
|
|
223
207
|
onClick={() => {
|
|
224
208
|
this.confirmDeployAllNow(outdatedServices, gitInfo.latestRef, true);
|
|
225
209
|
}}
|
|
226
210
|
>
|
|
227
211
|
<div>
|
|
228
|
-
{bigEmoji("⚡")} <span>{this.state.isDeploying && "⏳ Deploying..." || `Deploy All (${outdatedServices.length})
|
|
212
|
+
{bigEmoji("⚡")} <span>{this.state.isDeploying && "⏳ Deploying..." || `Deploy All (${outdatedServices.length}), No Overlap`}</span>
|
|
229
213
|
</div>
|
|
230
214
|
</button>
|
|
231
215
|
</>}
|