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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.395.0",
3
+ "version": "0.397.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
@@ -118,7 +118,7 @@ class ArchivesDisk {
118
118
  if (dir.endsWith(":")) break;
119
119
  let files = await fs.promises.readdir(this.LOCAL_ARCHIVE_FOLDER + dir);
120
120
  if (files.length > 0) break;
121
- await fs.promises.rmdir(this.LOCAL_ARCHIVE_FOLDER + dir);
121
+ await fs.promises.rm(this.LOCAL_ARCHIVE_FOLDER + dir, { recursive: true });
122
122
  }
123
123
  } catch { }
124
124
  }
@@ -176,7 +176,7 @@ class ArchivesDisk {
176
176
  } catch { }
177
177
  if (files.length === 0) {
178
178
  try {
179
- await fs.promises.rmdir(dir);
179
+ await fs.promises.rm(dir, { recursive: true });
180
180
  } catch { }
181
181
  }
182
182
  }
@@ -17,6 +17,7 @@ import { delay } from "socket-function/src/batching";
17
17
  import { formatTime } from "socket-function/src/formatting/format";
18
18
  import { waitForFirstTimeSync } from "socket-function/time/trueTimeShim";
19
19
  import { red } from "socket-function/src/formatting/logColors";
20
+ import { isNode } from "typesafecss";
20
21
 
21
22
  let callerInfo = new Map<CallerContext, {
22
23
  reconnectNodeId: string | undefined;
@@ -108,6 +109,7 @@ export interface ChangeIdentityPayload {
108
109
  certIssuer: string;
109
110
  serverId: string;
110
111
  mountedPort: number | undefined;
112
+ debugEntryPoint: string | undefined;
111
113
  }
112
114
  class IdentityControllerBase {
113
115
  // IMPORTANT! We HAVE to call changeIdentity NOT JUST because we can't use peer certificates in the browser, BUT, also
@@ -158,7 +160,22 @@ class IdentityControllerBase {
158
160
  pubKeyShort: getShortNumber(pubKey),
159
161
  });
160
162
 
161
- console.log(`Authenticated identity for ${caller.nodeId} to ${reconnectNodeId || caller.nodeId} in ${formatTime(Date.now() - time)}, at ${Date.now()}`);
163
+ let duration = Date.now() - time;
164
+ console.log(`Authenticated identity for ${caller.nodeId} in ${formatTime(duration)}, at ${Date.now()}`, {
165
+ clientId: caller.nodeId,
166
+ reconnectNodeId,
167
+ duration,
168
+ mountedPort: payload.mountedPort,
169
+ debugEntryPoint: payload.debugEntryPoint,
170
+ });
171
+
172
+ SocketFunction.onNextDisconnect(caller.nodeId, () => {
173
+ // NOTE: I don't really see any purpose of deleting from caller info. I don't think we're going to run out of memory because of too many callers authenticating.
174
+ // However, logging here is useful as it allows us to complete the life cycle so we know how long a client was connected for.
175
+ console.log(`Disconnected client`, {
176
+ clientId: caller.nodeId,
177
+ });
178
+ });
162
179
  }
163
180
  }
164
181
 
