querysub 0.356.0 → 0.358.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 (76) hide show
  1. package/.cursorrules +9 -0
  2. package/bin/movelogs.js +4 -0
  3. package/package.json +13 -6
  4. package/scripts/postinstall.js +23 -0
  5. package/src/-a-archives/archiveCache.ts +10 -12
  6. package/src/-a-archives/archives.ts +29 -0
  7. package/src/-a-archives/archivesBackBlaze.ts +60 -12
  8. package/src/-a-archives/archivesDisk.ts +39 -13
  9. package/src/-a-archives/archivesLimitedCache.ts +21 -0
  10. package/src/-a-archives/archivesMemoryCache.ts +374 -0
  11. package/src/-a-archives/archivesPrivateFileSystem.ts +22 -0
  12. package/src/-g-core-values/NodeCapabilities.ts +3 -0
  13. package/src/0-path-value-core/auditLogs.ts +5 -1
  14. package/src/0-path-value-core/pathValueCore.ts +7 -7
  15. package/src/4-dom/qreact.tsx +1 -0
  16. package/src/4-querysub/Querysub.ts +1 -5
  17. package/src/config.ts +5 -0
  18. package/src/deployManager/components/MachineDetailPage.tsx +43 -2
  19. package/src/deployManager/components/MachinesListPage.tsx +10 -2
  20. package/src/deployManager/machineApplyMainCode.ts +3 -3
  21. package/src/deployManager/machineSchema.ts +39 -0
  22. package/src/diagnostics/MachineThreadInfo.tsx +235 -0
  23. package/src/diagnostics/NodeViewer.tsx +5 -3
  24. package/src/diagnostics/logs/FastArchiveAppendable.ts +79 -42
  25. package/src/diagnostics/logs/FastArchiveController.ts +102 -63
  26. package/src/diagnostics/logs/FastArchiveViewer.tsx +36 -8
  27. package/src/diagnostics/logs/IndexedLogs/BufferIndex.ts +462 -0
  28. package/src/diagnostics/logs/IndexedLogs/BufferIndexCPP.cpp +327 -0
  29. package/src/diagnostics/logs/IndexedLogs/BufferIndexCPP.d.ts +18 -0
  30. package/src/diagnostics/logs/IndexedLogs/BufferIndexCPP.js +1 -0
  31. package/src/diagnostics/logs/IndexedLogs/BufferIndexHelpers.ts +222 -0
  32. package/src/diagnostics/logs/IndexedLogs/BufferIndexLogsOptimizationConstants.ts +22 -0
  33. package/src/diagnostics/logs/IndexedLogs/BufferIndexWAT.wat +1145 -0
  34. package/src/diagnostics/logs/IndexedLogs/BufferIndexWAT.wat.d.ts +178 -0
  35. package/src/diagnostics/logs/IndexedLogs/BufferListStreamer.ts +208 -0
  36. package/src/diagnostics/logs/IndexedLogs/BufferUnitIndex.ts +716 -0
  37. package/src/diagnostics/logs/IndexedLogs/BufferUnitSet.ts +146 -0
  38. package/src/diagnostics/logs/IndexedLogs/FilePathSelector.tsx +569 -0
  39. package/src/diagnostics/logs/IndexedLogs/FindProgressTracker.ts +45 -0
  40. package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +685 -0
  41. package/src/diagnostics/logs/IndexedLogs/LogStreamer.ts +47 -0
  42. package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +901 -0
  43. package/src/diagnostics/logs/IndexedLogs/TimeFileTree.ts +236 -0
  44. package/src/diagnostics/logs/IndexedLogs/binding.gyp +23 -0
  45. package/src/diagnostics/logs/IndexedLogs/moveIndexLogsToPublic.ts +251 -0
  46. package/src/diagnostics/logs/IndexedLogs/moveLogsEntry.ts +10 -0
  47. package/src/diagnostics/logs/LogViewer2.tsx +120 -55
  48. package/src/diagnostics/logs/TimeRangeSelector.tsx +5 -2
  49. package/src/diagnostics/logs/diskLogger.ts +32 -48
  50. package/src/diagnostics/logs/errorNotifications/ErrorNotificationController.ts +3 -2
  51. package/src/diagnostics/logs/errorNotifications/errorDigests.tsx +1 -0
  52. package/src/diagnostics/logs/errorNotifications2/errorNotifications2.ts +0 -0
  53. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCyclePages.tsx +150 -0
  54. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +150 -15
  55. package/src/diagnostics/logs/lifeCycleAnalysis/test.ts +0 -0
  56. package/src/diagnostics/logs/lifeCycleAnalysis/test.wat +106 -0
  57. package/src/diagnostics/logs/lifeCycleAnalysis/test.wat.d.ts +2 -0
  58. package/src/diagnostics/logs/lifeCycleAnalysis/testHoist.ts +5 -0
  59. package/src/diagnostics/logs/logViewerExtractField.ts +2 -3
  60. package/src/diagnostics/managementPages.tsx +10 -0
  61. package/src/diagnostics/trackResources.ts +1 -1
  62. package/src/functional/limitProcessing.ts +39 -0
  63. package/src/misc/lz4_wasm_nodejs.d.ts +34 -0
  64. package/src/misc/lz4_wasm_nodejs.js +178 -0
  65. package/src/misc/lz4_wasm_nodejs_bg.js +94 -0
  66. package/src/misc/lz4_wasm_nodejs_bg.wasm +0 -0
  67. package/src/misc/lz4_wasm_nodejs_bg.wasm.d.ts +15 -0
  68. package/src/storage/CompressedStream.ts +13 -0
  69. package/src/storage/LZ4.ts +32 -0
  70. package/src/storage/ZSTD.ts +10 -0
  71. package/src/wat/watCompiler.ts +1716 -0
  72. package/src/wat/watGrammar.pegjs +93 -0
  73. package/src/wat/watHandler.ts +179 -0
  74. package/src/wat/watInstructions.txt +707 -0
  75. package/src/zip.ts +3 -89
  76. package/src/diagnostics/logs/lifeCycleAnalysis/spec.md +0 -125
