querysub 0.633.0 → 0.635.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 +3 -3
- package/src/-c-identity/IdentityController.ts +6 -3
- package/src/-f-node-discovery/NodeDiscovery.ts +2 -3
- package/src/5-diagnostics/Table.tsx +2 -1
- package/src/deployManager/components/ProcessesView.tsx +22 -5
- package/src/deployManager/components/ServiceMeasureBars.tsx +3 -1
- package/src/deployManager/components/storage/StorageLogsPage.tsx +20 -29
- package/src/deployManager/components/storage/bucketRows.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "querysub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.635.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",
|
|
@@ -79,8 +79,8 @@
|
|
|
79
79
|
"node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
|
|
80
80
|
"pako": "^2.1.0",
|
|
81
81
|
"peggy": "^5.0.6",
|
|
82
|
-
"sliftutils": "^1.7.
|
|
83
|
-
"socket-function": "^1.2.
|
|
82
|
+
"sliftutils": "^1.7.105",
|
|
83
|
+
"socket-function": "^1.2.31",
|
|
84
84
|
"terser": "^5.31.0",
|
|
85
85
|
"typenode": "^6.6.1",
|
|
86
86
|
"typesafecss": "^0.32.0",
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import debugbreak from "debugbreak";
|
|
8
8
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
9
|
-
import { CallerContext } from "socket-function/SocketFunctionTypes";
|
|
9
|
+
import { CallerContext, ClientHookContext } from "socket-function/SocketFunctionTypes";
|
|
10
10
|
import { cache, cacheWeak, lazy } from "socket-function/src/caching";
|
|
11
11
|
import { getClientNodeId, getNodeId, getNodeIdDomain, getNodeIdIP, getNodeIdLocation, isClientNodeId } from "socket-function/src/nodeCache";
|
|
12
12
|
import { decodeNodeId, getCommonName, getIdentityCA, getMachineId, getOwnMachineId, getPublicIdentifier, getThreadKeyCert, parseCert, sign, validateCertificate, verify } from "sliftutils/misc/https/certs";
|
|
@@ -252,7 +252,7 @@ const changeIdentityOnce = cacheWeak(async function changeIdentityOnce(connectio
|
|
|
252
252
|
);
|
|
253
253
|
});
|
|
254
254
|
let startupTime = Date.now();
|
|
255
|
-
|
|
255
|
+
async function identityHook(context: ClientHookContext) {
|
|
256
256
|
if (context.call.classGuid === IdentityController._classGuid) return;
|
|
257
257
|
// This is for US to tell them our identity. And if they established the connection the identity will come from their original connection url (that they used to connect to us), and they validated it either being a real certificate, or they added the cert from the trusted backblaze bucket. If it just from a real certificate it means we identified them, but they might not have network trust. But that's fine, as IdentityController is JUST for identification, and if it's a real certificate we know who they are! (Which doesn't mean we trust them).
|
|
258
258
|
if (isClientNodeId(context.call.nodeId)) {
|
|
@@ -265,4 +265,7 @@ SocketFunction.addGlobalClientHook(async function identityHook(context) {
|
|
|
265
265
|
if (duration > 200 && now - startupTime > timeInMinute) {
|
|
266
266
|
console.log(red(`IdentityHook took ${formatTime(duration)} for ${context.connectionId.nodeId} ${context.call.classGuid}.${context.call.functionName}`));
|
|
267
267
|
}
|
|
268
|
-
}
|
|
268
|
+
}
|
|
269
|
+
// Don't measure this, as oftentimes it blocks on other servers, and so it isn't interesting to measure it.
|
|
270
|
+
identityHook.noMeasure = true;
|
|
271
|
+
SocketFunction.addGlobalClientHook(identityHook);
|
|
@@ -532,7 +532,8 @@ async function runServerSyncLoops() {
|
|
|
532
532
|
|
|
533
533
|
console.log(magenta(`Node discovery is loaded`));
|
|
534
534
|
|
|
535
|
-
|
|
535
|
+
await runInfinitePollCallAtStart(HEARTBEAT_INTERVAL, writeHeartbeat);
|
|
536
|
+
// Delayed by a bit as it might take a little bit for our heartbeat to show up on startup, especially if we are the storage node that is supposed to store our own heartbeat.
|
|
536
537
|
delay(HEARTBEAT_INTERVAL * 5).then(() => runInfinitePoll(HEARTBEAT_INTERVAL, async function nodeDiscoverHeartbeat() {
|
|
537
538
|
// If we waited too long, other nodes might think we are dead. In which case, we SHOULD terminate.
|
|
538
539
|
if (!isNoNetwork()) {
|
|
@@ -556,8 +557,6 @@ async function runServerSyncLoops() {
|
|
|
556
557
|
ignoreErrors(NodeDiscoveryController.nodes[nodeId].addNode(getOwnNodeId()));
|
|
557
558
|
}
|
|
558
559
|
}
|
|
559
|
-
|
|
560
|
-
await writeHeartbeat();
|
|
561
560
|
}));
|
|
562
561
|
}
|
|
563
562
|
|
|
@@ -63,7 +63,8 @@ export class Table<RowT extends RowType> extends qreact.Component<TableType<RowT
|
|
|
63
63
|
return (
|
|
64
64
|
<table class={css.borderCollapse("collapse") + this.props.class}>
|
|
65
65
|
<tr class={
|
|
66
|
-
|
|
66
|
+
// Above the rows, as they hold positioned content (bars, palettes) which otherwise paints over the sticky header as it scrolls under it
|
|
67
|
+
css.position("sticky").top(0).zIndex(1)
|
|
67
68
|
+ (this.props.dark && css.hsla(0, 0, 10, 0.9))
|
|
68
69
|
+ (!this.props.dark && css.hsla(0, 0, 95, 0.9))
|
|
69
70
|
}>
|
|
@@ -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
|
+
// Within this of the bottom still counts as "at the bottom", as scroll positions land a few pixels off the exact end
|
|
21
|
+
const OUTPUT_PIN_THRESHOLD_PX = 40;
|
|
20
22
|
const DETAIL_FONT_SIZE = 12;
|
|
21
23
|
const ANSI_SATURATION = 70;
|
|
22
24
|
const ANSI_LIGHTNESS = 70;
|
|
@@ -68,12 +70,27 @@ class ProcessOutput extends qreact.Component<{ record: ProcessRecord; machineNod
|
|
|
68
70
|
await stopWatchingProcessOutput({ callbackId });
|
|
69
71
|
});
|
|
70
72
|
}
|
|
73
|
+
private outputDiv: HTMLDivElement | undefined;
|
|
74
|
+
// The pane opens showing the newest output and follows it as it streams; scrolling up to read releases the pin, scrolling back down re-engages it
|
|
75
|
+
private pinnedToBottom = true;
|
|
76
|
+
componentDidUpdate() {
|
|
77
|
+
if (this.pinnedToBottom && this.outputDiv) {
|
|
78
|
+
this.outputDiv.scrollTop = this.outputDiv.scrollHeight;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
71
81
|
render() {
|
|
72
|
-
return <div
|
|
73
|
-
|
|
74
|
-
.
|
|
75
|
-
|
|
76
|
-
|
|
82
|
+
return <div
|
|
83
|
+
className={
|
|
84
|
+
css.fontFamily("monospace").whiteSpace("pre-wrap").fillWidth
|
|
85
|
+
.maxHeight(OUTPUT_MAX_HEIGHT).overflow("auto").pad2(10, 8)
|
|
86
|
+
.hsl(0, 0, 12).colorhsl(0, 0, 90)
|
|
87
|
+
}
|
|
88
|
+
ref={element => this.outputDiv = element as HTMLDivElement || undefined}
|
|
89
|
+
onScroll={element => {
|
|
90
|
+
let div = element.currentTarget;
|
|
91
|
+
this.pinnedToBottom = div.scrollHeight - div.scrollTop - div.clientHeight < OUTPUT_PIN_THRESHOLD_PX;
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
77
94
|
{parseAnsiColors(this.state.data).map(({ text, color }) => {
|
|
78
95
|
if (!color) return <span>{text}</span>;
|
|
79
96
|
// The pane is dark, so the ansi colour only sets the hue - the lightness has to stay readable against it
|
|
@@ -13,6 +13,8 @@ import { assertIsManagementUser } from "../../diagnostics/managementPages";
|
|
|
13
13
|
const NODE_CALL_TIMEOUT = timeInSecond * 10;
|
|
14
14
|
const BAR_HEIGHT_PX = 6;
|
|
15
15
|
const MIN_BAR_WIDTH_PX = 30;
|
|
16
|
+
// A percentage width would be against the bar's own content-sized wrapper, which has no width, so it resolves to zero and every bar sits at the minimum. The width is instead this many pixels at 100% CPU.
|
|
17
|
+
const FULL_BAR_WIDTH_PX = 300;
|
|
16
18
|
// Green at idle, red at fully busy.
|
|
17
19
|
const BAR_IDLE_HUE = 120;
|
|
18
20
|
const BAR_FLASH_THRESHOLD = 0.6;
|
|
@@ -73,7 +75,7 @@ export class ServiceMeasureBars extends qreact.Component<{
|
|
|
73
75
|
<div
|
|
74
76
|
className={
|
|
75
77
|
css.height(BAR_HEIGHT_PX)
|
|
76
|
-
.width(
|
|
78
|
+
.width(Math.round(clampedFraction * FULL_BAR_WIDTH_PX))
|
|
77
79
|
.minWidth(MIN_BAR_WIDTH_PX)
|
|
78
80
|
.button
|
|
79
81
|
.hsl(hue, 70, isExpanded ? 40 : 55)
|
|
@@ -3,6 +3,7 @@ import { SocketFunction } from "socket-function/SocketFunction";
|
|
|
3
3
|
import { qreact } from "../../../4-dom/qreact";
|
|
4
4
|
import { css } from "typesafecss";
|
|
5
5
|
import { Querysub } from "../../../4-querysub/Querysub";
|
|
6
|
+
import { t } from "../../../2-proxy/schema2";
|
|
6
7
|
import { isDefined } from "../../../misc";
|
|
7
8
|
import { formatNumber, formatTime, formatDateTime } from "socket-function/src/formatting/format";
|
|
8
9
|
import { timeInSecond } from "socket-function/src/misc";
|
|
@@ -35,20 +36,6 @@ type ServerLogListing = {
|
|
|
35
36
|
fetchTime?: number;
|
|
36
37
|
};
|
|
37
38
|
|
|
38
|
-
type SearchState = {
|
|
39
|
-
searching: boolean;
|
|
40
|
-
doneFiles: number;
|
|
41
|
-
totalFiles: number;
|
|
42
|
-
currentFile: string;
|
|
43
|
-
/** How long the last search took */
|
|
44
|
-
durationTime: number;
|
|
45
|
-
/** The oldest fetch time among the results being displayed */
|
|
46
|
-
resultsFetchTime: number;
|
|
47
|
-
rows: Record<string, unknown>[];
|
|
48
|
-
columns: string[];
|
|
49
|
-
error: string;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
39
|
/** Whether a log file could hold entries in the range - a live file (no endTime) runs to now. */
|
|
53
40
|
function fileOverlapsRange(file: LogFileInfo, startTime: number, endTime: number): boolean {
|
|
54
41
|
let fileEnd = file.endTime ?? Date.now();
|
|
@@ -56,17 +43,19 @@ function fileOverlapsRange(file: LogFileInfo, startTime: number, endTime: number
|
|
|
56
43
|
}
|
|
57
44
|
|
|
58
45
|
export class StorageLogsPage extends qreact.Component {
|
|
59
|
-
state
|
|
60
|
-
searching:
|
|
61
|
-
doneFiles:
|
|
62
|
-
totalFiles:
|
|
63
|
-
currentFile:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
46
|
+
state = t.state({
|
|
47
|
+
searching: t.boolean,
|
|
48
|
+
doneFiles: t.number,
|
|
49
|
+
totalFiles: t.number,
|
|
50
|
+
currentFile: t.string,
|
|
51
|
+
/** How long the last search took */
|
|
52
|
+
durationTime: t.number,
|
|
53
|
+
/** The oldest fetch time among the results being displayed */
|
|
54
|
+
resultsFetchTime: t.number,
|
|
55
|
+
rows: t.atomic<Record<string, unknown>[]>([]),
|
|
56
|
+
columns: t.atomic<string[]>([]),
|
|
57
|
+
error: t.string,
|
|
58
|
+
});
|
|
70
59
|
|
|
71
60
|
private getListings(): ServerLogListing[] {
|
|
72
61
|
let machineController = MachineServiceController(SocketFunction.browserNodeId());
|
|
@@ -102,7 +91,7 @@ export class StorageLogsPage extends qreact.Component {
|
|
|
102
91
|
let listings = this.getListings();
|
|
103
92
|
let files = this.getSelectedFiles(listings).map(file => ({ url: file.url, name: file.name }));
|
|
104
93
|
let query = storageLogSearchParam.value;
|
|
105
|
-
let { startTime, endTime } = getTimeRange();
|
|
94
|
+
let { startTime, endTime, searchFromStart } = getTimeRange();
|
|
106
95
|
this.state.searching = true;
|
|
107
96
|
this.state.error = "";
|
|
108
97
|
this.state.doneFiles = 0;
|
|
@@ -125,7 +114,7 @@ export class StorageLogsPage extends qreact.Component {
|
|
|
125
114
|
let columns: string[] = [...LEADING_COLUMNS];
|
|
126
115
|
for (let file of result.files) {
|
|
127
116
|
for (let entry of file.entries) {
|
|
128
|
-
let fields = entry && typeof entry === "object"
|
|
117
|
+
let fields: Record<string, unknown> = entry && typeof entry === "object" ? entry as Record<string, unknown> : { value: String(entry) };
|
|
129
118
|
// The file selection is only as granular as whole files, so the entries themselves still need the range applied
|
|
130
119
|
let time = fields.time;
|
|
131
120
|
if (typeof time === "number" && (time < startTime || time > endTime)) continue;
|
|
@@ -137,7 +126,9 @@ export class StorageLogsPage extends qreact.Component {
|
|
|
137
126
|
rows.push({ server: file.url, file: file.name, ...fields });
|
|
138
127
|
}
|
|
139
128
|
}
|
|
140
|
-
|
|
129
|
+
// Ordered the way the time range UI says: from the end (newest first) by default, oldest first when searching from the start
|
|
130
|
+
let direction = searchFromStart ? 1 : -1;
|
|
131
|
+
rows.sort((a, b) => direction * ((typeof a.time === "number" && a.time || 0) - (typeof b.time === "number" && b.time || 0)));
|
|
141
132
|
Querysub.commit(() => {
|
|
142
133
|
this.state.rows = rows;
|
|
143
134
|
this.state.columns = columns;
|
|
@@ -199,7 +190,7 @@ export class StorageLogsPage extends qreact.Component {
|
|
|
199
190
|
<InputLabelURL
|
|
200
191
|
flavor="large"
|
|
201
192
|
fillWidth
|
|
202
|
-
label="Search
|
|
193
|
+
label="Search"
|
|
203
194
|
url={storageLogSearchParam}
|
|
204
195
|
onKeyDown={e => {
|
|
205
196
|
if (e.key === "Enter") {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { formatNumber } from "socket-function/src/formatting/format";
|
|
2
2
|
import { sort } from "socket-function/src/misc";
|
|
3
3
|
import type { ArchivesConfig, RemoteConfig, HostedConfig } from "sliftutils/storage/IArchives";
|
|
4
|
-
import type { BucketWriteStats } from "sliftutils/storage/remoteStorage/
|
|
4
|
+
import type { BucketWriteStats } from "sliftutils/storage/remoteStorage/accessStats";
|
|
5
5
|
import type { BucketDiskInfo } from "sliftutils/storage/remoteStorage/bucketDisk";
|
|
6
6
|
import { parseHostedUrl } from "sliftutils/storage/remoteStorage/remoteConfig";
|
|
7
7
|
import { SourceTag, getSourceTags } from "./Tag";
|