@oneuptime/common 10.0.86 → 10.0.89

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 (126) hide show
  1. package/Models/DatabaseModels/EnterpriseLicense.ts +54 -0
  2. package/Models/DatabaseModels/GlobalConfig.ts +51 -0
  3. package/Server/API/EnterpriseLicenseAPI.ts +83 -0
  4. package/Server/API/GlobalConfigAPI.ts +59 -0
  5. package/Server/API/MetricAPI.ts +149 -0
  6. package/Server/API/TelemetryAPI.ts +24 -0
  7. package/Server/EnvironmentConfig.ts +10 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.ts +59 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  10. package/Server/Infrastructure/Queue.ts +4 -4
  11. package/Server/Services/AnalyticsDatabaseService.ts +21 -0
  12. package/Server/Services/MetricService.ts +193 -1
  13. package/Server/Services/TelemetryAttributeService.ts +37 -3
  14. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +174 -7
  15. package/Tests/Types/Date.test.ts +46 -0
  16. package/Types/Dashboard/DashboardComponentType.ts +3 -0
  17. package/Types/Dashboard/DashboardComponents/DashboardAlertListComponent.ts +13 -0
  18. package/Types/Dashboard/DashboardComponents/DashboardIncidentListComponent.ts +13 -0
  19. package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +13 -0
  20. package/Types/Date.ts +9 -4
  21. package/Types/JSONFunctions.ts +61 -1
  22. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +60 -21
  23. package/UI/Components/Dictionary/Dictionary.tsx +188 -26
  24. package/UI/Components/Dictionary/DictionaryFilterOperator.ts +357 -0
  25. package/UI/Components/Dictionary/DictionaryOfStrings.tsx +12 -7
  26. package/UI/Components/EditionLabel/EditionLabel.tsx +224 -10
  27. package/UI/Components/Filters/FilterViewer.tsx +81 -16
  28. package/UI/Components/Filters/FiltersForm.tsx +18 -3
  29. package/UI/Components/Filters/JSONFilter.tsx +11 -2
  30. package/UI/Components/Filters/Types/Filter.ts +3 -0
  31. package/UI/Components/Forms/Fields/FormField.tsx +6 -1
  32. package/UI/Components/Forms/Types/Field.ts +5 -0
  33. package/UI/Components/LogsViewer/LogsViewer.tsx +73 -4
  34. package/UI/Components/LogsViewer/components/LogSearchBar.tsx +77 -31
  35. package/UI/Components/LogsViewer/components/LogSearchSuggestions.tsx +44 -1
  36. package/UI/Components/LogsViewer/components/LogsFilterCard.tsx +7 -5
  37. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +6 -0
  38. package/UI/Components/TelemetryViewer/components/TelemetrySearchBar.tsx +84 -25
  39. package/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.tsx +44 -1
  40. package/Utils/Dashboard/Components/DashboardAlertListComponent.ts +86 -0
  41. package/Utils/Dashboard/Components/DashboardIncidentListComponent.ts +86 -0
  42. package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +85 -0
  43. package/Utils/Dashboard/Components/Index.ts +21 -0
  44. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +57 -0
  45. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  46. package/build/dist/Models/DatabaseModels/GlobalConfig.js +54 -0
  47. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  48. package/build/dist/Server/API/EnterpriseLicenseAPI.js +64 -1
  49. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  50. package/build/dist/Server/API/GlobalConfigAPI.js +47 -0
  51. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  52. package/build/dist/Server/API/MetricAPI.js +123 -0
  53. package/build/dist/Server/API/MetricAPI.js.map +1 -0
  54. package/build/dist/Server/API/TelemetryAPI.js +9 -0
  55. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  56. package/build/dist/Server/EnvironmentConfig.js +3 -0
  57. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  58. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.js +26 -0
  59. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.js.map +1 -0
  60. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  61. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  62. package/build/dist/Server/Infrastructure/Queue.js +3 -3
  63. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  64. package/build/dist/Server/Services/AnalyticsDatabaseService.js +18 -0
  65. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  66. package/build/dist/Server/Services/MetricService.js +151 -1
  67. package/build/dist/Server/Services/MetricService.js.map +1 -1
  68. package/build/dist/Server/Services/TelemetryAttributeService.js +36 -7
  69. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  70. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +135 -5
  71. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  72. package/build/dist/Tests/Types/Date.test.js +40 -0
  73. package/build/dist/Tests/Types/Date.test.js.map +1 -1
  74. package/build/dist/Types/Dashboard/DashboardComponentType.js +3 -0
  75. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  76. package/build/dist/Types/Dashboard/DashboardComponents/DashboardAlertListComponent.js +2 -0
  77. package/build/dist/Types/Dashboard/DashboardComponents/DashboardAlertListComponent.js.map +1 -0
  78. package/build/dist/Types/Dashboard/DashboardComponents/DashboardIncidentListComponent.js +2 -0
  79. package/build/dist/Types/Dashboard/DashboardComponents/DashboardIncidentListComponent.js.map +1 -0
  80. package/build/dist/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.js +2 -0
  81. package/build/dist/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.js.map +1 -0
  82. package/build/dist/Types/Date.js +7 -2
  83. package/build/dist/Types/Date.js.map +1 -1
  84. package/build/dist/Types/JSONFunctions.js +47 -1
  85. package/build/dist/Types/JSONFunctions.js.map +1 -1
  86. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +21 -10
  87. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  88. package/build/dist/UI/Components/Dictionary/Dictionary.js +109 -16
  89. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  90. package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js +263 -0
  91. package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js.map +1 -0
  92. package/build/dist/UI/Components/Dictionary/DictionaryOfStrings.js +10 -6
  93. package/build/dist/UI/Components/Dictionary/DictionaryOfStrings.js.map +1 -1
  94. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +124 -6
  95. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  96. package/build/dist/UI/Components/Filters/FilterViewer.js +50 -12
  97. package/build/dist/UI/Components/Filters/FilterViewer.js.map +1 -1
  98. package/build/dist/UI/Components/Filters/FiltersForm.js +5 -4
  99. package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
  100. package/build/dist/UI/Components/Filters/JSONFilter.js +1 -1
  101. package/build/dist/UI/Components/Filters/JSONFilter.js.map +1 -1
  102. package/build/dist/UI/Components/Forms/Fields/FormField.js +1 -1
  103. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  104. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +54 -5
  105. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  106. package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js +59 -29
  107. package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js.map +1 -1
  108. package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js +10 -2
  109. package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js.map +1 -1
  110. package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js +2 -5
  111. package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js.map +1 -1
  112. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +1 -1
  113. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  114. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js +59 -22
  115. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js.map +1 -1
  116. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js +10 -2
  117. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js.map +1 -1
  118. package/build/dist/Utils/Dashboard/Components/DashboardAlertListComponent.js +70 -0
  119. package/build/dist/Utils/Dashboard/Components/DashboardAlertListComponent.js.map +1 -0
  120. package/build/dist/Utils/Dashboard/Components/DashboardIncidentListComponent.js +70 -0
  121. package/build/dist/Utils/Dashboard/Components/DashboardIncidentListComponent.js.map +1 -0
  122. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +69 -0
  123. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -0
  124. package/build/dist/Utils/Dashboard/Components/Index.js +12 -0
  125. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  126. package/package.json +1 -1
