querysub 0.312.0 → 0.313.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.
Files changed (69) hide show
  1. package/.cursorrules +1 -1
  2. package/costsBenefits.txt +4 -1
  3. package/package.json +3 -2
  4. package/spec.txt +23 -18
  5. package/src/-0-hooks/hooks.ts +1 -1
  6. package/src/-a-archives/archives.ts +16 -3
  7. package/src/-a-archives/archivesBackBlaze.ts +51 -3
  8. package/src/-a-archives/archivesLimitedCache.ts +175 -0
  9. package/src/-a-archives/archivesPrivateFileSystem.ts +299 -0
  10. package/src/-a-auth/certs.ts +58 -31
  11. package/src/-b-authorities/cdnAuthority.ts +2 -2
  12. package/src/-b-authorities/dnsAuthority.ts +3 -2
  13. package/src/-c-identity/IdentityController.ts +3 -2
  14. package/src/-d-trust/NetworkTrust2.ts +17 -19
  15. package/src/-e-certs/EdgeCertController.ts +3 -4
  16. package/src/-e-certs/certAuthority.ts +1 -2
  17. package/src/-f-node-discovery/NodeDiscovery.ts +9 -7
  18. package/src/-g-core-values/NodeCapabilities.ts +6 -1
  19. package/src/0-path-value-core/NodePathAuthorities.ts +1 -1
  20. package/src/0-path-value-core/PathValueCommitter.ts +3 -3
  21. package/src/0-path-value-core/PathValueController.ts +3 -3
  22. package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +15 -37
  23. package/src/0-path-value-core/pathValueCore.ts +4 -3
  24. package/src/3-path-functions/PathFunctionRunner.ts +2 -2
  25. package/src/4-dom/qreact.tsx +4 -3
  26. package/src/4-querysub/Querysub.ts +2 -2
  27. package/src/4-querysub/QuerysubController.ts +2 -2
  28. package/src/5-diagnostics/GenericFormat.tsx +1 -0
  29. package/src/5-diagnostics/Table.tsx +3 -0
  30. package/src/5-diagnostics/diskValueAudit.ts +2 -1
  31. package/src/5-diagnostics/nodeMetadata.ts +0 -1
  32. package/src/deployManager/components/MachineDetailPage.tsx +9 -1
  33. package/src/deployManager/components/ServiceDetailPage.tsx +10 -1
  34. package/src/diagnostics/NodeViewer.tsx +3 -4
  35. package/src/diagnostics/logs/FastArchiveAppendable.ts +748 -0
  36. package/src/diagnostics/logs/FastArchiveController.ts +524 -0
  37. package/src/diagnostics/logs/FastArchiveViewer.tsx +863 -0
  38. package/src/diagnostics/logs/LogViewer2.tsx +349 -0
  39. package/src/diagnostics/logs/TimeRangeSelector.tsx +94 -0
  40. package/src/diagnostics/logs/diskLogger.ts +135 -305
  41. package/src/diagnostics/logs/diskShimConsoleLogs.ts +6 -29
  42. package/src/diagnostics/logs/errorNotifications/ErrorNotificationController.ts +577 -0
  43. package/src/diagnostics/logs/errorNotifications/ErrorSuppressionUI.tsx +225 -0
  44. package/src/diagnostics/logs/errorNotifications/ErrorWarning.tsx +207 -0
  45. package/src/diagnostics/logs/importLogsEntry.ts +38 -0
  46. package/src/diagnostics/logs/injectFileLocationToConsole.ts +7 -17
  47. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +0 -0
  48. package/src/diagnostics/logs/lifeCycleAnalysis/spec.md +151 -0
  49. package/src/diagnostics/managementPages.tsx +7 -16
  50. package/src/diagnostics/misc-pages/ComponentSyncStats.tsx +0 -1
  51. package/src/diagnostics/periodic.ts +5 -0
  52. package/src/diagnostics/watchdog.ts +2 -2
  53. package/src/functional/SocketChannel.ts +67 -0
  54. package/src/library-components/Input.tsx +1 -1
  55. package/src/library-components/InputLabel.tsx +5 -2
  56. package/src/misc.ts +111 -0
  57. package/src/src.d.ts +34 -1
  58. package/src/user-implementation/userData.ts +4 -3
  59. package/test.ts +13 -0
  60. package/testEntry2.ts +29 -0
  61. package/src/diagnostics/errorLogs/ErrorLogController.ts +0 -535
  62. package/src/diagnostics/errorLogs/ErrorLogCore.ts +0 -274
  63. package/src/diagnostics/errorLogs/LogClassifiers.tsx +0 -308
  64. package/src/diagnostics/errorLogs/LogFilterUI.tsx +0 -84
  65. package/src/diagnostics/errorLogs/LogNotify.tsx +0 -101
  66. package/src/diagnostics/errorLogs/LogTimeSelector.tsx +0 -723
  67. package/src/diagnostics/errorLogs/LogViewer.tsx +0 -757
  68. package/src/diagnostics/errorLogs/logFiltering.tsx +0 -149
  69. package/src/diagnostics/logs/DiskLoggerPage.tsx +0 -613
