@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,241 @@
1
+ import Validation from "../../../../UI/Components/Forms/Validation";
2
+ import Field from "../../../../UI/Components/Forms/Types/Field";
3
+ import FormFieldSchemaType from "../../../../UI/Components/Forms/Types/FormFieldSchemaType";
4
+ import FormValues from "../../../../UI/Components/Forms/Types/FormValues";
5
+ import Dictionary from "../../../../Types/Dictionary";
6
+ import { JSONObject, JSONValue } from "../../../../Types/JSON";
7
+ import { describe, expect, test } from "@jest/globals";
8
+
9
+ interface TestEntity extends JSONObject {
10
+ config?: JSONValue;
11
+ name?: JSONValue;
12
+ }
13
+
14
+ type MakeFieldFunction = (
15
+ overrides?: Partial<Field<TestEntity>>,
16
+ ) => Field<TestEntity>;
17
+
18
+ const makeJSONField: MakeFieldFunction = (
19
+ overrides?: Partial<Field<TestEntity>>,
20
+ ): Field<TestEntity> => {
21
+ return {
22
+ title: "Config",
23
+ name: "config",
24
+ field: { config: true },
25
+ fieldType: FormFieldSchemaType.JSON,
26
+ ...overrides,
27
+ } as Field<TestEntity>;
28
+ };
29
+
30
+ type ValidateValueFunction = (
31
+ value: unknown,
32
+ field?: Field<TestEntity> | undefined,
33
+ ) => string | null;
34
+
35
+ const validateValue: ValidateValueFunction = (
36
+ value: unknown,
37
+ field?: Field<TestEntity> | undefined,
38
+ ): string | null => {
39
+ return Validation.validateJSONSyntax(
40
+ value as never,
41
+ field || makeJSONField(),
42
+ );
43
+ };
44
+
45
+ describe("Validation.validateJSONSyntax — only acts on JSON fields", () => {
46
+ test("ignores every other field type", () => {
47
+ const textField: Field<TestEntity> = makeJSONField({
48
+ fieldType: FormFieldSchemaType.Text,
49
+ });
50
+
51
+ expect(validateValue("definitely not json", textField)).toBeNull();
52
+ });
53
+
54
+ test("names the field in the message", () => {
55
+ const message: string | null = validateValue('{"a":1,}');
56
+
57
+ expect(message).toMatch(/^Config is not valid JSON\./);
58
+ });
59
+ });
60
+
61
+ describe("Validation.validateJSONSyntax — catches the real mistakes", () => {
62
+ test("a trailing comma", () => {
63
+ expect(validateValue('{"a": 1,}')).toBeTruthy();
64
+ });
65
+
66
+ test("an unclosed brace", () => {
67
+ expect(validateValue('{"a": 1')).toBeTruthy();
68
+ });
69
+
70
+ test("unquoted keys and single quotes", () => {
71
+ expect(validateValue("{a: 1}")).toBeTruthy();
72
+ expect(validateValue("{'a': 1}")).toBeTruthy();
73
+ });
74
+
75
+ test("plain text in a JSON box", () => {
76
+ expect(validateValue("hello")).toBeTruthy();
77
+ });
78
+ });
79
+
80
+ describe("Validation.validateJSONSyntax — never fires on a working value", () => {
81
+ test("well-formed JSON", () => {
82
+ expect(validateValue('{"a": 1}')).toBeNull();
83
+ expect(validateValue("[1,2,3]")).toBeNull();
84
+ expect(validateValue('"a string"')).toBeNull();
85
+ });
86
+
87
+ /*
88
+ * The values that made a naive JSON.parse check unshippable. Each is a real
89
+ * thing a workflow author writes.
90
+ */
91
+ test("a whole-field template", () => {
92
+ expect(
93
+ validateValue(
94
+ "{{local.components.api-get-1.returnValues.response-body}}",
95
+ ),
96
+ ).toBeNull();
97
+ });
98
+
99
+ test("a template standing in for a bare value", () => {
100
+ expect(validateValue('{"retries": {{local.variables.count}}}')).toBeNull();
101
+ });
102
+
103
+ test("a template inside a string", () => {
104
+ expect(
105
+ validateValue('{"auth": "Bearer {{local.variables.token}}"}'),
106
+ ).toBeNull();
107
+ });
108
+
109
+ test("a loop, whose shape is only known at run time", () => {
110
+ expect(
111
+ validateValue(
112
+ '[{{#each local.components.a.returnValues.items}}{"id":"{{this}}"},{{/each}}]',
113
+ ),
114
+ ).toBeNull();
115
+ });
116
+
117
+ /*
118
+ * The seven model-backed forms elsewhere in the product bind real arrays and
119
+ * objects to a JSON field. Those must never be judged as text — this is the
120
+ * regression the raw-value plumbing exists to prevent.
121
+ */
122
+ test("an already-parsed object", () => {
123
+ expect(validateValue({ a: 1 })).toBeNull();
124
+ });
125
+
126
+ test("an already-parsed array", () => {
127
+ expect(validateValue(["https://a.example.com"])).toBeNull();
128
+ });
129
+
130
+ test("empty, so the required-field message stands instead", () => {
131
+ expect(validateValue("")).toBeNull();
132
+ expect(validateValue(" ")).toBeNull();
133
+ expect(validateValue(undefined)).toBeNull();
134
+ expect(validateValue(null)).toBeNull();
135
+ });
136
+ });
137
+
138
+ describe("Validation.validateJSONSyntax — allowJSON5", () => {
139
+ /*
140
+ * Some workflow argument types are read back with JSONFunctions.parse, which
141
+ * is JSON5. Judging those by JSON's rules would report a mistake in a
142
+ * workflow that runs perfectly well.
143
+ */
144
+ const looseField: Field<TestEntity> = makeJSONField({ allowJSON5: true });
145
+
146
+ test("accepts unquoted keys, single quotes and a trailing comma", () => {
147
+ expect(validateValue("{a: 1}", looseField)).toBeNull();
148
+ expect(validateValue("{'a': 1}", looseField)).toBeNull();
149
+ expect(validateValue('{"a": 1,}', looseField)).toBeNull();
150
+ });
151
+
152
+ test("still rejects text that is not parseable at all", () => {
153
+ expect(validateValue("{oops", looseField)).toBeTruthy();
154
+ expect(validateValue("hello", looseField)).toBeTruthy();
155
+ });
156
+
157
+ test("is off by default, so JSON fields stay strict", () => {
158
+ expect(validateValue('{"a": 1,}')).toBeTruthy();
159
+ });
160
+ });
161
+
162
+ describe("Validation.validate — end to end through the form validator", () => {
163
+ type RunValidateFunction = (
164
+ values: FormValues<TestEntity>,
165
+ fields: Array<Field<TestEntity>>,
166
+ ) => Dictionary<string>;
167
+
168
+ const runValidate: RunValidateFunction = (
169
+ values: FormValues<TestEntity>,
170
+ fields: Array<Field<TestEntity>>,
171
+ ): Dictionary<string> => {
172
+ return Validation.validate<TestEntity>({
173
+ formFields: fields,
174
+ values: values,
175
+ onValidate: undefined,
176
+ });
177
+ };
178
+
179
+ test("surfaces a JSON error against the field name", () => {
180
+ const errors: Dictionary<string> = runValidate(
181
+ { config: '{"a":1,}' } as FormValues<TestEntity>,
182
+ [makeJSONField()],
183
+ );
184
+
185
+ expect(errors["config"]).toMatch(/not valid JSON/);
186
+ });
187
+
188
+ test("says required, not malformed, when the box is empty", () => {
189
+ const errors: Dictionary<string> = runValidate(
190
+ { config: "" } as FormValues<TestEntity>,
191
+ [makeJSONField({ required: true })],
192
+ );
193
+
194
+ expect(errors["config"]).toMatch(/required/i);
195
+ expect(errors["config"]).not.toMatch(/not valid JSON/);
196
+ });
197
+
198
+ test("does not flag an object bound to a JSON field", () => {
199
+ const errors: Dictionary<string> = runValidate(
200
+ { config: { nested: { deep: true } } } as FormValues<TestEntity>,
201
+ [makeJSONField()],
202
+ );
203
+
204
+ expect(errors["config"]).toBeUndefined();
205
+ });
206
+
207
+ test("does not flag an array bound to a JSON field", () => {
208
+ const errors: Dictionary<string> = runValidate(
209
+ {
210
+ config: ["https://a.example.com", "https://b.example.com"],
211
+ } as FormValues<TestEntity>,
212
+ [makeJSONField()],
213
+ );
214
+
215
+ expect(errors["config"]).toBeUndefined();
216
+ });
217
+
218
+ test("leaves a hidden field alone", () => {
219
+ const errors: Dictionary<string> = runValidate(
220
+ { config: "{oops" } as FormValues<TestEntity>,
221
+ [
222
+ makeJSONField({
223
+ showIf: (): boolean => {
224
+ return false;
225
+ },
226
+ }),
227
+ ],
228
+ );
229
+
230
+ expect(errors["config"]).toBeUndefined();
231
+ });
232
+
233
+ test("reports nothing for a valid form", () => {
234
+ const errors: Dictionary<string> = runValidate(
235
+ { config: '{"a": 1}' } as FormValues<TestEntity>,
236
+ [makeJSONField()],
237
+ );
238
+
239
+ expect(Object.keys(errors)).toHaveLength(0);
240
+ });
241
+ });
@@ -3,6 +3,7 @@ import LogTimeRangePicker, {
3
3
  } from "../../../UI/Components/LogsViewer/components/LogTimeRangePicker";