@@ -20,7 +20,67 @@ export default class JSONFunctions {
20
20
  obj1: GenericObject,
21
21
  obj2: GenericObject,
22
22
  ): boolean {
23
- return JSON.stringify(obj1) !== JSON.stringify(obj2);
23
+ return !JSONFunctions.deepEqual(obj1, obj2);
24
+ }
25
+
26
+ /*
27
+ * Structural deep-equal that short-circuits at the first mismatch. The
28
+ * dashboard widget hot path was previously running JSON.stringify on
29
+ * both sides of nested metricQueryConfig objects every render, which
30
+ * dominated CPU when many widgets were on screen. This visits the
31
+ * tree once and bails on the first divergence.
32
+ */
33
+ public static deepEqual(a: unknown, b: unknown): boolean {
34
+ if (a === b) {
35
+ return true;
36
+ }
37
+
38
+ if (
39
+ a === null ||
40
+ b === null ||
41
+ typeof a !== "object" ||
42
+ typeof b !== "object"
43
+ ) {
44
+ return false;
45
+ }
46
+
47
+ if (a instanceof Date || b instanceof Date) {
48
+ return (
49
+ a instanceof Date && b instanceof Date && a.getTime() === b.getTime()
50
+ );
51
+ }
52
+
53
+ if (Array.isArray(a) || Array.isArray(b)) {
54
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
55
+ return false;
56
+ }
57
+ for (let i: number = 0; i < a.length; i++) {
58
+ if (!JSONFunctions.deepEqual(a[i], b[i])) {
59
+ return false;
60
+ }
61
+ }
62
+ return true;
63
+ }
64
+
65
+ const keysA: Array<string> = Object.keys(a as Record<string, unknown>);
66
+ const keysB: Array<string> = Object.keys(b as Record<string, unknown>);
67
+ if (keysA.length !== keysB.length) {
68
+ return false;
69
+ }
70
+ for (const key of keysA) {
71
+ if (!Object.prototype.hasOwnProperty.call(b, key)) {
72
+ return false;
73
+ }
74
+ if (
75
+ !JSONFunctions.deepEqual(
76
+ (a as Record<string, unknown>)[key],
77
+ (b as Record<string, unknown>)[key],
78
+ )
79
+ ) {
80
+ return false;
81
+ }
82
+ }
83
+ return true;
24
84
  }
