querysub 0.312.0 → 0.314.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/.cursorrules +1 -1
- package/costsBenefits.txt +4 -1
- package/package.json +3 -2
- package/spec.txt +23 -18
- package/src/-0-hooks/hooks.ts +1 -1
- package/src/-a-archives/archives.ts +16 -3
- package/src/-a-archives/archivesBackBlaze.ts +51 -3
- package/src/-a-archives/archivesLimitedCache.ts +175 -0
- package/src/-a-archives/archivesPrivateFileSystem.ts +299 -0
- package/src/-a-auth/certs.ts +58 -31
- package/src/-b-authorities/cdnAuthority.ts +2 -2
- package/src/-b-authorities/dnsAuthority.ts +3 -2
- package/src/-c-identity/IdentityController.ts +3 -2
- package/src/-d-trust/NetworkTrust2.ts +17 -19
- package/src/-e-certs/EdgeCertController.ts +19 -81
- package/src/-e-certs/certAuthority.ts +7 -2
- package/src/-f-node-discovery/NodeDiscovery.ts +9 -7
- package/src/-g-core-values/NodeCapabilities.ts +6 -1
- package/src/0-path-value-core/NodePathAuthorities.ts +1 -1
- package/src/0-path-value-core/PathValueCommitter.ts +3 -3
- package/src/0-path-value-core/PathValueController.ts +3 -3
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +15 -37
- package/src/0-path-value-core/pathValueCore.ts +4 -3
- package/src/3-path-functions/PathFunctionRunner.ts +2 -2
- package/src/4-dom/qreact.tsx +4 -3
- package/src/4-querysub/Querysub.ts +2 -2
- package/src/4-querysub/QuerysubController.ts +2 -2
- package/src/5-diagnostics/GenericFormat.tsx +1 -0
- package/src/5-diagnostics/Table.tsx +3 -0
- package/src/5-diagnostics/diskValueAudit.ts +2 -1
- package/src/5-diagnostics/nodeMetadata.ts +0 -1
- package/src/deployManager/components/MachineDetailPage.tsx +9 -1
- package/src/deployManager/components/ServiceDetailPage.tsx +10 -1
- package/src/deployManager/setupMachineMain.ts +8 -1
- package/src/diagnostics/NodeViewer.tsx +5 -6
- package/src/diagnostics/logs/FastArchiveAppendable.ts +757 -0
- package/src/diagnostics/logs/FastArchiveController.ts +524 -0
- package/src/diagnostics/logs/FastArchiveViewer.tsx +863 -0
- package/src/diagnostics/logs/LogViewer2.tsx +349 -0
- package/src/diagnostics/logs/TimeRangeSelector.tsx +94 -0
- package/src/diagnostics/logs/diskLogger.ts +135 -305
- package/src/diagnostics/logs/diskShimConsoleLogs.ts +6 -29
- package/src/diagnostics/logs/errorNotifications/ErrorNotificationController.ts +577 -0
- package/src/diagnostics/logs/errorNotifications/ErrorSuppressionUI.tsx +225 -0
- package/src/diagnostics/logs/errorNotifications/ErrorWarning.tsx +207 -0
- package/src/diagnostics/logs/importLogsEntry.ts +38 -0
- package/src/diagnostics/logs/injectFileLocationToConsole.ts +7 -17
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +0 -0
- package/src/diagnostics/logs/lifeCycleAnalysis/spec.md +153 -0
- package/src/diagnostics/managementPages.tsx +7 -16
- package/src/diagnostics/misc-pages/ComponentSyncStats.tsx +0 -1
- package/src/diagnostics/periodic.ts +5 -0
- package/src/diagnostics/watchdog.ts +2 -2
- package/src/functional/SocketChannel.ts +67 -0
- package/src/library-components/Input.tsx +1 -1
- package/src/library-components/InputLabel.tsx +5 -2
- package/src/misc.ts +111 -0
- package/src/src.d.ts +34 -1
- package/src/user-implementation/userData.ts +4 -3
- package/test.ts +13 -0
- package/testEntry2.ts +29 -0
- package/src/diagnostics/errorLogs/ErrorLogController.ts +0 -535
- package/src/diagnostics/errorLogs/ErrorLogCore.ts +0 -274
- package/src/diagnostics/errorLogs/LogClassifiers.tsx +0 -308
- package/src/diagnostics/errorLogs/LogFilterUI.tsx +0 -84
- package/src/diagnostics/errorLogs/LogNotify.tsx +0 -101
- package/src/diagnostics/errorLogs/LogTimeSelector.tsx +0 -723
- package/src/diagnostics/errorLogs/LogViewer.tsx +0 -757
- package/src/diagnostics/errorLogs/logFiltering.tsx +0 -149
- package/src/diagnostics/logs/DiskLoggerPage.tsx +0 -613
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
module.hotreload = true;
|
|
2
|
+
import { qreact } from "../../4-dom/qreact";
|
|
3
|
+
import { css } from "../../4-dom/css";
|
|
4
|
+
import { URLParam } from "../../library-components/URLParam";
|
|
5
|
+
import { InputLabelURL } from "../../library-components/InputLabel";
|
|
6
|
+
import { Button } from "../../library-components/Button";
|
|
7
|
+
import { formatDateTime, formatNiceDateTime, formatNumber, formatTime } from "socket-function/src/formatting/format";
|
|
8
|
+
import { TimeRangeSelector, endTimeParam, getTimeRange, startTimeParam } from "./TimeRangeSelector";
|
|
9
|
+
import { FastArchiveAppendable } from "./FastArchiveAppendable";
|
|
10
|
+
import { t } from "../../2-proxy/schema2";
|
|
11
|
+
import { measureFnc } from "socket-function/src/profiling/measure";
|
|
12
|
+
import { logErrors } from "../../errors";
|
|
13
|
+
import { batchFunction, runInSerial } from "socket-function/src/batching";
|
|
14
|
+
import { Querysub } from "../../4-querysub/QuerysubController";
|
|
15
|
+
import { sort, timeInDay, timeInHour } from "socket-function/src/misc";
|
|
16
|
+
import { FastArchiveViewer } from "./FastArchiveViewer";
|
|
17
|
+
import { LogDatum, getLoggers, LOG_LIMIT_FLAG } from "./diskLogger";
|
|
18
|
+
import { ColumnType, Table, TableType } from "../../5-diagnostics/Table";
|
|
19
|
+
import { formatDateJSX } from "../../misc/formatJSX";
|
|
20
|
+
import { InputPicker } from "../../library-components/InputPicker";
|
|
21
|
+
import { JSXFormatter } from "../../5-diagnostics/GenericFormat";
|
|
22
|
+
import { atomic } from "../../2-proxy/PathValueProxyWatcher";
|
|
23
|
+
import { ObjectDisplay } from "./ObjectDisplay";
|
|
24
|
+
import { endTime } from "../misc-pages/archiveViewerShared";
|
|
25
|
+
import { ErrorSuppressionUI } from "./errorNotifications/ErrorSuppressionUI";
|
|
26
|
+
import { FileMetadata } from "./FastArchiveController";
|
|
27
|
+
import { SuppressionListController, getSuppressEntryChecker } from "./errorNotifications/ErrorNotificationController";
|
|
28
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
29
|
+
|
|
30
|
+
const RENDER_INTERVAL = 1000;
|
|
31
|
+
|
|
32
|
+
export const testLogs = new FastArchiveAppendable("test/");
|
|
33
|
+
|
|
34
|
+
const enableLogsURL = new URLParam("enableLogs", false);
|
|
35
|
+
const enableInfosURL = new URLParam("enableInfos", true);
|
|
36
|
+
const enableWarningsURL = new URLParam("enableWarnings", true);
|
|
37
|
+
const enableErrorsURL = new URLParam("enableErrors", true);
|
|
38
|
+
const selectedFieldsURL = new URLParam("selectedFields", {} as Record<string, boolean>);
|
|
39
|
+
|
|
40
|
+
export const errorNotifyToggleURL = new URLParam("errorNotifyToggle", false);
|
|
41
|
+
|
|
42
|
+
const defaultSelectedFields = {
|
|
43
|
+
param0: true,
|
|
44
|
+
time: true,
|
|
45
|
+
__NAME__: true,
|
|
46
|
+
|
|
47
|
+
//__machineId: true,
|
|
48
|
+
__threadId: true,
|
|
49
|
+
__entry: true,
|
|
50
|
+
//__DIR__: true,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export class LogViewer2 extends qreact.Component {
|
|
54
|
+
state = t.state({
|
|
55
|
+
datumsSeqNum: t.atomic<number>(0),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
private example: string | undefined = undefined;
|
|
59
|
+
private datumCount = 0;
|
|
60
|
+
private matchedSize = 0;
|
|
61
|
+
private notMatchedSize = 0;
|
|
62
|
+
private errors = 0;
|
|
63
|
+
private notMatchedCount = 0;
|
|
64
|
+
private datums: LogDatum[] = [];
|
|
65
|
+
|
|
66
|
+
private lastRenderTime = 0;
|
|
67
|
+
private suppressionCounts = new Map<string, number>();
|
|
68
|
+
private expiredSuppressionCounts = new Map<string, number>();
|
|
69
|
+
private fastArchiveViewer: FastArchiveViewer<LogDatum> | undefined = undefined;
|
|
70
|
+
|
|
71
|
+
rerun() {
|
|
72
|
+
void this.fastArchiveViewer?.handleDownload();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
render() {
|
|
76
|
+
|
|
77
|
+
this.state.datumsSeqNum;
|
|
78
|
+
|
|
79
|
+
let logs: FastArchiveAppendable<LogDatum>[] = [];
|
|
80
|
+
let loggers = getLoggers();
|
|
81
|
+
if (!loggers) {
|
|
82
|
+
return `Loggers not available?`;
|
|
83
|
+
}
|
|
84
|
+
let { logLogs, warnLogs, infoLogs, errorLogs } = loggers;
|
|
85
|
+
if (errorNotifyToggleURL.value) {
|
|
86
|
+
logs.push(errorLogs);
|
|
87
|
+
logs.push(warnLogs);
|
|
88
|
+
} else {
|
|
89
|
+
if (enableLogsURL.value) {
|
|
90
|
+
logs.push(logLogs);
|
|
91
|
+
}
|
|
92
|
+
if (enableInfosURL.value) {
|
|
93
|
+
logs.push(infoLogs);
|
|
94
|
+
}
|
|
95
|
+
if (enableWarningsURL.value) {
|
|
96
|
+
logs.push(warnLogs);
|
|
97
|
+
}
|
|
98
|
+
if (enableErrorsURL.value) {
|
|
99
|
+
logs.push(errorLogs);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const timeRange = getTimeRange();
|
|
104
|
+
return (
|
|
105
|
+
<div className={css.vbox(20).pad2(20).fillBoth}>
|
|
106
|
+
<div className={css.hbox(10)}>
|
|
107
|
+
<div className={css.fontSize(14)}>
|
|
108
|
+
Log Viewer
|
|
109
|
+
</div>
|
|
110
|
+
<InputLabelURL
|
|
111
|
+
label="Error Notification Mode" checkbox url={errorNotifyToggleURL}
|
|
112
|
+
onChangeValue={(newValue) => {
|
|
113
|
+
if (newValue) {
|
|
114
|
+
let now = Date.now();
|
|
115
|
+
startTimeParam.value = now - timeInDay * 7;
|
|
116
|
+
endTimeParam.value = now + timeInHour * 2;
|
|
117
|
+
}
|
|
118
|
+
this.rerun();
|
|
119
|
+
}}
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{!errorNotifyToggleURL.value && <div className={css.hbox(10)}>
|
|
124
|
+
<Button hue={enableLogsURL.value ? 120 : undefined} onClick={() => enableLogsURL.value = !enableLogsURL.value}>
|
|
125
|
+
Logs
|
|
126
|
+
</Button>
|
|
127
|
+
<Button hue={enableInfosURL.value ? 120 : undefined} onClick={() => enableInfosURL.value = !enableInfosURL.value}>
|
|
128
|
+
Infos
|
|
129
|
+
</Button>
|
|
130
|
+
<Button hue={enableWarningsURL.value ? 120 : undefined} onClick={() => enableWarningsURL.value = !enableWarningsURL.value}>
|
|
131
|
+
Warnings
|
|
132
|
+
</Button>
|
|
133
|
+
<Button hue={enableErrorsURL.value ? 120 : undefined} onClick={() => enableErrorsURL.value = !enableErrorsURL.value}>
|
|
134
|
+
Errors
|
|
135
|
+
</Button>
|
|
136
|
+
</div>}
|
|
137
|
+
|
|
138
|
+
<FastArchiveViewer
|
|
139
|
+
ref2={x => this.fastArchiveViewer = x}
|
|
140
|
+
fastArchives={logs}
|
|
141
|
+
onStart={() => {
|
|
142
|
+
this.datumCount = 0;
|
|
143
|
+
this.notMatchedCount = 0;
|
|
144
|
+
this.errors = 0;
|
|
145
|
+
this.notMatchedSize = 0;
|
|
146
|
+
this.matchedSize = 0;
|
|
147
|
+
this.example = undefined;
|
|
148
|
+
this.datums = [];
|
|
149
|
+
this.suppressionCounts = new Map();
|
|
150
|
+
this.expiredSuppressionCounts = new Map();
|
|
151
|
+
}}
|
|
152
|
+
getWantData={async () => {
|
|
153
|
+
if (!Querysub.fastRead(() => errorNotifyToggleURL.value)) return undefined;
|
|
154
|
+
let suppressionList = await SuppressionListController(SocketFunction.browserNodeId()).getSuppressionList.promise();
|
|
155
|
+
let now = Date.now();
|
|
156
|
+
// Newest first, so when we add a new entry we can see what it matches, even if it takes it away from older entries
|
|
157
|
+
sort(suppressionList, x => -x.lastUpdateTime);
|
|
158
|
+
let checkers = suppressionList.map(x => getSuppressEntryChecker(x));
|
|
159
|
+
this.suppressionCounts = new Map(suppressionList.map(x => [x.key, 0]));
|
|
160
|
+
this.expiredSuppressionCounts = new Map(suppressionList.map(x => [x.key, 0]));
|
|
161
|
+
let updateCounts = batchFunction({ delay: 1000 }, () => {
|
|
162
|
+
Querysub.commit(() => {
|
|
163
|
+
this.state.datumsSeqNum++;
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
return (posStart, posEnd, data, file) => {
|
|
167
|
+
for (let checker of checkers) {
|
|
168
|
+
if (checker.fnc(data, posStart, posEnd)) {
|
|
169
|
+
if (checker.entry.expiresAt < now) {
|
|
170
|
+
let count = this.expiredSuppressionCounts.get(checker.entry.key) || 0;
|
|
171
|
+
count++;
|
|
172
|
+
this.expiredSuppressionCounts.set(checker.entry.key, count);
|
|
173
|
+
void updateCounts(undefined);
|
|
174
|
+
} else {
|
|
175
|
+
let count = this.suppressionCounts.get(checker.entry.key) || 0;
|
|
176
|
+
count++;
|
|
177
|
+
this.suppressionCounts.set(checker.entry.key, count);
|
|
178
|
+
void updateCounts(undefined);
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return true;
|
|
184
|
+
};
|
|
185
|
+
}}
|
|
186
|
+
onDatums={(source, datums, file) => {
|
|
187
|
+
this.datumCount += datums.length;
|
|
188
|
+
if (!this.example && datums.length > 0) {
|
|
189
|
+
this.example = JSON.stringify(datums[0]);
|
|
190
|
+
}
|
|
191
|
+
for (let datum of datums) {
|
|
192
|
+
let time = datum.time;
|
|
193
|
+
if (time && (time < timeRange.startTime || time > timeRange.endTime)) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
datum.__metadata = file;
|
|
197
|
+
this.datums.push(datum);
|
|
198
|
+
}
|
|
199
|
+
}}
|
|
200
|
+
onStats={(source, stats, file) => {
|
|
201
|
+
this.matchedSize += stats.matchedSize;
|
|
202
|
+
this.notMatchedSize += stats.notMatchedSize;
|
|
203
|
+
this.errors += stats.errors;
|
|
204
|
+
this.notMatchedCount += stats.notMatchedCount;
|
|
205
|
+
|
|
206
|
+
let now = Date.now();
|
|
207
|
+
if (now - this.lastRenderTime > RENDER_INTERVAL) {
|
|
208
|
+
this.lastRenderTime = now;
|
|
209
|
+
Querysub.commit(() => {
|
|
210
|
+
this.state.datumsSeqNum++;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}}
|
|
214
|
+
onFinish={() => {
|
|
215
|
+
Querysub.commit(() => {
|
|
216
|
+
sort(this.datums, x => -(x.time || 0));
|
|
217
|
+
this.state.datumsSeqNum++;
|
|
218
|
+
});
|
|
219
|
+
}}
|
|
220
|
+
>
|
|
221
|
+
{errorNotifyToggleURL.value && <ErrorSuppressionUI
|
|
222
|
+
dataSeqNum={this.state.datumsSeqNum}
|
|
223
|
+
suppressionCounts={this.suppressionCounts}
|
|
224
|
+
expiredSuppressionCounts={this.expiredSuppressionCounts}
|
|
225
|
+
datums={this.datums}
|
|
226
|
+
rerunFilters={() => this.rerun()}
|
|
227
|
+
/>}
|
|
228
|
+
{(() => {
|
|
229
|
+
let fieldNames = new Set<string>();
|
|
230
|
+
let anyLimited = false;
|
|
231
|
+
for (let i = 0; i < Math.min(1000, this.datums.length); i++) {
|
|
232
|
+
let datum = this.datums[i];
|
|
233
|
+
for (let key of Object.keys(datum)) {
|
|
234
|
+
fieldNames.add(key);
|
|
235
|
+
}
|
|
236
|
+
if (datum[LOG_LIMIT_FLAG]) {
|
|
237
|
+
anyLimited = true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
let selectedFields: string[] = [];
|
|
241
|
+
for (let field of Object.keys(defaultSelectedFields)) {
|
|
242
|
+
if (atomic(selectedFieldsURL.value[field]) === undefined) {
|
|
243
|
+
selectedFields.push(field);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
for (let [key, value] of Object.entries(selectedFieldsURL.value)) {
|
|
247
|
+
if (value && !selectedFields.includes(key)) {
|
|
248
|
+
selectedFields.splice(1, 0, key);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
let columns: TableType<LogDatum>["columns"] = {};
|
|
252
|
+
columns["log"] = {
|
|
253
|
+
title: "Log",
|
|
254
|
+
formatter: (x, context) => {
|
|
255
|
+
return <Button onClick={() => console.log(context?.row)}>
|
|
256
|
+
Log
|
|
257
|
+
</Button>;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
if (anyLimited) {
|
|
261
|
+
columns[LOG_LIMIT_FLAG] = {
|
|
262
|
+
title: "Limited",
|
|
263
|
+
formatter: x => x && <div className={css.hsl(0, 50, 50).colorhsl(0, 50, 95).boldStyle.pad2(10).ellipsis}>
|
|
264
|
+
Log Line Throttled
|
|
265
|
+
</div> || undefined
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
for (let field of selectedFields) {
|
|
269
|
+
let column: ColumnType<unknown, LogDatum> = {};
|
|
270
|
+
if (field === "time") {
|
|
271
|
+
column.formatter = (x: unknown) => formatDateTime(Number(x));
|
|
272
|
+
}
|
|
273
|
+
if (!column.formatter) {
|
|
274
|
+
column.formatter = (x: unknown) => <ObjectDisplay value={x} />;
|
|
275
|
+
}
|
|
276
|
+
columns[field] = column;
|
|
277
|
+
}
|
|
278
|
+
columns["fields"] = {
|
|
279
|
+
title: "Fields",
|
|
280
|
+
formatter: (x, context) => {
|
|
281
|
+
if (!context?.row) return undefined;
|
|
282
|
+
let datum = context.row;
|
|
283
|
+
let fields = Object.keys(datum);
|
|
284
|
+
fields = fields.filter(x => !(x in columns) && !x.startsWith("_"));
|
|
285
|
+
return <div className={css.hbox(4, 4).wrap}>
|
|
286
|
+
{fields.map(x => <div
|
|
287
|
+
className={css.pad2(4, 0).hsl(0, 0, 80).button}
|
|
288
|
+
onClick={() => {
|
|
289
|
+
let newValues = { ...selectedFieldsURL.value };
|
|
290
|
+
newValues[x] = true;
|
|
291
|
+
selectedFieldsURL.value = newValues;
|
|
292
|
+
}}
|
|
293
|
+
>
|
|
294
|
+
{x}
|
|
295
|
+
</div>)}
|
|
296
|
+
</div>;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
return <>
|
|
300
|
+
<InputPicker
|
|
301
|
+
label={<div className={css.hbox(10)}>
|
|
302
|
+
<div className={css.flexShrink0}>
|
|
303
|
+
Selected Fields
|
|
304
|
+
</div>
|
|
305
|
+
<Button onClick={() => {
|
|
306
|
+
let newValues = { ...selectedFieldsURL.value };
|
|
307
|
+
for (let key of Object.keys(newValues)) {
|
|
308
|
+
newValues[key] = key in defaultSelectedFields;
|
|
309
|
+
}
|
|
310
|
+
selectedFieldsURL.value = newValues;
|
|
311
|
+
}}>
|
|
312
|
+
Reset
|
|
313
|
+
</Button>
|
|
314
|
+
</div>}
|
|
315
|
+
picked={selectedFields}
|
|
316
|
+
options={Array.from(fieldNames).map(x => ({ value: x, label: x }))}
|
|
317
|
+
addPicked={x => {
|
|
318
|
+
let newValues = { ...selectedFieldsURL.value };
|
|
319
|
+
newValues[x] = true;
|
|
320
|
+
selectedFieldsURL.value = newValues;
|
|
321
|
+
}}
|
|
322
|
+
removePicked={x => {
|
|
323
|
+
let newValues = { ...selectedFieldsURL.value };
|
|
324
|
+
newValues[x] = false;
|
|
325
|
+
selectedFieldsURL.value = newValues;
|
|
326
|
+
}}
|
|
327
|
+
/>
|
|
328
|
+
<Table
|
|
329
|
+
rows={this.datums}
|
|
330
|
+
columns={columns}
|
|
331
|
+
lineLimit={4}
|
|
332
|
+
characterLimit={400}
|
|
333
|
+
getRowAttributes={row => {
|
|
334
|
+
let hue = -1;
|
|
335
|
+
if (row.__LOG_TYPE === "warn") hue = 40;
|
|
336
|
+
if (row.__LOG_TYPE === "error") hue = 0;
|
|
337
|
+
if (row.__LOG_TYPE === "info") hue = 200;
|
|
338
|
+
return {
|
|
339
|
+
className: hue !== -1 && css.hsl(hue, 40, 50).colorhsl(hue, 0, 100) || undefined,
|
|
340
|
+
};
|
|
341
|
+
}}
|
|
342
|
+
/>
|
|
343
|
+
</>;
|
|
344
|
+
})()}
|
|
345
|
+
</FastArchiveViewer>
|
|
346
|
+
</div>
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
module.hotreload = true;
|
|
2
|
+
|
|
3
|
+
import { qreact } from "../../4-dom/qreact";
|
|
4
|
+
import { URLParam } from "../../library-components/URLParam";
|
|
5
|
+
import { css } from "../../4-dom/css";
|
|
6
|
+
import { formatTime } from "socket-function/src/formatting/format";
|
|
7
|
+
import { InputLabel } from "../../library-components/InputLabel";
|
|
8
|
+
import { Button } from "../../library-components/Button";
|
|
9
|
+
import { Querysub } from "../../4-querysub/QuerysubController";
|
|
10
|
+
import { timeInHour, timeInMinute } from "socket-function/src/misc";
|
|
11
|
+
|
|
12
|
+
// URL parameters for time range
|
|
13
|
+
export const startTimeParam = new URLParam("startTime", undefined as number | undefined);
|
|
14
|
+
export const endTimeParam = new URLParam("endTime", undefined as number | undefined);
|
|
15
|
+
|
|
16
|
+
let now = Date.now();
|
|
17
|
+
/** Get the time range with default values if not set */
|
|
18
|
+
export function getTimeRange(): { startTime: number; endTime: number } {
|
|
19
|
+
const defaultStart = now - timeInHour * 24;
|
|
20
|
+
const defaultEnd = now + timeInHour;
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
startTime: startTimeParam.value ?? defaultStart,
|
|
24
|
+
endTime: endTimeParam.value ?? defaultEnd,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class TimeRangeSelector extends qreact.Component {
|
|
29
|
+
render() {
|
|
30
|
+
const timeRange = getTimeRange();
|
|
31
|
+
|
|
32
|
+
const resetToLastDay = () => {
|
|
33
|
+
now = Date.now();
|
|
34
|
+
startTimeParam.reset();
|
|
35
|
+
endTimeParam.reset();
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const formatDateTimeLocal = (timestamp: number) => {
|
|
39
|
+
// Convert to local datetime-local format: YYYY-MM-DDTHH:MM
|
|
40
|
+
return new Date(timestamp - new Date().getTimezoneOffset() * 60000)
|
|
41
|
+
.toISOString()
|
|
42
|
+
.slice(0, 16);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const parseDateTimeLocal = (dateTimeString: string) => {
|
|
46
|
+
// Parse datetime-local format and convert to timestamp
|
|
47
|
+
return new Date(dateTimeString).getTime();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
let now = Querysub.nowDelayed(timeInMinute);
|
|
52
|
+
return (
|
|
53
|
+
<div className={css.vbox(12).pad2(16).bord2(200, 20, 80).hsl(200, 10, 98)}>
|
|
54
|
+
<div className={css.hbox(12).wrap}>
|
|
55
|
+
<div className={css.hbox(8).wrap.opacity(0.8).width(200)}>
|
|
56
|
+
From {formatTime(now - timeRange.startTime)} AGO to {timeRange.endTime > now ? "now" : `${formatTime(now - timeRange.endTime)} AGO`}
|
|
57
|
+
</div>
|
|
58
|
+
<InputLabel
|
|
59
|
+
label="Start Time"
|
|
60
|
+
type="datetime-local"
|
|
61
|
+
value={formatDateTimeLocal(timeRange.startTime)}
|
|
62
|
+
onChange={e => {
|
|
63
|
+
startTimeParam.value = parseDateTimeLocal(e.currentTarget.value);
|
|
64
|
+
}}
|
|
65
|
+
outerClass={!startTimeParam.value && css.opacity(0.5) || ""}
|
|
66
|
+
/>
|
|
67
|
+
<InputLabel
|
|
68
|
+
label="End Time"
|
|
69
|
+
type="datetime-local"
|
|
70
|
+
value={formatDateTimeLocal(timeRange.endTime)}
|
|
71
|
+
onChange={e => {
|
|
72
|
+
endTimeParam.value = parseDateTimeLocal(e.currentTarget.value);
|
|
73
|
+
}}
|
|
74
|
+
outerClass={!endTimeParam.value && css.opacity(0.5) || ""}
|
|
75
|
+
/>
|
|
76
|
+
{(endTimeParam.value || startTimeParam.value) && <Button
|
|
77
|
+
hue={110} onClick={resetToLastDay}
|
|
78
|
+
>
|
|
79
|
+
Reset to Last Day
|
|
80
|
+
</Button>}
|
|
81
|
+
{(!startTimeParam.value || !endTimeParam.value) && <Button
|
|
82
|
+
hue={110}
|
|
83
|
+
onClick={() => {
|
|
84
|
+
startTimeParam.value = timeRange.startTime;
|
|
85
|
+
endTimeParam.value = timeRange.endTime;
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
Save Time Range
|
|
89
|
+
</Button>}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|