querysub 0.394.0 → 0.395.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 (30) hide show
  1. package/.cursorrules +8 -0
  2. package/package.json +1 -1
  3. package/src/-a-archives/archivesJSONT.ts +71 -8
  4. package/src/0-path-value-core/pathValueCore.ts +20 -1
  5. package/src/5-diagnostics/GenericFormat.tsx +1 -1
  6. package/src/deployManager/components/MachinesListPage.tsx +1 -2
  7. package/src/diagnostics/logs/IndexedLogs/BufferIndex.ts +12 -3
  8. package/src/diagnostics/logs/IndexedLogs/BufferIndexHelpers.ts +8 -3
  9. package/src/diagnostics/logs/IndexedLogs/BufferUnitIndex.ts +24 -9
  10. package/src/diagnostics/logs/IndexedLogs/BufferUnitSet.ts +0 -1
  11. package/src/diagnostics/logs/IndexedLogs/FindProgressTracker.ts +21 -5
  12. package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +10 -4
  13. package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +95 -124
  14. package/src/diagnostics/logs/IndexedLogs/RenderSearchStats.tsx +127 -0
  15. package/src/diagnostics/logs/IndexedLogs/bufferSearchFindMatcher.ts +3 -0
  16. package/src/diagnostics/logs/IndexedLogs/moveIndexLogsToPublic.ts +1 -1
  17. package/src/diagnostics/logs/TimeRangeSelector.tsx +11 -2
  18. package/src/diagnostics/logs/errorNotifications2/ErrorNotificationPage.tsx +1 -4
  19. package/src/diagnostics/logs/errorNotifications2/errorNotifications.ts +1 -1
  20. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCyclePage.tsx +946 -0
  21. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleMatching.ts +49 -0
  22. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleSearch.tsx +553 -0
  23. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +125 -90
  24. package/src/diagnostics/managementPages.tsx +17 -1
  25. package/src/functional/{limitProcessing.ts → throttleProcessing.ts} +1 -1
  26. package/src/library-components/StartEllipsis.tsx +13 -0
  27. package/src/misc.ts +4 -0
  28. package/src/diagnostics/logs/lifeCycleAnalysis/test.wat +0 -106
  29. package/src/diagnostics/logs/lifeCycleAnalysis/test.wat.d.ts +0 -2
  30. package/src/diagnostics/logs/lifeCycleAnalysis/testHoist.ts +0 -5
