@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.
- package/Models/DatabaseModels/EnterpriseLicense.ts +54 -0
- package/Models/DatabaseModels/GlobalConfig.ts +51 -0
- package/Server/API/EnterpriseLicenseAPI.ts +83 -0
- package/Server/API/GlobalConfigAPI.ts +59 -0
- package/Server/API/MetricAPI.ts +149 -0
- package/Server/API/TelemetryAPI.ts +24 -0
- package/Server/EnvironmentConfig.ts +10 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.ts +59 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Infrastructure/Queue.ts +4 -4
- package/Server/Services/AnalyticsDatabaseService.ts +21 -0
- package/Server/Services/MetricService.ts +193 -1
- package/Server/Services/TelemetryAttributeService.ts +37 -3
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +174 -7
- package/Tests/Types/Date.test.ts +46 -0
- package/Types/Dashboard/DashboardComponentType.ts +3 -0
- package/Types/Dashboard/DashboardComponents/DashboardAlertListComponent.ts +13 -0
- package/Types/Dashboard/DashboardComponents/DashboardIncidentListComponent.ts +13 -0
- package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +13 -0
- package/Types/Date.ts +9 -4
- package/Types/JSONFunctions.ts +61 -1
- package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +60 -21
- package/UI/Components/Dictionary/Dictionary.tsx +188 -26
- package/UI/Components/Dictionary/DictionaryFilterOperator.ts +357 -0
- package/UI/Components/Dictionary/DictionaryOfStrings.tsx +12 -7
- package/UI/Components/EditionLabel/EditionLabel.tsx +224 -10
- package/UI/Components/Filters/FilterViewer.tsx +81 -16
- package/UI/Components/Filters/FiltersForm.tsx +18 -3
- package/UI/Components/Filters/JSONFilter.tsx +11 -2
- package/UI/Components/Filters/Types/Filter.ts +3 -0
- package/UI/Components/Forms/Fields/FormField.tsx +6 -1
- package/UI/Components/Forms/Types/Field.ts +5 -0
- package/UI/Components/LogsViewer/LogsViewer.tsx +73 -4
- package/UI/Components/LogsViewer/components/LogSearchBar.tsx +77 -31
- package/UI/Components/LogsViewer/components/LogSearchSuggestions.tsx +44 -1
- package/UI/Components/LogsViewer/components/LogsFilterCard.tsx +7 -5
- package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +6 -0
- package/UI/Components/TelemetryViewer/components/TelemetrySearchBar.tsx +84 -25
- package/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.tsx +44 -1
- package/Utils/Dashboard/Components/DashboardAlertListComponent.ts +86 -0
- package/Utils/Dashboard/Components/DashboardIncidentListComponent.ts +86 -0
- package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +85 -0
- package/Utils/Dashboard/Components/Index.ts +21 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +57 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +54 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Server/API/EnterpriseLicenseAPI.js +64 -1
- package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
- package/build/dist/Server/API/GlobalConfigAPI.js +47 -0
- package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
- package/build/dist/Server/API/MetricAPI.js +123 -0
- package/build/dist/Server/API/MetricAPI.js.map +1 -0
- package/build/dist/Server/API/TelemetryAPI.js +9 -0
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +3 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Queue.js +3 -3
- package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +18 -0
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/MetricService.js +151 -1
- package/build/dist/Server/Services/MetricService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryAttributeService.js +36 -7
- package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +135 -5
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Tests/Types/Date.test.js +40 -0
- package/build/dist/Tests/Types/Date.test.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponentType.js +3 -0
- package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardAlertListComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardAlertListComponent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardIncidentListComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardIncidentListComponent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.js.map +1 -0
- package/build/dist/Types/Date.js +7 -2
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/JSONFunctions.js +47 -1
- package/build/dist/Types/JSONFunctions.js.map +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +21 -10
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
- package/build/dist/UI/Components/Dictionary/Dictionary.js +109 -16
- package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
- package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js +263 -0
- package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js.map +1 -0
- package/build/dist/UI/Components/Dictionary/DictionaryOfStrings.js +10 -6
- package/build/dist/UI/Components/Dictionary/DictionaryOfStrings.js.map +1 -1
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js +124 -6
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
- package/build/dist/UI/Components/Filters/FilterViewer.js +50 -12
- package/build/dist/UI/Components/Filters/FilterViewer.js.map +1 -1
- package/build/dist/UI/Components/Filters/FiltersForm.js +5 -4
- package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
- package/build/dist/UI/Components/Filters/JSONFilter.js +1 -1
- package/build/dist/UI/Components/Filters/JSONFilter.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +54 -5
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js +59 -29
- package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js +10 -2
- package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js +2 -5
- package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js +59 -22
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js +10 -2
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js.map +1 -1
- package/build/dist/Utils/Dashboard/Components/DashboardAlertListComponent.js +70 -0
- package/build/dist/Utils/Dashboard/Components/DashboardAlertListComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardIncidentListComponent.js +70 -0
- package/build/dist/Utils/Dashboard/Components/DashboardIncidentListComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +69 -0
- package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/Index.js +12 -0
- package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
- package/package.json +1 -1
package/Types/JSONFunctions.ts
CHANGED
|
@@ -20,7 +20,67 @@ export default class JSONFunctions {
|
|
|
20
20
|
obj1: GenericObject,
|
|
21
21
|
obj2: GenericObject,
|
|
22
22
|
): boolean {
|
|
23
|
-
return
|
|
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
|
|
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
|
-
{
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
aria-
|
|
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
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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<
|
|
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
|
|
93
|
+
if (typeof rawEntry === "number") {
|
|
78
94
|
valueType = ValueType.Number;
|
|
79
95
|
}
|
|
80
96
|
|
|
81
|
-
if (typeof
|
|
97
|
+
if (typeof rawEntry === "boolean") {
|
|
82
98
|
valueType = ValueType.Boolean;
|
|
83
99
|
}
|
|
84
100
|
}
|
|
85
101
|
|
|
86
|
-
const
|
|
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:
|
|
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<
|
|
150
|
+
const result: Dictionary<DictionaryEntryValue> = {};
|
|
113
151
|
data.forEach((item: Item) => {
|
|
114
|
-
|
|
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=
|
|
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
|
-
|
|
175
|
-
<
|
|
176
|
-
|
|
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=
|
|
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
|
-
{
|
|
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={
|
|
370
|
+
placeholder={
|
|
371
|
+
operatorOption.expectsNumericValue
|
|
372
|
+
? "Number"
|
|
373
|
+
: props.valuePlaceholder
|
|
374
|
+
}
|
|
225
375
|
suggestions={
|
|
226
|
-
|
|
227
|
-
?
|
|
228
|
-
:
|
|
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
|
}}
|