4
4
  import RangeStartAndEndDateTime from "../../../Types/Time/RangeStartAndEndDateTime";
5
5
  import TimeRange from "../../../Types/Time/TimeRange";
6
+ import InBetween from "../../../Types/BaseDatabase/InBetween";
6
7
  import "@testing-library/jest-dom";
7
8
  import { cleanup, fireEvent, render, screen } from "@testing-library/react";
8
9
  import React from "react";
@@ -135,4 +136,45 @@ describe("LogTimeRangePicker", () => {
135
136
  dropdown.parentElement?.classList.contains("relative") ?? false,
136
137
  ).toBe(true);
137
138
  });
139
+
140
+ test("keeps the date fields out of the narrow dropdown panel", () => {
141
+ const dropdown: HTMLElement = openDropdown({
142
+ range: TimeRange.CUSTOM,
143
+ startAndEndDate: new InBetween<Date>(
144
+ new Date("2024-01-01T00:00:00.000Z"),
145
+ new Date("2024-01-02T00:00:00.000Z"),
146
+ ),
147
+ });
148
+
149
+ expect(dropdown.querySelectorAll("input").length).toBe(0);
150
+ });
151
+
152
+ test("opens the custom range modal from the dropdown", () => {
153
+ openDropdown({ range: TimeRange.PAST_ONE_HOUR });
154
+
155
+ fireEvent.click(screen.getByTestId("log-time-range-picker-custom-option"));
156
+
157
+ expect(screen.getByTestId("custom-time-range-modal")).toBeInTheDocument();
158
+ expect(screen.queryByTestId(DROPDOWN_TEST_ID)).toBeNull();
159
+ });
160
+
161
+ test("applies a custom range picked in the modal", () => {
162
+ const emitted: Array<RangeStartAndEndDateTime> = [];
163
+ render(
164
+ <LogTimeRangePicker
165
+ value={{ range: TimeRange.PAST_ONE_HOUR }}
166
+ onChange={(value: RangeStartAndEndDateTime) => {
167
+ emitted.push(value);
168
+ }}
169
+ />,
170
+ );
171
+
172
+ fireEvent.click(screen.getByTestId(BUTTON_TEST_ID));
173
+ fireEvent.click(screen.getByTestId("log-time-range-picker-custom-option"));
174
+ fireEvent.click(screen.getByText("Apply"));
175
+
176
+ expect(emitted).toHaveLength(1);
177
+ expect(emitted[0]!.range).toBe(TimeRange.CUSTOM);
178
+ expect(emitted[0]!.startAndEndDate).toBeInstanceOf(InBetween);
179
+ });
138
180
  });
