@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
@@ -25,7 +25,20 @@ export interface ComponentProps {
25
25
  initialValue?: undefined | string | Date;
26
26
  id?: string | undefined;
27
27
  ariaLabelledby?: string | undefined;
28
+ // Set together by fields that open a popup, so the state is on the focusable element.
29
+ ariaHasPopup?: "dialog" | "listbox" | "menu" | "grid" | "tree" | undefined;
30
+ ariaExpanded?: boolean | undefined;
31
+ ariaControls?: string | undefined;
32
+ /*
33
+ * For fields that render their own error text - a picker whose message sits
34
+ * below the whole control rather than below this input.
35
+ */
36
+ ariaDescribedby?: string | undefined;
37
+ ariaInvalid?: boolean | undefined;
28
38
  onClick?: undefined | (() => void);
39
+ onKeyDown?:
40
+ | undefined
41
+ | ((event: React.KeyboardEvent<HTMLInputElement>) => void);
29
42
  placeholder?: undefined | string;
30
43
  className?: undefined | string;
31
44
  onChange?: undefined | ((value: string) => void);
@@ -164,8 +177,13 @@ const Input: FunctionComponent<ComponentProps> = (
164
177
  spellCheck={!props.disableSpellCheck}
165
178
  autoComplete={props.autoComplete}
166
179
  aria-labelledby={props.ariaLabelledby}
167
- aria-invalid={props.error ? "true" : undefined}
168
- aria-describedby={props.error ? "input-error-message" : undefined}
180
+ aria-haspopup={props.ariaHasPopup}
181
+ aria-expanded={props.ariaExpanded}
182
+ aria-controls={props.ariaControls}
183
+ aria-invalid={props.error || props.ariaInvalid ? "true" : undefined}
184
+ aria-describedby={
185
+ props.error ? "input-error-message" : props.ariaDescribedby
186
+ }
169
187
  onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
170
188
  const value: string | Date = e.target.value;
171
189
 
@@ -194,8 +212,18 @@ const Input: FunctionComponent<ComponentProps> = (
194
212
  }}
195
213
  tabIndex={props.tabIndex}
196
214
  onKeyDown={
197
- props.onEnterPress
215
+ props.onEnterPress || props.onKeyDown
198
216
  ? (event: React.KeyboardEvent<HTMLInputElement>) => {
217
+ props.onKeyDown?.(event);
218
+
219
+ /*
220
+ * A handler that claimed the key - a picker opening its popup
221
+ * on Enter, say - has already decided what it means.
222
+ */
223
+ if (event.defaultPrevented) {
224
+ return;
225
+ }
226
+
199
227
  if (event.key === "Enter") {
200
228
  props.onEnterPress?.();
201
229
  }
@@ -1,233 +1,28 @@
1
- import React, {
2
- FunctionComponent,
3
- ReactElement,
4
- useState,
5
- useRef,
6
- useEffect,
7
- useCallback,
8
- } from "react";
9
- import Icon from "../../Icon/Icon";
10
- import IconProp from "../../../../Types/Icon/IconProp";
1
+ import React, { FunctionComponent, ReactElement } from "react";
11
2
  import RangeStartAndEndDateTime from "../../../../Types/Time/RangeStartAndEndDateTime";
12
- import TimeRange from "../../../../Types/Time/TimeRange";
13
- import InBetween from "../../../../Types/BaseDatabase/InBetween";
14
- import StartAndEndDate, {
15
- StartAndEndDateType,
16
- } from "../../Date/StartAndEndDate";
17
- import {
18
- DropdownHorizontalAlignment,
19
- getDropdownAlignmentClassName,
20
- useDropdownHorizontalAlignment,
21
- } from "../../../Utils/DropdownAlignment";
3
+ import TimeRangePickerDropdown from "../../Date/TimeRangePickerDropdown";
22
4
 
23
5
  export interface LogTimeRangePickerProps {
24
6
  value: RangeStartAndEndDateTime;
25
7
  onChange: (value: RangeStartAndEndDateTime) => void;
26
8
  }
27
9
 
28
- // Matches the Tailwind `w-72` on the dropdown below (18rem).
10
+ // Matches the Tailwind `w-72` on the rendered dropdown (18rem).
29
11
  export const LOG_TIME_RANGE_DROPDOWN_WIDTH_IN_PX: number = 288;
30
12
 
31
- // Preset options to show in the dropdown (ordered for log investigation use)
32
- const PRESET_OPTIONS: Array<{ range: TimeRange; label: string }> = [
33
- { range: TimeRange.PAST_FIVE_MINS, label: "Past 5 Minutes" },
34
- { range: TimeRange.PAST_FIFTEEN_MINS, label: "Past 15 Minutes" },
35
- { range: TimeRange.PAST_THIRTY_MINS, label: "Past 30 Minutes" },
36
- { range: TimeRange.PAST_ONE_HOUR, label: "Past 1 Hour" },
37
- { range: TimeRange.PAST_TWO_HOURS, label: "Past 2 Hours" },
38
- { range: TimeRange.PAST_THREE_HOURS, label: "Past 3 Hours" },
39
- { range: TimeRange.PAST_ONE_DAY, label: "Past 1 Day" },
40
- { range: TimeRange.PAST_TWO_DAYS, label: "Past 2 Days" },
41
- { range: TimeRange.PAST_ONE_WEEK, label: "Past 1 Week" },
42
- { range: TimeRange.PAST_TWO_WEEKS, label: "Past 2 Weeks" },
43
- { range: TimeRange.PAST_ONE_MONTH, label: "Past 1 Month" },
44
- { range: TimeRange.PAST_THREE_MONTHS, label: "Past 3 Months" },
45
- ];
46
-
47
- function formatDateShort(date: Date): string {
48
- const month: string = date.toLocaleString("en-US", { month: "short" });
49
- const day: number = date.getDate();
50
- const hours: string = date.getHours().toString().padStart(2, "0");
51
- const minutes: string = date.getMinutes().toString().padStart(2, "0");
52
- return `${month} ${day}, ${hours}:${minutes}`;
53
- }
54
-
55
- function getButtonLabel(value: RangeStartAndEndDateTime): string {
56
- if (value.range === TimeRange.CUSTOM && value.startAndEndDate) {
57
- const start: string = formatDateShort(value.startAndEndDate.startValue);
58
- const end: string = formatDateShort(value.startAndEndDate.endValue);
59
- return `${start} – ${end}`;
60
- }
61
-
62
- const preset: { range: TimeRange; label: string } | undefined =
63
- PRESET_OPTIONS.find((opt: { range: TimeRange; label: string }) => {
64
- return opt.range === value.range;
65
- });
66
- return preset ? preset.label : value.range;
67
- }
13
+ export const LOG_TIME_RANGE_PICKER_TEST_ID_PREFIX: string =
14
+ "log-time-range-picker";
68
15
 
69
16
  const LogTimeRangePicker: FunctionComponent<LogTimeRangePickerProps> = (
70
17
  props: LogTimeRangePickerProps,
71
18
  ): ReactElement => {
72
- const [isOpen, setIsOpen] = useState<boolean>(false);
73
- const [showCustom, setShowCustom] = useState<boolean>(
74
- props.value.range === TimeRange.CUSTOM,
75
- );
76
- const containerRef: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(
77
- null!,
78
- );
79
- const buttonRef: React.RefObject<HTMLButtonElement> =
80
- useRef<HTMLButtonElement>(null!);
81
- const dropdownRef: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(
82
- null!,
83
- );
84
-
85
- const alignment: DropdownHorizontalAlignment = useDropdownHorizontalAlignment(
86
- {
87
- isOpen: isOpen,
88
- anchorRef: buttonRef,
89
- dropdownRef: dropdownRef,
90
- dropdownWidthInPx: LOG_TIME_RANGE_DROPDOWN_WIDTH_IN_PX,
91
- },
92
- );
93
-
94
- // Close on click outside
95
- useEffect(() => {
96
- const handleClickOutside: (e: MouseEvent) => void = (
97
- e: MouseEvent,
98
- ): void => {
99
- if (
100
- containerRef.current &&
101
- !containerRef.current.contains(e.target as Node)
102
- ) {
103
- setIsOpen(false);
104
- }
105
- };
106
-
107
- document.addEventListener("mousedown", handleClickOutside);
108
- return () => {
109
- document.removeEventListener("mousedown", handleClickOutside);
110
- };
111
- }, []);
112
-
113
- // Sync showCustom when value changes externally (e.g., histogram drag)
114
- useEffect(() => {
115
- setShowCustom(props.value.range === TimeRange.CUSTOM);
116
- }, [props.value.range]);
117
-
118
- const handlePresetSelect: (range: TimeRange) => void = useCallback(
119
- (range: TimeRange): void => {
120
- props.onChange({ range });
121
- setShowCustom(false);
122
- setIsOpen(false);
123
- },
124
- [props],
125
- );
126
-
127
- const handleCustomDateChange: (dateRange: InBetween<Date> | null) => void =
128
- useCallback(
129
- (dateRange: InBetween<Date> | null): void => {
130
- if (dateRange) {
131
- props.onChange({
132
- range: TimeRange.CUSTOM,
133
- startAndEndDate: dateRange,
134
- });
135
- }
136
- },
137
- [props],
138
- );
139
-
140
- const buttonLabel: string = getButtonLabel(props.value);
141
-
142
19
  return (
143
- <div ref={containerRef} className="relative">
144
- <button
145
- ref={buttonRef}
146
- type="button"
147
- data-testid="log-time-range-picker-button"
148
- aria-haspopup="true"
149
- aria-expanded={isOpen}
150
- className={`inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium shadow-sm transition-colors ${
151
- isOpen
152
- ? "border-indigo-300 bg-indigo-50 text-indigo-700"
153
- : "border-gray-200 bg-white text-gray-700 hover:border-gray-300 hover:bg-gray-50"
154
- }`}
155
- onClick={() => {
156
- setIsOpen(!isOpen);
157
- }}
158
- >
159
- <Icon icon={IconProp.Clock} className="h-3.5 w-3.5" />
160
- <span>{buttonLabel}</span>
161
- <Icon
162
- icon={IconProp.ChevronDown}
163
- className={`h-3 w-3 transition-transform ${isOpen ? "rotate-180" : ""}`}
164
- />
165
- </button>
166
-
167
- {isOpen && (
168
- <div
169
- ref={dropdownRef}
170
- data-testid="log-time-range-picker-dropdown"
171
- data-align={alignment}
172
- className={`absolute ${getDropdownAlignmentClassName(
173
- alignment,
174
- )} top-full z-50 mt-1 w-72 max-w-[calc(100vw-1rem)] rounded-lg border border-gray-200 bg-white shadow-lg`}
175
- >
176
- {/* Preset options */}
177
- <div className="max-h-64 overflow-y-auto py-1">
178
- {PRESET_OPTIONS.map(
179
- (option: { range: TimeRange; label: string }) => {
180
- const isActive: boolean = props.value.range === option.range;
181
-
182
- return (
183
- <button
184
- key={option.range}
185
- type="button"
186
- className={`flex w-full items-center px-3 py-1.5 text-left text-sm transition-colors ${
187
- isActive
188
- ? "bg-indigo-50 font-medium text-indigo-700"
189
- : "text-gray-700 hover:bg-gray-50"
190
- }`}
191
- onClick={() => {
192
- handlePresetSelect(option.range);
193
- }}
194
- >
195
- {option.label}
196
- </button>
197
- );
198
- },
199
- )}
200
-
201
- {/* Custom option */}
202
- <button
203
- type="button"
204
- className={`flex w-full items-center px-3 py-1.5 text-left text-sm transition-colors ${
205
- props.value.range === TimeRange.CUSTOM
206
- ? "bg-indigo-50 font-medium text-indigo-700"
207
- : "text-gray-700 hover:bg-gray-50"
208
- }`}
209
- onClick={() => {
210
- setShowCustom(true);
211
- }}
212
- >
213
- Custom Range...
214
- </button>
215
- </div>
216
-
217
- {/* Custom date inputs */}
218
- {showCustom && (
219
- <div className="border-t border-gray-100 p-3">
220
- <StartAndEndDate
221
- type={StartAndEndDateType.DateTime}
222
- value={props.value.startAndEndDate}
223
- hideTimeButtons={true}
224
- onValueChanged={handleCustomDateChange}
225
- />
226
- </div>
227
- )}
228
- </div>
229
- )}
230
- </div>
20
+ <TimeRangePickerDropdown
21
+ value={props.value}
22
+ onChange={props.onChange}
23
+ dataTestIdPrefix={LOG_TIME_RANGE_PICKER_TEST_ID_PREFIX}
24
+ dropdownWidthInPx={LOG_TIME_RANGE_DROPDOWN_WIDTH_IN_PX}
25
+ />
231
26
  );
232
27
  };
233
28
 
@@ -7,6 +7,7 @@ import ModalFooter from "./ModalFooter";
7
7
  import { VeryLightGray } from "../../../Types/BrandColors";
8
8
  import IconProp from "../../../Types/Icon/IconProp";
9
9
  import useTranslateValue from "../../Utils/Translation";
10
+ import { wasPressConsumedByAnAnchoredPopup } from "../../Types/LayeredDismissal";
10
11
  import { lockPageScroll, unlockPageScroll } from "../../Utils/PageScrollLock";
11
12
  import React, {
12
13
  FunctionComponent,
@@ -78,6 +79,8 @@ const Modal: FunctionComponent<ComponentProps> = (
78
79
  >(props.onClose);
79
80
  const backdropPressStartedOutsideRef: React.MutableRefObject<boolean> =
80
81
  useRef<boolean>(false);
82
+ const backdropPressEndedInsideRef: React.MutableRefObject<boolean> =
83
+ useRef<boolean>(false);
81
84
  const titleId: string = useId();
82
85
  const descriptionId: string = useId();
83
86
 
@@ -332,25 +335,54 @@ const Modal: FunctionComponent<ComponentProps> = (
332
335
  const onBackdropMouseDown: BackdropPressHandler = (
333
336
  event: React.MouseEvent<HTMLDivElement>,
334
337
  ): void => {
338
+ backdropPressEndedInsideRef.current = false;
339
+
340
+ /*
341
+ * A press that a portalled popup has already spent on closing itself is
342
+ * not a dismissal of this dialog. Without this, clicking the backdrop to
343
+ * put away an open colour picker closed the picker and the form behind it
344
+ * in one go.
345
+ */
346
+ if (wasPressConsumedByAnAnchoredPopup(event.nativeEvent)) {
347
+ backdropPressStartedOutsideRef.current = false;
348
+ return;
349
+ }
350
+
335
351
  backdropPressStartedOutsideRef.current = isEventOutsideModal(event);
336
352
  };
337
353
 
354
+ const onBackdropMouseUp: BackdropPressHandler = (
355
+ event: React.MouseEvent<HTMLDivElement>,
356
+ ): void => {
357
+ /*
358
+ * The click that follows is reported against the nearest common ancestor
359
+ * of press and release — this very layer — so by the time it arrives the
360
+ * two ends are indistinguishable. Where the release landed has to be
361
+ * recorded while it is still known.
362
+ */
363
+ backdropPressEndedInsideRef.current = !isEventOutsideModal(event);
364
+ };
365
+
338
366
  const onBackdropClick: BackdropPressHandler = (
339
367
  event: React.MouseEvent<HTMLDivElement>,
340
368
  ): void => {
341
369
  /*
342
- * Both ends of the click have to land on the backdrop. Selecting text in
343
- * the body and releasing the mouse past the edge of the panel is a drag,
344
- * not a dismissal, and it used to be the fastest way to lose a filled-in
345
- * form.
370
+ * Both ends of the click have to land on the backdrop. A press that
371
+ * wandered across the edge of the panel in either direction is a drag
372
+ * selecting text in the body and releasing outside, or pressing outside and
373
+ * releasing on the panel — and either used to be the fastest way to lose a
374
+ * filled-in form.
346
375
  */
347
376
  const pressStartedOutside: boolean = backdropPressStartedOutsideRef.current;
377
+ const pressEndedInside: boolean = backdropPressEndedInsideRef.current;
348
378
  backdropPressStartedOutsideRef.current = false;
379
+ backdropPressEndedInsideRef.current = false;
349
380
 
350
381
  if (
351
382
  props.disableCloseOnBackdropClick ||
352
383
  !props.onClose ||
353
384
  !pressStartedOutside ||
385
+ pressEndedInside ||
354
386
  !isEventOutsideModal(event) ||
355
387
  !isTopmostDialog()
356
388
  ) {
@@ -391,6 +423,7 @@ const Modal: FunctionComponent<ComponentProps> = (
391
423
  <div
392
424
  className="fixed inset-0 z-50 overflow-y-auto"
393
425
  onMouseDown={onBackdropMouseDown}
426
+ onMouseUp={onBackdropMouseUp}
394
427
  onClick={onBackdropClick}
395
428
  >
396
429
  <div className="flex min-h-full items-end justify-center p-0 text-center sm:items-center sm:p-6">
@@ -1,227 +1,28 @@
1
- import React, {
2
- FunctionComponent,
3
- ReactElement,
4
- useState,
5
- useRef,
6
- useEffect,
7
- useCallback,
8
- } from "react";
9
- import Icon from "../../Icon/Icon";
10
- import IconProp from "../../../../Types/Icon/IconProp";
1
+ import React, { FunctionComponent, ReactElement } from "react";
11
2
  import RangeStartAndEndDateTime from "../../../../Types/Time/RangeStartAndEndDateTime";
12
- import TimeRange from "../../../../Types/Time/TimeRange";
13
- import InBetween from "../../../../Types/BaseDatabase/InBetween";
14
- import StartAndEndDate, {
15
- StartAndEndDateType,
16
- } from "../../Date/StartAndEndDate";
17
- import {
18
- DropdownHorizontalAlignment,
19
- getDropdownAlignmentClassName,
20
- useDropdownHorizontalAlignment,
21
- } from "../../../Utils/DropdownAlignment";
3
+ import TimeRangePickerDropdown from "../../Date/TimeRangePickerDropdown";
22
4
 
23
5
  export interface TelemetryTimeRangePickerProps {
24
6
  value: RangeStartAndEndDateTime;
25
7
  onChange: (value: RangeStartAndEndDateTime) => void;
26
8
  }
27
9
 
28
- // Matches the Tailwind `w-72` on the dropdown below (18rem).
10
+ // Matches the Tailwind `w-72` on the rendered dropdown (18rem).
29
11
  export const TIME_RANGE_DROPDOWN_WIDTH_IN_PX: number = 288;
30
12
 
31
- const PRESET_OPTIONS: Array<{ range: TimeRange; label: string }> = [
32
- { range: TimeRange.PAST_FIVE_MINS, label: "Past 5 Minutes" },
33
- { range: TimeRange.PAST_FIFTEEN_MINS, label: "Past 15 Minutes" },
34
- { range: TimeRange.PAST_THIRTY_MINS, label: "Past 30 Minutes" },
35
- { range: TimeRange.PAST_ONE_HOUR, label: "Past 1 Hour" },
36
- { range: TimeRange.PAST_TWO_HOURS, label: "Past 2 Hours" },
37
- { range: TimeRange.PAST_THREE_HOURS, label: "Past 3 Hours" },
38
- { range: TimeRange.PAST_ONE_DAY, label: "Past 1 Day" },
39
- { range: TimeRange.PAST_TWO_DAYS, label: "Past 2 Days" },
40
- { range: TimeRange.PAST_ONE_WEEK, label: "Past 1 Week" },
41
- { range: TimeRange.PAST_TWO_WEEKS, label: "Past 2 Weeks" },
42
- { range: TimeRange.PAST_ONE_MONTH, label: "Past 1 Month" },
43
- { range: TimeRange.PAST_THREE_MONTHS, label: "Past 3 Months" },
44
- ];
45
-
46
- function formatDateShort(date: Date): string {
47
- const month: string = date.toLocaleString("en-US", { month: "short" });
48
- const day: number = date.getDate();
49
- const hours: string = date.getHours().toString().padStart(2, "0");
50
- const minutes: string = date.getMinutes().toString().padStart(2, "0");
51
- return `${month} ${day}, ${hours}:${minutes}`;
52
- }
53
-
54
- function getButtonLabel(value: RangeStartAndEndDateTime): string {
55
- if (value.range === TimeRange.CUSTOM && value.startAndEndDate) {
56
- const start: string = formatDateShort(value.startAndEndDate.startValue);
57
- const end: string = formatDateShort(value.startAndEndDate.endValue);
58
- return `${start} – ${end}`;
59
- }
60
-
61
- const preset: { range: TimeRange; label: string } | undefined =
62
- PRESET_OPTIONS.find((opt: { range: TimeRange; label: string }) => {
63
- return opt.range === value.range;
64
- });
65
- return preset ? preset.label : value.range;
66
- }
13
+ export const TELEMETRY_TIME_RANGE_PICKER_TEST_ID_PREFIX: string =
14
+ "telemetry-time-range-picker";
67
15
 
68
16
  const TelemetryTimeRangePicker: FunctionComponent<
69
17
  TelemetryTimeRangePickerProps
70
18
  > = (props: TelemetryTimeRangePickerProps): ReactElement => {
71
- const [isOpen, setIsOpen] = useState<boolean>(false);
72
- const [showCustom, setShowCustom] = useState<boolean>(
73
- props.value.range === TimeRange.CUSTOM,
74
- );
75
- const containerRef: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(
76
- null!,
77
- );
78
- const buttonRef: React.RefObject<HTMLButtonElement> =
79
- useRef<HTMLButtonElement>(null!);
80
- const dropdownRef: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(
81
- null!,
82
- );
83
-
84
- const alignment: DropdownHorizontalAlignment = useDropdownHorizontalAlignment(
85
- {
86
- isOpen: isOpen,
87
- anchorRef: buttonRef,
88
- dropdownRef: dropdownRef,
89
- dropdownWidthInPx: TIME_RANGE_DROPDOWN_WIDTH_IN_PX,
90
- },
91
- );
92
-
93
- useEffect(() => {
94
- const handleClickOutside: (e: MouseEvent) => void = (
95
- e: MouseEvent,
96
- ): void => {
97
- if (
98
- containerRef.current &&
99
- !containerRef.current.contains(e.target as Node)
100
- ) {
101
- setIsOpen(false);
102
- }
103
- };
104
-
105
- document.addEventListener("mousedown", handleClickOutside);
106
- return () => {
107
- document.removeEventListener("mousedown", handleClickOutside);
108
- };
109
- }, []);
110
-
111
- useEffect(() => {
112
- setShowCustom(props.value.range === TimeRange.CUSTOM);
113
- }, [props.value.range]);
114
-
115
- const handlePresetSelect: (range: TimeRange) => void = useCallback(
116
- (range: TimeRange): void => {
117
- props.onChange({ range });
118
- setShowCustom(false);
119
- setIsOpen(false);
120
- },
121
- [props],
122
- );
123
-
124
- const handleCustomDateChange: (dateRange: InBetween<Date> | null) => void =
125
- useCallback(
126
- (dateRange: InBetween<Date> | null): void => {
127
- if (dateRange) {
128
- props.onChange({
129
- range: TimeRange.CUSTOM,
130
- startAndEndDate: dateRange,
131
- });
132
- }
133
- },
134
- [props],
135
- );
136
-
137
- const buttonLabel: string = getButtonLabel(props.value);
138
-
139
19
  return (
140
- <div ref={containerRef} className="relative">
141
- <button
142
- ref={buttonRef}
143
- type="button"
144
- data-testid="telemetry-time-range-picker-button"
145
- aria-haspopup="true"
146
- aria-expanded={isOpen}
147
- className={`inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium shadow-sm transition-colors ${
148
- isOpen
149
- ? "border-indigo-300 bg-indigo-50 text-indigo-700"
150
- : "border-gray-200 bg-white text-gray-700 hover:border-gray-300 hover:bg-gray-50"
151
- }`}
152
- onClick={() => {
153
- setIsOpen(!isOpen);
154
- }}
155
- >
156
- <Icon icon={IconProp.Clock} className="h-3.5 w-3.5" />
157
- <span>{buttonLabel}</span>
158
- <Icon
159
- icon={IconProp.ChevronDown}
160
- className={`h-3 w-3 transition-transform ${isOpen ? "rotate-180" : ""}`}
161
- />
162
- </button>
163
-
164
- {isOpen && (
165
- <div
166
- ref={dropdownRef}
167
- data-testid="telemetry-time-range-picker-dropdown"
168
- data-align={alignment}
169
- className={`absolute ${getDropdownAlignmentClassName(
170
- alignment,
171
- )} top-full z-50 mt-1 w-72 max-w-[calc(100vw-1rem)] rounded-lg border border-gray-200 bg-white shadow-lg`}
172
- >
173
- <div className="max-h-64 overflow-y-auto py-1">
174
- {PRESET_OPTIONS.map(
175
- (option: { range: TimeRange; label: string }) => {
176
- const isActive: boolean = props.value.range === option.range;
177
-
178
- return (
179
- <button
180
- key={option.range}
181
- type="button"
182
- className={`flex w-full items-center px-3 py-1.5 text-left text-sm transition-colors ${
183
- isActive
184
- ? "bg-indigo-50 font-medium text-indigo-700"
185
- : "text-gray-700 hover:bg-gray-50"
186
- }`}
187
- onClick={() => {
188
- handlePresetSelect(option.range);
189
- }}
190
- >
191
- {option.label}
192
- </button>
193
- );
194
- },
195
- )}
196
-
197
- <button
198
- type="button"
199
- className={`flex w-full items-center px-3 py-1.5 text-left text-sm transition-colors ${
200
- props.value.range === TimeRange.CUSTOM
201
- ? "bg-indigo-50 font-medium text-indigo-700"
202
- : "text-gray-700 hover:bg-gray-50"
203
- }`}
204
- onClick={() => {
205
- setShowCustom(true);
206
- }}
207
- >
208
- Custom Range...
209
- </button>
210
- </div>
211
-
212
- {showCustom && (
213
- <div className="border-t border-gray-100 p-3">
214
- <StartAndEndDate
215
- type={StartAndEndDateType.DateTime}
216
- value={props.value.startAndEndDate}
217
- hideTimeButtons={true}
218
- onValueChanged={handleCustomDateChange}
219
- />
220
- </div>
221
- )}
222
- </div>
223
- )}
224
- </div>
20
+ <TimeRangePickerDropdown
21
+ value={props.value}
22
+ onChange={props.onChange}
23
+ dataTestIdPrefix={TELEMETRY_TIME_RANGE_PICKER_TEST_ID_PREFIX}
24
+ dropdownWidthInPx={TIME_RANGE_DROPDOWN_WIDTH_IN_PX}
25
+ />
225
26
  );
226
27
  };
227
28