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.
- package/.cursorrules +8 -0
- package/package.json +1 -1
- package/src/-a-archives/archivesJSONT.ts +71 -8
- package/src/0-path-value-core/pathValueCore.ts +20 -1
- package/src/5-diagnostics/GenericFormat.tsx +1 -1
- package/src/deployManager/components/MachinesListPage.tsx +1 -2
- package/src/diagnostics/logs/IndexedLogs/BufferIndex.ts +12 -3
- package/src/diagnostics/logs/IndexedLogs/BufferIndexHelpers.ts +8 -3
- package/src/diagnostics/logs/IndexedLogs/BufferUnitIndex.ts +24 -9
- package/src/diagnostics/logs/IndexedLogs/BufferUnitSet.ts +0 -1
- package/src/diagnostics/logs/IndexedLogs/FindProgressTracker.ts +21 -5
- package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +10 -4
- package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +95 -124
- package/src/diagnostics/logs/IndexedLogs/RenderSearchStats.tsx +127 -0
- package/src/diagnostics/logs/IndexedLogs/bufferSearchFindMatcher.ts +3 -0
- package/src/diagnostics/logs/IndexedLogs/moveIndexLogsToPublic.ts +1 -1
- package/src/diagnostics/logs/TimeRangeSelector.tsx +11 -2
- package/src/diagnostics/logs/errorNotifications2/ErrorNotificationPage.tsx +1 -4
- package/src/diagnostics/logs/errorNotifications2/errorNotifications.ts +1 -1
- package/src/diagnostics/logs/lifeCycleAnalysis/LifeCyclePage.tsx +946 -0
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleMatching.ts +49 -0
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleSearch.tsx +553 -0
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +125 -90
- package/src/diagnostics/managementPages.tsx +17 -1
- package/src/functional/{limitProcessing.ts → throttleProcessing.ts} +1 -1
- package/src/library-components/StartEllipsis.tsx +13 -0
- package/src/misc.ts +4 -0
- package/src/diagnostics/logs/lifeCycleAnalysis/test.wat +0 -106
- package/src/diagnostics/logs/lifeCycleAnalysis/test.wat.d.ts +0 -2
- 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
|
+
}
|