@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,477 @@
1
+ import DictionaryForm, {
2
+ ValueType,
3
+ } from "../../../UI/Components/Dictionary/Dictionary";
4
+ import { DictionaryEntryValue } from "../../../UI/Components/Dictionary/DictionaryFilterOperator";
5
+ import Includes from "../../../Types/BaseDatabase/Includes";
6
+ import IsNull from "../../../Types/BaseDatabase/IsNull";
7
+ import Dictionary from "../../../Types/Dictionary";
8
+ /*
9
+ * The main entry, not "/extend-expect": the latter no longer ships type
10
+ * declarations, so every jest-dom matcher in this file fails to typecheck and
11
+ * the whole suite is skipped before a single assertion runs.
12
+ */
13
+ import "@testing-library/jest-dom";
14
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
15
+ import React from "react";
16
+ import { afterEach, describe, expect, test } from "@jest/globals";
17
+
18
+ /*
19
+ * DictionaryForm is what "Filter by Attributes" is made of: FormField renders
20
+ * it for every FormFieldSchemaType.Dictionary field, and the only two fields in
21
+ * the app with that type are the attribute filters on the Log and Trace monitor
22
+ * steps — the criteria editor. So a row that misbehaves here misbehaves in
23
+ * exactly one user-visible place, and that place is the monitoring criteria
24
+ * modal.
25
+ *
26
+ * These tests cover the row lifecycle a user actually walks through: add a row,
27
+ * pick a key off the suggestion list, choose an operator and a type, type a
28
+ * value, delete it again. The regressions they pin are the ones that used to
29
+ * take the criteria modal down with them; see the "clearing a Number value"
30
+ * block for the specific defect.
31
+ */
32
+
33
+ const ATTRIBUTE_KEYS: Array<string> = [
34
+ "{OriginalFormat}",
35
+ "Action",
36
+ "ActionId",
37
+ "ActionName",
38
+ "ActionType",
39
+ "AlreadyCancelled",
40
+ ];
41
+
42
+ const ADD_BUTTON_TITLE: string = "Add Filter by Attributes";
43
+
44
+ /*
45
+ * AutocompleteTextInput and react-select both report role="combobox", so the
46
+ * only stable way to single out the hand-rolled ones is the listbox they own.
47
+ */
48
+ type AutocompleteInputsFunction = () => Array<HTMLInputElement>;
49
+
50
+ const autocompleteInputs: AutocompleteInputsFunction =
51
+ (): Array<HTMLInputElement> => {
52
+ return Array.from(
53
+ document.querySelectorAll<HTMLInputElement>(
54
+ 'input[aria-controls^="autocomplete-suggestions-"]',
55
+ ),
56
+ );
57
+ };
58
+
59
+ type KeyInputFunction = (rowIndex?: number) => HTMLInputElement;
60
+
61
+ const keyInput: KeyInputFunction = (rowIndex: number = 0): HTMLInputElement => {
62
+ /*
63
+ * A Text row contributes two autocompletes (key and value) and a Number or
64
+ * Boolean row only one (the key), so rows cannot be indexed by a fixed
65
+ * stride. Walking the labelled columns instead survives either shape.
66
+ */
67
+ const keyInputs: Array<HTMLInputElement> = autocompleteInputs().filter(
68
+ (input: HTMLInputElement) => {
69
+ return input.getAttribute("placeholder") !== "Value";
70
+ },
71
+ );
72
+
73
+ return keyInputs[rowIndex]!;
74
+ };
75
+
76
+ type NumberInputsFunction = () => Array<HTMLInputElement>;
77
+
78
+ const numberInputs: NumberInputsFunction = (): Array<HTMLInputElement> => {
79
+ return Array.from(
80
+ document.querySelectorAll<HTMLInputElement>('input[type="number"]'),
81
+ );
82
+ };
83
+
84
+ /*
85
+ * react-select opens on ArrowDown and its options are portalled to
86
+ * document.body, so they are reachable by role from `screen` but never from
87
+ * inside the form's own subtree.
88
+ */
89
+ type SelectOptionFunction = (combobox: HTMLElement, optionText: string) => void;
90
+
91
+ const selectOption: SelectOptionFunction = (
92
+ combobox: HTMLElement,
93
+ optionText: string,
94
+ ): void => {
95
+ fireEvent.keyDown(combobox, { key: "ArrowDown" });
96
+ const option: HTMLElement = screen.getByText(optionText);
97
+ fireEvent.mouseDown(option);
98
+ fireEvent.click(option);
99
+ };
100
+
101
+ type ComboboxesFunction = () => Array<HTMLElement>;
102
+
103
+ const comboboxes: ComboboxesFunction = (): Array<HTMLElement> => {
104
+ return screen.getAllByRole("combobox");
105
+ };
106
+
107
+ /*
108
+ * Column order inside a row, with operators enabled: key autocomplete,
109
+ * operator select, type select, value. Only the two selects are react-select.
110
+ */
111
+ type TypeSelectFunction = () => HTMLElement;
112
+
113
+ const typeSelect: TypeSelectFunction = (): HTMLElement => {
114
+ return comboboxes()[2]!;
115
+ };
116
+
117
+ type OperatorSelectFunction = () => HTMLElement;
118
+
119
+ const operatorSelect: OperatorSelectFunction = (): HTMLElement => {
120
+ return comboboxes()[1]!;
121
+ };
122
+
123
+ interface RenderResult {
124
+ emitted: Array<Dictionary<DictionaryEntryValue>>;
125
+ lastEmitted: () => Dictionary<DictionaryEntryValue> | undefined;
126
+ }
127
+
128
+ type RenderRowFunction = (
129
+ initialValue?: Dictionary<DictionaryEntryValue>,
130
+ ) => RenderResult;
131
+
132
+ const renderDictionary: RenderRowFunction = (
133
+ initialValue?: Dictionary<DictionaryEntryValue>,
134
+ ): RenderResult => {
135
+ const emitted: Array<Dictionary<DictionaryEntryValue>> = [];
136
+
137
+ render(
138
+ <DictionaryForm
139
+ keys={ATTRIBUTE_KEYS}
140
+ enableOperators={true}
141
+ valueTypes={[ValueType.Text, ValueType.Number, ValueType.Boolean]}
142
+ addButtonSuffix="Filter by Attributes"
143
+ keyPlaceholder="Key"
144
+ valuePlaceholder="Value"
145
+ initialValue={initialValue || {}}
146
+ onChange={(value: Dictionary<DictionaryEntryValue>) => {
147
+ emitted.push(value);
148
+ }}
149
+ />,
150
+ );
151
+
152
+ return {
153
+ emitted: emitted,
154
+ lastEmitted: () => {
155
+ return emitted.length > 0 ? emitted[emitted.length - 1] : undefined;
156
+ },
157
+ };
158
+ };
159
+
160
+ type AddRowFunction = () => void;
161
+
162
+ const addRow: AddRowFunction = (): void => {
163
+ fireEvent.click(screen.getByText(ADD_BUTTON_TITLE));
164
+ };
165
+
166
+ describe("Dictionary — the 'Filter by Attributes' row", () => {
167
+ afterEach(() => {
168
+ cleanup();
169
+ });
170
+
171
+ describe("picking a key off the suggestion list", () => {
172
+ test("adding a row renders an empty key, operator, type and value", () => {
173
+ renderDictionary();
174
+
175
+ expect(autocompleteInputs()).toHaveLength(0);
176
+
177
+ addRow();
178
+
179
+ expect(keyInput().value).toBe("");
180
+ expect(screen.getByText("Key")).toBeInTheDocument();
181
+ expect(screen.getByText("Operator")).toBeInTheDocument();
182
+ expect(screen.getByText("Type")).toBeInTheDocument();
183
+ expect(screen.getByText("Value")).toBeInTheDocument();
184
+ });
185
+
186
+ test("focusing the key input offers every known attribute key", () => {
187
+ renderDictionary();
188
+ addRow();
189
+
190
+ fireEvent.focus(keyInput());
191
+
192
+ ATTRIBUTE_KEYS.forEach((attributeKey: string) => {
193
+ expect(screen.getByText(attributeKey)).toBeInTheDocument();
194
+ });
195
+ });
196
+
197
+ test("picking a key fills the input and emits it", () => {
198
+ const rendered: RenderResult = renderDictionary();
199
+ addRow();
200
+
201
+ fireEvent.focus(keyInput());
202
+ fireEvent.mouseDown(screen.getByText("ActionName"));
203
+ fireEvent.click(screen.getByText("ActionName"));
204
+
205
+ expect(keyInput().value).toBe("ActionName");
206
+ expect(rendered.lastEmitted()).toEqual({ ActionName: "" });
207
+ });
208
+
209
+ test("typing narrows the suggestions to matching keys", () => {
210
+ renderDictionary();
211
+ addRow();
212
+
213
+ fireEvent.focus(keyInput());
214
+ fireEvent.change(keyInput(), { target: { value: "ActionN" } });
215
+
216
+ expect(screen.getByText("ActionName")).toBeInTheDocument();
217
+ expect(screen.queryByText("AlreadyCancelled")).not.toBeInTheDocument();
218
+ });
219
+
220
+ test("a key and a value round-trip into the emitted dictionary", () => {
221
+ const rendered: RenderResult = renderDictionary();
222
+ addRow();
223
+
224
+ fireEvent.focus(keyInput());
225
+ fireEvent.click(screen.getByText("ActionName"));
226
+
227
+ const valueInput: HTMLInputElement = autocompleteInputs().filter(
228
+ (input: HTMLInputElement) => {
229
+ return input.getAttribute("placeholder") === "Value";
230
+ },
231
+ )[0]!;
232
+ fireEvent.change(valueInput, { target: { value: "checkout" } });
233
+
234
+ expect(rendered.lastEmitted()).toEqual({ ActionName: "checkout" });
235
+ });
236
+ });
237
+
238
+ /*
239
+ * The regression this file exists for.
240
+ *
241
+ * The Number value input used to `delete newData[index]` when the box was
242
+ * emptied. Two things follow from deleting an array member rather than
243
+ * emptying it, and a user hit both in a row:
244
+ *
245
+ * 1. The row itself disappeared. Someone who cleared a number to retype it
246
+ * watched the whole filter — key, operator and all — vanish.
247
+ * 2. `delete` leaves a *hole*, and `Array.prototype.map` skips holes while
248
+ * the spread in `[...data, newItem]` materialises them as `undefined`.
249
+ * So the next "Add Filter by Attributes" click rendered an `undefined`
250
+ * row and threw `Cannot read properties of undefined (reading
251
+ * 'operator')`, taking the criteria modal down with it.
252
+ */
253
+ describe("clearing a Number value", () => {
254
+ type AddNumberRowFunction = () => void;
255
+
256
+ const addNumberRow: AddNumberRowFunction = (): void => {
257
+ addRow();
258
+ fireEvent.focus(keyInput());
259
+ fireEvent.click(screen.getByText("ActionId"));
260
+ selectOption(typeSelect(), "Number");
261
+ };
262
+
263
+ test("keeps the row instead of deleting it", () => {
264
+ renderDictionary();
265
+ addNumberRow();
266
+
267
+ fireEvent.change(numberInputs()[0]!, { target: { value: "5" } });
268
+ expect(numberInputs()).toHaveLength(1);
269
+
270
+ fireEvent.change(numberInputs()[0]!, { target: { value: "" } });
271
+
272
+ expect(numberInputs()).toHaveLength(1);
273
+ expect(keyInput().value).toBe("ActionId");
274
+ });
275
+
276
+ test("drops the key from the emitted dictionary", () => {
277
+ const rendered: RenderResult = renderDictionary();
278
+ addNumberRow();
279
+
280
+ fireEvent.change(numberInputs()[0]!, { target: { value: "5" } });
281
+ expect(rendered.lastEmitted()).toEqual({ ActionId: 5 });
282
+
283
+ fireEvent.change(numberInputs()[0]!, { target: { value: "" } });
284
+
285
+ expect(rendered.lastEmitted()).toEqual({});
286
+ });
287
+
288
+ test("does not stop the user retyping a number", () => {
289
+ const rendered: RenderResult = renderDictionary();
290
+ addNumberRow();
291
+
292
+ fireEvent.change(numberInputs()[0]!, { target: { value: "5" } });
293
+ fireEvent.change(numberInputs()[0]!, { target: { value: "" } });
294
+ fireEvent.change(numberInputs()[0]!, { target: { value: "42" } });
295
+
296
+ expect(rendered.lastEmitted()).toEqual({ ActionId: 42 });
297
+ });
298
+
299
+ test("leaves the form able to add another row", () => {
300
+ renderDictionary();
301
+ addNumberRow();
302
+
303
+ fireEvent.change(numberInputs()[0]!, { target: { value: "5" } });
304
+ fireEvent.change(numberInputs()[0]!, { target: { value: "" } });
305
+
306
+ // This is the click that used to render an `undefined` row and throw.
307
+ addRow();
308
+
309
+ expect(keyInput(0).value).toBe("ActionId");
310
+ expect(keyInput(1).value).toBe("");
311
+ expect(screen.getByText(ADD_BUTTON_TITLE)).toBeInTheDocument();
312
+ });
313
+
314
+ test("leaves the surviving row deletable", () => {
315
+ renderDictionary();
316
+ addNumberRow();
317
+
318
+ fireEvent.change(numberInputs()[0]!, { target: { value: "" } });
319
+
320
+ fireEvent.click(screen.getByTestId("delete-ActionId"));
321
+
322
+ expect(autocompleteInputs()).toHaveLength(0);
323
+ });
324
+ });
325
+
326
+ describe("switching the value type", () => {
327
+ test("an untouched Number row contributes nothing to the dictionary", () => {
328
+ const rendered: RenderResult = renderDictionary();
329
+ addRow();
330
+
331
+ fireEvent.focus(keyInput());
332
+ fireEvent.click(screen.getByText("ActionId"));
333
+ selectOption(typeSelect(), "Number");
334
+
335
+ /*
336
+ * Switching type blanks the value. Emitting `{ ActionId: "" }` would ship
337
+ * an empty string where the query expects a number.
338
+ */
339
+ expect(rendered.lastEmitted()).toEqual({});
340
+ });
341
+
342
+ test("a Boolean row emits a boolean", () => {
343
+ const rendered: RenderResult = renderDictionary();
344
+ addRow();
345
+
346
+ fireEvent.focus(keyInput());
347
+ fireEvent.click(screen.getByText("AlreadyCancelled"));
348
+ selectOption(typeSelect(), "Boolean");
349
+
350
+ expect(rendered.lastEmitted()).toEqual({ AlreadyCancelled: true });
351
+
352
+ selectOption(comboboxes()[3]!, "False");
353
+
354
+ expect(rendered.lastEmitted()).toEqual({ AlreadyCancelled: false });
355
+ });
356
+
357
+ test("going back to Text restores the value autocomplete", () => {
358
+ renderDictionary();
359
+ addRow();
360
+
361
+ fireEvent.focus(keyInput());
362
+ fireEvent.click(screen.getByText("ActionId"));
363
+
364
+ selectOption(typeSelect(), "Number");
365
+ expect(numberInputs()).toHaveLength(1);
366
+
367
+ selectOption(typeSelect(), "Text");
368
+ expect(numberInputs()).toHaveLength(0);
369
+ expect(keyInput().value).toBe("ActionId");
370
+ });
371
+ });
372
+
373
+ describe("operators", () => {
374
+ test("a value-less operator hides the value input and emits the operator", () => {
375
+ const rendered: RenderResult = renderDictionary();
376
+ addRow();
377
+
378
+ fireEvent.focus(keyInput());
379
+ fireEvent.click(screen.getByText("Action"));
380
+
381
+ selectOption(operatorSelect(), "is empty");
382
+
383
+ /*
384
+ * "is empty" has nothing to type into, so the value box is inert (the
385
+ * shared Input renders a disabled field as readOnly).
386
+ */
387
+ expect(
388
+ (screen.getByPlaceholderText("is empty") as HTMLInputElement).readOnly,
389
+ ).toBe(true);
390
+ expect(rendered.lastEmitted()!["Action"]).toBeInstanceOf(IsNull);
391
+ });
392
+
393
+ test("switching to a multi-value operator resets a scalar value", () => {
394
+ const rendered: RenderResult = renderDictionary();
395
+ addRow();
396
+
397
+ fireEvent.focus(keyInput());
398
+ fireEvent.click(screen.getByText("Action"));
399
+
400
+ const valueInput: HTMLInputElement = autocompleteInputs().filter(
401
+ (input: HTMLInputElement) => {
402
+ return input.getAttribute("placeholder") === "Value";
403
+ },
404
+ )[0]!;
405
+ fireEvent.change(valueInput, { target: { value: "checkout" } });
406
+ expect(rendered.lastEmitted()).toEqual({ Action: "checkout" });
407
+
408
+ selectOption(operatorSelect(), "is any of");
409
+
410
+ /*
411
+ * The scalar has to go: shipping "checkout" where the query now expects
412
+ * a list would be a stale value in the wrong shape.
413
+ */
414
+ const emittedValue: DictionaryEntryValue =
415
+ rendered.lastEmitted()!["Action"]!;
416
+ expect(emittedValue).toBeInstanceOf(Includes);
417
+ expect((emittedValue as Includes).values).toEqual([]);
418
+ });
419
+ });
420
+
421
+ describe("several rows", () => {
422
+ test("each row keeps its own key", () => {
423
+ const rendered: RenderResult = renderDictionary();
424
+
425
+ addRow();
426
+ fireEvent.focus(keyInput(0));
427
+ fireEvent.click(screen.getByText("Action"));
428
+
429
+ addRow();
430
+ fireEvent.focus(keyInput(1));
431
+ fireEvent.click(screen.getByText("ActionType"));
432
+
433
+ expect(keyInput(0).value).toBe("Action");
434
+ expect(keyInput(1).value).toBe("ActionType");
435
+ expect(rendered.lastEmitted()).toEqual({ Action: "", ActionType: "" });
436
+ });
437
+
438
+ test("deleting the first row leaves the second intact", () => {
439
+ const rendered: RenderResult = renderDictionary();
440
+
441
+ addRow();
442
+ fireEvent.focus(keyInput(0));
443
+ fireEvent.click(screen.getByText("Action"));
444
+
445
+ addRow();
446
+ fireEvent.focus(keyInput(1));
447
+ fireEvent.click(screen.getByText("ActionType"));
448
+
449
+ fireEvent.click(screen.getByTestId("delete-Action"));
450
+
451
+ expect(keyInput(0).value).toBe("ActionType");
452
+ expect(rendered.lastEmitted()).toEqual({ ActionType: "" });
453
+ });
454
+ });
455
+
456
+ describe("existing filters", () => {
457
+ test("an initial dictionary is rendered as rows", () => {
458
+ renderDictionary({ Action: "checkout", ActionId: 7 });
459
+
460
+ expect(keyInput(0).value).toBe("Action");
461
+ expect(keyInput(1).value).toBe("ActionId");
462
+ expect(numberInputs()[0]!.value).toBe("7");
463
+ });
464
+
465
+ test("clearing an existing Number filter keeps its row", () => {
466
+ const rendered: RenderResult = renderDictionary({
467
+ Action: "checkout",
468
+ ActionId: 7,
469
+ });
470
+
471
+ fireEvent.change(numberInputs()[0]!, { target: { value: "" } });
472
+
473
+ expect(keyInput(1).value).toBe("ActionId");
474
+ expect(rendered.lastEmitted()).toEqual({ Action: "checkout" });
475
+ });
476
+ });
477
+ });