@@ -75,9 +75,27 @@ type PressFunction = (element: HTMLElement) => void;
75
75
 
76
76
  const press: PressFunction = (element: HTMLElement): void => {
77
77
  fireEvent.mouseDown(element);
78
+ fireEvent.mouseUp(element);
78
79
  fireEvent.click(element);
79
80
  };
80
81
 
82
+ /*
83
+ * A press that begins on one element and ends on another. The browser reports
84
+ * the click against the nearest common ancestor of the two, which for anything
85
+ * inside the dialog is the backdrop layer itself — so the click alone cannot
86
+ * tell a drag from a dismissal, and the modal has to watch both ends.
87
+ */
88
+ type DragPressFunction = (from: HTMLElement, to: HTMLElement) => void;
89
+
90
+ const dragPress: DragPressFunction = (
91
+ from: HTMLElement,
92
+ to: HTMLElement,
93
+ ): void => {
94
+ fireEvent.mouseDown(from);
95
+ fireEvent.mouseUp(to);
96
+ fireEvent.click(getBackdropLayer().parentElement!);
97
+ };
98
+
81
99
  interface PortalHarness {
82
100
  onClose: () => void;
83
101
  }
@@ -472,6 +490,78 @@ describe("Modal dismissal and portalled dropdown menus", () => {
472
490
  expect(harness.onClose).toHaveBeenCalledTimes(1);
473
491
  });
