querysub 0.548.0 → 0.549.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.
|
|
3
|
+
"version": "0.549.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.35",
|
|
75
75
|
"socket-function": "^1.2.26",
|
|
76
76
|
"terser": "^5.31.0",
|
|
77
77
|
"typenode": "^6.6.1",
|
|
@@ -27,7 +27,6 @@ const FUTURE_COLOR = { h: 45, s: 80, l: 85 };
|
|
|
27
27
|
// Windows are days or hours wide, so the countdown only has to be roughly right
|
|
28
28
|
const TIME_REFRESH_INTERVAL = 60 * 1000;
|
|
29
29
|
const WATCH_POLL_INTERVAL = 60 * 1000;
|
|
30
|
-
const WATCH_ERROR_LIMIT = 500;
|
|
31
30
|
const WATCH_ACTIVE_COLOR = { h: 195, s: 60, l: 85 };
|
|
32
31
|
const WINDOW_HEADER_SIZE = 13;
|
|
33
32
|
const BUCKET_TITLE_SIZE = 15;
|
|
@@ -239,28 +238,28 @@ function getServerUrl(source: Source): string | undefined {
|
|
|
239
238
|
}
|
|
240
239
|
|
|
241
240
|
async function refreshWatch(key: string): Promise<void> {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
241
|
+
// Reads outside a synchronized context have to go through localRead, and the plain values are pulled out here so nothing proxied escapes into the async work
|
|
242
|
+
let target = Querysub.localRead(() => {
|
|
243
|
+
let watch = liveWatchData().watches[key];
|
|
244
|
+
if (!watch) return undefined;
|
|
245
|
+
return { serverUrl: watch.serverUrl, bucketName: watch.bucketName };
|
|
246
|
+
});
|
|
247
|
+
if (!target) return;
|
|
248
|
+
let { serverUrl, bucketName } = target;
|
|
249
|
+
Querysub.commit(() => {
|
|
246
250
|
let current = liveWatchData().watches[key];
|
|
247
251
|
if (current) current.loading = true;
|
|
248
252
|
});
|
|
249
|
-
let
|
|
253
|
+
let live = await getServerActiveBucket({ url: serverUrl, account: STORAGE_ACCOUNT, bucketName });
|
|
254
|
+
// "Not loaded here" comes back as a string - it's a normal result, not an error
|
|
250
255
|
let error: string | undefined;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
} else {
|
|
257
|
-
result = live.routing;
|
|
258
|
-
}
|
|
259
|
-
} catch (e) {
|
|
260
|
-
error = String((e as Error).stack ?? e).slice(0, WATCH_ERROR_LIMIT);
|
|
261
|
-
console.error(`Reading the live config of ${bucketName} on ${serverUrl} failed:`, (e as Error).stack ?? e);
|
|
256
|
+
let result: RemoteConfig | undefined;
|
|
257
|
+
if (typeof live === "string") {
|
|
258
|
+
error = live;
|
|
259
|
+
} else {
|
|
260
|
+
result = live.routing;
|
|
262
261
|
}
|
|
263
|
-
Querysub.
|
|
262
|
+
Querysub.commit(() => {
|
|
264
263
|
let current = liveWatchData().watches[key];
|
|
265
264
|
// The watch may have been removed while the call was in flight
|
|
266
265
|
if (!current) return;
|
|
@@ -277,8 +276,8 @@ function isWatched(serverUrl: string, bucketName: string): boolean {
|
|
|
277
276
|
|
|
278
277
|
function toggleWatch(serverUrl: string, bucketName: string): void {
|
|
279
278
|
let key = getWatchKey(serverUrl, bucketName);
|
|
280
|
-
let existed = !!liveWatchData().watches[key];
|
|
281
|
-
Querysub.
|
|
279
|
+
let existed = Querysub.localRead(() => !!liveWatchData().watches[key]);
|
|
280
|
+
Querysub.commit(() => {
|
|
282
281
|
if (existed) {
|
|
283
282
|
delete liveWatchData().watches[key];
|
|
284
283
|
return;
|
|
@@ -290,13 +289,15 @@ function toggleWatch(serverUrl: string, bucketName: string): void {
|
|
|
290
289
|
}
|
|
291
290
|
|
|
292
291
|
function refreshAllWatches(): void {
|
|
293
|
-
|
|
292
|
+
// Fired from a timer, so the key list has to be read through localRead
|
|
293
|
+
let keys = Querysub.localRead(() => Object.keys(liveWatchData().watches));
|
|
294
|
+
for (let key of keys) {
|
|
294
295
|
void refreshWatch(key);
|
|
295
296
|
}
|
|
296
297
|
}
|
|
297
298
|
|
|
298
299
|
function clearWatches(): void {
|
|
299
|
-
Querysub.
|
|
300
|
+
Querysub.commit(() => {
|
|
300
301
|
for (let key of Object.keys(liveWatchData().watches)) {
|
|
301
302
|
delete liveWatchData().watches[key];
|
|
302
303
|
}
|
|
@@ -347,7 +348,7 @@ function getSourceTags(source: Source): { icon: string; text: string }[] {
|
|
|
347
348
|
/** 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. */
|
|
348
349
|
class WatchButton extends qreact.Component<{ source: Source; bucketName: string }> {
|
|
349
350
|
render() {
|
|
350
|
-
|
|
351
|
+
const serverUrl = getServerUrl(this.props.source);
|
|
351
352
|
// Only storage servers have a live in-memory state to read
|
|
352
353
|
if (!serverUrl) return undefined;
|
|
353
354
|
let watching = isWatched(serverUrl, this.props.bucketName);
|
|
@@ -701,7 +701,7 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
701
701
|
Cancel Scheduled Deploy
|
|
702
702
|
</button>
|
|
703
703
|
<button
|
|
704
|
-
className={css.pad2(12, 8).button.bord2(
|
|
704
|
+
className={css.pad2(12, 8).button.bord2(45, 80, 35).hsl(45, 85, 82)}
|
|
705
705
|
disabled={this.state.isDeploying}
|
|
706
706
|
title="Changes the scheduled deploy to go live immediately"
|
|
707
707
|
onClick={() => {
|
|
@@ -818,8 +818,9 @@ export class ServiceDetailPage extends qreact.Component {
|
|
|
818
818
|
</span>}
|
|
819
819
|
<div className={css.hbox(8)}>
|
|
820
820
|
<button
|
|
821
|
-
className={css.pad2(12, 8).button.bord2(
|
|
821
|
+
className={css.pad2(12, 8).button.bord2(45, 80, 35).hsl(45, 85, 82)}
|
|
822
822
|
disabled={this.state.isDeploying}
|
|
823
|
+
title="Deploys immediately; the old instances keep running for their configured overlap before shutting down"
|
|
823
824
|
onClick={() => {
|
|
824
825
|
this.confirmForceDeployNow(makeDeployConfig(Date.now()));
|
|
825
826
|
}}>
|
|
@@ -11,7 +11,8 @@ import type { ArchivesConfig } from "sliftutils/storage/IArchives";
|
|
|
11
11
|
import { UsageBar, getUsageThresholds } from "../../library-components/UsageBar";
|
|
12
12
|
import { getSyncedController } from "../../library-components/SyncedController";
|
|
13
13
|
import { assertIsManagementUser } from "../../diagnostics/managementPages";
|
|
14
|
-
import {
|
|
14
|
+
import { getLiveServiceParameters, getMachineTargets, applyCommandTemplate, ServiceParameters, ServiceConfig, MachineServiceController, DEFAULT_OVERLAP_TIME } from "../machineSchema";
|
|
15
|
+
import { isDefined } from "../../misc";
|
|
15
16
|
import { STORAGE_ACCOUNT } from "../../-a-archives/archives2";
|
|
16
17
|
import { Table } from "../../5-diagnostics/Table";
|
|
17
18
|
import { RouteConfigView } from "./RouteConfigView";
|
|
@@ -20,18 +21,24 @@ module.hotreload = true;
|
|
|
20
21
|
|
|
21
22
|
const STORAGE_COMMAND_PREFIX = "yarn storageserve";
|
|
22
23
|
const URL_ARG_REGEX = /--url\s+(\S+)/g;
|
|
23
|
-
const ERROR_TEXT_LIMIT = 500;
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
/** The release a storage service is in the middle of. Read straight from the service configs, so it resolves without contacting any storage server - which is exactly when it matters most, since a deploy is the usual reason the servers don't answer. */
|
|
26
|
+
export type StorageService = {
|
|
26
27
|
serviceKey: string;
|
|
27
28
|
serviceTitle: string;
|
|
28
|
-
/** The release the service is in the middle of, so the page can warn that these servers are about to move (or just did) */
|
|
29
29
|
releaseTime?: number;
|
|
30
30
|
overlapTime: number;
|
|
31
31
|
hasOldParameters: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type StorageServer = StorageService & {
|
|
32
35
|
url: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type StorageServerBuckets = StorageServer & {
|
|
39
|
+
/** Still waiting on this server's own endpoint - each server loads independently */
|
|
40
|
+
loading?: boolean;
|
|
33
41
|
buckets?: ServerBucketInfo[];
|
|
34
|
-
error?: string;
|
|
35
42
|
};
|
|
36
43
|
|
|
37
44
|
function getStorageUrls(parameters: ServiceParameters): string[] {
|
|
@@ -46,35 +53,43 @@ function getStorageUrls(parameters: ServiceParameters): string[] {
|
|
|
46
53
|
return urls;
|
|
47
54
|
}
|
|
48
55
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
let
|
|
54
|
-
for (let
|
|
55
|
-
|
|
56
|
-
for (let
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
});
|
|
68
|
-
}
|
|
56
|
+
/** The storage servers a set of service configs runs, worked out on the client - it already has the configs, so asking the server to re-derive them would just make the whole page wait on one uncacheable call. */
|
|
57
|
+
export function getStorageServers(configs: ServiceConfig[]): StorageServer[] {
|
|
58
|
+
let serviceByUrl = new Map<string, StorageServer>();
|
|
59
|
+
for (let config of configs) {
|
|
60
|
+
let allParameters = [getLiveServiceParameters(config), config.parameters, config.oldParameters];
|
|
61
|
+
for (let parameters of allParameters) {
|
|
62
|
+
if (!parameters) continue;
|
|
63
|
+
for (let url of getStorageUrls(parameters)) {
|
|
64
|
+
if (serviceByUrl.has(url)) continue;
|
|
65
|
+
serviceByUrl.set(url, {
|
|
66
|
+
url,
|
|
67
|
+
serviceKey: parameters.key,
|
|
68
|
+
serviceTitle: config.info.title,
|
|
69
|
+
releaseTime: config.parameters.releaseTime,
|
|
70
|
+
// The overlap always comes from the newest parameters, even though it governs how long the old instances outlive the release
|
|
71
|
+
overlapTime: config.parameters.overlapTime ?? DEFAULT_OVERLAP_TIME,
|
|
72
|
+
hasOldParameters: !!config.oldParameters,
|
|
73
|
+
});
|
|
69
74
|
}
|
|
70
75
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
}
|
|
77
|
+
return [...serviceByUrl.values()];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getStorageServices(servers: StorageServer[]): StorageService[] {
|
|
81
|
+
let byKey = new Map<string, StorageService>();
|
|
82
|
+
for (let server of servers) {
|
|
83
|
+
if (byKey.has(server.serviceKey)) continue;
|
|
84
|
+
byKey.set(server.serviceKey, server);
|
|
85
|
+
}
|
|
86
|
+
return [...byKey.values()];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
class StoragePageControllerBase {
|
|
90
|
+
/** Purely a forwarder - browsers can't authenticate to a storage server themselves. One call per server, so each caches and refreshes on its own. */
|
|
91
|
+
public async getServerBuckets(url: string): Promise<ServerBucketInfo[]> {
|
|
92
|
+
return await listServerBuckets({ url, account: STORAGE_ACCOUNT });
|
|
78
93
|
}
|
|
79
94
|
}
|
|
80
95
|
|
|
@@ -82,7 +97,7 @@ export const StoragePageController = SocketFunction.register(
|
|
|
82
97
|
"StoragePageController-4f1c93ab-77e2-4d15-9a30-1c6b8f5d2e04",
|
|
83
98
|
new StoragePageControllerBase(),
|
|
84
99
|
() => ({
|
|
85
|
-
|
|
100
|
+
getServerBuckets: {},
|
|
86
101
|
}),
|
|
87
102
|
() => ({
|
|
88
103
|
hooks: [assertIsManagementUser],
|
|
@@ -149,8 +164,8 @@ function getBucketRows(servers: StorageServerBuckets[]): BucketRow[] {
|
|
|
149
164
|
syncing: undefined,
|
|
150
165
|
error: "",
|
|
151
166
|
};
|
|
152
|
-
if (server.
|
|
153
|
-
rows.push({ ...baseRow,
|
|
167
|
+
if (server.loading) {
|
|
168
|
+
rows.push({ ...baseRow, bucket: "Loading..." });
|
|
154
169
|
continue;
|
|
155
170
|
}
|
|
156
171
|
let buckets = [...server.buckets || []];
|
|
@@ -191,30 +206,6 @@ class ActivateBucketButton extends qreact.Component<{ serverUrl: string; bucketN
|
|
|
191
206
|
activating: false,
|
|
192
207
|
error: "",
|
|
193
208
|
};
|
|
194
|
-
private async activate() {
|
|
195
|
-
this.state.activating = true;
|
|
196
|
-
this.state.error = "";
|
|
197
|
-
try {
|
|
198
|
-
let result = await activateServerBucket({
|
|
199
|
-
url: this.props.serverUrl,
|
|
200
|
-
account: STORAGE_ACCOUNT,
|
|
201
|
-
bucketName: this.props.bucketName,
|
|
202
|
-
});
|
|
203
|
-
// The server reports a refusal as a string instead of throwing
|
|
204
|
-
if (typeof result === "string") {
|
|
205
|
-
this.state.error = result;
|
|
206
|
-
console.error(`Activating bucket ${this.props.bucketName} on ${this.props.serverUrl} was refused: ${result}`);
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
console.log(`Activated bucket ${this.props.bucketName} on ${this.props.serverUrl}`);
|
|
210
|
-
StorageSynced(SocketFunction.browserNodeId()).getStorageBuckets.resetAll();
|
|
211
|
-
} catch (e) {
|
|
212
|
-
this.state.error = String((e as Error).stack ?? e).slice(0, ERROR_TEXT_LIMIT);
|
|
213
|
-
console.error(`Activating bucket ${this.props.bucketName} on ${this.props.serverUrl} failed:`, (e as Error).stack ?? e);
|
|
214
|
-
} finally {
|
|
215
|
-
this.state.activating = false;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
209
|
render() {
|
|
219
210
|
return <div className={css.hbox(6).wrap}>
|
|
220
211
|
<div className={css.whiteSpace("nowrap")}>{INACTIVE_STATE}</div>
|
|
@@ -222,7 +213,29 @@ class ActivateBucketButton extends qreact.Component<{ serverUrl: string; bucketN
|
|
|
222
213
|
className={css.pad2(6, 1).button.bord2(0, 0, 20).hsl(0, 0, 100).whiteSpace("nowrap")}
|
|
223
214
|
disabled={this.state.activating}
|
|
224
215
|
onClick={() => {
|
|
225
|
-
|
|
216
|
+
// Props and state are synchronized, so everything the async work needs is copied into plain locals HERE, in the tracked part
|
|
217
|
+
let serverUrl = this.props.serverUrl;
|
|
218
|
+
let bucketName = this.props.bucketName;
|
|
219
|
+
this.state.activating = true;
|
|
220
|
+
this.state.error = "";
|
|
221
|
+
Querysub.onCommitFinished(async () => {
|
|
222
|
+
let result = await activateServerBucket({ url: serverUrl, account: STORAGE_ACCOUNT, bucketName });
|
|
223
|
+
// A refusal comes back as a string - it's a normal result, not an error
|
|
224
|
+
let refusal = typeof result === "string" && result || "";
|
|
225
|
+
if (refusal) {
|
|
226
|
+
console.log(`Activating bucket ${bucketName} on ${serverUrl} was refused: ${refusal}`);
|
|
227
|
+
} else {
|
|
228
|
+
console.log(`Activated bucket ${bucketName} on ${serverUrl}`);
|
|
229
|
+
}
|
|
230
|
+
Querysub.commit(() => {
|
|
231
|
+
this.state.activating = false;
|
|
232
|
+
this.state.error = refusal;
|
|
233
|
+
// Only this server's buckets changed
|
|
234
|
+
if (!refusal) {
|
|
235
|
+
StorageSynced(SocketFunction.browserNodeId()).getServerBuckets.reset(serverUrl);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
});
|
|
226
239
|
}}
|
|
227
240
|
>
|
|
228
241
|
{this.state.activating && "Activating..." || "Activate"}
|
|
@@ -315,43 +328,71 @@ class IndexSourcesCell extends qreact.Component<{ sources: ArchivesConfig["index
|
|
|
315
328
|
}
|
|
316
329
|
}
|
|
317
330
|
|
|
318
|
-
const
|
|
319
|
-
const
|
|
331
|
+
const DEPLOY_PENDING_HUE = { h: 35, s: 80 };
|
|
332
|
+
const DEPLOY_OVERLAP_HUE = { h: 280, s: 60 };
|
|
320
333
|
const DEPLOY_NOTICE_FONT_SIZE = 14;
|
|
334
|
+
const DEPLOY_NOTICE_TEXT_LIGHTNESS = 30;
|
|
335
|
+
const DEPLOY_NOTICE_BORDER_LIGHTNESS = 55;
|
|
336
|
+
const DEPLOY_NOTICE_BACKGROUND_LIGHTNESS = 94;
|
|
337
|
+
|
|
338
|
+
class DeployNotice extends qreact.Component<{
|
|
339
|
+
hue: { h: number; s: number };
|
|
340
|
+
icon: string;
|
|
341
|
+
title: string;
|
|
342
|
+
detail: string;
|
|
343
|
+
tooltip: string;
|
|
344
|
+
}> {
|
|
345
|
+
render() {
|
|
346
|
+
let { hue, icon, title, detail, tooltip } = this.props;
|
|
347
|
+
return <div
|
|
348
|
+
className={
|
|
349
|
+
css.hbox(8).pad2(12, 8).fontSize(DEPLOY_NOTICE_FONT_SIZE).alignItems("center")
|
|
350
|
+
.hsl(hue.h, hue.s, DEPLOY_NOTICE_BACKGROUND_LIGHTNESS)
|
|
351
|
+
.bord2(hue.h, hue.s, DEPLOY_NOTICE_BORDER_LIGHTNESS)
|
|
352
|
+
.colorhsl(hue.h, hue.s, DEPLOY_NOTICE_TEXT_LIGHTNESS)
|
|
353
|
+
}
|
|
354
|
+
title={tooltip}
|
|
355
|
+
>
|
|
356
|
+
<div className={css.fontSize(DEPLOY_NOTICE_FONT_SIZE + 4)}>{icon}</div>
|
|
357
|
+
<div className={css.vbox(2)}>
|
|
358
|
+
<div className={css.boldStyle}>{title}</div>
|
|
359
|
+
<div className={css.fontSize(DEPLOY_NOTICE_FONT_SIZE - 2)}>{detail}</div>
|
|
360
|
+
</div>
|
|
361
|
+
</div>;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
321
364
|
|
|
322
365
|
/** The services behind these storage servers may be mid-release, which moves (or duplicates) the servers the page just read from. */
|
|
323
|
-
class DeployNotices extends qreact.Component<{
|
|
366
|
+
class DeployNotices extends qreact.Component<{ services: StorageService[] }> {
|
|
324
367
|
render() {
|
|
325
368
|
let now = Querysub.nowDelayed(timeInSecond);
|
|
326
|
-
let byService = new Map<string, StorageServerBuckets>();
|
|
327
|
-
for (let server of this.props.servers) {
|
|
328
|
-
if (byService.has(server.serviceKey)) continue;
|
|
329
|
-
byService.set(server.serviceKey, server);
|
|
330
|
-
}
|
|
331
369
|
let notices: preact.ComponentChild[] = [];
|
|
332
|
-
for (let
|
|
370
|
+
for (let service of this.props.services) {
|
|
371
|
+
let serviceKey = service.serviceKey;
|
|
333
372
|
let releaseTime = service.releaseTime || 0;
|
|
334
373
|
// The old instances run out their overlap purely on time - we don't verify they're actually still up
|
|
335
374
|
if (!releaseTime || !service.hasOldParameters) continue;
|
|
336
375
|
let killTime = releaseTime + service.overlapTime;
|
|
337
376
|
if (now < releaseTime) {
|
|
338
|
-
notices.push(<
|
|
377
|
+
notices.push(<DeployNotice
|
|
339
378
|
key={serviceKey + "pending"}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
379
|
+
hue={DEPLOY_PENDING_HUE}
|
|
380
|
+
icon="🕐"
|
|
381
|
+
title={`Deploys in ${formatTime(releaseTime - now)}`}
|
|
382
|
+
detail={`${service.serviceTitle} (${serviceKey})`}
|
|
383
|
+
tooltip={`Deploys at ${formatDateTimeDetailed(releaseTime)}`}
|
|
384
|
+
/>);
|
|
345
385
|
continue;
|
|
346
386
|
}
|
|
347
387
|
if (now < killTime) {
|
|
348
|
-
notices.push(<
|
|
388
|
+
notices.push(<DeployNotice
|
|
349
389
|
key={serviceKey + "overlap"}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
390
|
+
hue={DEPLOY_OVERLAP_HUE}
|
|
391
|
+
icon="🔀"
|
|
392
|
+
title={`Old version still running for ${formatTime(killTime - now)}`}
|
|
393
|
+
detail={`${service.serviceTitle} (${serviceKey})`}
|
|
394
|
+
tooltip={`Old instances shut down at ${formatDateTimeDetailed(killTime)}`}
|
|
395
|
+
/>);
|
|
355
396
|
}
|
|
356
397
|
}
|
|
357
398
|
if (!notices.length) return undefined;
|
|
@@ -361,28 +402,29 @@ class DeployNotices extends qreact.Component<{ servers: StorageServerBuckets[] }
|
|
|
361
402
|
|
|
362
403
|
async function resetWriteStats(servers: StorageServerBuckets[]): Promise<void> {
|
|
363
404
|
await Promise.all(servers.map(async server => {
|
|
364
|
-
if (server.
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
console.log(`Cleared write stats for ${clearedBuckets} buckets on ${server.url}`);
|
|
368
|
-
} catch (e) {
|
|
369
|
-
console.error(`Clearing write stats on ${server.url} failed:`, (e as Error).stack ?? e);
|
|
370
|
-
}
|
|
405
|
+
if (server.loading) return;
|
|
406
|
+
let { clearedBuckets } = await clearServerWriteStats({ url: server.url, account: STORAGE_ACCOUNT });
|
|
407
|
+
console.log(`Cleared write stats for ${clearedBuckets} buckets on ${server.url}`);
|
|
371
408
|
}));
|
|
372
|
-
|
|
409
|
+
Querysub.commit(() => {
|
|
410
|
+
StorageSynced(SocketFunction.browserNodeId()).getServerBuckets.resetAll();
|
|
411
|
+
});
|
|
373
412
|
}
|
|
374
413
|
|
|
375
414
|
export class StoragePage extends qreact.Component {
|
|
376
415
|
render() {
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
416
|
+
const controller = StorageSynced(SocketFunction.browserNodeId());
|
|
417
|
+
const machineController = MachineServiceController(SocketFunction.browserNodeId());
|
|
418
|
+
const serviceList = machineController.getServiceList();
|
|
419
|
+
// The configs are already synchronized to the client, so the deploy state needs no round trip at all
|
|
420
|
+
const configs = (serviceList || []).map(serviceId => machineController.getServiceConfig(serviceId)).filter(isDefined);
|
|
421
|
+
const storageServers = getStorageServers(configs);
|
|
422
|
+
const servers: StorageServerBuckets[] = storageServers.map(server => {
|
|
423
|
+
let buckets = controller.getServerBuckets(server.url);
|
|
424
|
+
if (!buckets) return { ...server, loading: true };
|
|
425
|
+
return { ...server, buckets };
|
|
426
|
+
});
|
|
427
|
+
const header = <>
|
|
386
428
|
<div className={css.hbox(12)}>
|
|
387
429
|
<h2 className={css.flexGrow(1)}>Storage</h2>
|
|
388
430
|
<button className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(0, 0, 100)}
|
|
@@ -393,12 +435,27 @@ export class StoragePage extends qreact.Component {
|
|
|
393
435
|
</button>
|
|
394
436
|
<button className={css.pad2(12, 8).button.bord2(0, 0, 20).hsl(0, 0, 100)}
|
|
395
437
|
onClick={() => {
|
|
396
|
-
|
|
438
|
+
controller.getServerBuckets.resetAll();
|
|
397
439
|
}}>
|
|
398
440
|
Refresh
|
|
399
441
|
</button>
|
|
400
442
|
</div>
|
|
401
|
-
<DeployNotices
|
|
443
|
+
<DeployNotices services={getStorageServices(storageServers)} />
|
|
444
|
+
</>;
|
|
445
|
+
if (!serviceList) {
|
|
446
|
+
return <div className={css.vbox(16)}>
|
|
447
|
+
{header}
|
|
448
|
+
<div>Loading services...</div>
|
|
449
|
+
</div>;
|
|
450
|
+
}
|
|
451
|
+
if (!servers.length) {
|
|
452
|
+
return <div className={css.vbox(16)}>
|
|
453
|
+
{header}
|
|
454
|
+
<div>No services with a command starting with {JSON.stringify(STORAGE_COMMAND_PREFIX)} were found.</div>
|
|
455
|
+
</div>;
|
|
456
|
+
}
|
|
457
|
+
return <div className={css.vbox(16)}>
|
|
458
|
+
{header}
|
|
402
459
|
<Table
|
|
403
460
|
rows={getBucketRows(servers)}
|
|
404
461
|
columns={{
|
|
@@ -85,7 +85,7 @@ export class UpdateButtons extends qreact.Component<{
|
|
|
85
85
|
: "All services release immediately. Their old instances keep running for their configured overlap before shutting down."}</div>
|
|
86
86
|
<div className={css.hbox(10)}>
|
|
87
87
|
<button
|
|
88
|
-
className={css.pad2(12, 8).button.bord2(0,
|
|
88
|
+
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))}
|
|
89
89
|
onClick={() => {
|
|
90
90
|
modal && modal.close();
|
|
91
91
|
this.deployAll(outdatedServices, latestRef, noOverlap ? "nowNoOverlap" : "now");
|
|
@@ -174,7 +174,7 @@ export class UpdateButtons extends qreact.Component<{
|
|
|
174
174
|
</div>)}
|
|
175
175
|
</button>
|
|
176
176
|
<button
|
|
177
|
-
className={buttonStyle.hsl(
|
|
177
|
+
className={buttonStyle.hsl(45, 85, 82)}
|
|
178
178
|
disabled={this.state.isDeploying}
|
|
179
179
|
title="Deploys immediately; the old instances keep running for their configured overlap before shutting down"
|
|
180
180
|
onClick={() => {
|