@oneuptime/common 12.0.7 → 12.0.8

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 (186) hide show
  1. package/Models/DatabaseModels/TelemetryEntity.ts +45 -0
  2. package/Models/DatabaseModels/TelemetryEntityRelationship.ts +26 -0
  3. package/Models/DatabaseModels/WorkflowLog.ts +38 -0
  4. package/Server/API/UserAPI.ts +16 -1
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.ts +41 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.ts +17 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  8. package/Server/Middleware/MasterAdminAuthorization.ts +11 -6
  9. package/Server/Services/TelemetryEntityRelationshipService.ts +3 -0
  10. package/Server/Services/TelemetryEntityService.ts +114 -1
  11. package/Server/Types/Workflow/Components/API/Utils.ts +44 -1
  12. package/Server/Types/Workflow/TriggerCode.ts +12 -0
  13. package/Server/Types/Workflow/Workflow.ts +5 -0
  14. package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +689 -0
  15. package/Server/Utils/VM/VMAPI.ts +56 -8
  16. package/Tests/App/Dashboard/MonitorCriteriaAttributeFilter.test.tsx +574 -0
  17. package/Tests/Server/API/UserProjectsAPI.test.ts +478 -4
  18. package/Tests/Server/Services/TelemetryEntityManualCreate.test.ts +248 -0
  19. package/Tests/Server/Types/Workflow/Components/ApiComponentHeaders.test.ts +192 -0
  20. package/Tests/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.test.ts +152 -0
  21. package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +322 -0
  22. package/Tests/Server/Utils/VM/VMAPISubstitution.test.ts +243 -0
  23. package/Tests/Types/SerializableObjectDictionaryImportCycle.test.ts +197 -0
  24. package/Tests/Types/Telemetry/EntityTypeGroups.test.ts +140 -0
  25. package/Tests/Types/Workflow/StepTrace.test.ts +235 -0
  26. package/Tests/Types/Workflow/TemplateSyntax.test.ts +379 -0
  27. package/Tests/Types/Workflow/Templates.test.ts +403 -0
  28. package/Tests/UI/Components/CustomTimeRangeModal.test.tsx +459 -0
  29. package/Tests/UI/Components/DictionaryAttributeFilterRow.test.tsx +477 -0
  30. package/Tests/UI/Components/Forms/AnchoredFieldPopupKeyboard.test.tsx +382 -0
  31. package/Tests/UI/Components/Forms/ColorPickerPicking.test.tsx +569 -0
  32. package/Tests/UI/Components/Forms/ValidationJSON.test.ts +241 -0
  33. package/Tests/UI/Components/LogTimeRangePicker.test.tsx +42 -0
  34. package/Tests/UI/Components/ModalPortalDismissal.test.tsx +90 -0
  35. package/Tests/UI/Components/TelemetryTimeRangePicker.test.tsx +49 -7
  36. package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +325 -0
  37. package/Tests/UI/Components/Workflow/GraphLint.test.ts +821 -0
  38. package/Tests/UI/Components/Workflow/ModelColumnEditor.test.ts +287 -0
  39. package/Tests/UI/Components/Workflow/ModelColumnEditorServerContract.test.ts +185 -0
  40. package/Tests/UI/Components/Workflow/ModelSchema.test.ts +174 -0
  41. package/Tests/UI/Components/Workflow/RunStatusWatcher.test.ts +152 -0
  42. package/Tests/UI/Components/Workflow/Utils.test.ts +192 -0
  43. package/Tests/UI/Monitor/MonitorStepCriteriaView.test.tsx +432 -0
  44. package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +514 -0
  45. package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +240 -0
  46. package/Tests/Utils/Schema/AnalyticsModelSchema.test.ts +469 -0
  47. package/Tests/Utils/Schema/ModelSchema.test.ts +268 -0
  48. package/Tests/Utils/Telemetry/EntityKeyNonTelemetry.test.ts +193 -0
  49. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +31 -0
  50. package/Types/SerializableObjectDictionary.ts +133 -39
  51. package/Types/Telemetry/EntitySource.ts +40 -0
  52. package/Types/Telemetry/EntityType.ts +27 -0
  53. package/Types/Telemetry/EntityTypeGroups.ts +65 -0
  54. package/Types/Workflow/Component.ts +29 -0
  55. package/Types/Workflow/Components/API.ts +35 -0
  56. package/Types/Workflow/Components/BaseModel.ts +7 -3
  57. package/Types/Workflow/StepTrace.ts +181 -0
  58. package/Types/Workflow/TemplateSyntax.ts +499 -0
  59. package/Types/Workflow/Templates.ts +316 -0
  60. package/UI/Components/Date/CustomTimeRangeModal.tsx +278 -0
  61. package/UI/Components/Date/TimeRangePickerDropdown.tsx +250 -0
  62. package/UI/Components/Dictionary/Dictionary.tsx +23 -6
  63. package/UI/Components/Forms/Fields/ColorPicker.tsx +66 -10
  64. package/UI/Components/Forms/Fields/IconPicker.tsx +48 -10
  65. package/UI/Components/Forms/Types/Field.ts +7 -0
  66. package/UI/Components/Forms/Validation.ts +63 -1
  67. package/UI/Components/Input/Input.tsx +31 -3
  68. package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +11 -216
  69. package/UI/Components/Modal/Modal.tsx +37 -4
  70. package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +11 -210
  71. package/UI/Components/Workflow/ArgumentsForm.tsx +194 -7
  72. package/UI/Components/Workflow/Component.tsx +28 -26
  73. package/UI/Components/Workflow/ComponentSettingsModal.tsx +69 -8
  74. package/UI/Components/Workflow/GraphLint.ts +591 -0
  75. package/UI/Components/Workflow/ModelColumnEditor.tsx +396 -0
  76. package/UI/Components/Workflow/ModelSchema.ts +196 -0
  77. package/UI/Components/Workflow/RunForm.tsx +41 -7
  78. package/UI/Components/Workflow/RunStatusWatcher.ts +122 -0
  79. package/UI/Components/Workflow/StepTraceViewer.tsx +186 -0
  80. package/UI/Components/Workflow/Utils.ts +93 -1
  81. package/UI/Components/Workflow/Workflow.tsx +77 -4
  82. package/UI/Types/LayeredDismissal.ts +31 -0
  83. package/UI/Types/UseAnchoredFieldPopup.ts +99 -0
  84. package/Utils/Monitor/NetworkDeviceRoleUtil.ts +533 -0
  85. package/Utils/Monitor/NetworkTopologyUtil.ts +45 -7
  86. package/Utils/Telemetry/EntityKey.ts +66 -0
  87. package/build/dist/Models/DatabaseModels/TelemetryEntity.js +47 -0
  88. package/build/dist/Models/DatabaseModels/TelemetryEntity.js.map +1 -1
  89. package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js +27 -0
  90. package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js.map +1 -1
  91. package/build/dist/Models/DatabaseModels/WorkflowLog.js +39 -0
  92. package/build/dist/Models/DatabaseModels/WorkflowLog.js.map +1 -1
  93. package/build/dist/Server/API/UserAPI.js +16 -1
  94. package/build/dist/Server/API/UserAPI.js.map +1 -1
  95. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js +20 -0
  96. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js.map +1 -0
  97. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js +12 -0
  98. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js.map +1 -0
  99. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  100. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  101. package/build/dist/Server/Middleware/MasterAdminAuthorization.js +11 -6
  102. package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
  103. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +3 -0
  104. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
  105. package/build/dist/Server/Services/TelemetryEntityService.js +91 -1
  106. package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
  107. package/build/dist/Server/Types/Workflow/Components/API/Utils.js +28 -0
  108. package/build/dist/Server/Types/Workflow/Components/API/Utils.js.map +1 -1
  109. package/build/dist/Server/Types/Workflow/TriggerCode.js.map +1 -1
  110. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +507 -0
  111. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -0
  112. package/build/dist/Server/Utils/VM/VMAPI.js +51 -4
  113. package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
  114. package/build/dist/Types/SerializableObjectDictionary.js +133 -39
  115. package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
  116. package/build/dist/Types/Telemetry/EntitySource.js +39 -0
  117. package/build/dist/Types/Telemetry/EntitySource.js.map +1 -0
  118. package/build/dist/Types/Telemetry/EntityType.js +27 -0
  119. package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
  120. package/build/dist/Types/Telemetry/EntityTypeGroups.js +57 -0
  121. package/build/dist/Types/Telemetry/EntityTypeGroups.js.map +1 -0
  122. package/build/dist/Types/Workflow/Component.js +21 -0
  123. package/build/dist/Types/Workflow/Component.js.map +1 -1
  124. package/build/dist/Types/Workflow/Components/API.js +35 -0
  125. package/build/dist/Types/Workflow/Components/API.js.map +1 -1
  126. package/build/dist/Types/Workflow/Components/BaseModel.js +7 -3
  127. package/build/dist/Types/Workflow/Components/BaseModel.js.map +1 -1
  128. package/build/dist/Types/Workflow/StepTrace.js +104 -0
  129. package/build/dist/Types/Workflow/StepTrace.js.map +1 -0
  130. package/build/dist/Types/Workflow/TemplateSyntax.js +326 -0
  131. package/build/dist/Types/Workflow/TemplateSyntax.js.map +1 -0
  132. package/build/dist/Types/Workflow/Templates.js +218 -0
  133. package/build/dist/Types/Workflow/Templates.js.map +1 -0
  134. package/build/dist/UI/Components/Date/CustomTimeRangeModal.js +126 -0
  135. package/build/dist/UI/Components/Date/CustomTimeRangeModal.js.map +1 -0
  136. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +123 -0
  137. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -0
  138. package/build/dist/UI/Components/Dictionary/Dictionary.js +22 -6
  139. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  140. package/build/dist/UI/Components/Forms/Fields/ColorPicker.js +52 -13
  141. package/build/dist/UI/Components/Forms/Fields/ColorPicker.js.map +1 -1
  142. package/build/dist/UI/Components/Forms/Fields/IconPicker.js +28 -12
  143. package/build/dist/UI/Components/Forms/Fields/IconPicker.js.map +1 -1
  144. package/build/dist/UI/Components/Forms/Validation.js +44 -0
  145. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  146. package/build/dist/UI/Components/Input/Input.js +12 -4
  147. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  148. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +5 -108
  149. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
  150. package/build/dist/UI/Components/Modal/Modal.js +31 -5
  151. package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
  152. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +5 -105
  153. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
  154. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +140 -11
  155. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  156. package/build/dist/UI/Components/Workflow/Component.js +20 -19
  157. package/build/dist/UI/Components/Workflow/Component.js.map +1 -1
  158. package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js +31 -3
  159. package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js.map +1 -1
  160. package/build/dist/UI/Components/Workflow/GraphLint.js +396 -0
  161. package/build/dist/UI/Components/Workflow/GraphLint.js.map +1 -0
  162. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +225 -0
  163. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -0
  164. package/build/dist/UI/Components/Workflow/ModelSchema.js +118 -0
  165. package/build/dist/UI/Components/Workflow/ModelSchema.js.map +1 -0
  166. package/build/dist/UI/Components/Workflow/RunForm.js +22 -6
  167. package/build/dist/UI/Components/Workflow/RunForm.js.map +1 -1
  168. package/build/dist/UI/Components/Workflow/RunStatusWatcher.js +76 -0
  169. package/build/dist/UI/Components/Workflow/RunStatusWatcher.js.map +1 -0
  170. package/build/dist/UI/Components/Workflow/StepTraceViewer.js +76 -0
  171. package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -0
  172. package/build/dist/UI/Components/Workflow/Utils.js +73 -1
  173. package/build/dist/UI/Components/Workflow/Utils.js.map +1 -1
  174. package/build/dist/UI/Components/Workflow/Workflow.js +51 -6
  175. package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
  176. package/build/dist/UI/Types/LayeredDismissal.js +21 -0
  177. package/build/dist/UI/Types/LayeredDismissal.js.map +1 -0
  178. package/build/dist/UI/Types/UseAnchoredFieldPopup.js +74 -1
  179. package/build/dist/UI/Types/UseAnchoredFieldPopup.js.map +1 -1
  180. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +386 -0
  181. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -0
  182. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +35 -6
  183. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  184. package/build/dist/Utils/Telemetry/EntityKey.js +52 -0
  185. package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
  186. package/package.json +1 -1