474
492
 
493
+ test("a press that starts on the backdrop and ends on the panel is a drag, not a dismissal", () => {
494
+ const harness: PortalHarness = renderInModal(<div>Modal content</div>);
495
+
496
+ /*
497
+ * Pressing outside and releasing inside — reaching for the backdrop,
498
+ * changing your mind, letting go over the form. The click lands on the
499
+ * backdrop layer either way, so before the modal tracked the release this
500
+ * discarded whatever had been typed.
501
+ */
502
+ dragPress(getBackdropLayer(), screen.getByText("Modal content"));
503
+
504
+ expect(harness.onClose).not.toHaveBeenCalled();
505
+ expect(screen.getByTestId("modal")).toBeInTheDocument();
506
+ });
507
+
508
+ test("a press that starts on the panel and ends on the backdrop is a drag too", () => {
509
+ const harness: PortalHarness = renderInModal(<div>Modal content</div>);
510
+
511
+ // Selecting text in the body and releasing past the panel edge.
512
+ dragPress(screen.getByText("Modal content"), getBackdropLayer());
513
+
514
+ expect(harness.onClose).not.toHaveBeenCalled();
515
+ });
516
+
517
+ test("a release on a portalled menu does not dismiss the dialog either", () => {
518
+ const harness: PortalHarness = renderInModal(
519
+ <div>
520
+ {createPortal(
521
+ <button type="button" data-testid="portalled-option">
522
+ pick me
523
+ </button>,
524
+ document.body,
525
+ )}
526
+ </div>,
527
+ );
528
+
529
+ dragPress(getBackdropLayer(), screen.getByTestId("portalled-option"));
530
+
531
+ expect(harness.onClose).not.toHaveBeenCalled();
532
+ expect(screen.getByTestId("modal")).toBeInTheDocument();
533
+ });
534
+
535
+ test("dismissing an open picker leaves the modal behind it standing", () => {
536
+ const harness: PortalHarness = renderInModal(
537
+ <ColorPicker
538
+ dataTestId="color-value"
539
+ placeholder="Pick a color"
540
+ onChange={jest.fn()}
541
+ />,
542
+ );
543
+
544
+ fireEvent.click(screen.getByTestId("color-value"));
545
+ expect(screen.getByTestId("color-picker-popup")).toBeInTheDocument();
546
+
547
+ /*
548
+ * The popup covers a good part of the backdrop, so "click off it to put
549
+ * it away" is the obvious gesture — and one press used to close the
550
+ * picker and the form behind it together. Dismissal belongs to the
551
+ * topmost layer that is open.
552
+ */
553
+ press(getBackdropLayer());
554
+
555
+ expect(screen.queryByTestId("color-picker-popup")).toBeNull();
556
+ expect(harness.onClose).not.toHaveBeenCalled();
557
+ expect(screen.getByTestId("modal")).toBeInTheDocument();
558
+
559
+ // With the picker gone, the very next backdrop press dismisses as usual.
560
+ press(getBackdropLayer());
561
+
562
+ expect(harness.onClose).toHaveBeenCalledTimes(1);
563
+ });
564
+
475
565
  test("the close button still closes a modal holding a portalled menu", () => {
476
566
  const harness: PortalHarness = renderInModal(
477
567
  <AutocompleteTextInput
@@ -371,7 +371,12 @@ describe("TelemetryTimeRangePicker", () => {
371
371
  expect(optionLabels).toEqual(expectedLabels);
372
372
  });
373
373
 
374
- test("renders the custom range editor when a custom range is active", () => {
374
+ test("keeps the date fields out of the narrow dropdown panel", () => {
375
+ /*
376
+ * Regression: the custom editor used to render two `datetime-local`
377
+ * fields inside the 288px panel. It now lives in a modal, so the panel
378
+ * itself stays a plain list of options.
379
+ */
375
380
  renderPicker({
376
381
  range: TimeRange.CUSTOM,
377
382
  startAndEndDate: new InBetween<Date>(
@@ -382,14 +387,11 @@ describe("TelemetryTimeRangePicker", () => {
382
387
 
383
388
  const dropdown: HTMLElement = openDropdown();
384
389
 
385
- // The custom editor lives in the same panel, so alignment still applies.
386
390
  expect(hasClass(dropdown, "left-0")).toBe(true);
387
- expect(dropdown.querySelectorAll("input").length).toBeGreaterThan(0);
391
+ expect(dropdown.querySelectorAll("input").length).toBe(0);
388
392
  });
389
393
 
390
- test("keeps the taller custom panel inside the viewport near the right edge", () => {
391
- stubTriggerRect(1310, 1430);
392
-
394
+ test("marks the custom entry as selected while a custom range is active", () => {
393
395
  renderPicker({
394
396
  range: TimeRange.CUSTOM,
395
397
  startAndEndDate: new InBetween<Date>(
@@ -398,7 +400,47 @@ describe("TelemetryTimeRangePicker", () => {
398
400
  ),
399
401
  });
400
402
 
401
- expect(hasClass(openDropdown(), "right-0")).toBe(true);
403
+ openDropdown();
404
+
405
+ expect(
406
+ hasClass(
407
+ screen.getByTestId("telemetry-time-range-picker-custom-option"),
408
+ "text-indigo-700",
409
+ ),
410
+ ).toBe(true);
411
+ });
412
+
413
+ test("opens the custom range modal from the dropdown", () => {
414
+ renderPicker({ range: TimeRange.PAST_ONE_HOUR });
415
+ openDropdown();
416
+
417
+ fireEvent.click(
418
+ screen.getByTestId("telemetry-time-range-picker-custom-option"),
419
+ );
420
+
421
+ expect(screen.getByTestId("custom-time-range-modal")).toBeInTheDocument();
422
+ // The dropdown gets out of the way once the modal is up.
423
+ expect(screen.queryByTestId(DROPDOWN_TEST_ID)).toBeNull();
424
+ });
425
+
426
+ test("applies a custom range picked in the modal", () => {
427
+ const emitted: Array<RangeStartAndEndDateTime> = [];
428
+ renderPicker(
429
+ { range: TimeRange.PAST_ONE_HOUR },
430
+ (value: RangeStartAndEndDateTime) => {
431
+ emitted.push(value);
432
+ },
433
+ );
434
+ openDropdown();
435
+
436
+ fireEvent.click(
437
+ screen.getByTestId("telemetry-time-range-picker-custom-option"),
438
+ );
439
+ fireEvent.click(screen.getByText("Apply"));
440
+
441
+ expect(emitted).toHaveLength(1);
442
+ expect(emitted[0]!.range).toBe(TimeRange.CUSTOM);
443
+ expect(emitted[0]!.startAndEndDate).toBeInstanceOf(InBetween);
402
444
  });
403
445
  });
404
446
  });