querysub 0.395.0 → 0.397.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.
@@ -0,0 +1,291 @@
1
+ import { SocketFunction } from "socket-function/SocketFunction";
2
+ import { formatDateTime, formatTime } from "socket-function/src/formatting/format";
3
+ import { deepCloneJSON } from "socket-function/src/misc";
4
+ import { css } from "typesafecss";
5
+ import { t } from "../../../2-proxy/schema2";
6
+ import { qreact } from "../../../4-dom/qreact";
7
+ import { Querysub } from "../../../4-querysub/QuerysubController";
8
+ import { formatValue } from "../../../5-diagnostics/GenericFormat";
9
+ import { Button } from "../../../library-components/Button";
10
+ import { InputLabel } from "../../../library-components/InputLabel";
11
+ import { StartEllipsis } from "../../../library-components/StartEllipsis";
12
+ import { niceStringify } from "../../../niceStringify";
13
+ import { getPathStr } from "../../../path";
14
+ import { MachineThreadInfo } from "../../MachineThreadInfo";
15
+ import { LifeCycleEntryEditor } from "./LifeCycleEntryEditor";
16
+ import { lifecycleIdURL } from "./LifeCyclePage";
17
+ import { LifecycleInstance } from "./lifeCycleSearch";
18
+ import { LifeCycle, LifeCyclesController, LifeCycleEntry } from "./lifeCycles";
19
+
20
+ export class LifeCycleRenderer extends qreact.Component<{
21
+ lifeCycle: LifeCycle;
22
+ defaultEditMode?: boolean;
23
+ onSearch?: (lifeCycleId: string) => void;
24
+ }> {
25
+ state = t.state({
26
+ expanded: t.atomic<boolean>(false),
27
+ });
28
+
29
+ controller = LifeCyclesController(SocketFunction.browserNodeId());
30
+
31
+ render() {
32
+ let lifeCycle = this.props.lifeCycle;
33
+ let isSelected = lifecycleIdURL.value === lifeCycle.id;
34
+ let bgClass = isSelected && css.hsl(280, 50, 90) || css.hsl(200, 30, 95);
35
+ let borderHue = isSelected && 280 || 200;
36
+
37
+ return <div className={css.pad2(12).bord2(borderHue, 30, 70).vbox(8) + bgClass}>
38
+ <div className={css.hbox(12)}>
39
+ <Button
40
+ hue={borderHue}
41
+ onClick={() => {
42
+ this.state.expanded = !this.state.expanded;
43
+ }}
44
+ >
45
+ {this.state.expanded && "▼" || "▶"} {lifeCycle.title}
46
+ </Button>
47
+ <span>{lifeCycle.entries.length} entries</span>
48
+ {this.props.onSearch && (
49
+ <Button
50
+ hue={280}
51
+ onClick={() => {
52
+ if (this.props.onSearch) {
53
+ this.props.onSearch(lifeCycle.id);
54
+ }
55
+ }}
56
+ >
57
+ Search
58
+ </Button>
59
+ )}
60
+ {isSelected && (
61
+ <Button
62
+ hue={200}
63
+ onClick={() => {
64
+ lifecycleIdURL.value = "";
65
+ }}
66
+ >
67
+ Clear Selection
68
+ </Button>
69
+ ) || (
70
+ <Button
71
+ hue={280}
72
+ onClick={() => {
73
+ lifecycleIdURL.value = lifeCycle.id;
74
+ void this.props.onSearch?.(lifeCycle.id);
75
+ }}
76
+ >
77
+ Pin
78
+ </Button>
79
+ )}
80
+ <Button
81
+ hue={0}
82
+ onClick={() => {
83
+ if (!confirm(`Delete life cycle "${lifeCycle.title}"?`)) return;
84
+
85
+ Querysub.onCommitFinished(async () => {
86
+ await this.controller.deleteLifeCycle.promise(lifeCycle.id);
87
+ });
88
+ }}
89
+ >
90
+ Delete
91
+ </Button>
92
+ </div>
93
+
94
+ {this.state.expanded && (
95
+ <div className={css.vbox(8)}>
96
+ <InputLabel
97
+ label="Title"
98
+ value={lifeCycle.title}
99
+ onChangeValue={(value) => {
100
+ let title = value.trim();
101
+ if (!title) return;
102
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
103
+ updatedLifeCycle.title = title;
104
+ Querysub.onCommitFinished(async () => {
105
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
106
+ });
107
+ }}
108
+ className={css.width(500)}
109
+ />
110
+
111
+ <div className={css.vbox(4)}>
112
+ {lifeCycle.entries.map((entry, idx) => (
113
+ <LifeCycleEntryEditor
114
+ key={entry.matchPattern + idx}
115
+ lifeCycle={lifeCycle}
116
+ entry={entry}
117
+ entryIndex={idx}
118
+ defaultEditMode={this.props.defaultEditMode}
119
+ />
120
+ ))}
121
+
122
+ <InputLabel
123
+ placeholder="Add new entry (match pattern)"
124
+ onChangeValue={(value) => {
125
+ let matchPattern = value.trim();
126
+ if (matchPattern) {
127
+ let defaultGroupByKeys = lifeCycle.entries.length > 0 && lifeCycle.entries[0].groupByKeys && deepCloneJSON(lifeCycle.entries[0].groupByKeys) || [];
128
+ let newEntry: LifeCycleEntry = {
129
+ matchPattern,
130
+ sourceType: "info",
131
+ groupByKeys: defaultGroupByKeys,
132
+ variables: {},
133
+ };
134
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
135
+ let firstEndIndex = updatedLifeCycle.entries.findIndex(e => e.isEnd);
136
+ if (firstEndIndex !== -1) {
137
+ updatedLifeCycle.entries.splice(firstEndIndex, 0, newEntry);
138
+ } else {
139
+ updatedLifeCycle.entries.push(newEntry);
140
+ }
141
+ Querysub.onCommitFinished(async () => {
142
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
143
+ });
144
+ }
145
+ }}
146
+ className={css.width(500)}
147
+ />
148
+ </div>
149
+ </div>
150
+ )}
151
+ </div>;
152
+ }
153
+ }
154
+
155
+
156
+ export class LifeCycleInstanceRenderer extends qreact.Component<{
157
+ lifeCycle: LifeCycle;
158
+ instance: LifecycleInstance;
159
+ }> {
160
+ state = t.state({
161
+ expanded: t.atomic<boolean>(false),
162
+ });
163
+
164
+ render() {
165
+ let { lifeCycle, instance } = this.props;
166
+
167
+ let bgClass = (
168
+ instance.isMissingStart && css.hsl(60, 60, 90) ||
169
+ instance.isMissingEnd && css.hsl(200, 60, 90) ||
170
+ instance.isComplete && css.hsl(120, 30, 95) ||
171
+ instance.isTruncated && css.hsl(0, 60, 90) ||
172
+ css.hsl(280, 30, 95)
173
+ );
174
+
175
+ let borderHue = (
176
+ instance.isMissingStart && 60
177
+ || instance.isMissingEnd && 200
178
+ || instance.isComplete && 120
179
+ || instance.isTruncated && 1
180
+ || 280
181
+ );
182
+
183
+ let firstDatum = instance.entries[0]?.datum;
184
+
185
+ let statusTitle = (
186
+ instance.isMissingStart && "⚠ Missing start entry" ||
187
+ instance.isMissingEnd && "⚠ Missing end entry" ||
188
+ instance.isComplete && "✓ Complete" ||
189
+ instance.isTruncated && "⚠ Truncated (ended by new start)" ||
190
+ "Incomplete"
191
+ );
192
+
193
+ return <div
194
+ className={
195
+ css.pad2(4).bord2(borderHue, 40, 70)
196
+ .vbox(8)
197
+ .button.userSelect("auto", "important")
198
+ + " LifeCycleInstanceRenderer"
199
+ + bgClass
200
+ }
201
+ title={statusTitle}
202
+ onClick={(e) => {
203
+ if ((e.target as HTMLElement).closest(".LifeCycleEntryEditor")) {
204
+ return;
205
+ }
206
+ this.state.expanded = !this.state.expanded;
207
+ }}
208
+ >
209
+ <div className={css.hbox(12, 4)}>
210
+ <Button
211
+ hue={borderHue}
212
+ >
213
+ {this.state.expanded && "▼" || "▶"}
214
+ </Button>
215
+ <span className={css.minWidth(200).hbox(12)}>
216
+ <span className={css.colorhsl(220, 60, 50)}>
217
+ {formatDateTime(instance.startTime)}
218
+ </span>
219
+ {instance.endTime !== undefined && (
220
+ <span className={css.colorhsl(220, 60, 50)}>
221
+ ({formatTime(instance.endTime - instance.startTime)})
222
+ </span>
223
+ )}
224
+ </span>
225
+ <span>{instance.entries.length} entries</span>
226
+ {firstDatum && firstDatum.__machineId && (
227
+ <MachineThreadInfo
228
+ machineId={firstDatum.__machineId}
229
+ threadId={firstDatum.__threadId}
230
+ />
231
+ )}
232
+ <div className={css.hbox(8, 4).wrap.flexFillWidth}>
233
+ {instance.keys.filter(k => !["__threadId", "__machineId"].includes(k.key)).map((kv, idx, list) => (
234
+ <div key={idx} className={css.hbox(4)}>
235
+ <span className={css.colorhsl(200, 50, 60).boldStyle}>{kv.key}</span>
236
+ <span className={css.colorhsl(0, 0, 50)} title={String(kv.value)}>
237
+ <StartEllipsis maxWidth={Math.ceil(600 / list.length)}>
238
+ {niceStringify(kv.value)}
239
+ </StartEllipsis>
240
+ </span>
241
+ </div>
242
+ ))}
243
+ {(() => {
244
+ let uniqueVars = new Map<string, { key: string; title: string | undefined; value: unknown }>();
245
+ for (let entryData of instance.entries) {
246
+ let entry = lifeCycle.entries.find(e => e.matchPattern === entryData.matchPattern);
247
+ if (!entry) continue;
248
+ for (let [key, config] of Object.entries(entry.variables)) {
249
+ let value = entryData.datum[key];
250
+ if (value === undefined) continue;
251
+ // Now just the key, so the preview is easier to read
252
+ let uniqueId = key;
253
+ if (!uniqueVars.has(uniqueId)) {
254
+ uniqueVars.set(uniqueId, {
255
+ key: key,
256
+ title: config?.title || undefined,
257
+ value: value,
258
+ });
259
+ }
260
+ }
261
+ }
262
+ return Array.from(uniqueVars.values()).map((varData, idx) => (
263
+ <div key={idx} className={css.hbox(4)}>
264
+ <span className={css.colorhsl(0, 0, 0).boldStyle}>{varData.title || varData.key}</span>
265
+ <span className={css.colorhsl(0, 0, 50)} title={niceStringify(varData.value)}>{formatValue(varData.value)}</span>
266
+ </div>
267
+ ));
268
+ })()}
269
+ </div>
270
+ </div>
271
+
272
+ {this.state.expanded && (
273
+ <div className={css.vbox(8)}>
274
+ {instance.entries.map((entryData, idx) => {
275
+ let entryIndex = lifeCycle.entries.findIndex(e => e.matchPattern === entryData.matchPattern);
276
+ let entry = lifeCycle.entries[entryIndex];
277
+ return <LifeCycleEntryEditor
278
+ key={idx}
279
+ lifeCycle={lifeCycle}
280
+ entry={entry}
281
+ entryIndex={entryIndex}
282
+ defaultEditMode={false}
283
+ datum={entryData.datum}
284
+ />;
285
+ })}
286
+ </div>
287
+ )}
288
+ </div>;
289
+ }
290
+ }
291
+
@@ -0,0 +1,151 @@
1
+ import { SocketFunction } from "socket-function/SocketFunction";
2
+ import { css } from "typesafecss";
3
+ import { t } from "../../../2-proxy/schema2";
4
+ import { qreact } from "../../../4-dom/qreact";
5
+ import { formatValue } from "../../../5-diagnostics/GenericFormat";
6
+ import { Button } from "../../../library-components/Button";
7
+ import { niceStringify } from "../../../niceStringify";
8
+ import { LogDatum } from "../diskLogger";
9
+ import { LifeCycle, LifeCyclesController, getVariables } from "./lifeCycles";
10
+ import { createLifeCycleSearch, LifecycleInstance } from "./lifeCycleSearch";
11
+ import { IndexedLogResults } from "../IndexedLogs/BufferIndexHelpers";
12
+ import { formatSearchString } from "../IndexedLogs/LogViewerParams";
13
+ import { getPathStr, getPathStr2 } from "../../../path";
14
+
15
+ export class NestedLifeCycleInfo extends qreact.Component<{
16
+ keyName: string;
17
+ value: unknown;
18
+ aliases: string[];
19
+ currentLifeCycleId: string;
20
+ }> {
21
+ state = t.state({
22
+ searchingLifeCycleId: t.atomic<string | undefined>(undefined),
23
+ phase1Results: t.atomic<LogDatum[]>([]),
24
+ phase1InvalidResults: t.atomic<Array<{ datum: LogDatum; missingKeys: string[] }>>([]),
25
+ phase1Stats: t.atomic<IndexedLogResults | undefined>(undefined),
26
+ phase1Searching: t.boolean(false),
27
+ phase2Results: t.atomic<LogDatum[]>([]),
28
+ phase2Stats: t.atomic<IndexedLogResults | undefined>(undefined),
29
+ phase2Searching: t.boolean(false),
30
+ phase2HitLimit: t.boolean(false),
31
+ lifecycleInstances: t.atomic<LifecycleInstance[]>([]),
32
+ });
33
+
34
+ controller = LifeCyclesController(SocketFunction.browserNodeId());
35
+ search = createLifeCycleSearch(this);
36
+
37
+ render() {
38
+ let lifeCycles = this.controller.getLifeCycles();
39
+ if (!lifeCycles) return null;
40
+
41
+ let { keyName, value, aliases, currentLifeCycleId } = this.props;
42
+
43
+ let allKeysToMatch = [keyName, ...aliases].filter(x => !["__threadId", "__machineId"].includes(x));
44
+ let matchingLifeCycles: Array<{ lifecycle: LifeCycle; matchedKey: string }> = [];
45
+
46
+ for (let lifecycle of lifeCycles) {
47
+ if (lifecycle.id === currentLifeCycleId) continue;
48
+
49
+ let matchedKey: string | undefined = undefined;
50
+ for (let entry of lifecycle.entries) {
51
+ for (let groupByKey of entry.groupByKeys) {
52
+ if (allKeysToMatch.includes(groupByKey.ourKey)) {
53
+ matchedKey = groupByKey.ourKey;
54
+ break;
55
+ }
56
+ }
57
+ if (matchedKey !== undefined) break;
58
+ }
59
+
60
+ if (matchedKey !== undefined) {
61
+ matchingLifeCycles.push({ lifecycle, matchedKey });
62
+ }
63
+ }
64
+
65
+ if (matchingLifeCycles.length === 0) return null;
66
+
67
+ return <>
68
+ {matchingLifeCycles.map(({ lifecycle, matchedKey }) => {
69
+ let isSearchingThis = this.state.searchingLifeCycleId === lifecycle.id;
70
+ let isSearching = isSearchingThis && (this.state.phase1Searching || this.state.phase2Searching);
71
+ let hasSearched = isSearchingThis && !isSearching;
72
+
73
+ let instances = hasSearched && this.state.lifecycleInstances.filter(inst =>
74
+ inst.keys.some(k => k.key === matchedKey && k.value === value)
75
+ ) || [];
76
+ let hasResults = instances.length > 0;
77
+
78
+ let progressText = "";
79
+ if (isSearching && isSearchingThis) {
80
+ let parts: string[] = [];
81
+
82
+ if (this.state.phase1Searching || this.state.phase1Stats) {
83
+ let stats = this.state.phase1Stats;
84
+ let blocksScanned = stats && (stats.localFilesSearched + stats.backblazeFilesSearched) || 0;
85
+ let resultsCount = this.state.phase1Results.length;
86
+ parts.push(`${blocksScanned} > ${resultsCount}`);
87
+ }
88
+
89
+ if (this.state.phase2Searching) {
90
+ let stats = this.state.phase2Stats;
91
+ let blocksScanned = stats && (stats.localFilesSearched + stats.backblazeFilesSearched) || 0;
92
+ let resultsCount = this.state.phase2Results.length;
93
+ parts.push(`${blocksScanned} > ${resultsCount}`);
94
+ }
95
+
96
+ progressText = parts.length > 0 && `(${parts.join(" | ")})` || "(...)";
97
+ }
98
+
99
+ return <>
100
+ <Button
101
+ hue={180}
102
+ onClick={() => {
103
+ void this.search.searchLifeCycle({
104
+ lifeCycleId: lifecycle.id,
105
+ limit: 10,
106
+ additionalSearch: formatSearchString({ [matchedKey]: value })
107
+ });
108
+ }}
109
+ >
110
+ {lifecycle.title} {isSearching && "⏳" || ""} {progressText} {hasSearched && `(${instances.length})` || ""}
111
+ </Button>
112
+ {hasSearched && !hasResults && (
113
+ <div className={css.pad2(4).colorhsl(0, 0, 50)}>No results found</div>
114
+ )}
115
+ {hasResults && (() => {
116
+ let firstInstance = instances[0];
117
+ let totalCount = instances.length;
118
+ let hitLimit = totalCount >= 10;
119
+
120
+ let keys = lifecycle.entries.map(x => x.groupByKeys.map(y => y.ourKey).concat(Object.keys(x.variables))).flat();
121
+
122
+ function valueHash(key: string, value: unknown) {
123
+ return getPathStr2(key, String(value));
124
+ }
125
+ let values = new Map<string, { key: string; value: unknown }>();
126
+ for (let entry of firstInstance.entries) {
127
+ for (let [key, value] of Object.entries(entry.datum)) {
128
+ if (!keys.includes(key)) continue;
129
+ if (key === matchedKey) continue;
130
+ if (["__threadId", "__machineId"].includes(key)) continue;
131
+ values.set(valueHash(key, value), { key, value });
132
+ }
133
+ }
134
+
135
+ return <>
136
+ <div className={css.hbox(4)}>
137
+ {hitLimit && <span className={css.colorhsl(30, 80, 40).boldStyle}>(hit limit!)</span>}
138
+ </div>
139
+ {Array.from(values.values()).map(({ key, value }, idx) => (
140
+ <div key={idx} className={css.hbox(4)}>
141
+ <span className={css.colorhsl(200, 50, 60).boldStyle}>{key}:</span>
142
+ <span className={css.colorhsl(0, 0, 50)}>{niceStringify(value)}</span>
143
+ </div>
144
+ ))}
145
+ </>;
146
+ })()}
147
+ </>;
148
+ })}
149
+ </>;
150
+ }
151
+ }