@@ -0,0 +1,569 @@
1
+ import { formatDate, formatDateTime, formatNumber } from "socket-function/src/formatting/format";
2
+ import { css } from "typesafecss";
3
+ import { t } from "../../../2-proxy/schema2";
4
+ import { qreact } from "../../../4-dom/qreact";
5
+ import { Button } from "../../../library-components/Button";
6
+ import { TimeFilePathWithSize } from "./IndexedLogs";
7
+ import { keyBy, keyByArray } from "socket-function/src/misc";
8
+ import { MachineThreadInfo } from "../../MachineThreadInfo";
9
+
10
+ export type FilePathsByThread = {
11
+ threadId: string;
12
+ files: TimeFilePathWithSize[];
13
+ totalSize: number;
14
+ totalLogCount: number;
15
+ pendingCount: number;
16
+ errorCount: number;
17
+ };
18
+
19
+ export type FilePathsByMachine = {
20
+ machineId: string;
21
+ threads: FilePathsByThread[];
22
+ totalSize: number;
23
+ totalLogCount: number;
24
+ pendingCount: number;
25
+ errorCount: number;
26
+ };
27
+
28
+
29
+ export class FilePathSelector extends qreact.Component<{
30
+ paths: TimeFilePathWithSize[];
31
+ onChange: (paths: TimeFilePathWithSize[]) => void;
32
+ fileErrors?: { error: string; path: string; }[];
33
+ }> {
34
+ state = t.state({
35
+ showFullScreenModal: t.boolean,
36
+ });
37
+
38
+ getGroupedByMachine(): FilePathsByMachine[] {
39
+ let byMachine = keyByArray(this.props.paths, (path) => path.machineId || "unknown");
40
+ let errorsByPath = keyBy(this.props.fileErrors || [], x => x.path);
41
+
42
+ let result: FilePathsByMachine[] = [];
43
+ for (let [machineId, files] of byMachine) {
44
+ let byThread = keyByArray(files, (file) => file.threadId || "unknown");
45
+
46
+ let threads: FilePathsByThread[] = [];
47
+ let totalSize = 0;
48
+ let totalLogCount = 0;
49
+ let machinePendingCount = 0;
50
+ let machineErrorCount = 0;
51
+
52
+ for (let [threadId, threadFiles] of byThread) {
53
+ let threadSize = 0;
54
+ let threadLogCount = 0;
55
+ let threadPendingCount = 0;
56
+ let threadErrorCount = 0;
57
+ for (let file of threadFiles) {
58
+ threadSize += file.size;
59
+ threadLogCount += file.logCount || 0;
60
+ if (file.logCount === undefined) {
61
+ threadPendingCount++;
62
+ }
63
+ if (errorsByPath.get(file.fullPath)) {
64
+ threadErrorCount++;
65
+ }
66
+ }
67
+ threads.push({ threadId, files: threadFiles, totalSize: threadSize, totalLogCount: threadLogCount, pendingCount: threadPendingCount, errorCount: threadErrorCount });
68
+ totalSize += threadSize;
69
+ totalLogCount += threadLogCount;
70
+ machinePendingCount += threadPendingCount;
71
+ machineErrorCount += threadErrorCount;
72
+ }
73
+
74
+ result.push({ machineId, threads, totalSize, totalLogCount, pendingCount: machinePendingCount, errorCount: machineErrorCount });
75
+ }
76
+
77
+ return result;
78
+ }
79
+
80
+ getPendingCount(): number {
81
+ return this.props.paths.filter(p => p.logCount === undefined).length;
82
+ }
83
+
84
+ getUniqueThreadCount(): number {
85
+ let uniqueThreads = new Set<string>();
86
+ for (let path of this.props.paths) {
87
+ if (path.threadId !== undefined) {
88
+ uniqueThreads.add(path.threadId);
89
+ }
90
+ }
91
+ return uniqueThreads.size;
92
+ }
93
+
94
+ getTotals() {
95
+ let totalSize = 0;
96
+ let totalLogCount = 0;
97
+ let minTime = Infinity;
98
+ let maxTime = -Infinity;
99
+ for (let path of this.props.paths) {
100
+ totalSize += path.size;
101
+ totalLogCount += path.logCount || 0;
102
+ if (path.startTime < minTime) {
103
+ minTime = path.startTime;
104
+ }
105
+ if (path.startTime > maxTime) {
106
+ maxTime = path.startTime;
107
+ }
108
+ }
109
+ const errorCount = this.props.fileErrors?.length || 0;
110
+ return {
111
+ totalSize,
112
+ totalLogCount,
113
+ totalPending: this.getPendingCount(),
114
+ uniqueThreads: this.getUniqueThreadCount(),
115
+ minTime: minTime === Infinity ? undefined : minTime,
116
+ maxTime: maxTime === -Infinity ? undefined : maxTime,
117
+ errorCount
118
+ };
119
+ }
120
+
121
+ formatBytes(bytes: number): string {
122
+ return formatNumber(bytes) + "B";
123
+ }
124
+
125
+ render() {
126
+ let grouped = this.getGroupedByMachine();
127
+ let totals = this.getTotals();
128
+
129
+ return (
130
+ <div className={css.vbox(10)}>
131
+ <div
132
+ className={css.hbox(10).pad2(10).bord(1, { h: 0, s: 0, l: 80 }).hsl(240, 20, 95).hslhover(240, 30, 90).cursor("pointer").wrap}
133
+ onClick={() => this.state.showFullScreenModal = true}
134
+ >
135
+ <Button hue={220}>Edit</Button>
136
+ <div>|</div>
137
+ <div>Machines: {formatNumber(grouped.length)}</div>
138
+ <div>|</div>
139
+ <div>Threads: {formatNumber(totals.uniqueThreads)}</div>
140
+ <div>|</div>
141
+ <div>Files: {formatNumber(this.props.paths.length)}</div>
142
+ <div>|</div>
143
+ <div>Pending: {formatNumber(totals.totalPending)}</div>
144
+ <div>|</div>
145
+ <div>Size: {this.formatBytes(totals.totalSize)}</div>
146
+ <div>|</div>
147
+ <div>Log Count: {formatNumber(totals.totalLogCount)}</div>
148
+ {totals.errorCount > 0 && (
149
+ <>
150
+ <div>|</div>
151
+ <div className={css.colorhsl(0, 80, 40)}>Errors: {formatNumber(totals.errorCount)}</div>
152
+ </>
153
+ )}
154
+ {totals.minTime !== undefined && totals.maxTime !== undefined && (
155
+ <>
156
+ <div>|</div>
157
+ <div>{formatDateTime(totals.minTime)} - {formatDateTime(totals.maxTime)}</div>
158
+ </>
159
+ )}
160
+ </div>
161
+
162
+ {this.state.showFullScreenModal && (
163
+ <div
164
+ className={css.fixed.pos(0, 0).size("100%", "100%").hsla(0, 0, 0, 0.5).vbox(0).zIndex(1000)}
165
+ onClick={() => this.state.showFullScreenModal = false}
166
+ >
167
+ <FilePathSelectorModal
168
+ allPaths={this.props.paths}
169
+ grouped={grouped}
170
+ onSave={(selectedPaths) => {
171
+ this.props.onChange(selectedPaths);
172
+ this.state.showFullScreenModal = false;
173
+ }}
174
+ onCancel={() => this.state.showFullScreenModal = false}
175
+ formatBytes={(bytes) => this.formatBytes(bytes)}
176
+ fileErrors={this.props.fileErrors}
177
+ />
178
+ </div>
179
+ )}
180
+ </div>
181
+ );
182
+ }
183
+ }
184
+
185
+
186
+ export class FilePathSelectorModal extends qreact.Component<{
187
+ allPaths: TimeFilePathWithSize[];
188
+ grouped: FilePathsByMachine[];
189
+ onSave: (selectedPaths: TimeFilePathWithSize[]) => void;
190
+ onCancel: () => void;
191
+ formatBytes: (bytes: number) => string;
192
+ fileErrors?: { error: string; path: string; }[];
193
+ }> {
194
+ state = t.state({
195
+ expandedMachines: t.lookup({
196
+ expanded: t.boolean
197
+ }),
198
+ expandedThreads: t.lookup({
199
+ expanded: t.boolean
200
+ }),
201
+ selectedPaths: t.atomic<Set<string>>(new Set()),
202
+ });
203
+
204
+ componentDidMount(): void {
205
+ this.initializeSelectedPaths();
206
+ this.initializeExpandedMachines();
207
+ }
208
+
209
+ initializeSelectedPaths() {
210
+ let selected = new Set<string>();
211
+ for (let path of this.props.allPaths) {
212
+ selected.add(path.fullPath);
213
+ }
214
+ this.state.selectedPaths = selected;
215
+ }
216
+
217
+ initializeExpandedMachines() {
218
+ for (let machine of this.props.grouped) {
219
+ this.state.expandedMachines[machine.machineId] = { expanded: true };
220
+ }
221
+ }
222
+
223
+ toggleMachine(machineId: string) {
224
+ let machine = this.props.grouped.find(m => m.machineId === machineId);
225
+ if (!machine) return;
226
+
227
+ let allFiles = machine.threads.flatMap(t => t.files);
228
+ let allSelected = allFiles.every(f => this.state.selectedPaths.has(f.fullPath));
229
+ let newSelected = new Set(this.state.selectedPaths);
230
+
231
+ if (allSelected) {
232
+ for (let file of allFiles) {
233
+ newSelected.delete(file.fullPath);
234
+ }
235
+ } else {
236
+ for (let file of allFiles) {
237
+ newSelected.add(file.fullPath);
238
+ }
239
+ }
240
+
241
+ this.state.selectedPaths = newSelected;
242
+ }
243
+
244
+ toggleThread(machineId: string, threadId: string) {
245
+ let machine = this.props.grouped.find(m => m.machineId === machineId);
246
+ if (!machine) return;
247
+
248
+ let thread = machine.threads.find(t => t.threadId === threadId);
249
+ if (!thread) return;
250
+
251
+ let allSelected = thread.files.every(f => this.state.selectedPaths.has(f.fullPath));
252
+ let newSelected = new Set(this.state.selectedPaths);
253
+
254
+ if (allSelected) {
255
+ for (let file of thread.files) {
256
+ newSelected.delete(file.fullPath);
257
+ }
258
+ } else {
259
+ for (let file of thread.files) {
260
+ newSelected.add(file.fullPath);
261
+ }
262
+ }
263
+
264
+ this.state.selectedPaths = newSelected;
265
+ }
266
+
267
+ toggleFile(fullPath: string) {
268
+ let newSelected = new Set(this.state.selectedPaths);
269
+ if (newSelected.has(fullPath)) {
270
+ newSelected.delete(fullPath);
271
+ } else {
272
+ newSelected.add(fullPath);
273
+ }
274
+ this.state.selectedPaths = newSelected;
275
+ }
276
+
277
+ save() {
278
+ let selectedPaths = this.props.allPaths.filter(p => this.state.selectedPaths.has(p.fullPath));
279
+ this.props.onSave(selectedPaths);
280
+ }
281
+
282
+ expandAll() {
283
+ for (let machine of this.props.grouped) {
284
+ this.state.expandedMachines[machine.machineId] = { expanded: true };
285
+ for (let thread of machine.threads) {
286
+ let threadKey = `${machine.machineId}:${thread.threadId}`;
287
+ this.state.expandedThreads[threadKey] = { expanded: true };
288
+ }
289
+ }
290
+ }
291
+
292
+ collapseAll() {
293
+ for (let machineId in this.state.expandedMachines) {
294
+ delete this.state.expandedMachines[machineId];
295
+ }
296
+ for (let threadKey in this.state.expandedThreads) {
297
+ delete this.state.expandedThreads[threadKey];
298
+ }
299
+ }
300
+
301
+ getSelectedSummary() {
302
+ let selectedFiles = this.props.allPaths.filter(p => this.state.selectedPaths.has(p.fullPath));
303
+ let errorsByPath = keyBy(this.props.fileErrors || [], x => x.path);
304
+ let totalSize = 0;
305
+ let totalLogCount = 0;
306
+ let pendingCount = 0;
307
+ let errorCount = 0;
308
+ let uniqueMachines = new Set<string>();
309
+ let uniqueThreads = new Set<string>();
310
+
311
+ for (let file of selectedFiles) {
312
+ totalSize += file.size;
313
+ totalLogCount += file.logCount || 0;
314
+ if (file.logCount === undefined) {
315
+ pendingCount++;
316
+ }
317
+ if (file.machineId) {
318
+ uniqueMachines.add(file.machineId);
319
+ }
320
+ if (file.threadId) {
321
+ uniqueThreads.add(file.threadId);
322
+ }
323
+ if (errorsByPath.get(file.fullPath)) {
324
+ errorCount++;
325
+ }
326
+ }
327
+
328
+ return {
329
+ fileCount: selectedFiles.length,
330
+ machineCount: uniqueMachines.size,
331
+ threadCount: uniqueThreads.size,
332
+ totalSize,
333
+ totalLogCount,
334
+ pendingCount,
335
+ errorCount
336
+ };
337
+ }
338
+
339
+ getSelectedSummaryBySource(): Map<string, { fileCount: number; size: number; logCount: number; pendingCount: number; errorCount: number; }> {
340
+ let selectedFiles = this.props.allPaths.filter(p => this.state.selectedPaths.has(p.fullPath));
341
+ let errorsByPath = keyBy(this.props.fileErrors || [], x => x.path);
342
+ let bySource = new Map<string, { fileCount: number; size: number; logCount: number; pendingCount: number; errorCount: number; }>();
343
+ for (let file of selectedFiles) {
344
+ let sourceName = file.sourceName || "unknown";
345
+ let existing = bySource.get(sourceName);
346
+ if (!existing) {
347
+ existing = { fileCount: 0, size: 0, logCount: 0, pendingCount: 0, errorCount: 0 };
348
+ bySource.set(sourceName, existing);
349
+ }
350
+ existing.fileCount++;
351
+ existing.size += file.size;
352
+ existing.logCount += file.logCount || 0;
353
+ if (file.logCount === undefined) {
354
+ existing.pendingCount++;
355
+ }
356
+ if (errorsByPath.get(file.fullPath)) {
357
+ existing.errorCount++;
358
+ }
359
+ }
360
+ return bySource;
361
+ }
362
+
363
+ renderFile(file: TimeFilePathWithSize, machineId: string, errorsByPath: Map<string, { error: string; path: string; }>) {
364
+ let isSelected = this.state.selectedPaths.has(file.fullPath);
365
+ let isPending = file.logCount === undefined;
366
+ const errorObj = errorsByPath.get(file.fullPath);
367
+ const error = errorObj?.error;
368
+ let rowClassName = error && css.hsl(0, 30, 95) || isPending && css.hsl(40, 30, 95) || undefined;
369
+ return (
370
+ <tr key={file.fullPath} className={rowClassName}>
371
+ <td className={css.pad2(2)}>
372
+ <Button
373
+ onClick={() => this.toggleFile(file.fullPath)}
374
+ className={css.minWidth(100).pad2(5, 2)}
375
+ hue={isSelected ? 120 : undefined}
376
+ >
377
+ {isSelected ? "Selected" : "Not Selected"}
378
+ </Button>
379
+ </td>
380
+ <td className={css.pad2(2)}>{this.props.formatBytes(file.size)}</td>
381
+ <td className={css.pad2(2)}>{file.logCount !== undefined ? formatNumber(file.logCount) : <span className={css.colorhsl(40, 60, 40)}>pending</span>}</td>
382
+ <td className={css.pad2(2)}>{file.sourceName}</td>
383
+ <td className={css.pad2(2)}>{file.dedupe}</td>
384
+ <td className={css.pad2(2)}>{formatDateTime(file.startTime)}</td>
385
+ <td className={css.pad2(2)}>
386
+ {error && (
387
+ <div className={css.colorhsl(0, 80, 40).fontWeight(600)} title={error}>ERROR</div>
388
+ )}
389
+ </td>
390
+ </tr>
391
+ );
392
+ }
393
+
394
+ render() {
395
+ let summary = this.getSelectedSummary();
396
+ let bySource = this.getSelectedSummaryBySource();
397
+ let errorsByPath = keyBy(this.props.fileErrors || [], x => x.path);
398
+
399
+ return (
400
+ <div
401
+ className={css.vbox(10).pad2(10).hsl(0, 0, 100).overflowAuto.width("75vw").height("75vh").marginAuto}
402
+ onClick={(e) => e.stopPropagation()}
403
+ >
404
+ <div className={css.hbox(10).alignItems("start")}>
405
+ <Button onClick={() => this.state.selectedPaths = new Set()}>
406
+ Unselect All
407
+ </Button>
408
+ <Button onClick={() => this.state.selectedPaths = new Set(this.props.allPaths.map(p => p.fullPath))}>
409
+ Select All
410
+ </Button>
411
+ <Button onClick={() => this.expandAll()}>
412
+ Expand All
413
+ </Button>
414
+ <Button onClick={() => this.collapseAll()}>
415
+ Collapse All
416
+ </Button>
417
+
418
+ <div className={css.vbox(5)}>
419
+ <div className={css.hbox(10)}>
420
+ <div className={css.minWidth(120)}>Machines: {formatNumber(summary.machineCount)}</div>
421
+ <div className={css.minWidth(110)}>Threads: {formatNumber(summary.threadCount)}</div>
422
+ <div className={css.minWidth(140)}>Files: {formatNumber(summary.fileCount)} ({formatNumber(summary.pendingCount)} pending)</div>
423
+ <div className={css.minWidth(120)}>Size: {this.props.formatBytes(summary.totalSize)}</div>
424
+ <div className={css.minWidth(100)}>Logs: {formatNumber(summary.totalLogCount)}</div>
425
+ {summary.errorCount > 0 && (
426
+ <div className={css.minWidth(100) + css.colorhsl(0, 80, 40)}>Errors: {formatNumber(summary.errorCount)}</div>
427
+ )}
428
+ </div>
429
+
430
+ {bySource.size > 0 && (
431
+ <div className={css.vbox(5)}>
432
+ {Array.from(bySource.entries()).map(([sourceName, stats]) => (
433
+ <div key={sourceName} className={css.hbox(10)}>
434
+ <div className={css.minWidth(240)}>{sourceName}:</div>
435
+ <div className={css.minWidth(140)}>Files: {formatNumber(stats.fileCount)} ({formatNumber(stats.pendingCount)} pending)</div>
436
+ <div className={css.minWidth(120)}>Size: {this.props.formatBytes(stats.size)}</div>
437
+ <div className={css.minWidth(100)}>Logs: {formatNumber(stats.logCount)}</div>
438
+ {stats.errorCount > 0 && (
439
+ <div className={css.minWidth(100) + css.colorhsl(0, 80, 40)}>Errors: {formatNumber(stats.errorCount)}</div>
440
+ )}
441
+ </div>
442
+ ))}
443
+ </div>
444
+ )}
445
+ </div>
446
+ </div>
447
+
448
+
449
+ {this.props.grouped.map((machine) => {
450
+ let isMachineExpanded = machine.machineId in this.state.expandedMachines;
451
+ let allFiles = machine.threads.flatMap(t => t.files);
452
+ let allSelected = allFiles.every(f => this.state.selectedPaths.has(f.fullPath));
453
+ let someSelected = allFiles.some(f => this.state.selectedPaths.has(f.fullPath));
454
+ let totalFileCount = allFiles.length;
455
+
456
+ return (
457
+ <div key={machine.machineId} className={css.vbox(0).bord(1, { h: 0, s: 0, l: 80 })}>
458
+ <div
459
+ className={css.hbox(5).button}
460
+ onMouseDown={() => {
461
+ if (isMachineExpanded) {
462
+ delete this.state.expandedMachines[machine.machineId];
463
+ } else {
464
+ this.state.expandedMachines[machine.machineId] = { expanded: true };
465
+ }
466
+ }}
467
+ >
468
+ <div>
469
+ {isMachineExpanded ? "▼" : "▶"}
470
+ </div>
471
+ <Button
472
+ onClick={(e) => {
473
+ e.stopPropagation();
474
+ this.toggleMachine(machine.machineId);
475
+ }}
476
+ onMouseDown={(e) => e.stopPropagation()}
477
+ className={css.minWidth(100)}
478
+ hue={allSelected ? 120 : undefined}
479
+ >
480
+ {allSelected ? "Selected" : someSelected ? "Partial" : "Not Selected"}
481
+ </Button>
482
+ <MachineThreadInfo machineId={machine.machineId} />
483
+ <div>Threads: {formatNumber(machine.threads.length)}</div>
484
+ <div className={css.minWidth(120)}>Files: {formatNumber(totalFileCount)} ({formatNumber(machine.pendingCount)} pending)</div>
485
+ <div className={css.minWidth(100)}>Size: {this.props.formatBytes(machine.totalSize)}</div>
486
+ <div className={css.minWidth(100)}>Logs: {formatNumber(machine.totalLogCount)}</div>
487
+ {machine.errorCount > 0 && (
488
+ <div className={css.minWidth(100) + css.colorhsl(0, 80, 40)}>Errors: {formatNumber(machine.errorCount)}</div>
489
+ )}
490
+ </div>
491
+
492
+ {isMachineExpanded && machine.threads.map((thread) => {
493
+ let threadKey = `${machine.machineId}:${thread.threadId}`;
494
+ let isThreadExpanded = threadKey in this.state.expandedThreads;
495
+ let allThreadSelected = thread.files.every(f => this.state.selectedPaths.has(f.fullPath));
496
+ let someThreadSelected = thread.files.some(f => this.state.selectedPaths.has(f.fullPath));
497
+
498
+ return (
499
+ <div key={threadKey} className={css.vbox(0).bord(1, { h: 0, s: 0, l: 85 }).marginLeft(10)}>
500
+ <div
501
+ className={css.hbox(5).button}
502
+ onMouseDown={() => {
503
+ if (isThreadExpanded) {
504
+ delete this.state.expandedThreads[threadKey];
505
+ } else {
506
+ this.state.expandedThreads[threadKey] = { expanded: true };
507
+ }
508
+ }}
509
+ >
510
+ <div>
511
+ {isThreadExpanded ? "▼" : "▶"}
512
+ </div>
513
+ <Button
514
+ onClick={(e) => {
515
+ e.stopPropagation();
516
+ this.toggleThread(machine.machineId, thread.threadId);
517
+ }}
518
+ onMouseDown={(e) => e.stopPropagation()}
519
+ className={css.minWidth(100)}
520
+ hue={allThreadSelected ? 120 : undefined}
521
+ >
522
+ {allThreadSelected ? "Selected" : someThreadSelected ? "Partial" : "Not Selected"}
523
+ </Button>
524
+ <div className={css.minWidth(120)}>Files: {formatNumber(thread.files.length)} ({formatNumber(thread.pendingCount)} pending)</div>
525
+ <div className={css.minWidth(100)}>Size: {this.props.formatBytes(thread.totalSize)}</div>
526
+ <div className={css.minWidth(100)}>Logs: {formatNumber(thread.totalLogCount)}</div>
527
+ {thread.errorCount > 0 && (
528
+ <div className={css.minWidth(100) + css.colorhsl(0, 80, 40)}>Errors: {formatNumber(thread.errorCount)}</div>
529
+ )}
530
+ <MachineThreadInfo machineId={machine.machineId} threadId={thread.threadId} onlyShowThread />
531
+ </div>
532
+
533
+ {isThreadExpanded && (
534
+ <table className={css.width("100%")}>
535
+ <thead>
536
+ <tr>
537
+ <th className={css.pad2(2).textAlign("left")}>Selection</th>
538
+ <th className={css.pad2(2).textAlign("left")}>Size</th>
539
+ <th className={css.pad2(2).textAlign("left")}>Logs</th>
540
+ <th className={css.pad2(2).textAlign("left")}>Source</th>
541
+ <th className={css.pad2(2).textAlign("left")}>Dedupe</th>
542
+ <th className={css.pad2(2).textAlign("left")}>Start Time</th>
543
+ <th className={css.pad2(2).textAlign("left")}>Status</th>
544
+ </tr>
545
+ </thead>
546
+ <tbody>
547
+ {thread.files.map((file) => this.renderFile(file, machine.machineId, errorsByPath))}
548
+ </tbody>
549
+ </table>
550
+ )}
551
+ </div>
552
+ );
553
+ })}
554
+ </div>
555
+ );
556
+ })}
557
+
558
+ <div className={css.hbox(10)}>
559
+ <Button onClick={() => this.save()} hue={120}>
560
+ Save
561
+ </Button>
562
+ <Button onClick={() => this.props.onCancel()}>
563
+ Cancel
564
+ </Button>
565
+ </div>
566
+ </div>
567
+ );
568
+ }
569
+ }
@@ -0,0 +1,45 @@
1
+ import { binarySearchBasic2 } from "socket-function/src/misc";
2
+ import { SearchParams } from "./BufferIndexHelpers";
3
+
4
+ export class FindProgressTracker<T> {
5
+ public constructor(private config: {
6
+ params: SearchParams;
7
+ deserialize: (result: Buffer) => T;
8
+ getTime: (result: T) => number | undefined;
9
+ onResult: (result: T) => void;
10
+ }) { }
11
+
12
+
13
+ private results: { time: number; result: T }[] = [];
14
+
15
+ public addResult(buffer: Buffer, source: { startTime: number; endTime: number; }): boolean {
16
+ let result = this.config.deserialize(buffer);
17
+ let time = this.config.getTime(result) ?? source.endTime;
18
+
19
+ if (time < this.config.params.startTime) return false;
20
+ if (time >= this.config.params.endTime) return false;
21
+
22
+ if (this.results.length >= this.config.params.limit && time < this.results[0].time) {
23
+ return false;
24
+ }
25
+
26
+ let newObj = { time, result };
27
+ let index = binarySearchBasic2(this.results, x => x.time, newObj);
28
+ if (index < 0) index = ~index;
29
+ this.results.splice(index, 0, newObj);
30
+
31
+ if (this.results.length > this.config.params.limit) {
32
+ this.results = this.results.slice(-this.config.params.limit);
33
+ }
34
+
35
+ this.config.onResult(result);
36
+ return true;
37
+ }
38
+
39
+ public isSourceRelevant(source: { startTime: number; endTime: number; }): boolean {
40
+ if (this.results.length >= this.config.params.limit && source.endTime < this.results[0].time) {
41
+ return false;
42
+ }
43
+ return source.startTime <= this.config.params.endTime && source.endTime >= this.config.params.startTime;
44
+ }
45
+ }