@@ -0,0 +1,396 @@
1
+ /*
2
+ * A row-based editor for the two workflow arguments that are keyed on a
3
+ * model's columns: the Query that picks which records a component acts on,
4
+ * and the record of values a create/update writes.
5
+ *
6
+ * Both were bare JSON editors, with the column names — and, for a query, the
7
+ * operator syntax — left entirely to memory. The description on those
8
+ * arguments had to carry a hint about "_id" versus "id" precisely because
9
+ * nothing else told the builder what the columns were called.
10
+ *
11
+ * Everything needed to do better already existed: /model-schema/:tableName
12
+ * knows the columns, and DictionaryForm's operator mode emits exactly the
13
+ * {_type, value} shape that JSONFunctions.deserialize hydrates on the server
14
+ * (FindManyBaseModel and friends). This wires the two together, and keeps a
15
+ * JSON escape hatch for the values rows cannot express.
16
+ */
17
+
18
+ import CodeEditor from "../CodeEditor/CodeEditor";
19
+ import ComponentLoader from "../ComponentLoader/ComponentLoader";
20
+ import DictionaryForm, { ValueType } from "../Dictionary/Dictionary";
21
+ import { DictionaryEntryValue } from "../Dictionary/DictionaryFilterOperator";
22
+ import Icon from "../Icon/Icon";
23
+ import {
24
+ ModelSchemaColumn,
25
+ ModelSchemaState,
26
+ findColumn,
27
+ useModelSchema,
28
+ } from "./ModelSchema";
29
+ import CodeType from "../../../Types/Code/CodeType";
30
+ import Dictionary from "../../../Types/Dictionary";
31
+ import IconProp from "../../../Types/Icon/IconProp";
32
+ import { JSONObject, ObjectType } from "../../../Types/JSON";
33
+ import React, {
34
+ FunctionComponent,
35
+ ReactElement,
36
+ useMemo,
37
+ useRef,
38
+ useState,
39
+ } from "react";
40
+
41
+ export enum ModelColumnEditorMode {
42
+ /** Conditions that select records: column, operator, value. */
43
+ Query = "Query",
44
+ /** Values to write: column, value. No operators — you cannot save a ">". */
45
+ Record = "Record",
46
+ }
47
+
48
+ export interface ComponentProps {
49
+ tableName: string;
50
+ mode: ModelColumnEditorMode;
51
+ initialValue?: string | JSONObject | null | undefined;
52
+ onChange: (value: string) => void;
53
+ error?: string | undefined;
54
+ placeholder?: string | undefined;
55
+ tabIndex?: number | undefined;
56
+ }
57
+
58
+ type ViewMode = "builder" | "json";
59
+
60
+ /*
61
+ * Operator wrappers the row editor can show. Anything else in a stored query
62
+ * (an InBetween, a relation sub-object, a raw QueryHelper construct) keeps the
63
+ * JSON editor rather than being silently flattened.
64
+ */
65
+ const REPRESENTABLE_OPERATOR_TYPES: Array<string> = [
66
+ ObjectType.EqualTo,
67
+ ObjectType.NotEqual,
68
+ ObjectType.Search,
69
+ ObjectType.NotContains,
70
+ ObjectType.StartsWith,
71
+ ObjectType.EndsWith,
72
+ ObjectType.GreaterThan,
73
+ ObjectType.GreaterThanOrEqual,
74
+ ObjectType.LessThan,
75
+ ObjectType.LessThanOrEqual,
76
+ ObjectType.IsNull,
77
+ ObjectType.NotNull,
78
+ ObjectType.Includes,
79
+ ObjectType.IncludesNone,
80
+ ];
81
+
82
+ interface ParsedQuery {
83
+ /** The raw text, always kept so the JSON editor can show it verbatim. */
84
+ text: string;
85
+ /** The parsed object, or null when the text is not a JSON object. */
86
+ parsed: JSONObject | null;
87
+ }
88
+
89
+ type NormalizeInitialValueFunction = (
90
+ value: string | JSONObject | null | undefined,
91
+ ) => ParsedQuery;
92
+
93
+ const normalizeInitialValue: NormalizeInitialValueFunction = (
94
+ value: string | JSONObject | null | undefined,
95
+ ): ParsedQuery => {
96
+ if (value === null || value === undefined || value === "") {
97
+ return { text: "", parsed: {} };
98
+ }
99
+
100
+ if (typeof value === "object" && !Array.isArray(value)) {
101
+ return { text: JSON.stringify(value, null, 2), parsed: value };
102
+ }
103
+
104
+ if (typeof value !== "string") {
105
+ return { text: String(value), parsed: null };
106
+ }
107
+
108
+ try {
109
+ const parsed: unknown = JSON.parse(value);
110
+
111
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
112
+ return { text: value, parsed: parsed as JSONObject };
113
+ }
114
+
115
+ return { text: value, parsed: null };
116
+ } catch {
117
+ return { text: value, parsed: null };
118
+ }
119
+ };
120
+
121
+ interface CompatibilityResult {
122
+ compatible: boolean;
123
+ /** Shown to the builder so a locked JSON editor is never unexplained. */
124
+ reasons: Array<string>;
125
+ }
126
+
127
+ type ClassifyCompatibilityFunction = (
128
+ parsed: JSONObject | null,
129
+ columns: Array<ModelSchemaColumn>,
130
+ mode: ModelColumnEditorMode,
131
+ ) => CompatibilityResult;
132
+
133
+ /**
134
+ * Can this value be shown as rows without losing anything?
135
+ *
136
+ * Deliberately conservative: when in doubt the JSON editor stays, because a
137
+ * value silently rewritten into something that means something else is far
138
+ * worse than one the builder has to keep editing as text.
139
+ */
140
+ export const classifyColumnValueCompatibility: ClassifyCompatibilityFunction = (
141
+ parsed: JSONObject | null,
142
+ columns: Array<ModelSchemaColumn>,
143
+ mode: ModelColumnEditorMode,
144
+ ): CompatibilityResult => {
145
+ if (parsed === null) {
146
+ return {
147
+ compatible: false,
148
+ reasons: ["The current value isn't a JSON object."],
149
+ };
150
+ }
151
+
152
+ const reasons: Array<string> = [];
153
+
154
+ for (const key of Object.keys(parsed)) {
155
+ /*
156
+ * Columns are only checked when the schema actually loaded. An unknown key
157
+ * is worth saying out loud — it is the "id" versus "_id" mistake — but it
158
+ * does not force the JSON editor, since the runner accepts column names
159
+ * this endpoint does not surface.
160
+ */
161
+ if (columns.length > 0 && !findColumn(columns, key)) {
162
+ reasons.push(`"${key}" isn't a known column on this model.`);
163
+ }
164
+
165
+ const value: unknown = parsed[key];
166
+
167
+ if (value === null) {
168
+ continue;
169
+ }
170
+
171
+ if (
172
+ typeof value === "string" ||
173
+ typeof value === "number" ||
174
+ typeof value === "boolean"
175
+ ) {
176
+ continue;
177
+ }
178
+
179
+ if (Array.isArray(value)) {
180
+ reasons.push(
181
+ `"${key}" holds a list. Use the "is any of" operator instead, or keep editing as JSON.`,
182
+ );
183
+ continue;
184
+ }
185
+
186
+ if (typeof value === "object") {
187
+ const objectType: unknown = (value as JSONObject)["_type"];
188
+
189
+ if (
190
+ mode === ModelColumnEditorMode.Query &&
191
+ typeof objectType === "string" &&
192
+ REPRESENTABLE_OPERATOR_TYPES.includes(objectType)
193
+ ) {
194
+ continue;
195
+ }
196
+
197
+ if (typeof objectType === "string") {
198
+ reasons.push(`"${key}" uses ${objectType}, which the rows can't show.`);
199
+ continue;
200
+ }
201
+
202
+ reasons.push(
203
+ `"${key}" holds a nested record, which the rows can't show.`,
204
+ );
205
+ continue;
206
+ }
207
+
208
+ reasons.push(`"${key}" has an unexpected value.`);
209
+ }
210
+
211
+ /*
212
+ * An unknown column name is a warning, not a reason to lock the editor —
213
+ * the builder can still fix it in the row that shows it.
214
+ */
215
+ const blocking: Array<string> = reasons.filter((reason: string) => {
216
+ return !reason.includes("isn't a known column");
217
+ });
218
+
219
+ return { compatible: blocking.length === 0, reasons: reasons };
220
+ };
221
+
222
+ type BuildQueryJsonFunction = (
223
+ value: Dictionary<DictionaryEntryValue>,
224
+ ) => string;
225
+
226
+ /**
227
+ * The rows, as the string stored on the workflow argument.
228
+ *
229
+ * Empty collapses to "" rather than "{}" so a required argument still reads as
230
+ * empty to form validation and to the graph linter.
231
+ */
232
+ export const buildColumnValueJson: BuildQueryJsonFunction = (
233
+ value: Dictionary<DictionaryEntryValue>,
234
+ ): string => {
235
+ const entries: JSONObject = {};
236
+
237
+ for (const key of Object.keys(value)) {
238
+ if (key.trim() === "") {
239
+ continue;
240
+ }
241
+
242
+ entries[key] = value[key] as never;
243
+ }
244
+
245
+ if (Object.keys(entries).length === 0) {
246
+ return "";
247
+ }
248
+
249
+ /*
250
+ * The operator wrappers serialize themselves through toJSON() into
251
+ * {_type, value}, which is exactly what the server rehydrates with
252
+ * JSONFunctions.deserialize.
253
+ */
254
+ return JSON.stringify(entries);
255
+ };
256
+
257
+ const ModelColumnEditor: FunctionComponent<ComponentProps> = (
258
+ props: ComponentProps,
259
+ ): ReactElement => {
260
+ const schema: ModelSchemaState = useModelSchema(props.tableName);
261
+ const isQuery: boolean = props.mode === ModelColumnEditorMode.Query;
262
+
263
+ const initial: ParsedQuery = useMemo(() => {
264
+ return normalizeInitialValue(props.initialValue);
265
+ }, []);
266
+
267
+ const [jsonText, setJsonText] = useState<string>(initial.text);
268
+ const [viewMode, setViewMode] = useState<ViewMode>("builder");
269
+
270
+ /*
271
+ * The row editor is uncontrolled after mount (DictionaryForm seeds itself
272
+ * once), so the initial value is captured rather than recomputed.
273
+ */
274
+ const initialRows: React.MutableRefObject<Dictionary<DictionaryEntryValue>> =
275
+ useRef<Dictionary<DictionaryEntryValue>>(
276
+ (initial.parsed || {}) as Dictionary<DictionaryEntryValue>,
277
+ );
278
+
279
+ const compatibility: CompatibilityResult = useMemo(() => {
280
+ return classifyColumnValueCompatibility(
281
+ initial.parsed,
282
+ schema.columns || [],
283
+ props.mode,
284
+ );
285
+ }, [schema.columns, props.mode]);
286
+
287
+ const columnKeys: Array<string> = useMemo(() => {
288
+ return (schema.columns || []).map((column: ModelSchemaColumn) => {
289
+ return column.id;
290
+ });
291
+ }, [schema.columns]);
292
+
293
+ if (schema.isLoading) {
294
+ return <ComponentLoader />;
295
+ }
296
+
297
+ const isLockedToJson: boolean = !compatibility.compatible;
298
+ const effectiveMode: ViewMode = isLockedToJson ? "json" : viewMode;
299
+
300
+ type OnJsonChangeFunction = (value: string) => void;
301
+
302
+ const onJsonChange: OnJsonChangeFunction = (value: string): void => {
303
+ setJsonText(value);
304
+ props.onChange(value);
305
+ };
306
+
307
+ type OnRowsChangeFunction = (value: Dictionary<DictionaryEntryValue>) => void;
308
+
309
+ const onRowsChange: OnRowsChangeFunction = (
310
+ value: Dictionary<DictionaryEntryValue>,
311
+ ): void => {
312
+ const asJson: string = buildColumnValueJson(value);
313
+ setJsonText(asJson);
314
+ props.onChange(asJson);
315
+ };
316
+
317
+ return (
318
+ <div>
319
+ {schema.error && (
320
+ <p className="text-sm text-amber-600 mb-2">
321
+ Couldn&apos;t load this model&apos;s columns ({schema.error}). You can
322
+ still write the query as JSON.
323
+ </p>
324
+ )}
325
+
326
+ {isLockedToJson && compatibility.reasons.length > 0 && (
327
+ <div className="mb-2 rounded-md border border-amber-200 bg-amber-50 p-3">
328
+ <p className="text-sm text-amber-800">Editing as JSON, because:</p>
329
+ <ul className="mt-1 list-disc pl-5 text-sm text-amber-700">
330
+ {compatibility.reasons.map((reason: string, i: number) => {
331
+ return <li key={i}>{reason}</li>;
332
+ })}
333
+ </ul>
334
+ </div>
335
+ )}
336
+
337
+ {!isLockedToJson && compatibility.reasons.length > 0 && (
338
+ <div className="mb-2 rounded-md border border-amber-200 bg-amber-50 p-3">
339
+ <ul className="list-disc pl-5 text-sm text-amber-700">
340
+ {compatibility.reasons.map((reason: string, i: number) => {
341
+ return <li key={i}>{reason}</li>;
342
+ })}
343
+ </ul>
344
+ </div>
345
+ )}
346
+
347
+ {effectiveMode === "builder" ? (
348
+ <DictionaryForm
349
+ keys={columnKeys}
350
+ enableOperators={isQuery}
351
+ valueTypes={[ValueType.Text, ValueType.Number, ValueType.Boolean]}
352
+ addButtonSuffix={isQuery ? "condition" : "field"}
353
+ keyPlaceholder="Column"
354
+ valuePlaceholder="Value"
355
+ initialValue={initialRows.current}
356
+ onChange={onRowsChange}
357
+ />
358
+ ) : (
359
+ <CodeEditor
360
+ type={CodeType.JSON}
361
+ tabIndex={props.tabIndex}
362
+ error={props.error}
363
+ initialValue={initial.text}
364
+ value={jsonText}
365
+ placeholder={props.placeholder}
366
+ onChange={onJsonChange}
367
+ />
368
+ )}
369
+
370
+ {!isLockedToJson && (
371
+ <div className="mt-2">
372
+ <button
373
+ type="button"
374
+ className="text-sm underline text-blue-500 hover:text-blue-600 cursor-pointer inline-flex items-center gap-1"
375
+ onClick={() => {
376
+ setViewMode(effectiveMode === "builder" ? "json" : "builder");
377
+ }}
378
+ >
379
+ <Icon icon={IconProp.Code} className="h-3.5 w-3.5" />
380
+ {effectiveMode === "builder"
381
+ ? "Edit as JSON"
382
+ : isQuery
383
+ ? "Back to conditions"
384
+ : "Back to fields"}
385
+ </button>
386
+ </div>
387
+ )}
388
+
389
+ {props.error && effectiveMode === "builder" && (
390
+ <p className="mt-1 text-sm text-red-400">{props.error}</p>
391
+ )}
392
+ </div>
393
+ );
394
+ };
395
+
396
+ export default ModelColumnEditor;
@@ -0,0 +1,196 @@
1
+ /*
2
+ * Shared access to the workflow /model-schema/:tableName endpoint.
3
+ *
4
+ * Three builder inputs are backed by a model's column list — the field picker
5
+ * for Select arguments, the query builder for Query arguments, and the record
6
+ * form for create/update arguments — and all three want the same fetch, the
7
+ * same loading and error handling, and the same column shape.
8
+ */
9
+
10
+ import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
11
+ import HTTPResponse from "../../../Types/API/HTTPResponse";
12
+ import URL from "../../../Types/API/URL";
13
+ import { JSONObject } from "../../../Types/JSON";
14
+ import { WORKFLOW_URL } from "../../Config";
15
+ import API from "../../Utils/API/API";
16
+ import { useEffect, useState } from "react";
17
+
18
+ /**
19
+ * A column as the endpoint describes it. `type` is deliberately a bare string
20
+ * rather than the server-side TableColumnType enum, so this file stays on the
21
+ * browser side of the wire format.
22
+ */
23
+ export interface ModelSchemaColumn {
24
+ id: string;
25
+ title: string;
26
+ description?: string;
27
+ type: string;
28
+ isRelation: boolean;
29
+ relatedTableName?: string | undefined;
30
+ relatedColumns?: Array<ModelSchemaColumn> | undefined;
31
+ }
32
+
33
+ export interface ModelSchemaState {
34
+ columns: Array<ModelSchemaColumn> | null;
35
+ isLoading: boolean;
36
+ error: string | null;
37
+ }
38
+
39
+ export type FetchModelSchemaFunction = (
40
+ tableName: string,
41
+ ) => Promise<Array<ModelSchemaColumn>>;
42
+
43
+ export const fetchModelSchema: FetchModelSchemaFunction = async (
44
+ tableName: string,
45
+ ): Promise<Array<ModelSchemaColumn>> => {
46
+ const url: URL = URL.fromString(WORKFLOW_URL.toString()).addRoute(
47
+ `/model-schema/${encodeURIComponent(tableName)}`,
48
+ );
49
+
50
+ const result: HTTPResponse<JSONObject> | HTTPErrorResponse =
51
+ await API.get<JSONObject>({ url });
52
+
53
+ if (result instanceof HTTPErrorResponse) {
54
+ throw result;
55
+ }
56
+
57
+ const data: JSONObject = result.data as JSONObject;
58
+
59
+ return (data["columns"] as unknown as Array<ModelSchemaColumn>) || [];
60
+ };
61
+
62
+ export type UseModelSchemaFunction = (
63
+ tableName: string | undefined,
64
+ ) => ModelSchemaState;
65
+
66
+ /**
67
+ * Load a model's columns, once per table name.
68
+ *
69
+ * Never throws: a failure leaves `columns` as an empty array and puts the
70
+ * message in `error`, so a caller can fall back to its JSON editor rather than
71
+ * showing the builder a blank panel.
72
+ */
73
+ export const useModelSchema: UseModelSchemaFunction = (
74
+ tableName: string | undefined,
75
+ ): ModelSchemaState => {
76
+ const [columns, setColumns] = useState<Array<ModelSchemaColumn> | null>(null);
77
+ const [isLoading, setIsLoading] = useState<boolean>(Boolean(tableName));
78
+ const [error, setError] = useState<string | null>(null);
79
+
80
+ useEffect(() => {
81
+ if (!tableName) {
82
+ setColumns([]);
83
+ setIsLoading(false);
84
+ return;
85
+ }
86
+
87
+ let cancelled: boolean = false;
88
+
89
+ const load: () => Promise<void> = async (): Promise<void> => {
90
+ setIsLoading(true);
91
+ setError(null);
92
+
93
+ try {
94
+ const loaded: Array<ModelSchemaColumn> =
95
+ await fetchModelSchema(tableName);
96
+
97
+ if (cancelled) {
98
+ return;
99
+ }
100
+
101
+ setColumns(loaded);
102
+ } catch (err) {
103
+ if (cancelled) {
104
+ return;
105
+ }
106
+
107
+ setError(API.getFriendlyMessage(err));
108
+ setColumns([]);
109
+ } finally {
110
+ if (!cancelled) {
111
+ setIsLoading(false);
112
+ }
113
+ }
114
+ };
115
+
116
+ void load();
117
+
118
+ return () => {
119
+ cancelled = true;
120
+ };
121
+ }, [tableName]);
122
+
123
+ return { columns: columns, isLoading: isLoading, error: error };
124
+ };
125
+
126
+ /*
127
+ * Column types that hold a single scalar the builder can sensibly type into a
128
+ * box. Everything else (relations, JSON blobs, buffers) needs the JSON editor.
129
+ */
130
+ const SCALAR_COLUMN_TYPES: Array<string> = [
131
+ "ShortText",
132
+ "LongText",
133
+ "VeryLongText",
134
+ "Slug",
135
+ "Email",
136
+ "Phone",
137
+ "Color",
138
+ "Domain",
139
+ "Name",
140
+ "Description",
141
+ "ObjectID",
142
+ "Number",
143
+ "PositiveNumber",
144
+ "Boolean",
145
+ "Date",
146
+ "Password",
147
+ "HashedString",
148
+ "Port",
149
+ "Hostname",
150
+ "URL",
151
+ "Route",
152
+ "IP",
153
+ "Version",
154
+ "Markdown",
155
+ "HTML",
156
+ "JavaScript",
157
+ "CSS",
158
+ ];
159
+
160
+ export type IsScalarColumnFunction = (column: ModelSchemaColumn) => boolean;
161
+
162
+ export const isScalarColumn: IsScalarColumnFunction = (
163
+ column: ModelSchemaColumn,
164
+ ): boolean => {
165
+ return !column.isRelation && SCALAR_COLUMN_TYPES.includes(column.type);
166
+ };
167
+
168
+ export type IsNumericColumnFunction = (column: ModelSchemaColumn) => boolean;
169
+
170
+ export const isNumericColumn: IsNumericColumnFunction = (
171
+ column: ModelSchemaColumn,
172
+ ): boolean => {
173
+ return column.type === "Number" || column.type === "PositiveNumber";
174
+ };
175
+
176
+ export type IsBooleanColumnFunction = (column: ModelSchemaColumn) => boolean;
177
+
178
+ export const isBooleanColumn: IsBooleanColumnFunction = (
179
+ column: ModelSchemaColumn,
180
+ ): boolean => {
181
+ return column.type === "Boolean";
182
+ };
183
+
184
+ export type FindColumnFunction = (
185
+ columns: Array<ModelSchemaColumn>,
186
+ columnId: string,
187
+ ) => ModelSchemaColumn | undefined;
188
+
189
+ export const findColumn: FindColumnFunction = (
190
+ columns: Array<ModelSchemaColumn>,
191
+ columnId: string,
192
+ ): ModelSchemaColumn | undefined => {
193
+ return columns.find((column: ModelSchemaColumn) => {
194
+ return column.id === columnId;
195
+ });
196
+ };
@@ -1,9 +1,16 @@
1
1
  import ErrorMessage from "../ErrorMessage/ErrorMessage";