@@ -1,757 +0,0 @@
1
-
2
- import preact from "preact";
3
- import { qreact } from "../../../src/4-dom/qreact";
4
- import { SocketFunction } from "socket-function/SocketFunction";
5
- import { ErrorLogControllerBase } from "../../../src/diagnostics/errorLogs/ErrorLogController";
6
- import { getSyncedController } from "../../../src/library-components/SyncedController";
7
- import { getBrowserUrlNode } from "../../../src/-f-node-discovery/NodeDiscovery";
8
- import { LogBlock, LogBlockInfo, LogClass, LogClassSummary, LogRaw, LogType, getLogClassCategorizer, isClassifierMatch, isUngroupedClass, logBlockFromBuffer, sanitizeMessageForMatch, ungroupedPrefix } from "../../../src/diagnostics/errorLogs/ErrorLogCore";
9
- import { css } from "typesafecss";
10
- import { cacheShallowConfigArgEqual } from "socket-function/src/caching";
11
- import { nextId, sort } from "socket-function/src/misc";
12
- import { URLParam } from "../../../src/library-components/URLParam";
13
- import { ATag } from "../../../src/library-components/ATag";
14
- import { Button } from "../../../src/library-components/Button";
15
- import { doAtomicWrites } from "../../../src/2-proxy/PathValueProxyWatcher";
16
- import { formatNumber } from "socket-function/src/formatting/format";
17
- import { LogTimeSelector, TimelineInfo, getLogFilterRange } from "./LogTimeSelector";
18
- import { errorColor, fatalColor, filterMachineIdsURL, filterThreadIdsURL, getLogFilterClasses, getLogFilterMachineIds, getLogFilterThreadIds, getPathArrayLax, infoColor, showErrorURL, showFatalURL, showInfoURL, showLiveLogsURL, showOldestLogsFirstURL, showWarningURL, warnColor } from "./logFiltering";
19
- import { logErrors } from "../../../src/errors";
20
- import { Querysub } from "../../../src/4-querysub/Querysub";
21
- import { InputLabel } from "../../../src/library-components/InputLabel";
22
- import { LogFilterUI } from "./LogFilterUI";
23
- import { Table } from "../../../src/5-diagnostics/Table";
24
- import { JSXFormatter, toSpaceCase } from "../../../src/5-diagnostics/GenericFormat";
25
- import { SuppressUntil, logClassIdURL } from "./LogClassifiers";
26
- import { measureFnc } from "socket-function/src/profiling/measure";
27
- import { Icon } from "../../library-components/icons";
28
- import { assertIsManagementUser, managementPageURL } from "../managementPages";
29
- import { NodeViewerController } from "../NodeViewer";
30
-
31
- export const downloadLogsOnceURL = new URLParam("downloadLogsOnce", false);
32
-
33
- export type LogsParsed = {
34
- class: LogClass;
35
- count: number;
36
- firstTime: number;
37
- lastTime: number;
38
- };
39
- export type LogExample = {
40
- message: string;
41
- time: number;
42
- threadId: string;
43
- machineId: string;
44
- class: LogClass;
45
- };
46
- export type LogProcessedData = {
47
- examples: LogExample[];
48
- parsed: LogsParsed[];
49
- timeline: TimelineInfo[];
50
- };
51
-
52
- export class LogViewer extends qreact.Component {
53
- state = {
54
- allData: null as LogProcessedData | null,
55
- ungroupedData: null as LogProcessedData | null,
56
- unsuppressedData: null as LogProcessedData | null,
57
-
58
- allMachineIds: null as Set<string> | null,
59
- allThreadIds: null as Set<string> | null,
60
- downloading: false,
61
- };
62
- blockBuffers: Buffer[] = [];
63
-
64
- componentDidMount(): void {
65
- if (downloadLogsOnceURL.value) {
66
- downloadLogsOnceURL.value = false;
67
- logErrors(this.download());
68
- }
69
- }
70
-
71
- @measureFnc
72
- async download() {
73
- Querysub.localCommit(() => {
74
- this.state.downloading = true;
75
- });
76
- try {
77
-
78
- const logController = LogViewerController.nodes[getBrowserUrlNode()];
79
-
80
- let timeFilter = getLogFilterRange();
81
-
82
- let blockInfos = await logController.scanBlocks();
83
-
84
- // Only blocks in the time range
85
- blockInfos = blockInfos.filter(info =>
86
- !(info.endTime <= timeFilter.startTime || info.startTime >= timeFilter.endTime)
87
- );
88
- // NOTE: We COULD filter blocks to only blocks which contain the types we want, but...
89
- // 1) We might have reclassified the examples
90
- // 2) Blocks are fairly small (for now), so downloading them isn't that big of a deal.
91
-
92
- let blockBuffers = await logController.readBlocks({ files: blockInfos.map(info => info.fileName) });
93
-
94
- if (showLiveLogsURL.value) {
95
- let liveBlocks = await logController.readAllLiveBlocks();
96
- blockBuffers.push(...liveBlocks);
97
- }
98
- this.blockBuffers = blockBuffers;
99
- await this.parse();
100
- } finally {
101
- Querysub.localCommit(() => {
102
- this.state.downloading = false;
103
- });
104
- }
105
- }
106
- @measureFnc
107
- async parse() {
108
- let timeFilter = getLogFilterRange();
109
-
110
- let byClass = new Map<string, LogsParsed>();
111
- function getByClass(classObj: LogClass) {
112
- let obj = byClass.get(classObj.id);
113
- if (!obj) {
114
- obj = { class: classObj, count: 0, firstTime: Number.MAX_SAFE_INTEGER, lastTime: 0, };
115
- byClass.set(classObj.id, obj);
116
- }
117
- return obj;
118
- }
119
- let examples: LogExample[] = [];
120
- let timeline: (TimelineInfo & { class: LogClass })[] = [];
121
-
122
- let byClassUnsuppressed = new Map<string, LogsParsed>();
123
- function getByClassUnsuppressed(classObj: LogClass) {
124
- let obj = byClassUnsuppressed.get(classObj.id);
125
- if (!obj) {
126
- obj = { class: classObj, count: 0, firstTime: Number.MAX_SAFE_INTEGER, lastTime: 0, };
127
- byClassUnsuppressed.set(classObj.id, obj);
128
- }
129
- return obj;
130
- }
131
- let examplesUnsuppressed: LogExample[] = [];
132
- let timelineUnsuppressed: (TimelineInfo & { class: LogClass })[] = [];
133
-
134
- const blockBuffers = this.blockBuffers;
135
- const logController = LogViewerController.nodes[getBrowserUrlNode()];
136
-
137
- let classObjs = await logController.getClasses();
138
- let classObjLookup = new Map(classObjs.map(obj => [obj.id, obj] as const));
139
-
140
- let classIds = new Set(getLogFilterClasses());
141
- let machineIds = new Set(getLogFilterMachineIds());
142
- let threadIds = new Set(getLogFilterThreadIds());
143
-
144
- let showFatal = showFatalURL.value;
145
- let showError = showErrorURL.value;
146
- let showWarning = showWarningURL.value;
147
- let showInfo = showInfoURL.value;
148
- function isClassInclude(classObj: LogClass) {
149
- if (classIds.size > 0) {
150
- if (!classIds.has(classObj.id)) {
151
- return false;
152
- }
153
- }
154
- if (!showFatal && classObj.type === "fatal") return false;
155
- if (!showError && classObj.type === "error") return false;
156
- if (!showWarning && classObj.type === "warn") return false;
157
- if (!showInfo && classObj.type === "info") return false;
158
- return true;
159
- }
160
-
161
- let allMachineIds = new Set<string>();
162
- let allThreadIds = new Set<string>();
163
-
164
- let blocks: LogBlock[] = [];
165
- for (let buffer of blockBuffers) {
166
- let block = await logBlockFromBuffer(buffer);
167
- allMachineIds.add(block.machineId);
168
- allThreadIds.add(block.threadId);
169
-
170
- if (machineIds.size > 0 && !machineIds.has(block.machineId)) {
171
- continue;
172
- }
173
- if (threadIds.size > 0 && !threadIds.has(block.threadId)) {
174
- continue;
175
- }
176
-
177
- blocks.push(block);
178
- }
179
-
180
- let classifier = getLogClassCategorizer(classObjs);
181
-
182
- for (let block of blocks) {
183
- // Only look at classes, ignore summary data (except for threadId/machineId)
184
- const threadId = block.threadId;
185
- const machineId = block.machineId;
186
- function addToClass(classSummary: LogsParsed, config: {
187
- count: number;
188
- time: number;
189
- }) {
190
- classSummary.count += config.count;
191
- classSummary.firstTime = Math.min(classSummary.firstTime, config.time);
192
- classSummary.lastTime = Math.max(classSummary.lastTime, config.time);
193
- }
194
-
195
- function addRaw(classObj: LogClass, log: LogRaw) {
196
- if (!isClassInclude(classObj)) return;
197
- let example: LogExample = {
198
- class: classObj,
199
- message: log.message,
200
- threadId,
201
- machineId,
202
- time: log.time,
203
- };
204
- let timelineObj: TimelineInfo & { class: LogClass } = {
205
- count: 1,
206
- startTime: log.time,
207
- endTime: log.time,
208
- fatalCount: classObj.type === "fatal" ? 1 : 0,
209
- errorCount: classObj.type === "error" ? 1 : 0,
210
- warnCount: classObj.type === "warn" ? 1 : 0,
211
- infoCount: classObj.type === "info" ? 1 : 0,
212
- class: classObj,
213
- };
214
- examples.push(example);
215
- timeline.push(timelineObj);
216
- addToClass(getByClass(classObj), { count: 1, time: example.time });
217
-
218
- if (example.time > classObj.suppressUntil) {
219
- examplesUnsuppressed.push(example);
220
- timelineUnsuppressed.push(timelineObj);
221
- addToClass(getByClassUnsuppressed(classObj), { count: 1, time: example.time });
222
- }
223
- }
224
- function addClassSummary(classObj: LogClass, summary: LogClassSummary) {
225
- if (!isClassInclude(classObj)) return;
226
- let timelineObj: TimelineInfo & { class: LogClass } = {
227
- count: summary.count,
228
- startTime: summary.firstTime,
229
- endTime: summary.lastTime,
230
- fatalCount: classObj.type === "fatal" ? summary.count : 0,
231
- errorCount: classObj.type === "error" ? summary.count : 0,
232
- warnCount: classObj.type === "warn" ? summary.count : 0,
233
- infoCount: classObj.type === "info" ? summary.count : 0,
234
- class: classObj,
235
- };
236
- timeline.push(timelineObj);
237
- addToClass(getByClass(classObj), { count: summary.count, time: summary.firstTime });
238
- addToClass(getByClass(classObj), { count: 0, time: summary.lastTime });
239
- // NOTE: If the user suppressed to the current moment, pending blocks will not be caught,
240
- // as their last time will keep increasing. BUT, users should suppress for at least a day,
241
- // so this shouldn't be an issue.
242
- if (summary.lastTime > classObj.suppressUntil) {
243
- timelineUnsuppressed.push(timelineObj);
244
- addToClass(getByClassUnsuppressed(classObj), { count: summary.count, time: summary.firstTime });
245
- addToClass(getByClassUnsuppressed(classObj), { count: 0, time: summary.lastTime });
246
- }
247
- }
248
-
249
- for (let classSummary of Object.values(block.classes)) {
250
- let classObj: LogClass = classObjLookup.get(classSummary.logClassId) || {
251
- id: ungroupedPrefix + classSummary.logClassType,
252
- title: "missing Class",
253
- description: "missing class",
254
- createTime: 0,
255
- type: "fatal",
256
- match: [],
257
- suppressUntil: 0
258
- };
259
- // ALWAYS reclassify examples
260
- {
261
- classSummary = { ...classSummary };
262
- for (let example of classSummary.examples) {
263
- if (!(timeFilter.startTime <= example.time && example.time < timeFilter.endTime)) {
264
- continue;
265
- }
266
- let newClass = classifier(example, classObj.type);
267
- addRaw(newClass, example);
268
- }
269
-
270
- classSummary.count -= classSummary.examples.length;
271
- if (classSummary.count <= 0) continue;
272
- }
273
- addClassSummary(classObj, classSummary);
274
- }
275
- }
276
-
277
- let classList = Array.from(byClass.values());
278
- let classListUnsuppressed = Array.from(byClassUnsuppressed.values());
279
- if (showOldestLogsFirstURL.value) {
280
- sort(classList, x => x.count);
281
- sort(examples, x => x.time);
282
- sort(timeline, x => x.startTime);
283
-
284
- sort(classListUnsuppressed, x => x.firstTime);
285
- sort(examplesUnsuppressed, x => x.time);
286
- sort(timelineUnsuppressed, x => x.startTime);
287
- } else {
288
- sort(classList, x => -x.count);
289
- sort(examples, x => -x.time);
290
- sort(timeline, x => -x.startTime);
291
-
292
- sort(classListUnsuppressed, x => -x.firstTime);
293
- sort(examplesUnsuppressed, x => -x.time);
294
- sort(timelineUnsuppressed, x => -x.startTime);
295
- }
296
-
297
-
298
- Querysub.localCommit(() => {
299
- doAtomicWrites(() => {
300
- this.state.allMachineIds = allMachineIds;
301
- this.state.allThreadIds = allThreadIds;
302
-
303
- this.state.allData = {
304
- examples,
305
- parsed: classList,
306
- timeline,
307
- };
308
- this.state.ungroupedData = {
309
- examples: examples.filter(a => isUngroupedClass(a.class)),
310
- parsed: classList.filter(a => isUngroupedClass(a.class)),
311
- timeline: timeline.filter(a => isUngroupedClass(a.class)),
312
- };
313
- this.state.unsuppressedData = {
314
- examples: examplesUnsuppressed.filter(a => !isUngroupedClass(a.class)),
315
- parsed: classListUnsuppressed.filter(a => !isUngroupedClass(a.class)),
316
- timeline: timelineUnsuppressed.filter(a => !isUngroupedClass(a.class)),
317
- };
318
- });
319
- });
320
- }
321
- render() {
322
- let blockInfos = LogViewerSynced(getBrowserUrlNode()).scanBlocks();
323
- blockInfos = filterBlocks({
324
- blockInfos,
325
- machineIdsURL: filterMachineIdsURL.value,
326
- threadIdsURL: filterThreadIdsURL.value,
327
- });
328
-
329
- const reparse = () => this.parse();
330
-
331
- function divBorder(
332
- outer: string,
333
- borderColor: string,
334
- content: preact.ComponentChild
335
- ) {
336
- let border = 10;
337
- const fullMinusBorder = `calc(100% - ${border}px)` as const;
338
- let createBorder = () => <div
339
- class={
340
- css.fillBoth
341
- .backgroundImage(`
342
- repeating-linear-gradient(
343
- -45deg,
344
- ${borderColor},
345
- ${borderColor} 12px,
346
- transparent 10px,
347
- transparent 23px
348
- )
349
- `)
350
- }
351
- />;
352
- return (
353
- <div
354
- class={css.pad2(border).relative + outer}
355
- >
356
- <div class={css.absolute.pos(0, 0).size("100%", border)}>
357
- {createBorder()}
358
- </div>
359
- <div class={css.absolute.pos(0, border).size(border, fullMinusBorder)}>
360
- {createBorder()}
361
- </div>
362
- <div class={css.absolute.pos(border, fullMinusBorder).size(fullMinusBorder, border)}>
363
- {createBorder()}
364
- </div>
365
- <div class={css.absolute.pos(fullMinusBorder, border).size(border, fullMinusBorder)}>
366
- {createBorder()}
367
- </div>
368
- <div class={css.pad2(5)}>
369
- {content}
370
- </div>
371
- </div>
372
- );
373
- }
374
-
375
- return (
376
- <div class={css.pad2(10).vbox(10).fillWidth.center}>
377
- <div class={css.fillWidth.hbox(10)}>
378
- <div class={css.marginAuto} />
379
-
380
- <h1>File Overview ({blockInfos?.length} files)</h1>
381
-
382
- <div class={css.marginAuto} />
383
- </div>
384
- <LogTimeSelector infos={blockInfos} hotkeys />
385
-
386
- <div class={css.fillWidth.hbox(10).alignItems("start")}>
387
- <Button
388
- flavor="large"
389
- class={css.fontSize(40, "important").flexShrink0}
390
- showHotkeys
391
- hotkeys={["Enter", "Space"]}
392
- onClick={async () => this.download()}
393
- >
394
- Download
395
- </Button>
396
- <LogFilterUI
397
- allMachineIds={this.state.allMachineIds}
398
- allThreadIds={this.state.allThreadIds}
399
- />
400
- </div>
401
-
402
- <style>
403
- {`
404
- .loadingSpinner {
405
- animation: spin 3s linear infinite;
406
- }
407
- @keyframes spin {
408
- 0% { transform: rotate(0deg); }
409
- 100% { transform: rotate(360deg); }
410
- }
411
- `}
412
- </style>
413
- {this.state.downloading &&
414
- <div class={css.fillWidth.center.paddingTop(200) + " loadingSpinner"}>
415
- {Icon.loadingSymbol({ size: 300 })}
416
- </div>
417
- }
418
-
419
- {!!this.state.ungroupedData?.examples.length && (
420
- divBorder(
421
- css.hsl(-5, 40, 60),
422
- "hsl(-5, 60%, 50%)",
423
- <LogSection
424
- key="ungroupedData"
425
- reparse={reparse}
426
- title="Add accurate and specific classifiers for these immediately, don't worry about fixing them yet"
427
- data={this.state.ungroupedData}
428
- showExamplesByDefault
429
- />
430
- )
431
- )}
432
- {!!this.state.unsuppressedData?.examples.length && (
433
- divBorder(
434
- css.hsl(40, 40, 60),
435
- "hsl(40, 60%, 50%)",
436
- <LogSection
437
- key="unsuppressedData"
438
- reparse={reparse}
439
- title="Create tickets for these logs, and then suppress them"
440
- addSuppressColumns
441
- data={this.state.unsuppressedData}
442
- />
443
- )
444
- )}
445
- {this.state.allData && (
446
- divBorder(
447
- //css.hsl(210, 40, 60),
448
- "",
449
- "hsl(210, 60%, 50%)",
450
- <LogSection
451
- key="allData"
452
- reparse={reparse}
453
- title="All Data"
454
- data={this.state.allData}
455
- />
456
- )
457
- )}
458
- </div>
459
- );
460
- }
461
- }
462
-
463
- const filterBlocks = cacheShallowConfigArgEqual((
464
- config: {
465
- blockInfos: LogBlockInfo[] | undefined;
466
- machineIdsURL: string;
467
- threadIdsURL: string;
468
- }
469
- ): LogBlockInfo[] => {
470
- const { blockInfos, machineIdsURL, threadIdsURL } = config;
471
- let machineIds = new Set(getPathArrayLax(machineIdsURL));
472
- let threadIds = new Set(getPathArrayLax(threadIdsURL));
473
-
474
- return blockInfos?.filter(info =>
475
- (!machineIds.size || machineIds.has(info.machineId)) &&
476
- (!threadIds.size || threadIds.has(info.threadId))
477
- ) || [];
478
- });
479
-
480
- let lastClassifierUpdated: LogClass | undefined;
481
- class LogSection extends qreact.Component<{
482
- title: string;
483
- data: LogProcessedData;
484
- showExamplesByDefault?: boolean;
485
- addSuppressColumns?: boolean;
486
- reparse: () => void;
487
- }> {
488
- state = {
489
- previewFilter: "",
490
- summaryExpanded: { value: true },
491
- examplesExpanded: { value: !!this.props.showExamplesByDefault },
492
- };
493
- render() {
494
- let { title, data } = this.props;
495
- let previewFilter = this.state.previewFilter;
496
- if (previewFilter) {
497
- data = { ...data };
498
- let tempClassifier: LogClass = {
499
- id: "",
500
- title: "",
501
- createTime: 0,
502
- description: "",
503
- match: [{ type: "includes", value: previewFilter }],
504
- suppressUntil: 0,
505
- type: "info",
506
- };
507
- data.examples = data.examples.filter(example =>
508
- isClassifierMatch(tempClassifier, example)
509
- );
510
- }
511
- let { reparse } = this.props;
512
- const createClassifier = async (type: LogType) => {
513
- let previewFilter = this.state.previewFilter;
514
- // Wait for the blur to finish before resetting previewFilter
515
- Querysub.onCommitFinished(() => {
516
- setImmediate(() => {
517
- Querysub.localCommit(() => {
518
- this.state.previewFilter = "";
519
- });
520
- });
521
- });
522
- let newClass: LogClass = {
523
- id: nextId(),
524
- title: toSpaceCase(previewFilter),
525
- createTime: Date.now(),
526
- description: previewFilter,
527
- match: [{ type: "includes", value: previewFilter }],
528
- suppressUntil: 0,
529
- type,
530
- };
531
- lastClassifierUpdated = newClass;
532
- await LogViewerController.nodes[getBrowserUrlNode()].updateClass(newClass);
533
- await reparse();
534
- };
535
- const classFormatter: JSXFormatter<LogClass> = (x) => {
536
- return (
537
- <div class={css.hbox(4).wrap}>
538
- {this.props.addSuppressColumns && (
539
- <SuppressUntil
540
- type={x.type}
541
- time={x.suppressUntil}
542
- updateClass={async newValue => {
543
- let newClass = { ...x, ...newValue };
544
- lastClassifierUpdated = newClass;
545
- await LogViewerController.nodes[getBrowserUrlNode()].updateClass(newClass);
546
- reparse();
547
- }}
548
- />
549
- )}
550
- <ATag
551
- target="_blank"
552
- class={css.button.textDecoration("none", "important").hbox(0)}
553
- values={[
554
- { param: logClassIdURL, value: x.id },
555
- { param: managementPageURL, value: "LogClassifiers" },
556
- ]}
557
- >
558
- (Edit {formatTypeColor(x.type, x.title)})
559
- </ATag>
560
- </div>
561
- );
562
- };
563
- let totalCount = data.parsed.reduce((sum, x) => sum + x.count, 0);
564
- return (
565
- <div class={css.fillWidth.vbox(10).maxHeight("80vh")}>
566
- <h1>{title}</h1>
567
- <LogTimeSelector infos={data.timeline} />
568
- <SideCollapsible
569
- expanded={this.state.summaryExpanded}
570
- collapsedTitle={`Summary (${formatNumber(data.parsed.length)})`}
571
- >
572
- <Table
573
- columns={{
574
- class: { formatter: classFormatter },
575
- count: {
576
- formatter: (x) => {
577
- return (
578
- <div class={css.relative}>
579
- <div class={
580
- css.absolute.pos(0, 0).size(`${x / totalCount * 100}%`, "100%")
581
- .hsla(0, 0, 100, 0.75)
582
- } />
583
- <span class={css.relative}>{formatNumber(x)}</span>
584
- </div>
585
- );
586
- }
587
- },
588
- firstTime: {},
589
- lastTime: {},
590
- }}
591
- rows={data.parsed}
592
- />
593
- </SideCollapsible>
594
- {lastClassifierUpdated &&
595
- <ATag
596
- // NOTE: Not only is this nice, but... without it we would need
597
- // to explicitly invalidate our cache, which is annoying to do with ATag.
598
- target="_blank"
599
- values={[
600
- { param: logClassIdURL, value: lastClassifierUpdated.id },
601
- { param: managementPageURL, value: "LogClassifiers" },
602
- ]}
603
- class={css.hbox(4).marginLeft(10).hsla(0, 0, 100, 0.5).pad2(4, 1)}
604
- >
605
- View last updated classifier: {formatTypeColor(lastClassifierUpdated.type, lastClassifierUpdated.title)}
606
- </ATag>
607
- }
608
- {/* NOTE: We show classifiers for all views, as you might want to reclassify even classified data,
609
- by using a higher priority. */}
610
- {this.state.examplesExpanded.value && <div class={css.hbox(10)}>
611
- <InputLabel
612
- label="Create Classifier"
613
- value={previewFilter}
614
- hot
615
- onChangeValue={(value) => {
616
- this.state.previewFilter = value;
617
- }}
618
- onKeyDown={async e => {
619
- if (e.key === "Enter") {
620
- await createClassifier(data.examples[0]?.class.type || "error");
621
- }
622
- }}
623
- />
624
- {previewFilter && (
625
- <div class={css.hbox(4).hsla(0, 0, 100, 0.5).pad2(4, 1)}>
626
- <span>Matches</span>
627
- <span>
628
- {formatNumber(data.examples.length)} / {formatNumber(this.props.data.examples.length)}
629
- </span>
630
- <Button class={css.hbox(6).hsl(fatalColor.h, fatalColor.s, fatalColor.l)} onClick={() => createClassifier("fatal")}>
631
- Fatal
632
- </Button>
633
- <Button class={css.hbox(6).hsl(errorColor.h, errorColor.s, errorColor.l)} onClick={() => createClassifier("error")}>
634
- Error
635
- </Button>
636
- <Button class={css.hbox(6).hsl(warnColor.h, warnColor.s, warnColor.l)} onClick={() => createClassifier("warn")}>
637
- Warning
638
- </Button>
639
- <Button class={css.hbox(6).hsl(infoColor.h, infoColor.s, infoColor.l)} onClick={() => createClassifier("info")}>
640
- Info
641
- </Button>
642
- </div>
643
- )}
644
- </div>}
645
-
646
- <SideCollapsible
647
- expanded={this.state.examplesExpanded}
648
- collapsedTitle={`Examples (${formatNumber(data.examples.length)})`}
649
- defaultCollapsed={!this.props.showExamplesByDefault}
650
- >
651
- <Table
652
- columns={{
653
- class: {
654
- formatter: classFormatter,
655
- },
656
- time: {},
657
- machineId: {},
658
- threadId: {},
659
- message: {
660
- formatter: (x) => {
661
- if (!previewFilter) return x;
662
- let matchIndex = sanitizeMessageForMatch(x).indexOf(previewFilter.toLowerCase());
663
- if (matchIndex === -1) return x;
664
- let before = x.slice(0, matchIndex);
665
- let match = x.slice(matchIndex, matchIndex + previewFilter.length);
666
- let after = x.slice(matchIndex + previewFilter.length);
667
- return (
668
- <div>
669
- {before}
670
- <span class={css.hsl(60, 75, 50)}>{match}</span>
671
- {after}
672
- </div>
673
- );
674
- },
675
- },
676
- }}
677
- rows={data.examples}
678
- />
679
- </SideCollapsible>
680
- </div>
681
- );
682
- }
683
- }
684
- class SideCollapsible extends qreact.Component<{
685
- defaultCollapsed?: boolean;
686
- collapsedTitle?: string;
687
- expanded: { value: boolean }
688
- }> {
689
- render() {
690
- let expanded = this.props.expanded.value;
691
- return (
692
- <div class={
693
- css.fillWidth.hbox(10).alignItems("start").overflowAuto
694
- //+ css.minHeight(expanded ? 100 : 50)
695
- + (expanded && css.minHeight(100).maxHeight("60%") || css.minHeight(50))
696
- }>
697
- <Button
698
- square
699
- flavor="noui"
700
- onClick={() => this.props.expanded.value = !expanded}
701
- class={
702
- (expanded && css.hsla(0, 0, 100, 0.5) || css.hsla(0, 0, 100, 0.25))
703
- + css.background(expanded ? "hsla(0, 0%, 100%, 0.25)" : "hsla(0, 0%, 100%, 0.5)", "hover")
704
- + css.button.borderWidth(0).flex
705
- + css.outline("none", "important")
706
- }
707
- >
708
- {(expanded && Icon.chevronDown || Icon.chevronRight)({
709
- size: 30,
710
- style: {
711
- position: "relative",
712
- top: expanded ? -3 : 0,
713
- left: expanded ? 0 : 4,
714
- }
715
- })}
716
- {!expanded && <div class={css.fontSize(20).pad2(8, 2)}>{this.props.collapsedTitle}</div> || ""}
717
- </Button>
718
-
719
- {expanded && this.props.children}
720
- </div>
721
- );
722
- }
723
- }
724
-
725
- export function formatTypeColor(type: LogType, contents: preact.ComponentChild) {
726
- let color = (
727
- (type === "fatal" && fatalColor) ||
728
- (type === "error" && errorColor) ||
729
- (type === "warn" && warnColor) ||
730
- (type === "info" && infoColor) ||
731
- fatalColor
732
- );
733
- return (
734
- <div class={css.pad2(4, 1).color("white").hsl(color.h, color.s, color.l)}>
735
- {contents}
736
- </div>
737
- );
738
- }
739
-
740
- export const LogViewerController = SocketFunction.register(
741
- "LogViewerController-257d81a9-a170-441e-9ff3-7d9d86b6e80a",
742
- new ErrorLogControllerBase(),
743
- () => ({
744
- scanBlocks: {},
745
- readBlocks: {},
746
- readAllLiveBlocks: {},
747
- getClasses: {},
748
- updateClass: {},
749
- onClassesUpdated: {},
750
- }),
751
- () => ({
752
- hooks: [assertIsManagementUser],
753
- }),
754
- );
755
-
756
- export const LogViewerSynced = getSyncedController(LogViewerController);
757
- const NodeViewerSynced = getSyncedController(NodeViewerController);