querysub 0.606.0 → 0.608.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 +1 -1
- package/src/-a-archives/archives2.ts +8 -3
- package/src/4-deploy/edgeBootstrap.ts +8 -0
- package/src/4-deploy/edgeNodes.ts +71 -75
- package/src/deployManager/MachinesPage.tsx +1 -1
- package/src/deployManager/components/{RouteConfigView.tsx → storage/RouteConfigView.tsx} +9 -8
- package/src/deployManager/components/storage/StoragePage.tsx +151 -0
- package/src/deployManager/components/{Tag.tsx → storage/Tag.tsx} +1 -1
- package/src/deployManager/components/storage/bucketCells.tsx +170 -0
- package/src/deployManager/components/storage/bucketRows.ts +145 -0
- package/src/deployManager/components/storage/deployNotices.tsx +81 -0
- package/src/deployManager/components/storage/operationsView.tsx +217 -0
- package/src/deployManager/components/storage/sourceNames.tsx +43 -0
- package/src/deployManager/components/storage/storageController.ts +76 -0
- package/src/deployManager/components/storage/storageServers.ts +73 -0
- package/src/library-components/UsageBar.tsx +5 -2
- package/src/deployManager/components/StoragePage.tsx +0 -652
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
2
|
+
import { listServerBuckets, clearServerWriteStats, activateServerBucket, getServerActiveBucket } from "sliftutils/storage/remoteStorage/createArchives";
|
|
3
|
+
import type { ServerBucketInfo, ActiveBucketInfo } from "sliftutils/storage/remoteStorage/storageServerState";
|
|
4
|
+
import { parseStorageUrl, authenticateStorage } from "sliftutils/storage/remoteStorage/ArchivesRemote";
|
|
5
|
+
import { RemoteStorageController, STORAGE_NOT_AUTHENTICATED } from "sliftutils/storage/remoteStorage/storageController";
|
|
6
|
+
import type { AccessTotals, AccessSummaryState } from "sliftutils/storage/remoteStorage/accessStats";
|
|
7
|
+
import type { SummaryEntry } from "sliftutils/treeSummary";
|
|
8
|
+
import { getSyncedController } from "../../../library-components/SyncedController";
|
|
9
|
+
import { assertIsManagementUser } from "../../../diagnostics/managementPages";
|
|
10
|
+
import { STORAGE_ACCOUNT } from "../../../-a-archives/archives2";
|
|
11
|
+
|
|
12
|
+
// The registered controller and its synced wrapper have to keep a stable identity, so this file is a full-refresh boundary rather than a hot-reload one (matching SyncedController itself).
|
|
13
|
+
module.hotreload = false;
|
|
14
|
+
|
|
15
|
+
/** The access-stats endpoints have no client wrapper in sliftutils (a pinned dependency), so we reconnect and authenticate the same way its own callServer does. */
|
|
16
|
+
async function callStorageServer<T>(url: string, run: (controller: typeof RemoteStorageController.nodes[string]) => Promise<T>): Promise<T> {
|
|
17
|
+
SocketFunction.ENABLE_CLIENT_MODE = true;
|
|
18
|
+
let parsed = parseStorageUrl(url);
|
|
19
|
+
let nodeId = SocketFunction.connect({ address: parsed.address, port: parsed.port });
|
|
20
|
+
let controller = RemoteStorageController.nodes[nodeId];
|
|
21
|
+
try {
|
|
22
|
+
return await run(controller);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
if (!String((e as Error).stack ?? e).includes(STORAGE_NOT_AUTHENTICATED)) throw e;
|
|
25
|
+
await authenticateStorage({ address: parsed.address, port: parsed.port, nodeId });
|
|
26
|
+
return await run(controller);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Every storage call is proxied through here: they authenticate with this machine's identity CA, which the browser has no access to. */
|
|
31
|
+
class StoragePageControllerBase {
|
|
32
|
+
/** One call per server, so each caches and refreshes on its own. */
|
|
33
|
+
public async getServerBuckets(url: string): Promise<ServerBucketInfo[]> {
|
|
34
|
+
return await listServerBuckets({ url, account: STORAGE_ACCOUNT });
|
|
35
|
+
}
|
|
36
|
+
public async activateBucket(url: string, bucketName: string): Promise<ActiveBucketInfo | string> {
|
|
37
|
+
return await activateServerBucket({ url, account: STORAGE_ACCOUNT, bucketName });
|
|
38
|
+
}
|
|
39
|
+
public async getActiveBucket(url: string, bucketName: string): Promise<ActiveBucketInfo | string> {
|
|
40
|
+
return await getServerActiveBucket({ url, account: STORAGE_ACCOUNT, bucketName });
|
|
41
|
+
}
|
|
42
|
+
public async clearWriteStats(url: string): Promise<{ clearedBuckets: number }> {
|
|
43
|
+
return await clearServerWriteStats({ url, account: STORAGE_ACCOUNT });
|
|
44
|
+
}
|
|
45
|
+
/** Per-operation totals since startup (or the last reset), aggregated across every bucket in the account. */
|
|
46
|
+
public async getAccessStats(url: string): Promise<AccessTotals> {
|
|
47
|
+
return await callStorageServer(url, controller => controller.getAccessStats({ account: STORAGE_ACCOUNT }));
|
|
48
|
+
}
|
|
49
|
+
/** The path breakdown of one operation. weightBySize orders it (and the totals) by bytes rather than call count. */
|
|
50
|
+
public async getAccessSummaries(url: string, operation: string, maxCount: number, weightBySize: boolean): Promise<SummaryEntry<AccessSummaryState>[]> {
|
|
51
|
+
return await callStorageServer(url, controller => controller.getAccessSummaries({ account: STORAGE_ACCOUNT, operation, maxCount, weightBySize }));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const StoragePageController = SocketFunction.register(
|
|
56
|
+
"StoragePageController-4f1c93ab-77e2-4d15-9a30-1c6b8f5d2e04",
|
|
57
|
+
new StoragePageControllerBase(),
|
|
58
|
+
() => ({
|
|
59
|
+
getServerBuckets: {},
|
|
60
|
+
activateBucket: {},
|
|
61
|
+
getActiveBucket: {},
|
|
62
|
+
clearWriteStats: {},
|
|
63
|
+
getAccessStats: {},
|
|
64
|
+
getAccessSummaries: {},
|
|
65
|
+
}),
|
|
66
|
+
() => ({
|
|
67
|
+
hooks: [assertIsManagementUser],
|
|
68
|
+
}),
|
|
69
|
+
{
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
export const StorageSynced = getSyncedController(StoragePageController, {
|
|
74
|
+
reads: {},
|
|
75
|
+
writes: {},
|
|
76
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ServerBucketInfo } from "sliftutils/storage/remoteStorage/storageServerState";
|
|
2
|
+
import { getLiveServiceParameters, getMachineTargets, applyCommandTemplate, ServiceParameters, ServiceConfig, DEFAULT_OVERLAP_TIME } from "../../machineSchema";
|
|
3
|
+
import { STORAGE_COMMAND_PREFIX } from "../../serviceCategories";
|
|
4
|
+
|
|
5
|
+
module.hotreload = true;
|
|
6
|
+
|
|
7
|
+
const URL_ARG_REGEX = /--url\s+(\S+)/g;
|
|
8
|
+
|
|
9
|
+
/** 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. */
|
|
10
|
+
export type StorageService = {
|
|
11
|
+
serviceKey: string;
|
|
12
|
+
serviceTitle: string;
|
|
13
|
+
releaseTime?: number;
|
|
14
|
+
overlapTime: number;
|
|
15
|
+
hasOldParameters: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type StorageServer = StorageService & {
|
|
19
|
+
url: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type StorageServerBuckets = StorageServer & {
|
|
23
|
+
/** Still waiting on this server's own endpoint - each server loads independently */
|
|
24
|
+
loading?: boolean;
|
|
25
|
+
buckets?: ServerBucketInfo[];
|
|
26
|
+
/** This server could not be read at all. Kept per server rather than thrown, so one bad server costs its own row and nothing else. */
|
|
27
|
+
error?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function getStorageUrls(parameters: ServiceParameters): string[] {
|
|
31
|
+
if (!parameters.command.trimStart().startsWith(STORAGE_COMMAND_PREFIX)) return [];
|
|
32
|
+
let urls: string[] = [];
|
|
33
|
+
for (let target of getMachineTargets(parameters)) {
|
|
34
|
+
let command = applyCommandTemplate(parameters.command, target.variables);
|
|
35
|
+
for (let match of command.matchAll(URL_ARG_REGEX)) {
|
|
36
|
+
urls.push(match[1]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return urls;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 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. */
|
|
43
|
+
export function getStorageServers(configs: ServiceConfig[]): StorageServer[] {
|
|
44
|
+
let serviceByUrl = new Map<string, StorageServer>();
|
|
45
|
+
for (let config of configs) {
|
|
46
|
+
let allParameters = [getLiveServiceParameters(config)];
|
|
47
|
+
for (let parameters of allParameters) {
|
|
48
|
+
if (!parameters) continue;
|
|
49
|
+
for (let url of getStorageUrls(parameters)) {
|
|
50
|
+
if (serviceByUrl.has(url)) continue;
|
|
51
|
+
serviceByUrl.set(url, {
|
|
52
|
+
url,
|
|
53
|
+
serviceKey: parameters.key,
|
|
54
|
+
serviceTitle: config.info.title,
|
|
55
|
+
releaseTime: config.parameters.releaseTime,
|
|
56
|
+
// The overlap always comes from the newest parameters, even though it governs how long the old instances outlive the release
|
|
57
|
+
overlapTime: config.parameters.overlapTime ?? DEFAULT_OVERLAP_TIME,
|
|
58
|
+
hasOldParameters: !!config.oldParameters,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return [...serviceByUrl.values()];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getStorageServices(servers: StorageServer[]): StorageService[] {
|
|
67
|
+
let byKey = new Map<string, StorageService>();
|
|
68
|
+
for (let server of servers) {
|
|
69
|
+
if (byKey.has(server.serviceKey)) continue;
|
|
70
|
+
byKey.set(server.serviceKey, server);
|
|
71
|
+
}
|
|
72
|
+
return [...byKey.values()];
|
|
73
|
+
}
|
|
@@ -24,13 +24,16 @@ export class UsageBar extends qreact.Component<{
|
|
|
24
24
|
label: string;
|
|
25
25
|
value: number;
|
|
26
26
|
max: number;
|
|
27
|
+
/** Appended after each formatted number, so byte bars can read "5.2GB" instead of a bare "5.2G". */
|
|
28
|
+
unit?: string;
|
|
27
29
|
/** Fraction of max at which the bar fill turns yellow. */
|
|
28
30
|
warningThreshold?: number;
|
|
29
31
|
/** Fraction of max at which the bar fill turns red and flashes. */
|
|
30
32
|
errorThreshold?: number;
|
|
31
33
|
}> {
|
|
32
34
|
render() {
|
|
33
|
-
let { label, value, max, warningThreshold, errorThreshold } = this.props;
|
|
35
|
+
let { label, value, max, unit, warningThreshold, errorThreshold } = this.props;
|
|
36
|
+
let suffix = unit || "";
|
|
34
37
|
let fraction = max && value / max || 0;
|
|
35
38
|
let isError = errorThreshold !== undefined && fraction >= errorThreshold;
|
|
36
39
|
let isWarning = !isError && warningThreshold !== undefined && fraction >= warningThreshold;
|
|
@@ -42,7 +45,7 @@ export class UsageBar extends qreact.Component<{
|
|
|
42
45
|
+ (isError && (" " + FLASH_CLASS_NAME) || "")
|
|
43
46
|
} />
|
|
44
47
|
<div className={css.relative}>
|
|
45
|
-
{label} ({formatNumber(value)} / {formatNumber(max)})
|
|
48
|
+
{label} ({formatNumber(value)}{suffix} / {formatNumber(max)}{suffix})
|
|
46
49
|
</div>
|
|
47
50
|
{isError && <style>{`
|
|
48
51
|
@keyframes ${FLASH_CLASS_NAME}-anim {
|