@@ -188,6 +205,7 @@ const changeIdentityOnce = cacheWeak(async function changeIdentityOnce(connectio
188
205
  cert: threadKeyCert.cert.toString(),
189
206
  certIssuer: issuer.cert.toString(),
190
207
  mountedPort: getNodeIdLocation(SocketFunction.mountedNodeId)?.port,
208
+ debugEntryPoint: isNode() ? process.argv[1] : "browser",
191
209
  };
192
210
  let signature = sign(threadKeyCert, payload);
193
211
  await timeoutToError(
@@ -1249,14 +1249,14 @@ class PathWatcher {
1249
1249
  if (newPathsWatched.size > 0 || newParentsWatched.size > 0) {
1250
1250
  if (isOwnNodeId(config.callback)) {
1251
1251
  for (let path of newPathsWatched) {
1252
- auditLog("new local WATCH", { path });
1252
+ auditLog("new local WATCH VALUE", { path });
1253
1253
  }
1254
1254
  for (let path of newParentsWatched) {
1255
1255
  auditLog("new local WATCH PARENT", { path });
1256
1256
  }
1257
1257
  } else {
1258
1258
  for (let path of newPathsWatched) {
1259
- auditLog("new non-local WATCH", { path, watcher: config.callback });
1259
+ auditLog("new non-local WATCH VALUE", { path, watcher: config.callback });
1260
1260
  }
1261
1261
  for (let path of newParentsWatched) {
1262
1262
  auditLog("new non-local WATCH PARENT", { path, watcher: config.callback });
@@ -1381,9 +1381,9 @@ class PathWatcher {
1381
1381
  watchers.watchers.delete(callback);
1382
1382
 
1383
1383
  if (isOwnNodeId(callback)) {
1384
- auditLog("local UNWATCH", { path });
1384
+ auditLog("local UNWATCH VALUE", { path });
1385
1385
  } else {
1386
- auditLog("non-local UNWATCH", { path, watcher: callback });
1386
+ auditLog("non-local UNWATCH VALUE", { path, watcher: callback });
1387
1387
  }
1388
1388
 
1389
1389
  if (watchers.watchers.size === 0) {
@@ -1781,9 +1781,9 @@ class WriteValidStorage {
1781
1781
  }
1782
1782
  public setWriteValidState(write: WriteState, lockless?: boolean): boolean {
1783
1783
  if (write.isValid) {
1784
- auditLog(write.reason || "ACCEPTING VALUE", { path: write.path, time: write.time.time });
1784
+ auditLog("ACCEPTING VALUE", { path: write.path, time: write.time.time, reason: write.reason });
1785
1785
  } else {
1786
- auditLog(write.reason || "REJECTING VALUE", { path: write.path, time: write.time.time });
1786
+ auditLog("REJECTING VALUE", { path: write.path, time: write.time.time, reason: write.reason });
1787
1787
  }
1788
1788
  this.ensureGarbageCollectOldState();
1789
1789
 
@@ -2359,8 +2359,8 @@ export const lockWatchDeduper = new LockWatchDeduper();
2359
2359
  // can trigger the import of memoryValueAudit for all clients as consistently
2360
2360
  // as the core.
2361
2361
  setImmediate(() => {
2362
- logErrors(import("../5-diagnostics/memoryValueAudit").then(x => x.startMemoryAuditLoop()));
2363
- logErrors(import("../5-diagnostics/diskValueAudit").then(x => x.startDiskAuditLoop()));
2362
+ //logErrors(import("../5-diagnostics/memoryValueAudit").then(x => x.startMemoryAuditLoop()));
2363
+ //logErrors(import("../5-diagnostics/diskValueAudit").then(x => x.startDiskAuditLoop()));
2364
2364
  logErrors(import("../5-diagnostics/synchronousLagTracking").then(x => x.trackSynchronousLag()));
2365
2365
  });
2366
2366
 
@@ -170,7 +170,7 @@ let moduleResolver = async (spec: {
170
170
 
171
171
  // Remove any previous attempt to sync it
172
172
  if (fs.existsSync(repoPath)) {
173
- await fs.promises.rmdir(repoPath, { recursive: true });
173
+ await fs.promises.rm(repoPath, { recursive: true });
174
174
  }
175
175
  // Clone it
176
176
  await executeCommand("git", ["clone", gitURL, repoPath]);
@@ -182,7 +182,7 @@ let moduleResolver = async (spec: {
182
182
  // Delete querysub, and replace it with a symlink. Otherwise the synchronization code
183
183
  // will run again, and a lot of setup code will run again, etc, and nothing will work correctly.
184
184
  let querysubPath = repoPath + "node_modules/querysub";
185
- await fs.promises.rmdir(querysubPath, { recursive: true });
185
+ await fs.promises.rm(querysubPath, { recursive: true });
186
186
 
187
187
  let actualQuerysubPath = path.resolve("./node_modules/querysub");
188
188
  await fs.promises.symlink(actualQuerysubPath, querysubPath, "junction");
@@ -600,7 +600,7 @@ const ensureGitSynced = measureWrap(async function ensureGitSynced(config: {
600
600
  await setGitRef(config);
601
601
  } catch (e: any) {
602
602
  console.warn(`Failed to set git ref, trying to delete and clone again (${config.gitFolder}): ${e.stack}`);
603
- await fs.promises.rmdir(config.gitFolder, { recursive: true });
603
+ await fs.promises.rm(config.gitFolder, { recursive: true });
604
604
  await runPromise(`git clone ${config.repoUrl} ${config.gitFolder}`);
605
605
  await setGitRef(config);
606
606
  }
@@ -30,13 +30,12 @@ import { readProductionLogsURL, searchTextURL } from "./LogViewerParams";
30
30
  import { RenderSearchStats } from "./RenderSearchStats";
31
31
  import { LifeCyclesController, LifeCycle, LifeCycleEntry } from "../lifeCycleAnalysis/lifeCycles";
32
32
  import { getLifecycleMatchesForDatum } from "../lifeCycleAnalysis/lifeCycleMatching";
33
- import { lifecycleIdURL } from "../lifeCycleAnalysis/LifeCyclePage";
33
+ import { lifecycleIdURL, additionalSearchURL } from "../lifeCycleAnalysis/LifeCyclePage";
34
34
  import { ATag } from "../../../library-components/ATag";
35
35
  import { SocketFunction } from "socket-function/SocketFunction";
36
36
  import { managementPageURL } from "../../managementPages";
37
37
  import { startTimeParam, endTimeParam } from "../TimeRangeSelector";
38
38
  import { formatSearchString } from "./LogViewerParams";
39
- import { additionalSearchURL } from "../lifeCycleAnalysis/lifeCycleSearch";
40
39
 
41
40
  let excludePendingResults = new URLParam("excludePendingResults", false);
42
41
  let limitURL = new URLParam("limit", 100);
@@ -564,7 +563,7 @@ export class LogViewer3 extends qreact.Component {
564
563
  startTimeParam.getOverride(datum.time - timeInHour),
565
564
  endTimeParam.getOverride(datum.time + timeInHour),
566
565
  ]}>
567
- {match.lifecycle.title}
566
+ View {JSON.stringify(match.lifecycle.title)}
568
567
  </ATag>
569
568
  </div>
570
569
  ))}
@@ -576,7 +575,7 @@ export class LogViewer3 extends qreact.Component {
576
575
  endTimeParam.getOverride(maxTime + timeInHour),
577
576
  ]}>
578
577
  <Button hue={280} className={css.boldStyle}>
579
- View All {lifecycleTitle}
578
+ View All
580
579
  </Button>
581
580
  </ATag>
582
581
  </div>;
@@ -9,6 +9,7 @@ const SEARCH_OR_CHAR = "|";
9
9
  const SEARCH_AND_BYTE = 38;
10
10
  const SEARCH_AND_CHAR = "&";
11
11
 
12
+ // IMPORTANT! While technically THIS structure can support negation, the index structure above us cannot, as it only stores information at the block level (multiple buffers per block). So... if we supported negation, it couldn't, so it would give us a lot of false positives (cases where negation removes the results). So... never use negation, just turn everything into a positive signal.
12
13
  export type MatchStructure = {
13
14
  type: "or";
14
15
  parts: MatchStructure[];
@@ -12,7 +12,7 @@ import { ErrorWarning } from "./ErrorWarning";
12
12
  import { getErrorNotificationsManager } from "./errorWatcher";
13
13
  import { createMatchesPattern } from "../IndexedLogs/bufferSearchFindMatcher";
14
14
  import { cacheLimited } from "socket-function/src/caching";
15
- import { managementPageURL } from "../../managementPages";
15
+ import { managementPageURL, showingManagementURL } from "../../managementPages";
16
16
  import { searchTextURL, readProductionLogsURL } from "../IndexedLogs/LogViewerParams";
17
17
  import { startTimeParam, endTimeParam } from "../TimeRangeSelector";
18
18
  import { timeInHour } from "socket-function/src/misc";
@@ -56,6 +56,7 @@ export class LogDatumRenderer extends qreact.Component<{
56
56
  let logViewerValues: URLOverride[] | undefined;
57
57
  {
58
58
  logViewerValues = [
59
+ showingManagementURL.getOverride(true),
59
60
  managementPageURL.getOverride("LogViewer3"),
60
61
  readProductionLogsURL.getOverride(isPublic()),
61
62
  startTimeParam.getOverride(datum.time - timeInHour),
@@ -382,6 +383,7 @@ class SuppressionItem extends qreact.Component<{
382
383
  <div className={css.hbox(12)}>
383
384
  <ATag
384
385
  values={[
386
+ showingManagementURL.getOverride(true),
385
387
  managementPageURL.getOverride("LogViewer3"),
386
388
  searchTextURL.getOverride(suppression.pattern),
387
389
  readProductionLogsURL.getOverride(true),
@@ -0,0 +1,358 @@
1
+ import { SocketFunction } from "socket-function/SocketFunction";
2
+ import { deepCloneJSON } from "socket-function/src/misc";
3
+ import { css } from "typesafecss";
4
+ import { t } from "../../../2-proxy/schema2";
5
+ import { qreact } from "../../../4-dom/qreact";
6
+ import { Querysub } from "../../../4-querysub/QuerysubController";
7
+ import { Button } from "../../../library-components/Button";
8
+ import { InputLabel } from "../../../library-components/InputLabel";
9
+ import { LogDatum } from "../diskLogger";
10
+ import { LifeCycleEntryReadMode } from "./LifeCycleEntryReadMode";
11
+ import { LifeCycle, LifeCycleEntry, LifeCyclesController } from "./lifeCycles";
12
+
13
+ export class LifeCycleEntryEditor extends qreact.Component<{
14
+ lifeCycle: LifeCycle;
15
+ entry: LifeCycleEntry;
16
+ entryIndex: number;
17
+ defaultEditMode?: boolean;
18
+ datum?: LogDatum;
19
+ }> {
20
+ state = t.state({
21
+ editMode: t.atomic<boolean>(false),
22
+ });
23
+
24
+ controller = LifeCyclesController(SocketFunction.browserNodeId());
25
+
26
+ componentDidMount() {
27
+ this.state.editMode = this.props.defaultEditMode || false;
28
+ }
29
+
30
+ render() {
31
+ let { lifeCycle, entry, entryIndex } = this.props;
32
+ let isConfigured = entry.groupByKeys && entry.groupByKeys.length > 0 && entry.groupByKeys.every(k => k.ourKey.trim() !== "");
33
+ let bgClass = isConfigured && css.hsl(150, 30, 95) || css.hsl(30, 50, 90);
34
+
35
+ return <div className={
36
+ css.pad2(4).bord2(150, 30, 70)
37
+ .vbox(4)
38
+ .cursor("auto")
39
+ + bgClass
40
+ + " LifeCycleEntryEditor"
41
+ }>
42
+ <div className={css.hbox(12)}>
43
+ <span className={css.boldStyle}>{entry.description || entry.matchPattern}</span>
44
+ {entry.description && <span className={css.colorhsl(0, 0, 50)}>({entry.matchPattern})</span>}
45
+ <span className={css.colorhsl(220, 70, 50)}>[{entry.sourceType}]</span>
46
+ {entry.isStart && <span className={css.colorhsl(120, 80, 30).boldStyle}>START</span>}
47
+ {entry.isEnd && <span className={css.colorhsl(0, 80, 30).boldStyle}>END</span>}
48
+ {!isConfigured && !this.state.editMode && <span className={css.colorhsl(30, 80, 40).boldStyle}>(not fully configured)</span>}
49
+ <Button
50
+ hue={150}
51
+ onClick={() => {
52
+ this.state.editMode = !this.state.editMode;
53
+ }}
54
+ >
55
+ {this.state.editMode && "Read Mode" || "Edit Mode"}
56
+ </Button>
57
+ <Button
58
+ hue={0}
59
+ onClick={() => {
60
+ if (!confirm("Delete this entry?")) return;
61
+
62
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
63
+ updatedLifeCycle.entries.splice(entryIndex, 1);
64
+ Querysub.onCommitFinished(async () => {
65
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
66
+ });
67
+ }}
68
+ >
69
+ Delete
70
+ </Button>
71
+ </div>
72
+
73
+ {this.state.editMode && (
74
+ <div className={css.vbox(12)}>
75
+ <div className={css.hbox(10)}>
76
+ <InputLabel
77
+ label="Match Pattern"
78
+ value={entry.matchPattern}
79
+ onChangeValue={(value) => {
80
+ let matchPattern = value.trim();
81
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
82
+ updatedLifeCycle.entries[entryIndex].matchPattern = matchPattern;
83
+ Querysub.onCommitFinished(async () => {
84
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
85
+ });
86
+ }}
87
+ className={css.width(500)}
88
+ />
89
+ <InputLabel
90
+ label="Description"
91
+ value={entry.description || ""}
92
+ onChangeValue={(value) => {
93
+ let description = value.trim() || undefined;
94
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
95
+ updatedLifeCycle.entries[entryIndex].description = description;
96
+ Querysub.onCommitFinished(async () => {
97
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
98
+ });
99
+ }}
100
+ className={css.width(500)}
101
+ />
102
+ </div>
103
+
104
+ <div className={css.hbox(12)}>
105
+ <div className={css.vbox(4)}>
106
+ <span>Source Type</span>
107
+ <select
108
+ value={entry.sourceType}
109
+ onChange={(e) => {
110
+ let sourceType = e.currentTarget.value as "log" | "error" | "info" | "warning";
111
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
112
+ updatedLifeCycle.entries[entryIndex].sourceType = sourceType;
113
+ Querysub.onCommitFinished(async () => {
114
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
115
+ });
116
+ }}
117
+ className={css.pad2(4, 2)}
118
+ >
119
+ <option value="log">log</option>
120
+ <option value="info">info</option>
121
+ <option value="warning">warning</option>
122
+ <option value="error">error</option>
123
+ </select>
124
+ </div>
125
+ </div>
126
+
127
+ <div className={css.hbox(12)}>
128
+ <InputLabel
129
+ label="Is Start"
130
+ checkbox
131
+ checked={entry.isStart}
132
+ onChange={(e) => {
133
+ let isStart = e.currentTarget.checked || undefined;
134
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
135
+ updatedLifeCycle.entries[entryIndex].isStart = isStart;
136
+ Querysub.onCommitFinished(async () => {
137
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
138
+ });
139
+ }}
140
+ />
141
+ <InputLabel
142
+ label="Is End"
143
+ checkbox
144
+ checked={entry.isEnd}
145
+ onChange={(e) => {
146
+ let isEnd = e.currentTarget.checked || undefined;
147
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
148
+ updatedLifeCycle.entries[entryIndex].isEnd = isEnd;
149
+ Querysub.onCommitFinished(async () => {
150
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
151
+ });
152
+ }}
153
+ />
154
+ </div>
155
+
156
+ <div className={css.vbox(8)}>
157
+ <div className={!isConfigured && css.colorhsl(30, 80, 40).boldStyle || ""}>
158
+ Group By Keys{!isConfigured && " (at least one required)" || ""}
159
+ </div>
160
+ {Array.isArray(entry.groupByKeys) && entry.groupByKeys.map((keyConfig, keyIdx) => {
161
+ let keyExistsInAllOtherEntries = lifeCycle.entries.every((e, idx) => {
162
+ if (idx === entryIndex) return true;
163
+ return e.groupByKeys && e.groupByKeys.some(k => k.ourKey === keyConfig.ourKey);
164
+ });
165
+
166
+ return <div key={keyIdx} className={css.hbox(8).pad2(4).bord2(200, 30, 80).hsl(200, 30, 97)}>
167
+ <Button
168
+ hue={160}
169
+ disabled={keyExistsInAllOtherEntries}
170
+ onClick={() => {
171
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
172
+ updatedLifeCycle.entries.forEach((e, idx) => {
173
+ if (idx === entryIndex) return;
174
+ if (!e.groupByKeys.some(k => k.ourKey === keyConfig.ourKey)) {
175
+ e.groupByKeys.push({ ourKey: keyConfig.ourKey });
176
+ }
177
+ });
178
+ Querysub.onCommitFinished(async () => {
179
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
180
+ });
181
+ }}
182
+ >
183
+ Copy to All
184
+ </Button>
185
+ <InputLabel
186
+ label="Key"
187
+ value={keyConfig.ourKey}
188
+ onChangeValue={(value) => {
189
+ let ourKey = value.trim();
190
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
191
+ updatedLifeCycle.entries[entryIndex].groupByKeys[keyIdx].ourKey = ourKey;
192
+ Querysub.onCommitFinished(async () => {
193
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
194
+ });
195
+ }}
196
+ className={css.width(250)}
197
+ />
198
+ <InputLabel
199
+ label="Different key in start entry (not usually needed)"
200
+ value={keyConfig.startKey || ""}
201
+ onChangeValue={(value) => {
202
+ let startKey = value.trim() || undefined;
203
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
204
+ updatedLifeCycle.entries[entryIndex].groupByKeys[keyIdx].startKey = startKey;
205
+ Querysub.onCommitFinished(async () => {
206
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
207
+ });
208
+ }}
209
+ className={css.width(250)}
210
+ />
211
+ <InputLabel
212
+ label="Aliases (comma-separated)"
213
+ value={keyConfig.aliases && keyConfig.aliases.join(", ") || ""}
214
+ onChangeValue={(value) => {
215
+ let aliases = value.trim() && value.split(",").map(a => a.trim()).filter(a => a) || undefined;
216
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
217
+ updatedLifeCycle.entries.forEach((e) => {
218
+ let matchingKey = e.groupByKeys.find(k => k.ourKey === keyConfig.ourKey);
219
+ if (matchingKey) {
220
+ matchingKey.aliases = aliases;
221
+ }
222
+ });
223
+ Querysub.onCommitFinished(async () => {
224
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
225
+ });
226
+ }}
227
+ className={css.width(250)}
228
+ />
229
+ <Button
230
+ hue={0}
231
+ onClick={() => {
232
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
233
+ updatedLifeCycle.entries[entryIndex].groupByKeys.splice(keyIdx, 1);
234
+ Querysub.onCommitFinished(async () => {
235
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
236
+ });
237
+ }}
238
+ >
239
+ Delete
240
+ </Button>
241
+ </div>;
242
+ })}
243
+ <InputLabel
244
+ label="Add new key"
245
+ onChangeValue={(value) => {
246
+ let ourKey = value.trim();
247
+ if (ourKey) {
248
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
249
+ updatedLifeCycle.entries[entryIndex].groupByKeys.push({ ourKey });
250
+ Querysub.onCommitFinished(async () => {
251
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
252
+ });
253
+ }
254
+ }}
255
+ className={css.width(250)}
256
+ />
257
+ </div>
258
+
259
+ <div className={css.vbox(4)}>
260
+ {Object.entries(entry.variables).map(([key, varData]) => {
261
+ let keyExistsInAllOtherEntries = lifeCycle.entries.every((e, idx) => {
262
+ if (idx === entryIndex) return true;
263
+ return key in e.variables;
264
+ });
265
+
266
+ return <div key={key} className={css.hbox(8).pad2(4).bord2(0, 0, 80).hsl(0, 0, 97)}>
267
+ <Button
268
+ hue={160}
269
+ disabled={keyExistsInAllOtherEntries}
270
+ onClick={() => {
271
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
272
+ updatedLifeCycle.entries.forEach((e, idx) => {
273
+ if (idx === entryIndex) return;
274
+ if (!(key in e.variables)) {
275
+ e.variables[key] = {};
276
+ }
277
+ });
278
+ Querysub.onCommitFinished(async () => {
279
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
280
+ });
281
+ }}
282
+ >
283
+ Copy to All
284
+ </Button>
285
+ <span className={css.minWidth(150).boldStyle}>{key}</span>
286
+ <InputLabel
287
+ placeholder="Variable title"
288
+ value={varData.title || ""}
289
+ onChangeValue={(value) => {
290
+ let title = value.trim() || undefined;
291
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
292
+ updatedLifeCycle.entries[entryIndex].variables[key].title = title;
293
+ Querysub.onCommitFinished(async () => {
294
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
295
+ });
296
+ }}
297
+ className={css.width(300)}
298
+ />
299
+ <InputLabel
300
+ placeholder="Aliases (comma-separated)"
301
+ value={varData.aliases && varData.aliases.join(", ") || ""}
302
+ onChangeValue={(value) => {
303
+ let aliases = value.trim() && value.split(",").map(a => a.trim()).filter(a => a) || undefined;
304
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
305
+ updatedLifeCycle.entries.forEach((e) => {
306
+ if (key in e.variables) {
307
+ e.variables[key].aliases = aliases;
308
+ }
309
+ });
310
+ Querysub.onCommitFinished(async () => {
311
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
312
+ });
313
+ }}
314
+ className={css.width(300)}
315
+ />
316
+ <Button
317
+ hue={0}
318
+ onClick={() => {
319
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
320
+ delete updatedLifeCycle.entries[entryIndex].variables[key];
321
+ Querysub.onCommitFinished(async () => {
322
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
323
+ });
324
+ }}
325
+ >
326
+ Delete
327
+ </Button>
328
+ </div>;
329
+ })}
330
+
331
+ <InputLabel
332
+ label="Add new variable key"
333
+ onChangeValue={(value) => {
334
+ let key = value.trim();
335
+ if (key) {
336
+ let updatedLifeCycle = deepCloneJSON(lifeCycle);
337
+ updatedLifeCycle.entries[entryIndex].variables[key] = {};
338
+ Querysub.onCommitFinished(async () => {
339
+ await this.controller.setLifeCycle.promise(updatedLifeCycle);
340
+ });
341
+ }
342
+ }}
343
+ className={css.width(300)}
344
+ />
345
+ </div>
346
+ </div>
347
+ )}
348
+
349
+ {!this.state.editMode && <LifeCycleEntryReadMode
350
+ lifeCycle={lifeCycle}
351
+ entry={entry}
352
+ entryIndex={entryIndex}
353
+ datum={this.props.datum}
354
+ />}
355
+ </div>;
356
+ }
357
+ }
358
+