querysub 0.559.0 → 0.560.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.559.0",
3
+ "version": "0.560.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.40",
74
+ "sliftutils": "^1.7.41",
75
75
  "socket-function": "^1.2.26",
76
76
  "terser": "^5.31.0",
77
77
  "typenode": "^6.6.1",
@@ -8,7 +8,7 @@ import { sort, timeInSecond, nextId } from "socket-function/src/misc";
8
8
  import { MachineController, watchProcessOutput, stopWatchingProcessOutput } from "../machineController";
9
9
  import type { ProcessRecord } from "../processLogs";
10
10
  import { Button } from "../../library-components/Button";
11
- import { parseAnsiColors } from "../../diagnostics/logs/ansiFormat";
11
+ import { parseAnsiColors, rgbToHsl } from "../../diagnostics/logs/ansiFormat";
12
12
 
13
13
  module.hotreload = true;
14
14
 
@@ -17,6 +17,8 @@ const DEAD_COLOR = { h: 0, s: 0, l: 92 };
17
17
  const OUTPUT_BUFFER_LIMIT = 1_000_000;
18
18
  const OUTPUT_BUFFER_KEPT = 100_000;
19
19
  const OUTPUT_MAX_HEIGHT = "40vh";
20
+ const ANSI_SATURATION = 70;
21
+ const ANSI_LIGHTNESS = 70;
20
22
 
21
23
  /** One process's live output. Mounting starts the stream, unmounting stops it, so the watch lifetime is exactly the time it is on screen. */
22
24
  class ProcessOutput extends qreact.Component<{ record: ProcessRecord; machineNodeId: string }> {
@@ -70,7 +72,11 @@ class ProcessOutput extends qreact.Component<{ record: ProcessRecord; machineNod
70
72
  .maxHeight(OUTPUT_MAX_HEIGHT).overflow("auto").pad2(10, 8)
71
73
  .hsl(0, 0, 12).colorhsl(0, 0, 90)
72
74
  }>
73
- {parseAnsiColors(this.state.data)}
75
+ {parseAnsiColors(this.state.data).map(({ text, color }) => {
76
+ if (!color) return <span>{text}</span>;
77
+ // The pane is dark, so the ansi colour only sets the hue - the lightness has to stay readable against it
78
+ return <span className={css.colorhsl(rgbToHsl(color).h, ANSI_SATURATION, ANSI_LIGHTNESS)}>{text}</span>;
79
+ })}
74
80
  </div>;
75
81
  }
76
82
  }
@@ -12,6 +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, NO_FULL_SYNC_TAG } from "./Tag";
15
16
  import { StorageSynced, type StorageServerBuckets } from "./StoragePage";
16
17
 
17
18
  module.hotreload = true;
@@ -19,8 +20,7 @@ module.hotreload = true;
19
20
  type Source = HostedConfig | BackblazeConfig;
20
21
 
21
22
  const DEFAULT_HTTPS_PORT = 443;
22
- const TAG_COLOR = { h: 210, s: 45, l: 88 };
23
- const WARNING_COLOR = { h: 35, s: 90, l: 85 };
23
+ const WARNING_COLOR = TAG_WARNING_COLOR;
24
24
  const NOTICE_COLOR = { h: 205, s: 70, l: 88 };
25
25
  const SOURCE_BORDER_COLOR = { h: 0, s: 0, l: 75 };
26
26
  const ACTIVE_COLOR = { h: 130, s: 55, l: 85 };
@@ -38,7 +38,6 @@ const WINDOW_TIME_LIGHTNESS = 12;
38
38
  const BUCKET_TITLE_SIZE = 15;
39
39
  const GROUP_BODY_INDENT = 64;
40
40
  const WINDOW_BODY_INDENT = 64;
41
- const TAG_FONT_SIZE = 11;
42
41
  const JSON_INDENT = 4;
43
42
 
44
43
  /** String sources are the shorthand for an always-valid, unsharded backblaze bucket. */
@@ -130,7 +129,7 @@ type WindowCluster = {
130
129
  sources: Source[];
131
130
  };
132
131
 