2
2
  import BasicForm, { FormProps } from "../Forms/BasicForm";
3
3
  import FormValues from "../Forms/Types/FormValues";
4
- import { componentInputTypeToFormFieldType } from "./Utils";
4
+ import {
5
+ componentInputTypeToFormFieldType,
6
+ parseStringDictionaryValue,
7
+ } from "./Utils";
5
8
  import { JSONObject } from "../../../Types/JSON";
6
- import { Argument, NodeDataProp } from "../../../Types/Workflow/Component";
9
+ import {
10
+ Argument,
11
+ ComponentInputType,
12
+ NodeDataProp,
13
+ } from "../../../Types/Workflow/Component";
7
14
  import React, {
8
15
  FunctionComponent,
9
16
  ReactElement,
@@ -34,6 +41,29 @@ const RunForm: FunctionComponent<ComponentProps> = (
34
41
  props.onFormChange(component);
35
42
  }, [component]);
36
43
 
44
+ /*
45
+ * Same hydration ArgumentsForm does: a StringDictionary renders as key/value
46
+ * rows, and those need a real object rather than the JSON string older
47
+ * workflows stored. The field type is chosen from the same value, so the two
48
+ * cannot disagree about which editor is on screen.
49
+ */
50
+ const formInitialValues: JSONObject = { ...(component.arguments || {}) };
51
+
52
+ for (const argument of component.metadata.runWorkflowManuallyArguments ||
53
+ []) {
54
+ if (argument.type !== ComponentInputType.StringDictionary) {
55
+ continue;
56
+ }
57
+
58
+ const parsed: JSONObject | null = parseStringDictionaryValue(
59
+ formInitialValues[argument.id],
60
+ );
61
+
62
+ if (parsed !== null) {
63
+ formInitialValues[argument.id] = parsed;
64
+ }
65
+ }
66
+
37
67
  return (
38
68
  <div className="mb-3 mt-3">
39
69
  <div className="mt-5 mb-5">
@@ -57,7 +87,7 @@ const RunForm: FunctionComponent<ComponentProps> = (
57
87
  hideSubmitButton={true}
58
88
  ref={formRef}
59
89
  initialValues={{
60
- ...(component.arguments || {}),
90
+ ...formInitialValues,
61
91
  }}
62
92
  onChange={(values: FormValues<JSONObject>) => {
63
93
  setComponent({
@@ -86,12 +116,16 @@ const RunForm: FunctionComponent<ComponentProps> = (
86
116
  },
87
117
  required: argument.required,
88
118
  placeholder: argument.placeholder,
119
+ /*
120
+ * Decided from the value the form actually renders.
121
+ * This used to read component.returnValues, which is not
122
+ * where RunForm writes — so for a field holding a value
123
+ * the row editor cannot represent, the editor on screen
124
+ * and the value behind it could disagree.
125
+ */
89
126
  ...componentInputTypeToFormFieldType(
90
127
  argument.type,
91
- component.returnValues &&
92
- component.returnValues[argument.id]
93
- ? component.returnValues[argument.id]
94
- : null,
128
+ formInitialValues[argument.id] ?? null,
95
129
  ),
96
130
  };
97
131
  },