@@ -0,0 +1,946 @@
1
+ import { qreact } from "../../../4-dom/qreact";
2
+ import { t } from "../../../2-proxy/schema2";
3
+ import { css } from "typesafecss";
4
+ import { deepCloneJSON, nextId } from "socket-function/src/misc";
5
+ import { Querysub } from "../../../4-querysub/QuerysubController";
6
+ import { InputLabel } from "../../../library-components/InputLabel";
7
+ import { Button } from "../../../library-components/Button";
8
+ import { LifeCycle, LifeCycleEntry, LifeCyclesController, getVariables } from "./lifeCycles";
9
+ import { SocketFunction } from "socket-function/SocketFunction";
10
+ import { LogDatum } from "../diskLogger";
11
+ import { TimeRangeSelector } from "../TimeRangeSelector";
12
+ import { URLParam } from "../../../library-components/URLParam";
13
+ import { InputLabelURL } from "../../../library-components/InputLabel";
14
+ import { RenderSearchStats } from "../IndexedLogs/RenderSearchStats";
15
+ import { IndexedLogResults } from "../IndexedLogs/BufferIndexHelpers";
16
+ import { ellipsisMiddle, ellipsisStart, matchFilter } from "../../../misc";
17
+ import { MachineThreadInfo } from "../../MachineThreadInfo";
18
+ import { formatDateTime, formatTime } from "socket-function/src/formatting/format";
19
+ import { getPathStr } from "../../../path";
20
+ import { niceStringify } from "../../../niceStringify";
21
+ import { createLifeCycleSearch, LifecycleInstance, limitURL, additionalSearchURL } from "./lifeCycleSearch";
22
+ import { getLifecycleMatchesForDatum } from "./lifeCycleMatching";
23
+ import { managementPageURL } from "../../managementPages";
24
+ import { startTimeParam, endTimeParam } from "../TimeRangeSelector";
25
+ import { formatValue } from "../../../5-diagnostics/GenericFormat";
26
+ import { StartEllipsis } from "../../../library-components/StartEllipsis";
27
+ export let lifecycleIdURL = new URLParam("lifecycleid", "");
28
+
29
+ export class LifeCyclePage extends qreact.Component {
30
+ controller = LifeCyclesController(SocketFunction.browserNodeId());
31
+
32
+ state = t.state({
33
+ filterText: t.string(""),
34
+ searchingLifeCycleId: t.atomic<string | undefined>(undefined),
35
+ phase1Results: t.atomic<LogDatum[]>([]),
36
+ phase1InvalidResults: t.atomic<Array<{ datum: LogDatum; missingKeys: string[] }>>([]),
37
+ phase1Stats: t.atomic<IndexedLogResults | undefined>(undefined),
38
+ phase1Searching: t.boolean(false),
39
+ phase2Results: t.atomic<LogDatum[]>([]),
40
+ phase2Stats: t.atomic<IndexedLogResults | undefined>(undefined),
41
+ phase2Searching: t.boolean(false),
42
+ lifecycleInstances: t.atomic<LifecycleInstance[]>([]),
43
+ });
44
+
45
+ search = createLifeCycleSearch(this);
46
+
47
+ searchLifeCycle = (lifeCycleId: string) => {
48
+ return this.search.searchLifeCycle(lifeCycleId);
49
+ };
50
+
51
+ componentDidMount() {
52
+ if (lifecycleIdURL.value) {
53
+ void this.searchLifeCycle(lifecycleIdURL.value);
54
+ }
55
+ }
56
+
57
+ render() {
58
+ let lifeCycles = this.controller.getLifeCycles();
59
+ if (!lifeCycles) {
60
+ return <div className={css.vbox(16).pad2(16)}>
61
+ <div>Loading...</div>
62
+ </div>;
63
+ }
64
+ let sortedLifeCycles = deepCloneJSON(lifeCycles);
65
+
66
+ let filteredLifeCycles = sortedLifeCycles.filter(x =>
67
+ matchFilter({ value: this.state.filterText }, JSON.stringify(x))
68
+ );
69
+
70
+ if (lifecycleIdURL.value) {
71
+ filteredLifeCycles = filteredLifeCycles.filter(lc => lc.id === lifecycleIdURL.value);
72
+ }
73
+
74
+ if (this.state.searchingLifeCycleId) {
75
+ filteredLifeCycles = filteredLifeCycles.filter(lc => lc.id === this.state.searchingLifeCycleId);
76
+ }
77
+
78
+ return <div className={css.vbox(16).pad2(16).fillWidth}>
79
+ <style>{`
80
+ .LifeCycleInstanceRenderer:has(.LifeCycleEntryEditor:hover) {
81
+ filter: brightness(1.0) !important;
82
+ }
83
+ `}</style>
84
+ <div className={css.vbox(8).fillWidth}>
85
+ <div className={css.hbox(12).wrap}>
86
+ <InputLabelURL
87
+ label="Limit"
88
+ number
89
+ url={limitURL}
90
+ />
91
+ <TimeRangeSelector />
92
+ </div>
93
+
94
+ <InputLabelURL
95
+ label="Additional Search"
96
+ placeholder="Additional search filter..."
97
+ url={additionalSearchURL}
98
+ fillWidth
99
+ />
100
+
101
+ <div className={css.hbox(12).wrap}>
102
+ <InputLabel
103
+ placeholder="Add new life cycle"
104
+ onChangeValue={(value) => {
105
+ let title = value.trim();
106
+ if (title) {
107
+ let newLifeCycle: LifeCycle = {
108
+ id: nextId(),
109
+ title,
110
+ entries: [],
111
+ };
112
+ Querysub.onCommitFinished(async () => {
113
+ await this.controller.setLifeCycle.promise(newLifeCycle);
114
+ });
115
+ }
116
+ }}
117
+ className={css.width(500)}
118
+ />
119
+ <InputLabel
120
+ placeholder="Filter life cycles..."
121
+ value={this.state.filterText}
122
+ onChangeValue={(value) => {
123
+ this.state.filterText = value;
124
+ }}
125
+ className={css.width(500)}
126
+ />
127
+ <span>({filteredLifeCycles.length} / {sortedLifeCycles.length})</span>
128
+ {this.state.searchingLifeCycleId && (
129
+ <Button
130
+ hue={120}
131
+ onClick={() => {
132
+ this.state.searchingLifeCycleId = undefined;
133
+ this.state.phase1Results = [];
134
+ this.state.phase1InvalidResults = [];
135
+ this.state.phase1Stats = undefined;
136
+ this.state.phase2Results = [];
137
+ this.state.phase2Stats = undefined;
138
+ this.state.lifecycleInstances = [];
139
+ }}
140
+ >
141
+ Show All Life Cycles
142
+ </Button>
143
+ )}
144
+ </div>
145
+ </div>
146
+
147
+ <div className={
148
+ css.vbox(12).pad2(12).bord2(200, 20, 80)
149
+ + (this.state.lifecycleInstances.length > 0 && css.maxHeight("30vh").overflowAuto)
150
+ }>
151
+ {filteredLifeCycles.length === 0 && <div>No life cycles match the filter</div>}
152
+ <div className={css.vbox(6)}>
153
+ {filteredLifeCycles.map((lifeCycle) => (
154
+ <LifeCycleRenderer
155
+ key={lifeCycle.id}
156
+ lifeCycle={lifeCycle}
157
+ defaultEditMode={false}
158
+ onSearch={(id) => this.searchLifeCycle(id)}
159
+ />
160
+ ))}
161
+ </div>
162
+ </div>
163
+
164
+ {(this.state.phase1Searching || this.state.phase1Results.length > 0) && (
165
+ <div className={css.vbox(8)}>
166
+ <div className={css.boldStyle}>
167
+ Phase 1: Start Entries ({this.state.phase1Results.length} valid)
168
+ {this.state.phase1Searching && " - Searching..."}
169
+ </div>
170
+ {this.state.phase1Stats && (
171
+ <RenderSearchStats
172
+ stats={this.state.phase1Stats}
173
+ searching={this.state.phase1Searching}
174
+ limit={limitURL.value}
175
+ />
176
+ )}
177
+ </div>
178
+ )}
179
+
180
+ {this.state.phase1InvalidResults.length > 0 && (() => {
181
+ let allMissingKeys = new Set<string>();
182
+ for (let invalid of this.state.phase1InvalidResults) {
183
+ for (let key of invalid.missingKeys) {
184
+ allMissingKeys.add(key);
185
+ }
186
+ }
187
+ return <div className={css.vbox(8).pad2(12).bord2(30, 60, 70).hsl(30, 50, 95)}>
188
+ <div className={css.boldStyle.colorhsl(30, 80, 40)}>
189
+ ⚠ Phase 1: Invalid Results Missing Keys ({this.state.phase1InvalidResults.length})
190
+ </div>
191
+ <div className={css.hbox(8).wrap}>
192
+ <span>Missing keys across all results:</span>
193
+ {Array.from(allMissingKeys).map(key => (
194
+ <span key={key} className={css.colorhsl(0, 70, 40).boldStyle}>{key}</span>
195
+ ))}
196
+ </div>
197
+ <div className={css.vbox(4).maxHeight("200px").overflowAuto}>
198
+ {this.state.phase1InvalidResults.map((invalid, idx) => (
199
+ <div key={idx} className={css.pad2(4).bord2(30, 40, 80).hsl(30, 40, 97).hbox(12, 4).wrap}>
200
+ <span className={css.colorhsl(220, 60, 50)}>
201
+ {formatDateTime(invalid.datum.time)}
202
+ </span>
203
+ <span className={css.ellipsis.flexShrink0.maxWidth(400)}>
204
+ {String(invalid.datum.param0)}
205
+ </span>
206
+ <div className={css.hbox(4)}>
207
+ <span>Missing:</span>
208
+ {invalid.missingKeys.map(key => (
209
+ <span key={key} className={css.colorhsl(0, 70, 40)}>{key}</span>
210
+ ))}
211
+ </div>
212
+ </div>
213
+ ))}
214
+ </div>
215
+ </div>;
216
+ })()}
217
+
218
+ {(this.state.phase2Searching || this.state.phase2Results.length > 0) && (
219
+ <div className={css.vbox(8)}>
220
+ <div className={css.boldStyle}>
221
+ Phase 2: All Lifecycle Entries ({this.state.phase2Results.length})
222
+ {this.state.phase2Searching && " - Searching..."}
223
+ </div>
224
+ {this.state.phase2Stats && (
225
+ <RenderSearchStats
226
+ stats={this.state.phase2Stats}
227
+ searching={this.state.phase2Searching}
228
+ limit={limitURL.value * (this.state.searchingLifeCycleId && lifeCycles?.find(lc => lc.id === this.state.searchingLifeCycleId)?.entries.length || 1)}
229
+ />
230
+ )}
231
+ </div>
232
+ )}
233
+
234
+ {this.state.lifecycleInstances.length > 0 && (() => {
235
+ let searchedLifeCycle = sortedLifeCycles.find(lc => lc.id === this.state.searchingLifeCycleId);
236
+ if (!searchedLifeCycle) return <div>Cannot find life cycle instance {this.state.searchingLifeCycleId}</div>;
237
+
238
+ let lc = searchedLifeCycle;
239
+
240
+ return <div className={css.vbox(8)}>
241
+ <h1>{lc.title} ({this.state.lifecycleInstances.length})</h1>
242
+ <div className={css.vbox(2)}>
243
+ {this.state.lifecycleInstances.map((instance, idx) => (
244
+ <LifeCycleInstanceRenderer
245
+ key={getPathStr(instance.entries.map(x => x.matchPattern)) + "_" + idx}
246
+ lifeCycle={lc}
247
+ instance={instance}
248
+ />
249
+ ))}
250
+ </div>
251
+ </div>;
252
+ })()}
253
+ </div>;
254
+ }
255
+ }
256
+
257
+ export class LifeCycleRenderer extends qreact.Component<{
258
+ lifeCycle: LifeCycle;
259
+ defaultEditMode?: boolean;
260
+ onSearch?: (lifeCycleId: string) => void;
261
+ }> {
262
+ state = t.state({
263
+ expanded: t.atomic<boolean>(false),
264
+ });
265
+
266
+ controller = LifeCyclesController(SocketFunction.browserNodeId());
267
+
268
+ render() {
269
+ let lifeCycle = this.props.lifeCycle;
270
+ let isSelected = lifecycleIdURL.value === lifeCycle.id;
271
+ let bgClass = isSelected && css.hsl(280, 50, 90) || css.hsl(200, 30, 95);
272
+ let borderHue = isSelected && 280 || 200;
273
+
274
+ return <div className={css.pad2(12).bord2(borderHue, 30, 70).vbox(8) + bgClass}>
275
+ <div className={css.hbox(12)}>
276
+ <Button
277
+ hue={borderHue}
278
+ onClick={() => {
279
+ this.state.expanded = !this.state.expanded;
280
+ }}
281
+ >
282
+ {this.state.expanded && "▼" || "▶"} {lifeCycle.title}
283
+ </Button>
284
+ <span>{lifeCycle.entries.length} entries</span>
285
+ {this.props.onSearch && (
286
+ <Button
287
+ hue={280}
288
+ onClick={() => {
289
+ if (this.props.onSearch) {
290
+ this.props.onSearch(lifeCycle.id);
291
+ }
292
+ }}
293
+ >
294
+ Search
295
+ </Button>
296
+ )}
297
+ {isSelected && (
298
+ <Button
299
+ hue={200}
300
+ onClick={() => {
301
+ lifecycleIdURL.value = "";
302
+ }}
303
+ >
304
+ Clear Selection
305
+ </Button>
306
+ ) || (
307
+ <Button
308
+ hue={280}
309
+ onClick={() => {
310
+ lifecycleIdURL.value = lifeCycle.id;
311
+ }}
312
+ >
313
+ Pin
314
+ </Button>
315
+ )}
316
+ <Button
317
+ hue={0}
318
+ onClick={() => {
319
+ if (!confirm(`Delete life cycle "${lifeCycle.title}"?`)) return;
320
+
321
+ Querysub.onCommitFinished(async () => {
322
+ await this.controller.deleteLifeCycle.promise(lifeCycle.id);
323
+ });
324
+ }}
325
+ >
326
+ Delete
327
+ </Button>
328
+ </div>
329
+
330
+ {this.state.expanded && (
331
+ <div className={css.vbox(8)}>
332
+ <InputLabel
333
+ label="Title"
334
+ value={lifeCycle.title}
335
+ onChangeValue={(value) => {
336
+ let title = value.trim();
337
+ if (!title) return;
338
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
339
+ updatedLifeCycle.title = title;
340
+ Querysub.onCommitFinished(async () => {
341
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
342
+ });
343
+ }}
344
+ className={css.width(500)}
345
+ />
346
+
347
+ <div className={css.vbox(4)}>
348
+ {lifeCycle.entries.map((entry, idx) => (
349
+ <LifeCycleEntryEditor
350
+ key={entry.matchPattern + idx}
351
+ lifeCycle={lifeCycle}
352
+ entry={entry}
353
+ entryIndex={idx}
354
+ defaultEditMode={this.props.defaultEditMode}
355
+ />
356
+ ))}
357
+
358
+ <InputLabel
359
+ placeholder="Add new entry (match pattern)"
360
+ onChangeValue={(value) => {
361
+ let matchPattern = value.trim();
362
+ if (matchPattern) {
363
+ let defaultGroupByKeys = lifeCycle.entries.length > 0 && lifeCycle.entries[0].groupByKeys && deepCloneJSON(lifeCycle.entries[0].groupByKeys) || [];
364
+ let newEntry: LifeCycleEntry = {
365
+ matchPattern,
366
+ sourceType: "info",
367
+ groupByKeys: defaultGroupByKeys,
368
+ variables: {},
369
+ };
370
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
371
+ let firstEndIndex = updatedLifeCycle.entries.findIndex(e => e.isEnd);
372
+ if (firstEndIndex !== -1) {
373
+ updatedLifeCycle.entries.splice(firstEndIndex, 0, newEntry);
374
+ } else {
375
+ updatedLifeCycle.entries.push(newEntry);
376
+ }
377
+ Querysub.onCommitFinished(async () => {
378
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
379
+ });
380
+ }
381
+ }}
382
+ className={css.width(500)}
383
+ />
384
+ </div>
385
+ </div>
386
+ )}
387
+ </div>;
388
+ }
389
+ }
390
+
391
+
392
+ class LifeCycleInstanceRenderer extends qreact.Component<{
393
+ lifeCycle: LifeCycle;
394
+ instance: LifecycleInstance;
395
+ }> {
396
+ state = t.state({
397
+ expanded: t.atomic<boolean>(false),
398
+ });
399
+
400
+ render() {
401
+ let { lifeCycle, instance } = this.props;
402
+
403
+ let bgClass = instance.isWarning && css.hsl(30, 60, 90) ||
404
+ instance.isTruncated && css.hsl(40, 50, 92) ||
405
+ instance.isComplete && css.hsl(120, 30, 95) ||
406
+ css.hsl(200, 30, 95);
407
+
408
+ let borderHue = instance.isWarning && 30 || instance.isTruncated && 40 || instance.isComplete && 120 || 200;
409
+
410
+ let firstDatum = instance.entries[0]?.datum;
411
+
412
+ let statusTitle = instance.isWarning && `⚠ ${instance.warningMessage}` ||
413
+ instance.isTruncated && "⚠ Truncated (ended by new start)" ||
414
+ instance.isComplete && "✓ Complete" ||
415
+ "Incomplete";
416
+
417
+ return <div
418
+ className={
419
+ css.pad2(4).bord2(borderHue, 40, 70)
420
+ .vbox(8)
421
+ .button.userSelect("auto", "important")
422
+ + " LifeCycleInstanceRenderer"
423
+ + bgClass
424
+ }
425
+ title={statusTitle}
426
+ onClick={(e) => {
427
+ if ((e.target as HTMLElement).closest(".LifeCycleEntryEditor")) {
428
+ return;
429
+ }
430
+ this.state.expanded = !this.state.expanded;
431
+ }}
432
+ >
433
+ <div className={css.hbox(12, 4)}>
434
+ <Button
435
+ hue={borderHue}
436
+ >
437
+ {this.state.expanded && "▼" || "▶"}
438
+ </Button>
439
+ <span className={css.minWidth(200).hbox(12)}>
440
+ <span className={css.colorhsl(220, 60, 50)}>
441
+ {formatDateTime(instance.startTime)}
442
+ </span>
443
+ {instance.endTime !== undefined && (
444
+ <span className={css.colorhsl(220, 60, 50)}>
445
+ ({formatTime(instance.endTime - instance.startTime)})
446
+ </span>
447
+ )}
448
+ </span>
449
+ <span>{instance.entries.length} entries</span>
450
+ {firstDatum && firstDatum.__machineId && (
451
+ <MachineThreadInfo
452
+ machineId={firstDatum.__machineId}
453
+ threadId={firstDatum.__threadId}
454
+ />
455
+ )}
456
+ <div className={css.hbox(8, 4).wrap.flexFillWidth}>
457
+ {instance.keys.filter(k => !["__threadId", "__machineId"].includes(k.key)).map((kv, idx, list) => (
458
+ <div key={idx} className={css.hbox(4)}>
459
+ <span className={css.colorhsl(200, 50, 60).boldStyle}>{kv.key}</span>
460
+ <span className={css.colorhsl(0, 0, 50)} title={String(kv.value)}>
461
+ <StartEllipsis maxWidth={Math.ceil(600 / list.length)}>
462
+ {niceStringify(kv.value)}
463
+ </StartEllipsis>
464
+ </span>
465
+ </div>
466
+ ))}
467
+ {(() => {
468
+ let uniqueVars = new Map<string, { key: string; title: string | undefined; value: unknown }>();
469
+ for (let entryData of instance.entries) {
470
+ let entry = lifeCycle.entries.find(e => e.matchPattern === entryData.matchPattern);
471
+ if (!entry) continue;
472
+ for (let [key, config] of Object.entries(entry.variables)) {
473
+ let value = entryData.datum[key];
474
+ if (value === undefined) continue;
475
+ let uniqueId = getPathStr([key, JSON.stringify(value)]);
476
+ if (!uniqueVars.has(uniqueId)) {
477
+ uniqueVars.set(uniqueId, {
478
+ key: key,
479
+ title: config?.title || undefined,
480
+ value: value,
481
+ });
482
+ }
483
+ }
484
+ }
485
+ return Array.from(uniqueVars.values()).map((varData, idx) => (
486
+ <div key={idx} className={css.hbox(4)}>
487
+ <span className={css.colorhsl(0, 0, 0).boldStyle}>{varData.title || varData.key}</span>
488
+ <span className={css.colorhsl(0, 0, 50)} title={niceStringify(varData.value)}>{formatValue(varData.value)}</span>
489
+ </div>
490
+ ));
491
+ })()}
492
+ </div>
493
+ </div>
494
+
495
+ {this.state.expanded && (
496
+ <div className={css.vbox(8)}>
497
+ {instance.entries.map((entryData, idx) => {
498
+ let entryIndex = lifeCycle.entries.findIndex(e => e.matchPattern === entryData.matchPattern);
499
+ let entry = lifeCycle.entries[entryIndex];
500
+ return <LifeCycleEntryEditor
501
+ key={idx}
502
+ lifeCycle={lifeCycle}
503
+ entry={entry}
504
+ entryIndex={entryIndex}
505
+ defaultEditMode={false}
506
+ datum={entryData.datum}
507
+ />;
508
+ })}
509
+ </div>
510
+ )}
511
+ </div>;
512
+ }
513
+ }
514
+
515
+
516
+ class LifeCycleEntryEditor extends qreact.Component<{
517
+ lifeCycle: LifeCycle;
518
+ entry: LifeCycleEntry;
519
+ entryIndex: number;
520
+ defaultEditMode?: boolean;
521
+ datum?: LogDatum;
522
+ }> {
523
+ state = t.state({
524
+ editMode: t.atomic<boolean>(false),
525
+ });
526
+
527
+ controller = LifeCyclesController(SocketFunction.browserNodeId());
528
+
529
+ componentDidMount() {
530
+ this.state.editMode = this.props.defaultEditMode || false;
531
+ }
532
+
533
+ render() {
534
+ let { lifeCycle, entry, entryIndex } = this.props;
535
+ let isConfigured = entry.groupByKeys && entry.groupByKeys.length > 0 && entry.groupByKeys.every(k => k.ourKey.trim() !== "");
536
+ let bgClass = isConfigured && css.hsl(150, 30, 95) || css.hsl(30, 50, 90);
537
+
538
+ return <div className={
539
+ css.pad2(4).bord2(150, 30, 70)
540
+ .vbox(4)
541
+ .cursor("auto")
542
+ + bgClass
543
+ + " LifeCycleEntryEditor"
544
+ }>
545
+ <div className={css.hbox(12)}>
546
+ <span className={css.boldStyle}>{entry.description || entry.matchPattern}</span>
547
+ {entry.description && <span className={css.colorhsl(0, 0, 50)}>({entry.matchPattern})</span>}
548
+ <span className={css.colorhsl(220, 70, 50)}>[{entry.sourceType}]</span>
549
+ {entry.isStart && <span className={css.colorhsl(120, 80, 30).boldStyle}>START</span>}
550
+ {entry.isEnd && <span className={css.colorhsl(0, 80, 30).boldStyle}>END</span>}
551
+ {!isConfigured && !this.state.editMode && <span className={css.colorhsl(30, 80, 40).boldStyle}>(not fully configured)</span>}
552
+ <Button
553
+ hue={150}
554
+ onClick={() => {
555
+ this.state.editMode = !this.state.editMode;
556
+ }}
557
+ >
558
+ {this.state.editMode && "Read Mode" || "Edit Mode"}
559
+ </Button>
560
+ <Button
561
+ hue={0}
562
+ onClick={() => {
563
+ if (!confirm("Delete this entry?")) return;
564
+
565
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
566
+ updatedLifeCycle.entries.splice(entryIndex, 1);
567
+ Querysub.onCommitFinished(async () => {
568
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
569
+ });
570
+ }}
571
+ >
572
+ Delete
573
+ </Button>
574
+ </div>
575
+
576
+ {this.state.editMode && (
577
+ <div className={css.vbox(12)}>
578
+ <div className={css.hbox(10)}>
579
+ <InputLabel
580
+ label="Match Pattern"
581
+ value={entry.matchPattern}
582
+ onChangeValue={(value) => {
583
+ let matchPattern = value.trim();
584
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
585
+ updatedLifeCycle.entries[entryIndex].matchPattern = matchPattern;
586
+ Querysub.onCommitFinished(async () => {
587
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
588
+ });
589
+ }}
590
+ className={css.width(500)}
591
+ />
592
+ <InputLabel
593
+ label="Description"
594
+ value={entry.description || ""}
595
+ onChangeValue={(value) => {
596
+ let description = value.trim() || undefined;
597
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
598
+ updatedLifeCycle.entries[entryIndex].description = description;
599
+ Querysub.onCommitFinished(async () => {
600
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
601
+ });
602
+ }}
603
+ className={css.width(500)}
604
+ />
605
+ </div>
606
+
607
+ <div className={css.hbox(12)}>
608
+ <div className={css.vbox(4)}>
609
+ <span>Source Type</span>
610
+ <select
611
+ value={entry.sourceType}
612
+ onChange={(e) => {
613
+ let sourceType = e.currentTarget.value as "log" | "error" | "info" | "warning";
614
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
615
+ updatedLifeCycle.entries[entryIndex].sourceType = sourceType;
616
+ Querysub.onCommitFinished(async () => {
617
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
618
+ });
619
+ }}
620
+ className={css.pad2(4, 2)}
621
+ >
622
+ <option value="log">log</option>
623
+ <option value="info">info</option>
624
+ <option value="warning">warning</option>
625
+ <option value="error">error</option>
626
+ </select>
627
+ </div>
628
+ </div>
629
+
630
+ <div className={css.hbox(12)}>
631
+ <InputLabel
632
+ label="Is Start"
633
+ checkbox
634
+ checked={entry.isStart}
635
+ onChange={(e) => {
636
+ let isStart = e.currentTarget.checked || undefined;
637
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
638
+ updatedLifeCycle.entries[entryIndex].isStart = isStart;
639
+ Querysub.onCommitFinished(async () => {
640
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
641
+ });
642
+ }}
643
+ />
644
+ <InputLabel
645
+ label="Is End"
646
+ checkbox
647
+ checked={entry.isEnd}
648
+ onChange={(e) => {
649
+ let isEnd = e.currentTarget.checked || undefined;
650
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
651
+ updatedLifeCycle.entries[entryIndex].isEnd = isEnd;
652
+ Querysub.onCommitFinished(async () => {
653
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
654
+ });
655
+ }}
656
+ />
657
+ </div>
658
+
659
+ <div className={css.vbox(8)}>
660
+ <div className={!isConfigured && css.colorhsl(30, 80, 40).boldStyle || ""}>
661
+ Group By Keys{!isConfigured && " (at least one required)" || ""}
662
+ </div>
663
+ {Array.isArray(entry.groupByKeys) && entry.groupByKeys.map((keyConfig, keyIdx) => {
664
+ let keyExistsInAllOtherEntries = lifeCycle.entries.every((e, idx) => {
665
+ if (idx === entryIndex) return true;
666
+ return e.groupByKeys && e.groupByKeys.some(k => k.ourKey === keyConfig.ourKey);
667
+ });
668
+
669
+ return <div key={keyIdx} className={css.hbox(8).pad2(4).bord2(200, 30, 80).hsl(200, 30, 97)}>
670
+ <Button
671
+ hue={160}
672
+ disabled={keyExistsInAllOtherEntries}
673
+ onClick={() => {
674
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
675
+ updatedLifeCycle.entries.forEach((e, idx) => {
676
+ if (idx === entryIndex) return;
677
+ if (!e.groupByKeys.some(k => k.ourKey === keyConfig.ourKey)) {
678
+ e.groupByKeys.push({ ourKey: keyConfig.ourKey });
679
+ }
680
+ });
681
+ Querysub.onCommitFinished(async () => {
682
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
683
+ });
684
+ }}
685
+ >
686
+ Copy to All
687
+ </Button>
688
+ <InputLabel
689
+ label="Key"
690
+ value={keyConfig.ourKey}
691
+ onChangeValue={(value) => {
692
+ let ourKey = value.trim();
693
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
694
+ updatedLifeCycle.entries[entryIndex].groupByKeys[keyIdx].ourKey = ourKey;
695
+ Querysub.onCommitFinished(async () => {
696
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
697
+ });
698
+ }}
699
+ className={css.width(250)}
700
+ />
701
+ <InputLabel
702
+ label="Different key in start entry (not usually needed)"
703
+ value={keyConfig.startKey || ""}
704
+ onChangeValue={(value) => {
705
+ let startKey = value.trim() || undefined;
706
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
707
+ updatedLifeCycle.entries[entryIndex].groupByKeys[keyIdx].startKey = startKey;
708
+ Querysub.onCommitFinished(async () => {
709
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
710
+ });
711
+ }}
712
+ className={css.width(250)}
713
+ />
714
+ <Button
715
+ hue={0}
716
+ onClick={() => {
717
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
718
+ updatedLifeCycle.entries[entryIndex].groupByKeys.splice(keyIdx, 1);
719
+ Querysub.onCommitFinished(async () => {
720
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
721
+ });
722
+ }}
723
+ >
724
+ Delete
725
+ </Button>
726
+ </div>;
727
+ })}
728
+ <InputLabel
729
+ label="Add new key"
730
+ onChangeValue={(value) => {
731
+ let ourKey = value.trim();
732
+ if (ourKey) {
733
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
734
+ updatedLifeCycle.entries[entryIndex].groupByKeys.push({ ourKey });
735
+ Querysub.onCommitFinished(async () => {
736
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
737
+ });
738
+ }
739
+ }}
740
+ className={css.width(250)}
741
+ />
742
+ </div>
743
+
744
+ <div className={css.vbox(4)}>
745
+ {Object.entries(entry.variables).map(([key, varData]) => {
746
+ let keyExistsInAllOtherEntries = lifeCycle.entries.every((e, idx) => {
747
+ if (idx === entryIndex) return true;
748
+ return key in e.variables;
749
+ });
750
+
751
+ return <div key={key} className={css.hbox(8).pad2(4).bord2(0, 0, 80).hsl(0, 0, 97)}>
752
+ <Button
753
+ hue={160}
754
+ disabled={keyExistsInAllOtherEntries}
755
+ onClick={() => {
756
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
757
+ updatedLifeCycle.entries.forEach((e, idx) => {
758
+ if (idx === entryIndex) return;
759
+ if (!(key in e.variables)) {
760
+ e.variables[key] = {};
761
+ }
762
+ });
763
+ Querysub.onCommitFinished(async () => {
764
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
765
+ });
766
+ }}
767
+ >
768
+ Copy to All
769
+ </Button>
770
+ <span className={css.minWidth(150).boldStyle}>{key}</span>
771
+ <InputLabel
772
+ placeholder="Variable title"
773
+ value={varData.title || ""}
774
+ onChangeValue={(value) => {
775
+ let title = value.trim() || undefined;
776
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
777
+ updatedLifeCycle.entries[entryIndex].variables[key].title = title;
778
+ Querysub.onCommitFinished(async () => {
779
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
780
+ });
781
+ }}
782
+ className={css.width(300)}
783
+ />
784
+ <Button
785
+ hue={0}
786
+ onClick={() => {
787
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
788
+ delete updatedLifeCycle.entries[entryIndex].variables[key];
789
+ Querysub.onCommitFinished(async () => {
790
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
791
+ });
792
+ }}
793
+ >
794
+ Delete
795
+ </Button>
796
+ </div>;
797
+ })}
798
+
799
+ <InputLabel
800
+ label="Add new variable key"
801
+ onChangeValue={(value) => {
802
+ let key = value.trim();
803
+ if (key) {
804
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
805
+ updatedLifeCycle.entries[entryIndex].variables[key] = {};
806
+ Querysub.onCommitFinished(async () => {
807
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
808
+ });
809
+ }
810
+ }}
811
+ className={css.width(300)}
812
+ />
813
+ </div>
814
+ </div>
815
+ )}
816
+
817
+ {!this.state.editMode && <LifeCycleEntryReadMode
818
+ lifeCycle={lifeCycle}
819
+ entry={entry}
820
+ entryIndex={entryIndex}
821
+ datum={this.props.datum}
822
+ />}
823
+ </div>;
824
+ }
825
+ }
826
+
827
+ class LifeCycleEntryReadMode extends qreact.Component<{
828
+ lifeCycle: LifeCycle;
829
+ entry: LifeCycleEntry;
830
+ entryIndex: number;
831
+ datum: LogDatum | undefined;
832
+ }> {
833
+ state = t.state({
834
+ showAllVariables: t.atomic<boolean>(false),
835
+ });
836
+
837
+ controller = LifeCyclesController(SocketFunction.browserNodeId());
838
+
839
+ render() {
840
+ let { lifeCycle, entry, entryIndex } = this.props;
841
+ const datum = this.props.datum;
842
+
843
+ let variables = getVariables(entry);
844
+ let renderEditUI = (key: string) => {
845
+ let updateLifeCycle = (fnc: (updatedLifeCycle: LifeCycle) => void) => {
846
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
847
+ fnc(updatedLifeCycle);
848
+ Querysub.onCommitFinished(async () => {
849
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
850
+ });
851
+ };
852
+
853
+ let isVariable = key in entry.variables;
854
+ let isGroupByKey = Array.isArray(entry.groupByKeys) && entry.groupByKeys.some(k => k.ourKey === key);
855
+
856
+ return <>
857
+ <span
858
+ className={css.button.pad2(4).bord2(0, 0, 60).hsl(220, 50, 60) + (!isGroupByKey && css.opacity(0.4))}
859
+ onClick={() => {
860
+ updateLifeCycle((updated) => {
861
+ if (isGroupByKey) {
862
+ updated.entries.forEach((e) => {
863
+ e.groupByKeys = e.groupByKeys.filter(k => k.ourKey !== key);
864
+ });
865
+ } else {
866
+ updated.entries.forEach((e) => {
867
+ if (!e.groupByKeys.some(k => k.ourKey === key)) {
868
+ e.groupByKeys.push({ ourKey: key });
869
+ }
870
+ });
871
+ }
872
+ });
873
+ }}
874
+ title={isGroupByKey && "Remove group by key from all entries" || "Add group by key to all entries"}
875
+ >
876
+ 🔑
877
+ </span>
878
+ <span
879
+ className={css.button.pad2(4).bord2(0, 0, 60).hsl(120, 50, 60) + (!isVariable && css.opacity(0.4))}
880
+ onClick={() => {
881
+ updateLifeCycle((updated) => {
882
+ if (isVariable) {
883
+ updated.entries.forEach((e) => {
884
+ delete e.variables[key];
885
+ });
886
+ } else {
887
+ updated.entries.forEach((e) => {
888
+ if (!(key in e.variables)) {
889
+ e.variables[key] = {};
890
+ }
891
+ });
892
+ }
893
+ });
894
+ }}
895
+ title={isVariable && "Remove variable from all entries" || "Add variable to all entries"}
896
+ >
897
+ 📌
898
+ </span>
899
+ </>;
900
+ };
901
+
902
+ return <div className={css.vbox(8)}>
903
+ {Array.isArray(entry.groupByKeys) && entry.groupByKeys.some(k => k.startKey) && (
904
+ <div className={css.vbox(2)}>
905
+ {entry.groupByKeys.filter(k => k.startKey).map((k, idx) => (
906
+ <div key={idx}>{k.ourKey} start override: {k.startKey}</div>
907
+ ))}
908
+ </div>
909
+ )}
910
+ {datum && <div className={css.vbox(4)}>
911
+ {variables.map(({ key, title }) => {
912
+ let value = datum[key];
913
+ return <div key={key} className={css.hbox(8)}>
914
+ {renderEditUI(key)}
915
+ <span className={css.minWidth(150).colorhsl(0, 0, 50)}>
916
+ {title || key}{title && ` (${key})` || ""}:
917
+ </span>
918
+ <span className={css.whiteSpace("pre-wrap")}>{value !== undefined && niceStringify(value) || "(not found)"}</span>
919
+ </div>;
920
+ })}
921
+ {this.state.showAllVariables && (() => {
922
+ let configuredKeys = new Set(getVariables(entry).map(v => v.key));
923
+ let allKeys = Object.keys(datum).filter(key => !configuredKeys.has(key));
924
+ return allKeys.map((key) => {
925
+ let value = datum[key];
926
+ return <div key={key} className={css.hbox(8)}>
927
+ {renderEditUI(key)}
928
+ <span className={css.minWidth(150).colorhsl(0, 0, 50)}>
929
+ {key}
930
+ </span>
931
+ <span className={css.whiteSpace("pre-wrap")}>{value !== undefined && niceStringify(value) || "(not found)"}</span>
932
+ </div>;
933
+ });
934
+ })()}
935
+ <Button
936
+ hue={220}
937
+ onClick={() => {
938
+ this.state.showAllVariables = !this.state.showAllVariables;
939
+ }}
940
+ >
941
+ {this.state.showAllVariables && "Hide Additional Variables" || "Show All Variables"}
942
+ </Button>
943
+ </div>}
944
+ </div>;
945
+ }
946
+ }