25
85
 
26
86
  public static nestJson(obj: JSONObject): JSONObject {
@@ -21,6 +21,9 @@ export interface ComponentProps {
21
21
  onBlur?: (() => void) | undefined;
22
22
  outerDivClassName?: string | undefined;
23
23
  disableSpellCheck?: boolean | undefined;
24
+ isLoadingSuggestions?: boolean | undefined;
25
+ loadingMessage?: string | undefined;
26
+ noSuggestionsMessage?: string | undefined;
24
27
  }
25
28
 
26
29
  const BASE_INPUT_CLASS: string =
@@ -90,7 +93,9 @@ const AutocompleteTextInput: FunctionComponent<ComponentProps> = (
90
93
  .slice(0, MAX_SUGGESTIONS);
91
94
  }, [inputValue, props.suggestions]);
92
95
 
93
- const showMenu: boolean = isMenuVisible && suggestions.length > 0;
96
+ const isLoadingSuggestions: boolean = Boolean(props.isLoadingSuggestions);
97
+ const showMenu: boolean =
98
+ isMenuVisible && (suggestions.length > 0 || isLoadingSuggestions);
94
99
 
95
100
  const getInputClassName: () => string = (): string => {
96
101
  let className: string = props.className || BASE_INPUT_CLASS;
@@ -145,7 +150,7 @@ const AutocompleteTextInput: FunctionComponent<ComponentProps> = (
145
150
  const handleKeyDown: (
146
151
  event: React.KeyboardEvent<HTMLInputElement>,
147
152
  ) => void = (event: React.KeyboardEvent<HTMLInputElement>) => {
148
- if (!showMenu) {
153
+ if (!showMenu || suggestions.length === 0) {
149
154
  return;
150
155
  }
151
156
 
@@ -221,26 +226,60 @@ const AutocompleteTextInput: FunctionComponent<ComponentProps> = (
221
226
  id={listboxIdRef.current}
222
227
  role="listbox"
223
228
  >
224
- {suggestions.map((suggestion: string, index: number) => {
225
- const isActive: boolean = index === highlightedIndex;
226
- return (
227
- <button
228
- key={`${suggestion}-${index}`}
229
- type="button"
230
- role="option"
231
- aria-selected={isActive}
232
- className={`flex w-full items-center px-3 py-2 text-left hover:bg-indigo-50 ${isActive ? "bg-indigo-600 text-white hover:bg-indigo-500" : "text-gray-700"}`}
233
- onMouseDown={(event: React.MouseEvent<HTMLButtonElement>) => {
234
- event.preventDefault();
235
- }}
236
- onClick={() => {
237
- handleSuggestionSelect(suggestion);
238
- }}
229
+ {isLoadingSuggestions && (
230
+ <div className="flex w-full items-center px-3 py-2 text-left text-gray-500">
231
+ <svg
232
+ className="animate-spin -ml-0.5 mr-2 h-4 w-4 text-indigo-500"
233
+ xmlns="http://www.w3.org/2000/svg"
234
+ fill="none"
235
+ viewBox="0 0 24 24"
236
+ aria-hidden="true"
239
237
  >
240
- {suggestion}
241
- </button>
242
- );
243
- })}
238
+ <circle
239
+ className="opacity-25"
240
+ cx="12"
241
+ cy="12"
242
+ r="10"
243
+ stroke="currentColor"
244
+ strokeWidth="4"
245
+ ></circle>
246
+ <path
247
+ className="opacity-75"
248
+ fill="currentColor"
249
+ d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
250
+ ></path>
251
+ </svg>
252
+ <span>{props.loadingMessage || "Loading..."}</span>
253
+ </div>
254
+ )}
255
+ {!isLoadingSuggestions &&
256
+ suggestions.length === 0 &&
257
+ props.noSuggestionsMessage && (
258
+ <div className="flex w-full items-center px-3 py-2 text-left text-gray-500">
259
+ {props.noSuggestionsMessage}
260
+ </div>
261
+ )}
262
+ {!isLoadingSuggestions &&
263
+ suggestions.map((suggestion: string, index: number) => {
264
+ const isActive: boolean = index === highlightedIndex;
265
+ return (
266
+ <button
267
+ key={`${suggestion}-${index}`}
268
+ type="button"
269
+ role="option"
270
+ aria-selected={isActive}
271
+ className={`flex w-full items-center px-3 py-2 text-left hover:bg-indigo-50 ${isActive ? "bg-indigo-600 text-white hover:bg-indigo-500" : "text-gray-700"}`}
272
+ onMouseDown={(event: React.MouseEvent<HTMLButtonElement>) => {
273
+ event.preventDefault();
274
+ }}
275
+ onClick={() => {
276
+ handleSuggestionSelect(suggestion);
277
+ }}
278
+ >
279
+ {suggestion}
280
+ </button>
281
+ );
282
+ })}
244
283
  </div>
245
284
  )}
246
285
  </div>
@@ -11,6 +11,15 @@ import React, {
11
11
  } from "react";
12
12
  import AutocompleteTextInput from "../AutocompleteTextInput/AutocompleteTextInput";
13
13
  import FieldLabelElement from "../Forms/Fields/FieldLabel";
14
+ import {
15
+ DICTIONARY_FILTER_OPERATOR_OPTIONS,
16
+ DictionaryEntryValue,
17
+ DictionaryFilterOperator,
18
+ DictionaryFilterOperatorOption,
19
+ buildDictionaryValue,
20
+ detectOperatorFromValue,
21
+ getOperatorOption,
22
+ } from "./DictionaryFilterOperator";
14
23
 
15
24
  export enum ValueType {
16
25
  Text = "Text",
@@ -19,10 +28,8 @@ export enum ValueType {
19
28
  }
20
29
 
21
30
  export interface ComponentProps {
22
- onChange?:
23
- | undefined
24
- | ((value: Dictionary<string | boolean | number>) => void);
25
- initialValue?: Dictionary<string | boolean | number>;
31
+ onChange?: undefined | ((value: Dictionary<DictionaryEntryValue>) => void);
32
+ initialValue?: Dictionary<DictionaryEntryValue>;
26
33
  keyPlaceholder?: string;
27
34
  valuePlaceholder?: string;
28
35
  addButtonSuffix?: string | undefined;
@@ -30,12 +37,21 @@ export interface ComponentProps {
30
37
  keys?: Array<string> | undefined;
31
38
  valueSuggestions?: Record<string, Array<string>> | undefined;
32
39
  onKeySelected?: ((key: string) => void) | undefined;
40
+ isLoadingKeys?: boolean | undefined;
41
+ loadingValueKeys?: Array<string> | undefined;
42
+ /*
43
+ * When true, render an operator dropdown (=, !=, contains, etc.)
44
+ * between the key and value inputs. Defaults to false for backwards
45
+ * compatibility with simple key/value forms.
46
+ */
47
+ enableOperators?: boolean | undefined;
33
48
  }
34
49
 
35
50
  interface Item {
36
51
  key: string;
37
52
  value: string | number | boolean;
38
53
  type: ValueType;
54
+ operator: DictionaryFilterOperator;
39
55
  }
40
56
 
41
57
  const DictionaryForm: FunctionComponent<ComponentProps> = (
@@ -57,12 +73,10 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
57
73
  const [data, setData] = useState<Array<Item>>([]);
58
74
  const [isInitialValueSet, setIsInitialValueSet] = useState<boolean>(false);
59
75
 
60
- type UpdateDataFunction = (
61
- json: Dictionary<string | number | boolean>,
62
- ) => void;
76
+ type UpdateDataFunction = (json: Dictionary<DictionaryEntryValue>) => void;
63
77
 
64
78
  const updateData: UpdateDataFunction = (
65
- json: Dictionary<string | number | boolean>,
79
+ json: Dictionary<DictionaryEntryValue>,
66
80
  ): void => {
67
81
  const newData: Array<Item> = Object.keys(json).map((key: string) => {
68
82
  // check if the value type is in data
@@ -73,22 +87,46 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
73
87
 
74
88
  let valueType: ValueType = valueTypeInData || ValueType.Text;
75
89
 
90
+ const rawEntry: DictionaryEntryValue | undefined = json[key];
91
+
76
92
  if (!valueTypeInData) {
77
- if (typeof json[key] === "number") {
93
+ if (typeof rawEntry === "number") {
78
94
  valueType = ValueType.Number;
79
95
  }
80
96
 
81
- if (typeof json[key] === "boolean") {
97
+ if (typeof rawEntry === "boolean") {
82
98
  valueType = ValueType.Boolean;
83
99
  }
84
100
  }
85
101
 
86
- const value: string | number | boolean | undefined = json[key];
102
+ const detected: {
103
+ operator: DictionaryFilterOperator;
104
+ rawValue: string;
105
+ } = detectOperatorFromValue(rawEntry);
106
+
107
+ /*
108
+ * Restore typed values (number/boolean) for the form input from
109
+ * the stringified raw representation when the column type allows.
110
+ */
111
+ let restoredValue: string | number | boolean = detected.rawValue;
112
+ if (valueType === ValueType.Number && detected.rawValue !== "") {
113
+ const parsed: number = Number(detected.rawValue);
114
+ restoredValue = isNaN(parsed) ? detected.rawValue : parsed;
115
+ } else if (valueType === ValueType.Boolean) {
116
+ if (typeof rawEntry === "boolean") {
117
+ restoredValue = rawEntry;
118
+ } else if (detected.rawValue.toLowerCase() === "true") {
119
+ restoredValue = true;
120
+ } else if (detected.rawValue.toLowerCase() === "false") {
121
+ restoredValue = false;
122
+ }
123
+ }
87
124
 
88
125
  return {
89
126
  key: key,
90
- value: value === undefined || value === null ? "" : value,
127
+ value: restoredValue,
91
128
  type: valueType,
129
+ operator: detected.operator,
92
130
  };
93
131
  });
94
132
 
@@ -109,9 +147,35 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
109
147
  type OnDataChangeFunction = (data: Array<Item>) => void;
110
148
 
111
149
  const onDataChange: OnDataChangeFunction = (data: Array<Item>): void => {
112
- const result: Dictionary<string | number | boolean> = {};
150
+ const result: Dictionary<DictionaryEntryValue> = {};
113
151
  data.forEach((item: Item) => {
114
- result[item.key] = item.value;
152
+ /*
153
+ * Non-text types skip the operator system — the existing
154
+ * numeric/boolean inputs always represent equality.
155
+ */
156
+ if (item.type === ValueType.Number) {
157
+ result[item.key] = item.value as number;
158
+ return;
159
+ }
160
+ if (item.type === ValueType.Boolean) {
161
+ result[item.key] = item.value as boolean;
162
+ return;
163
+ }
164
+
165
+ const operatorOption: DictionaryFilterOperatorOption = getOperatorOption(
166
+ item.operator,
167
+ );
168
+ if (operatorOption.hidesValueInput) {
169
+ result[item.key] = buildDictionaryValue({
170
+ operator: item.operator,
171
+ rawValue: "",
172
+ });
173
+ return;
174
+ }
175
+ result[item.key] = buildDictionaryValue({
176
+ operator: item.operator,
177
+ rawValue: String(item.value ?? ""),
178
+ });
115
179
  });
116
180
  if (props.onChange) {
117
181
  props.onChange(result);
@@ -138,13 +202,36 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
138
202
  return "";
139
203
  };
140
204
 
205
+ const operatorDropdownOptions: Array<DropdownOption> =
206
+ DICTIONARY_FILTER_OPERATOR_OPTIONS.map(
207
+ (option: DictionaryFilterOperatorOption) => {
208
+ return {
209
+ label: option.symbol,
210
+ value: option.operator,
211
+ };
212
+ },
213
+ );
214
+
141
215
  return (
142
216
  <div>
143
217
  <div>
144
218
  {data.map((item: Item, index: number) => {
219
+ const operatorOption: DictionaryFilterOperatorOption =
220
+ getOperatorOption(item.operator);
221
+ const showOperatorSelector: boolean = Boolean(props.enableOperators);
222
+ const hideValueInput: boolean = Boolean(
223
+ showOperatorSelector && operatorOption.hidesValueInput,
224
+ );
225
+ const valueColumnClass: string = showOperatorSelector
226
+ ? "ml-1 w-2/5"
227
+ : "ml-1 w-1/2";
228
+ const keyColumnClass: string = showOperatorSelector
229
+ ? "mr-1 w-2/5"
230
+ : "mr-1 w-1/2";
231
+
145
232
  return (
146
233
  <div key={index} className="flex items-start mb-4 last:mb-0">
147
- <div className="mr-1 w-1/2">
234
+ <div className={keyColumnClass}>
148
235
  <div className="mb-1">
149
236
  <FieldLabelElement
150
237
  title="Key"
@@ -157,6 +244,8 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
157
244
  value={item.key}
158
245
  placeholder={props.keyPlaceholder}
159
246
  suggestions={props.keys}
247
+ isLoadingSuggestions={props.isLoadingKeys}
248
+ loadingMessage="Loading attributes..."
160
249
  onChange={(value: string) => {
161
250
  const newData: Array<Item> = [...data];
162
251
  newData[index]!.key = value;
@@ -171,9 +260,56 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
171
260
  />
172
261
  </div>
173
262
 
174
- <div className="mr-1 ml-1 flex items-center justify-center pt-8">
175
- <span className="text-slate-500 text-2xl leading-none">=</span>
176
- </div>
263
+ {showOperatorSelector ? (
264
+ <div className="mr-1 ml-1 w-1/5 min-w-[120px]">
265
+ <div className="mb-1">
266
+ <FieldLabelElement
267
+ title="Operator"
268
+ required={true}
269
+ hideOptionalLabel={true}
270
+ className="block text-xs text-gray-500 font-normal flex justify-between"
271
+ />
272
+ </div>
273
+ <Dropdown
274
+ value={operatorDropdownOptions.find(
275
+ (option: DropdownOption) => {
276
+ return option.value === item.operator;
277
+ },
278
+ )}
279
+ options={operatorDropdownOptions}
280
+ isMultiSelect={false}
281
+ onChange={(
282
+ selectedOption:
283
+ | DropdownValue
284
+ | Array<DropdownValue>
285
+ | null,
286
+ ) => {
287
+ const newOperator: DictionaryFilterOperator =
288
+ (selectedOption as DictionaryFilterOperator) ||
289
+ DictionaryFilterOperator.EqualTo;
290
+ const newOperatorOption: DictionaryFilterOperatorOption =
291
+ getOperatorOption(newOperator);
292
+ const newData: Array<Item> = [...data];
293
+ newData[index]!.operator = newOperator;
294
+ /*
295
+ * Reset the value when switching to a value-less
296
+ * operator so we don't ship stale text downstream.
297
+ */
298
+ if (newOperatorOption.hidesValueInput) {
299
+ newData[index]!.value = "";
300
+ }
301
+ setData(newData);
302
+ onDataChange(newData);
303
+ }}
304
+ />
305
+ </div>
306
+ ) : (
307
+ <div className="mr-1 ml-1 flex items-center justify-center pt-8">
308
+ <span className="text-slate-500 text-2xl leading-none">
309
+ =
310
+ </span>
311
+ </div>
312
+ )}
177
313
  {valueTypes.length > 1 && (
178
314
  <div className="ml-1 w-1/2">
179
315
  <div className="mb-1">
@@ -209,7 +345,7 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
209
345
  />
210
346
  </div>
211
347
  )}
212
- <div className="ml-1 w-1/2">
348
+ <div className={valueColumnClass}>
213
349
  <div className="mb-1">
214
350
  <FieldLabelElement
215
351
  title="Value"
@@ -218,15 +354,40 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
218
354
  className="block text-xs text-gray-500 font-normal flex justify-between"
219
355
  />
220
356
  </div>
221
- {item.type === ValueType.Text && (
357
+ {hideValueInput && (
358
+ <Input
359
+ value=""
360
+ placeholder={operatorOption.label}
361
+ disabled={true}
362
+ onChange={() => {
363
+ // no-op — IsEmpty/IsNotEmpty have no value
364
+ }}
365
+ />
366
+ )}
367
+ {!hideValueInput && item.type === ValueType.Text && (
222
368
  <AutocompleteTextInput
223
369
  value={item.value.toString()}
224
- placeholder={props.valuePlaceholder}
370
+ placeholder={
371
+ operatorOption.expectsNumericValue
372
+ ? "Number"
373
+ : props.valuePlaceholder
374
+ }
225
375
  suggestions={
226
- item.key && props.valueSuggestions?.[item.key]
227
- ? props.valueSuggestions[item.key]
228
- : undefined
376
+ operatorOption.expectsNumericValue
377
+ ? undefined
378
+ : item.key && props.valueSuggestions?.[item.key]
379
+ ? props.valueSuggestions[item.key]
380
+ : undefined
381
+ }
382
+ isLoadingSuggestions={
383
+ operatorOption.expectsNumericValue
384
+ ? false
385
+ : Boolean(
386
+ item.key &&
387
+ props.loadingValueKeys?.includes(item.key),
388
+ )
229
389
  }
390
+ loadingMessage="Loading values..."
230
391
  onChange={(value: string) => {
231
392
  const newData: Array<Item> = [...data];
232
393
  newData[index]!.value = value;
@@ -236,7 +397,7 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
236
397
  />
237
398
  )}
238
399
 
239
- {item.type === ValueType.Number && (
400
+ {!hideValueInput && item.type === ValueType.Number && (
240
401
  <Input
241
402
  value={item.value.toString()}
242
403
  placeholder={props.valuePlaceholder}
@@ -256,7 +417,7 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
256
417
  />
257
418
  )}
258
419
 
259
- {item.type === ValueType.Boolean && (
420
+ {!hideValueInput && item.type === ValueType.Boolean && (
260
421
  <Dropdown
261
422
  value={
262
423
  item.value === true
@@ -315,6 +476,7 @@ const DictionaryForm: FunctionComponent<ComponentProps> = (
315
476
  key: "",
316
477
  value: getDefaultValueForType(valueTypes[0] as ValueType),
317
478
  type: valueTypes[0] as ValueType,
479
+ operator: DictionaryFilterOperator.EqualTo,
318
480
  },
319
481
  ]);
320
482
  }}