@oneuptime/common 10.0.85 → 10.0.88
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/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/TelemetryAttributeService.ts +37 -3
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +174 -7
- package/Tests/Types/Date.test.ts +46 -0
- package/Types/Date.ts +9 -4
- 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/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/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/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/Date.js +7 -2
- package/build/dist/Types/Date.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/package.json +1 -1
|
@@ -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
|
}}
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import EqualTo from "../../../Types/BaseDatabase/EqualTo";
|
|
2
|
+
import NotEqual from "../../../Types/BaseDatabase/NotEqual";
|
|
3
|
+
import Search from "../../../Types/BaseDatabase/Search";
|
|
4
|
+
import NotContains from "../../../Types/BaseDatabase/NotContains";
|
|
5
|
+
import StartsWith from "../../../Types/BaseDatabase/StartsWith";
|
|
6
|
+
import EndsWith from "../../../Types/BaseDatabase/EndsWith";
|
|
7
|
+
import GreaterThan from "../../../Types/BaseDatabase/GreaterThan";
|
|
8
|
+
import GreaterThanOrEqual from "../../../Types/BaseDatabase/GreaterThanOrEqual";
|
|
9
|
+
import LessThan from "../../../Types/BaseDatabase/LessThan";
|
|
10
|
+
import LessThanOrEqual from "../../../Types/BaseDatabase/LessThanOrEqual";
|
|
11
|
+
import IsNull from "../../../Types/BaseDatabase/IsNull";
|
|
12
|
+
import NotNull from "../../../Types/BaseDatabase/NotNull";
|
|
13
|
+
import { ObjectType } from "../../../Types/JSON";
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* UI-facing operator identifiers. We store these in the Dictionary form
|
|
17
|
+
* state and translate to the corresponding backend operator wrapper at
|
|
18
|
+
* the API boundary so the existing serialization pipeline works.
|
|
19
|
+
*/
|
|
20
|
+
export enum DictionaryFilterOperator {
|
|
21
|
+
EqualTo = "EqualTo",
|
|
22
|
+
NotEqual = "NotEqual",
|
|
23
|
+
Contains = "Contains",
|
|
24
|
+
NotContains = "NotContains",
|
|
25
|
+
StartsWith = "StartsWith",
|
|
26
|
+
EndsWith = "EndsWith",
|
|
27
|
+
GreaterThan = "GreaterThan",
|
|
28
|
+
LessThan = "LessThan",
|
|
29
|
+
GreaterThanOrEqual = "GreaterThanOrEqual",
|
|
30
|
+
LessThanOrEqual = "LessThanOrEqual",
|
|
31
|
+
IsEmpty = "IsEmpty",
|
|
32
|
+
IsNotEmpty = "IsNotEmpty",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface DictionaryFilterOperatorOption {
|
|
36
|
+
operator: DictionaryFilterOperator;
|
|
37
|
+
label: string;
|
|
38
|
+
symbol: string;
|
|
39
|
+
// Operators like IsEmpty/IsNotEmpty don't take a user-supplied value.
|
|
40
|
+
hidesValueInput?: boolean | undefined;
|
|
41
|
+
// Numeric operators force a numeric value input.
|
|
42
|
+
expectsNumericValue?: boolean | undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const DICTIONARY_FILTER_OPERATOR_OPTIONS: ReadonlyArray<DictionaryFilterOperatorOption> =
|
|
46
|
+
[
|
|
47
|
+
{
|
|
48
|
+
operator: DictionaryFilterOperator.EqualTo,
|
|
49
|
+
label: "equals",
|
|
50
|
+
symbol: "=",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
operator: DictionaryFilterOperator.NotEqual,
|
|
54
|
+
label: "does not equal",
|
|
55
|
+
symbol: "!=",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
operator: DictionaryFilterOperator.Contains,
|
|
59
|
+
label: "contains",
|
|
60
|
+
symbol: "contains",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
operator: DictionaryFilterOperator.NotContains,
|
|
64
|
+
label: "does not contain",
|
|
65
|
+
symbol: "does not contain",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
operator: DictionaryFilterOperator.StartsWith,
|
|
69
|
+
label: "starts with",
|
|
70
|
+
symbol: "starts with",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
operator: DictionaryFilterOperator.EndsWith,
|
|
74
|
+
label: "ends with",
|
|
75
|
+
symbol: "ends with",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
operator: DictionaryFilterOperator.GreaterThan,
|
|
79
|
+
label: "greater than",
|
|
80
|
+
symbol: ">",
|
|
81
|
+
expectsNumericValue: true,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
operator: DictionaryFilterOperator.GreaterThanOrEqual,
|
|
85
|
+
label: "greater than or equal",
|
|
86
|
+
symbol: ">=",
|
|
87
|
+
expectsNumericValue: true,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
operator: DictionaryFilterOperator.LessThan,
|
|
91
|
+
label: "less than",
|
|
92
|
+
symbol: "<",
|
|
93
|
+
expectsNumericValue: true,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
operator: DictionaryFilterOperator.LessThanOrEqual,
|
|
97
|
+
label: "less than or equal",
|
|
98
|
+
symbol: "<=",
|
|
99
|
+
expectsNumericValue: true,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
operator: DictionaryFilterOperator.IsEmpty,
|
|
103
|
+
label: "is empty",
|
|
104
|
+
symbol: "is empty",
|
|
105
|
+
hidesValueInput: true,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
operator: DictionaryFilterOperator.IsNotEmpty,
|
|
109
|
+
label: "is not empty",
|
|
110
|
+
symbol: "is not empty",
|
|
111
|
+
hidesValueInput: true,
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
export type DictionaryEntryValue =
|
|
116
|
+
| string
|
|
117
|
+
| number
|
|
118
|
+
| boolean
|
|
119
|
+
| EqualTo<string>
|
|
120
|
+
| NotEqual<string>
|
|
121
|
+
| Search<string>
|
|
122
|
+
| NotContains<string>
|
|
123
|
+
| StartsWith<string>
|
|
124
|
+
| EndsWith<string>
|
|
125
|
+
| GreaterThan<number>
|
|
126
|
+
| GreaterThanOrEqual<number>
|
|
127
|
+
| LessThan<number>
|
|
128
|
+
| LessThanOrEqual<number>
|
|
129
|
+
| IsNull
|
|
130
|
+
| NotNull;
|
|
131
|
+
|
|
132
|
+
export const getOperatorOption: (
|
|
133
|
+
operator: DictionaryFilterOperator,
|
|
134
|
+
) => DictionaryFilterOperatorOption = (
|
|
135
|
+
operator: DictionaryFilterOperator,
|
|
136
|
+
): DictionaryFilterOperatorOption => {
|
|
137
|
+
return (
|
|
138
|
+
DICTIONARY_FILTER_OPERATOR_OPTIONS.find(
|
|
139
|
+
(option: DictionaryFilterOperatorOption) => {
|
|
140
|
+
return option.operator === operator;
|
|
141
|
+
},
|
|
142
|
+
) ?? DICTIONARY_FILTER_OPERATOR_OPTIONS[0]!
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/*
|
|
147
|
+
* Detect operator wrapper instances or `_type`-tagged plain objects
|
|
148
|
+
* (already-deserialized vs raw-from-storage).
|
|
149
|
+
*/
|
|
150
|
+
type ObjectTypeLike = { _type?: string };
|
|
151
|
+
|
|
152
|
+
const matchesObjectType: (value: unknown, type: ObjectType) => boolean = (
|
|
153
|
+
value: unknown,
|
|
154
|
+
type: ObjectType,
|
|
155
|
+
): boolean => {
|
|
156
|
+
return Boolean(
|
|
157
|
+
value &&
|
|
158
|
+
typeof value === "object" &&
|
|
159
|
+
(value as ObjectTypeLike)._type === type,
|
|
160
|
+
);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
interface RawValueAndOperator {
|
|
164
|
+
operator: DictionaryFilterOperator;
|
|
165
|
+
rawValue: string;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Inspect a stored dictionary entry value (which may be a plain string,
|
|
170
|
+
* a hydrated operator instance, or a raw `{_type, value}` JSON shape)
|
|
171
|
+
* and recover the operator + display value used to populate the form.
|
|
172
|
+
*/
|
|
173
|
+
export const detectOperatorFromValue: (
|
|
174
|
+
value: unknown,
|
|
175
|
+
) => RawValueAndOperator = (value: unknown): RawValueAndOperator => {
|
|
176
|
+
if (value === null || value === undefined) {
|
|
177
|
+
return {
|
|
178
|
+
operator: DictionaryFilterOperator.EqualTo,
|
|
179
|
+
rawValue: "",
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (
|
|
184
|
+
typeof value === "string" ||
|
|
185
|
+
typeof value === "number" ||
|
|
186
|
+
typeof value === "boolean"
|
|
187
|
+
) {
|
|
188
|
+
return {
|
|
189
|
+
operator: DictionaryFilterOperator.EqualTo,
|
|
190
|
+
rawValue: String(value),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (value instanceof IsNull || matchesObjectType(value, ObjectType.IsNull)) {
|
|
195
|
+
return { operator: DictionaryFilterOperator.IsEmpty, rawValue: "" };
|
|
196
|
+
}
|
|
197
|
+
if (
|
|
198
|
+
value instanceof NotNull ||
|
|
199
|
+
matchesObjectType(value, ObjectType.NotNull)
|
|
200
|
+
) {
|
|
201
|
+
return { operator: DictionaryFilterOperator.IsNotEmpty, rawValue: "" };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const wrapperValue: string =
|
|
205
|
+
value instanceof Object && "value" in value
|
|
206
|
+
? String(
|
|
207
|
+
(value as { value?: unknown }).value === undefined ||
|
|
208
|
+
(value as { value?: unknown }).value === null
|
|
209
|
+
? ""
|
|
210
|
+
: (value as { value?: unknown }).value,
|
|
211
|
+
)
|
|
212
|
+
: "";
|
|
213
|
+
|
|
214
|
+
if (
|
|
215
|
+
value instanceof NotEqual ||
|
|
216
|
+
matchesObjectType(value, ObjectType.NotEqual)
|
|
217
|
+
) {
|
|
218
|
+
return {
|
|
219
|
+
operator: DictionaryFilterOperator.NotEqual,
|
|
220
|
+
rawValue: wrapperValue,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
if (
|
|
224
|
+
value instanceof EqualTo ||
|
|
225
|
+
matchesObjectType(value, ObjectType.EqualTo)
|
|
226
|
+
) {
|
|
227
|
+
return {
|
|
228
|
+
operator: DictionaryFilterOperator.EqualTo,
|
|
229
|
+
rawValue: wrapperValue,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
if (value instanceof Search || matchesObjectType(value, ObjectType.Search)) {
|
|
233
|
+
return {
|
|
234
|
+
operator: DictionaryFilterOperator.Contains,
|
|
235
|
+
rawValue: wrapperValue,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (
|
|
239
|
+
value instanceof NotContains ||
|
|
240
|
+
matchesObjectType(value, ObjectType.NotContains)
|
|
241
|
+
) {
|
|
242
|
+
return {
|
|
243
|
+
operator: DictionaryFilterOperator.NotContains,
|
|
244
|
+
rawValue: wrapperValue,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (
|
|
248
|
+
value instanceof StartsWith ||
|
|
249
|
+
matchesObjectType(value, ObjectType.StartsWith)
|
|
250
|
+
) {
|
|
251
|
+
return {
|
|
252
|
+
operator: DictionaryFilterOperator.StartsWith,
|
|
253
|
+
rawValue: wrapperValue,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (
|
|
257
|
+
value instanceof EndsWith ||
|
|
258
|
+
matchesObjectType(value, ObjectType.EndsWith)
|
|
259
|
+
) {
|
|
260
|
+
return {
|
|
261
|
+
operator: DictionaryFilterOperator.EndsWith,
|
|
262
|
+
rawValue: wrapperValue,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
if (
|
|
266
|
+
value instanceof GreaterThan ||
|
|
267
|
+
matchesObjectType(value, ObjectType.GreaterThan)
|
|
268
|
+
) {
|
|
269
|
+
return {
|
|
270
|
+
operator: DictionaryFilterOperator.GreaterThan,
|
|
271
|
+
rawValue: wrapperValue,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (
|
|
275
|
+
value instanceof GreaterThanOrEqual ||
|
|
276
|
+
matchesObjectType(value, ObjectType.GreaterThanOrEqual)
|
|
277
|
+
) {
|
|
278
|
+
return {
|
|
279
|
+
operator: DictionaryFilterOperator.GreaterThanOrEqual,
|
|
280
|
+
rawValue: wrapperValue,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
if (
|
|
284
|
+
value instanceof LessThan ||
|
|
285
|
+
matchesObjectType(value, ObjectType.LessThan)
|
|
286
|
+
) {
|
|
287
|
+
return {
|
|
288
|
+
operator: DictionaryFilterOperator.LessThan,
|
|
289
|
+
rawValue: wrapperValue,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
if (
|
|
293
|
+
value instanceof LessThanOrEqual ||
|
|
294
|
+
matchesObjectType(value, ObjectType.LessThanOrEqual)
|
|
295
|
+
) {
|
|
296
|
+
return {
|
|
297
|
+
operator: DictionaryFilterOperator.LessThanOrEqual,
|
|
298
|
+
rawValue: wrapperValue,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Unknown structure — fall back to bare equality with stringified value.
|
|
303
|
+
return {
|
|
304
|
+
operator: DictionaryFilterOperator.EqualTo,
|
|
305
|
+
rawValue: wrapperValue,
|
|
306
|
+
};
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Build the actual stored dictionary value for an operator + raw value.
|
|
311
|
+
* `EqualTo` produces a bare string for backwards compatibility with
|
|
312
|
+
* existing saved filters; everything else produces an operator wrapper
|
|
313
|
+
* instance.
|
|
314
|
+
*/
|
|
315
|
+
export const buildDictionaryValue: (input: {
|
|
316
|
+
operator: DictionaryFilterOperator;
|
|
317
|
+
rawValue: string;
|
|
318
|
+
}) => DictionaryEntryValue = (input: {
|
|
319
|
+
operator: DictionaryFilterOperator;
|
|
320
|
+
rawValue: string;
|
|
321
|
+
}): DictionaryEntryValue => {
|
|
322
|
+
const { operator, rawValue } = input;
|
|
323
|
+
const trimmed: string = rawValue ?? "";
|
|
324
|
+
|
|
325
|
+
switch (operator) {
|
|
326
|
+
case DictionaryFilterOperator.EqualTo:
|
|
327
|
+
return trimmed;
|
|
328
|
+
case DictionaryFilterOperator.NotEqual:
|
|
329
|
+
return new NotEqual<string>(trimmed);
|
|
330
|
+
case DictionaryFilterOperator.Contains:
|
|
331
|
+
/*
|
|
332
|
+
* Statement.serialize already wraps Search instances with `%...%`,
|
|
333
|
+
* so pass the bare needle here.
|
|
334
|
+
*/
|
|
335
|
+
return new Search<string>(trimmed);
|
|
336
|
+
case DictionaryFilterOperator.NotContains:
|
|
337
|
+
return new NotContains<string>(trimmed);
|
|
338
|
+
case DictionaryFilterOperator.StartsWith:
|
|
339
|
+
return new StartsWith<string>(trimmed);
|
|
340
|
+
case DictionaryFilterOperator.EndsWith:
|
|
341
|
+
return new EndsWith<string>(trimmed);
|
|
342
|
+
case DictionaryFilterOperator.GreaterThan:
|
|
343
|
+
return new GreaterThan<number>(Number(trimmed));
|
|
344
|
+
case DictionaryFilterOperator.GreaterThanOrEqual:
|
|
345
|
+
return new GreaterThanOrEqual<number>(Number(trimmed));
|
|
346
|
+
case DictionaryFilterOperator.LessThan:
|
|
347
|
+
return new LessThan<number>(Number(trimmed));
|
|
348
|
+
case DictionaryFilterOperator.LessThanOrEqual:
|
|
349
|
+
return new LessThanOrEqual<number>(Number(trimmed));
|
|
350
|
+
case DictionaryFilterOperator.IsEmpty:
|
|
351
|
+
return new IsNull();
|
|
352
|
+
case DictionaryFilterOperator.IsNotEmpty:
|
|
353
|
+
return new NotNull();
|
|
354
|
+
default:
|
|
355
|
+
return trimmed;
|
|
356
|
+
}
|
|
357
|
+
};
|