133
- /** Every distinct valid window in the config, latest-ending first, each listing every source that overlaps it. A source spanning several windows appears under each of them - what matters is what is valid during a window, not which window a source "belongs" to. Sources keep their config order, since that order decides which one shadows which. */
132
+ /** Every distinct valid window in the config, latest-starting first, each listing every source that overlaps it. A source spanning several windows appears under each of them - what matters is what is valid during a window, not which window a source "belongs" to. Sources keep their config order, since that order decides which one shadows which. */
134
133
  function getWindowRanges(sources: Source[]): WindowCluster[] {
135
134
  let byKey = new Map<string, [number, number]>();
136
135
  for (let source of sources) {
@@ -139,9 +138,9 @@ function getWindowRanges(sources: Source[]): WindowCluster[] {
139
138
  byKey.set(key, [source.validWindow[0], source.validWindow[1]]);
140
139
  }
141
140
  let windows = [...byKey.values()];
142
- // Sorted by start first, so the (stable) sort by end leaves later starts first within one end time
143
- sort(windows, x => -x[0]);
141
+ // Sorted by end first, so the (stable) sort by start leaves later ends first within one start time
144
142
  sort(windows, x => -x[1]);
143
+ sort(windows, x => -x[0]);
145
144
  return windows.map(window => ({
146
145
  window,
147
146
  sources: sources.filter(x => windowsOverlap(x.validWindow, window)),
@@ -344,24 +343,11 @@ function showConfigModal(bucketName: string, rawConfig: RemoteConfig): void {
344
343
  });
345
344
  }
346
345
 
347
- class Tag extends qreact.Component<{ icon: string; text: string; warning?: boolean; color?: { h: number; s: number; l: number }; title?: string; }> {
348
- render() {
349
- let color = this.props.color || this.props.warning && WARNING_COLOR || TAG_COLOR;
350
- return <div className={
351
- css.hbox(4).pad2(6, 1).fontSize(TAG_FONT_SIZE).whiteSpace("nowrap")
352
- .hsl(color.h, color.s, color.l).bord2(color.h, color.s, color.l - 20)
353
- } title={this.props.title}>
354
- <span>{this.props.icon}</span>
355
- <span>{this.props.text}</span>
356
- </div>;
357
- }
358
- }
359
-
360
346
  function getSourceTags(source: Source): { icon: string; text: string }[] {
361
347
  let tags: { icon: string; text: string }[] = [];
362
348
  if (source.public) tags.push({ icon: "🌐", text: "public" });
363
349
  if (source.immutable) tags.push({ icon: "🔒", text: "immutable" });
364
- if (source.noFullSync) tags.push({ icon: "⇣", text: "noFullSync" });
350
+ if (source.noFullSync) tags.push(NO_FULL_SYNC_TAG);
365
351
  if (source.intermediate) tags.push({ icon: "⏳", text: "intermediate" });
366
352
  if (source.type === "remote") {
367
353
  if (source.fast) tags.push({ icon: "⚡", text: "fast" });
@@ -7,7 +7,8 @@ import preact from "preact";
7
7
  import { Querysub } from "../../4-querysub/Querysub";
8
8
  import { listServerBuckets, clearServerWriteStats, activateServerBucket, getServerActiveBucket } from "sliftutils/storage/remoteStorage/createArchives";
9
9
  import type { ServerBucketInfo, BucketDiskInfo, BucketWriteStats, ActiveBucketInfo } from "sliftutils/storage/remoteStorage/storageServerState";
10
- import type { ArchivesConfig, SyncActivity } from "sliftutils/storage/IArchives";
10
+ import type { ArchivesConfig, SyncActivity, RemoteConfig, HostedConfig } from "sliftutils/storage/IArchives";
11
+ import { parseHostedUrl } from "sliftutils/storage/remoteStorage/remoteConfig";
11
12
  import { UsageBar, getUsageThresholds } from "../../library-components/UsageBar";
12
13
  import { getSyncedController } from "../../library-components/SyncedController";
13
14
  import { assertIsManagementUser } from "../../diagnostics/managementPages";
@@ -16,6 +17,7 @@ import { isDefined } from "../../misc";
16
17
  import { STORAGE_ACCOUNT } from "../../-a-archives/archives2";
17
18
  import { Table } from "../../5-diagnostics/Table";
18
19
  import { RouteConfigView } from "./RouteConfigView";
20
+ import { Tag, NO_FULL_SYNC_TAG } from "./Tag";
19
21
 
20
22
  module.hotreload = true;
21
23
 
@@ -132,6 +134,7 @@ type BucketRow = {
132
134
  state: string;
133
135
  files: string;
134
136
  bytes: string;
137
+ noFullSync: boolean;
135
138
  indexSources: ArchivesConfig["indexSources"];
136
139
  readerDiskLimit: string;
137
140
  writes: string;
@@ -144,10 +147,35 @@ type BucketRow = {
144
147
  };
145
148
 
146
149
  const GAIN_DECIMALS = 1;
150
+ const DEFAULT_HTTPS_PORT = 443;
147
151
  const DISK_BAR_TYPE = "DISK";
148
152
  const ACTIVE_STATE = "active";
149
153
  const INACTIVE_STATE = "inactive";
150
154
 
155
+ /** Whether this server only caches what is read of the bucket instead of keeping a full copy, read off its own entry in the bucket's routing config. */
156
+ function getNoFullSync(serverUrl: string, bucketName: string, remoteConfig: RemoteConfig | undefined): boolean {
157
+ let host: URL;
158
+ try {
159
+ host = new URL(serverUrl);
160
+ } catch {
161
+ return false;
162
+ }
163
+ let selfEntries: HostedConfig[] = [];
164
+ for (let source of remoteConfig?.sources || []) {
165
+ if (typeof source === "string" || source.type !== "remote") continue;
166
+ try {
167
+ let parsed = parseHostedUrl(source.url);
168
+ if (parsed.address !== host.hostname) continue;
169
+ if (String(parsed.port) !== (host.port || String(DEFAULT_HTTPS_PORT))) continue;
170
+ if (parsed.bucketName !== bucketName) continue;
171
+ selfEntries.push(source);
172
+ } catch {
173
+ // A malformed url in the config is not this column's problem
174
+ }
175
+ }
176
+ return selfEntries.some(x => x.noFullSync);
177
+ }
178
+
151
179
  /** How much fast-mode coalescing saved: every accepted write over the ones that actually reached a source. */
152
180
  function getWriteGain(stats: BucketWriteStats | undefined): string {
153
181
  if (!stats) return "";
@@ -167,6 +195,7 @@ function getBucketRows(servers: StorageServerBuckets[]): BucketRow[] {
167
195
  state: "",
168
196
  files: "",
169
197
  bytes: "",
198
+ noFullSync: false,
170
199
  indexSources: undefined,
171
200
  readerDiskLimit: "",
172
201
  writes: "",
@@ -196,6 +225,7 @@ function getBucketRows(servers: StorageServerBuckets[]): BucketRow[] {
196
225
  state: bucket.active && ACTIVE_STATE || INACTIVE_STATE,
197
226
  files: config?.index && formatNumber(config.index.fileCount) || "",
198
227
  bytes: config?.index && formatNumber(config.index.byteCount) + "B" || "",
228
+ noFullSync: getNoFullSync(server.url, bucket.bucketName, config?.remoteConfig),
199
229
  indexSources: config?.indexSources,
200
230
  readerDiskLimit: config?.readerDiskLimit && formatNumber(config.readerDiskLimit) + "B" || "",
201
231
  writes: bucket.writeStats && formatNumber(bucket.writeStats.originalWrites) || "",
@@ -551,6 +581,11 @@ export class StoragePage extends qreact.Component {
551
581
  },
552
582
  files: { title: "Files" },
553
583
  bytes: { title: "Bytes" },
584
+ noFullSync: {
585
+ title: "Sync",
586
+ // Nothing to show when a full copy is kept - that is the default, and the tag is the flag
587
+ formatter: noFullSync => noFullSync && <Tag icon={NO_FULL_SYNC_TAG.icon} text={NO_FULL_SYNC_TAG.text} /> || ""
588
+ },
554
589
  indexSources: {
555
590
  title: "Index sources",
556
591
  formatter: sources => <IndexSourcesCell sources={sources} />
@@ -0,0 +1,26 @@
1
+ import { qreact } from "../../4-dom/qreact";
2
+ import { css } from "typesafecss";
3
+
4
+ module.hotreload = true;
5
+
6
+ export type TagColor = { h: number; s: number; l: number };
7
+
8
+ export const TAG_COLOR: TagColor = { h: 210, s: 45, l: 88 };
9
+ export const TAG_WARNING_COLOR: TagColor = { h: 35, s: 90, l: 85 };
10
+ export const TAG_FONT_SIZE = 11;
11
+
12
+ /** A source flag, shown wherever that flag matters. The tag is only ever rendered when the flag is on - an absent tag is the default, so there is nothing to show for it. */
13
+ export const NO_FULL_SYNC_TAG = { icon: "⇣", text: "noFullSync" };
14
+
15
+ export class Tag extends qreact.Component<{ icon: string; text: string; warning?: boolean; color?: TagColor; title?: string; }> {
16
+ render() {
17
+ let color = this.props.color || this.props.warning && TAG_WARNING_COLOR || TAG_COLOR;
18
+ return <div className={
19
+ css.hbox(4).pad2(6, 1).fontSize(TAG_FONT_SIZE).whiteSpace("nowrap")
20
+ .hsl(color.h, color.s, color.l).bord2(color.h, color.s, color.l - 20)
21
+ } title={this.props.title}>
22
+ <span>{this.props.icon}</span>
23
+ <span>{this.props.text}</span>
24
+ </div>;
25
+ }
26
+ }