@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
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DictionaryFilterOperator,
|
|
3
|
+
DictionaryFilterOperatorOption,
|
|
4
|
+
detectOperatorFromValue,
|
|
5
|
+
getOperatorOption,
|
|
6
|
+
} from "../Dictionary/DictionaryFilterOperator";
|
|
1
7
|
import Icon, { SizeProp } from "../Icon/Icon";
|
|
2
8
|
import Includes from "../../../Types/BaseDatabase/Includes";
|
|
3
9
|
import IncludesAll from "../../../Types/BaseDatabase/IncludesAll";
|
|
@@ -81,20 +87,50 @@ const FilterComponent: FilterComponentFunction = <T extends GenericObject>(
|
|
|
81
87
|
const formatJson: FormatJsonFunction = (
|
|
82
88
|
json: Dictionary<string | number | boolean>,
|
|
83
89
|
): ReactElement => {
|
|
90
|
+
/*
|
|
91
|
+
* Ignore entries with empty key or empty value, and translate
|
|
92
|
+
* operator wrappers into their display symbol so chips like
|
|
93
|
+
* `host.name != prod-1` or `path contains api` render correctly.
|
|
94
|
+
*/
|
|
95
|
+
const isMeaningfulEntry: (key: string, value: unknown) => boolean = (
|
|
96
|
+
key: string,
|
|
97
|
+
value: unknown,
|
|
98
|
+
): boolean => {
|
|
99
|
+
if (key.trim() === "" || value === undefined || value === null) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
const detected: { operator: DictionaryFilterOperator; rawValue: string } =
|
|
103
|
+
detectOperatorFromValue(value);
|
|
104
|
+
const option: DictionaryFilterOperatorOption = getOperatorOption(
|
|
105
|
+
detected.operator,
|
|
106
|
+
);
|
|
107
|
+
if (option.hidesValueInput) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
return detected.rawValue.trim() !== "";
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const visibleKeys: Array<string> = Object.keys(json).filter(
|
|
114
|
+
(key: string) => {
|
|
115
|
+
return isMeaningfulEntry(key, json[key]);
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
|
|
84
119
|
return (
|
|
85
120
|
<div className="flex space-x-2 -mt-1">
|
|
86
|
-
{
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
121
|
+
{visibleKeys.map((key: string, i: number) => {
|
|
122
|
+
const rawValue: unknown = json[key];
|
|
123
|
+
const detected: {
|
|
124
|
+
operator: DictionaryFilterOperator;
|
|
125
|
+
rawValue: string;
|
|
126
|
+
} = detectOperatorFromValue(rawValue);
|
|
127
|
+
const option: DictionaryFilterOperatorOption = getOperatorOption(
|
|
128
|
+
detected.operator,
|
|
129
|
+
);
|
|
95
130
|
|
|
96
|
-
|
|
97
|
-
|
|
131
|
+
let jsonText: string = detected.rawValue;
|
|
132
|
+
if (typeof rawValue === "boolean") {
|
|
133
|
+
jsonText = rawValue ? "True" : "False";
|
|
98
134
|
}
|
|
99
135
|
|
|
100
136
|
return (
|
|
@@ -102,8 +138,14 @@ const FilterComponent: FilterComponentFunction = <T extends GenericObject>(
|
|
|
102
138
|
key={i}
|
|
103
139
|
className="rounded-full h-7 bg-gray-100 text-gray-500 border-2 border-gray-200 p-1 pr-2 pl-2 text-xs"
|
|
104
140
|
>
|
|
105
|
-
<span className="font-medium">{key}</span>
|
|
106
|
-
<span
|
|
141
|
+
<span className="font-medium">{key}</span>{" "}
|
|
142
|
+
<span>{option.symbol}</span>
|
|
143
|
+
{!option.hidesValueInput && (
|
|
144
|
+
<>
|
|
145
|
+
{" "}
|
|
146
|
+
<span className="font-medium">{jsonText}</span>
|
|
147
|
+
</>
|
|
148
|
+
)}
|
|
107
149
|
</div>
|
|
108
150
|
);
|
|
109
151
|
})}
|
|
@@ -334,13 +376,36 @@ const FilterComponent: FilterComponentFunction = <T extends GenericObject>(
|
|
|
334
376
|
key
|
|
335
377
|
] as Dictionary<string | number | boolean>;
|
|
336
378
|
|
|
337
|
-
|
|
379
|
+
/*
|
|
380
|
+
* Count only meaningful entries — empty key/value pairs aren't
|
|
381
|
+
* real filters, and value-less operators (IsEmpty/IsNotEmpty) are
|
|
382
|
+
* valid even with no value.
|
|
383
|
+
*/
|
|
384
|
+
const nonEmptyEntryCount: number = Object.keys(json).filter(
|
|
385
|
+
(entryKey: string) => {
|
|
386
|
+
const value: unknown = json[entryKey];
|
|
387
|
+
if (entryKey.trim() === "" || value === undefined || value === null) {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
const detected: {
|
|
391
|
+
operator: DictionaryFilterOperator;
|
|
392
|
+
rawValue: string;
|
|
393
|
+
} = detectOperatorFromValue(value);
|
|
394
|
+
const option: DictionaryFilterOperatorOption = getOperatorOption(
|
|
395
|
+
detected.operator,
|
|
396
|
+
);
|
|
397
|
+
if (option.hidesValueInput) {
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
return detected.rawValue.trim() !== "";
|
|
401
|
+
},
|
|
402
|
+
).length;
|
|
338
403
|
|
|
339
|
-
if (
|
|
404
|
+
if (nonEmptyEntryCount === 0) {
|
|
340
405
|
return null;
|
|
341
406
|
}
|
|
342
407
|
|
|
343
|
-
const isPlural: boolean =
|
|
408
|
+
const isPlural: boolean = nonEmptyEntryCount > 1;
|
|
344
409
|
|
|
345
410
|
return (
|
|
346
411
|
<span className="inline-flex items-center space-x-1">
|
|
@@ -111,13 +111,21 @@ const FiltersForm: FiltersFormFunction = <T extends GenericObject>(
|
|
|
111
111
|
props.filterData[filter.key] !== undefined &&
|
|
112
112
|
props.filterData[filter.key] !== null;
|
|
113
113
|
|
|
114
|
+
const isMultiRowFilter: boolean = filter.type === FieldType.JSON;
|
|
115
|
+
|
|
114
116
|
return (
|
|
115
117
|
<div
|
|
116
118
|
key={i}
|
|
117
|
-
className=
|
|
119
|
+
className={`grid grid-cols-[140px_1fr_auto] gap-3 ${
|
|
120
|
+
isMultiRowFilter ? "items-start" : "items-center"
|
|
121
|
+
}`}
|
|
118
122
|
>
|
|
119
123
|
{/* Label column */}
|
|
120
|
-
<div
|
|
124
|
+
<div
|
|
125
|
+
className={`flex items-center min-w-0 ${
|
|
126
|
+
isMultiRowFilter ? "pt-7" : ""
|
|
127
|
+
}`}
|
|
128
|
+
>
|
|
121
129
|
<label className="text-sm font-medium text-gray-700 truncate">
|
|
122
130
|
{filter.title}
|
|
123
131
|
</label>
|
|
@@ -169,11 +177,18 @@ const FiltersForm: FiltersFormFunction = <T extends GenericObject>(
|
|
|
169
177
|
jsonKeys={filter.jsonKeys}
|
|
170
178
|
jsonValueSuggestions={filter.jsonValueSuggestions}
|
|
171
179
|
onJsonKeySelected={filter.onJsonKeySelected}
|
|
180
|
+
isLoadingJsonKeys={filter.isLoadingJsonKeys}
|
|
181
|
+
loadingJsonValueKeys={filter.loadingJsonValueKeys}
|
|
182
|
+
enableOperators={filter.jsonEnableOperators}
|
|
172
183
|
/>
|
|
173
184
|
</div>
|
|
174
185
|
|
|
175
186
|
{/* Clear column */}
|
|
176
|
-
<div
|
|
187
|
+
<div
|
|
188
|
+
className={`flex items-center ${
|
|
189
|
+
isMultiRowFilter ? "pt-7" : ""
|
|
190
|
+
}`}
|
|
191
|
+
>
|
|
177
192
|
{hasValue && filter.key ? (
|
|
178
193
|
<button
|
|
179
194
|
type="button"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FindWhereProperty } from "../../../Types/BaseDatabase/Query";
|
|
2
2
|
import DictionaryForm, { ValueType } from "../Dictionary/Dictionary";
|
|
3
|
+
import { DictionaryEntryValue } from "../Dictionary/DictionaryFilterOperator";
|
|
3
4
|
import FieldType from "../Types/FieldType";
|
|
4
5
|
import Filter from "./Types/Filter";
|
|
5
6
|
import FilterData from "./Types/FilterData";
|
|
@@ -14,6 +15,9 @@ export interface ComponentProps<T extends GenericObject> {
|
|
|
14
15
|
jsonKeys?: Array<string> | undefined;
|
|
15
16
|
jsonValueSuggestions?: Record<string, Array<string>> | undefined;
|
|
16
17
|
onJsonKeySelected?: ((key: string) => void) | undefined;
|
|
18
|
+
isLoadingJsonKeys?: boolean | undefined;
|
|
19
|
+
loadingJsonValueKeys?: Array<string> | undefined;
|
|
20
|
+
enableOperators?: boolean | undefined;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
type JSONFilterFunction = <T extends GenericObject>(
|
|
@@ -32,12 +36,17 @@ const JSONFilter: JSONFilterFunction = <T extends GenericObject>(
|
|
|
32
36
|
keys={props.jsonKeys}
|
|
33
37
|
valueSuggestions={props.jsonValueSuggestions}
|
|
34
38
|
onKeySelected={props.onJsonKeySelected}
|
|
39
|
+
isLoadingKeys={props.isLoadingJsonKeys}
|
|
40
|
+
loadingValueKeys={props.loadingJsonValueKeys}
|
|
41
|
+
enableOperators={props.enableOperators}
|
|
35
42
|
addButtonSuffix={filter.title}
|
|
36
43
|
keyPlaceholder={"Key"}
|
|
37
44
|
valuePlaceholder={"Value"}
|
|
38
45
|
valueTypes={[ValueType.Text]}
|
|
39
|
-
initialValue={
|
|
40
|
-
|
|
46
|
+
initialValue={
|
|
47
|
+
(filterData[filter.key] as Dictionary<DictionaryEntryValue>) || {}
|
|
48
|
+
}
|
|
49
|
+
onChange={(value: Dictionary<DictionaryEntryValue>) => {
|
|
41
50
|
// if no keys in the dictionary, remove the filter
|
|
42
51
|
|
|
43
52
|
if (Object.keys(value).length > 0) {
|
|
@@ -10,5 +10,8 @@ export default interface Filter<T extends GenericObject> {
|
|
|
10
10
|
jsonKeys?: Array<string> | undefined;
|
|
11
11
|
jsonValueSuggestions?: Record<string, Array<string>> | undefined;
|
|
12
12
|
onJsonKeySelected?: ((key: string) => void) | undefined;
|
|
13
|
+
isLoadingJsonKeys?: boolean | undefined;
|
|
14
|
+
loadingJsonValueKeys?: Array<string> | undefined;
|
|
15
|
+
jsonEnableOperators?: boolean | undefined;
|
|
13
16
|
isAdvancedFilter?: boolean | undefined;
|
|
14
17
|
}
|
|
@@ -457,6 +457,11 @@ const FormField: <T extends GenericObject>(
|
|
|
457
457
|
{props.field.fieldType === FormFieldSchemaType.Dictionary && (
|
|
458
458
|
<DictionaryForm
|
|
459
459
|
keys={props.field.jsonKeysForDictionary}
|
|
460
|
+
valueSuggestions={props.field.dictionaryValueSuggestions}
|
|
461
|
+
onKeySelected={props.field.onDictionaryKeySelected}
|
|
462
|
+
isLoadingKeys={props.field.isLoadingDictionaryKeys}
|
|
463
|
+
loadingValueKeys={props.field.loadingDictionaryValueKeys}
|
|
464
|
+
enableOperators={props.field.dictionaryEnableOperators}
|
|
460
465
|
addButtonSuffix={props.field.title}
|
|
461
466
|
keyPlaceholder={"Key"}
|
|
462
467
|
valuePlaceholder={"Value"}
|
|
@@ -467,7 +472,7 @@ const FormField: <T extends GenericObject>(
|
|
|
467
472
|
? (props.currentValues as any)[props.fieldName]
|
|
468
473
|
: props.field.defaultValue || {}
|
|
469
474
|
}
|
|
470
|
-
onChange={(value: Dictionary<
|
|
475
|
+
onChange={(value: Dictionary<any>) => {
|
|
471
476
|
onChange(value);
|
|
472
477
|
props.setFieldValue(props.fieldName, value);
|
|
473
478
|
}}
|
|
@@ -122,6 +122,11 @@ export default interface Field<TEntity> {
|
|
|
122
122
|
|
|
123
123
|
//
|
|
124
124
|
jsonKeysForDictionary?: Array<string> | undefined;
|
|
125
|
+
isLoadingDictionaryKeys?: boolean | undefined;
|
|
126
|
+
dictionaryValueSuggestions?: Record<string, Array<string>> | undefined;
|
|
127
|
+
loadingDictionaryValueKeys?: Array<string> | undefined;
|
|
128
|
+
onDictionaryKeySelected?: ((key: string) => void) | undefined;
|
|
129
|
+
dictionaryEnableOperators?: boolean | undefined;
|
|
125
130
|
|
|
126
131
|
hideOptionalLabel?: boolean | undefined;
|
|
127
132
|
|
|
@@ -197,6 +197,15 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
197
197
|
const [attributesLoaded, setAttributesLoaded] = useState<boolean>(false);
|
|
198
198
|
const [attributesLoading, setAttributesLoading] = useState<boolean>(false);
|
|
199
199
|
|
|
200
|
+
// Per-attribute value suggestions, populated lazily as the user types `@key:`.
|
|
201
|
+
const [attributeValueSuggestions, setAttributeValueSuggestions] = useState<
|
|
202
|
+
Record<string, Array<string>>
|
|
203
|
+
>({});
|
|
204
|
+
const [attributeValuesLoading, setAttributeValuesLoading] =
|
|
205
|
+
useState<boolean>(false);
|
|
206
|
+
const lastValueSuggestionKeyRef: React.MutableRefObject<string> =
|
|
207
|
+
useRef<string>("");
|
|
208
|
+
|
|
200
209
|
const [isPageLoading, setIsPageLoading] = useState<boolean>(true);
|
|
201
210
|
const [pageError, setPageError] = useState<string>("");
|
|
202
211
|
|
|
@@ -441,6 +450,58 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
441
450
|
}
|
|
442
451
|
}, [attributesLoaded, attributesLoading, loadAttributes]);
|
|
443
452
|
|
|
453
|
+
/*
|
|
454
|
+
* Lazily fetch values for the attribute the user is currently typing.
|
|
455
|
+
* Triggered by `@key:` patterns in the search bar — the same approach
|
|
456
|
+
* used by Traces/Metrics so the value dropdown can populate.
|
|
457
|
+
*/
|
|
458
|
+
useEffect(() => {
|
|
459
|
+
const currentWord: string = (searchQuery.split(/\s+/).pop() || "").trim();
|
|
460
|
+
if (!currentWord.startsWith("@") || !currentWord.includes(":")) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const colonIdx: number = currentWord.indexOf(":");
|
|
464
|
+
const attrKey: string = currentWord.substring(1, colonIdx);
|
|
465
|
+
|
|
466
|
+
if (!attrKey || attrKey === lastValueSuggestionKeyRef.current) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
lastValueSuggestionKeyRef.current = attrKey;
|
|
470
|
+
|
|
471
|
+
const loadValues: () => Promise<void> = async (): Promise<void> => {
|
|
472
|
+
try {
|
|
473
|
+
setAttributeValuesLoading(true);
|
|
474
|
+
const response: HTTPResponse<JSONObject> | HTTPErrorResponse =
|
|
475
|
+
await API.post({
|
|
476
|
+
url: URL.fromString(APP_API_URL.toString()).addRoute(
|
|
477
|
+
"/telemetry/logs/get-attribute-values",
|
|
478
|
+
),
|
|
479
|
+
data: { attributeKey: attrKey },
|
|
480
|
+
headers: {
|
|
481
|
+
...ModelAPI.getCommonHeaders(),
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
if (response instanceof HTTPErrorResponse) {
|
|
485
|
+
throw response;
|
|
486
|
+
}
|
|
487
|
+
const values: Array<string> = (response.data["values"] ||
|
|
488
|
+
[]) as Array<string>;
|
|
489
|
+
setAttributeValueSuggestions(
|
|
490
|
+
(
|
|
491
|
+
prev: Record<string, Array<string>>,
|
|
492
|
+
): Record<string, Array<string>> => {
|
|
493
|
+
return { ...prev, [attrKey]: values };
|
|
494
|
+
},
|
|
495
|
+
);
|
|
496
|
+
} catch {
|
|
497
|
+
// non-critical — leave the dropdown empty so users can still free-type
|
|
498
|
+
} finally {
|
|
499
|
+
setAttributeValuesLoading(false);
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
void loadValues();
|
|
503
|
+
}, [searchQuery]);
|
|
504
|
+
|
|
444
505
|
// Reset focused row when displayed logs change
|
|
445
506
|
useEffect(() => {
|
|
446
507
|
setFocusedRowIndex(-1);
|
|
@@ -669,17 +730,23 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
669
730
|
}, [props.activeFilters, serviceMap]);
|
|
670
731
|
|
|
671
732
|
/*
|
|
672
|
-
* Replace serviceId UUIDs with human-readable names in value suggestions
|
|
733
|
+
* Replace serviceId UUIDs with human-readable names in value suggestions,
|
|
734
|
+
* and merge in the lazily-fetched attribute value suggestions.
|
|
673
735
|
* Must be before early returns to maintain consistent hook call order.
|
|
674
736
|
*/
|
|
675
737
|
const resolvedValueSuggestions: Record<string, Array<string>> | undefined =
|
|
676
738
|
useMemo(() => {
|
|
677
|
-
|
|
739
|
+
const hasParentSuggestions: boolean = Boolean(props.valueSuggestions);
|
|
740
|
+
const hasAttributeSuggestions: boolean =
|
|
741
|
+
Object.keys(attributeValueSuggestions).length > 0;
|
|
742
|
+
|
|
743
|
+
if (!hasParentSuggestions && !hasAttributeSuggestions) {
|
|
678
744
|
return undefined;
|
|
679
745
|
}
|
|
680
746
|
|
|
681
747
|
const suggestions: Record<string, Array<string>> = {
|
|
682
|
-
...props.valueSuggestions,
|
|
748
|
+
...(props.valueSuggestions || {}),
|
|
749
|
+
...attributeValueSuggestions,
|
|
683
750
|
};
|
|
684
751
|
|
|
685
752
|
if (suggestions["serviceId"] && Object.keys(serviceMap).length > 0) {
|
|
@@ -692,7 +759,7 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
692
759
|
}
|
|
693
760
|
|
|
694
761
|
return suggestions;
|
|
695
|
-
}, [props.valueSuggestions, serviceMap]);
|
|
762
|
+
}, [props.valueSuggestions, serviceMap, attributeValueSuggestions]);
|
|
696
763
|
|
|
697
764
|
/*
|
|
698
765
|
* Wrap onFieldValueSelect to resolve service names back to UUIDs
|
|
@@ -828,6 +895,8 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
|
|
828
895
|
onSearchSubmit={handleSearchSubmit}
|
|
829
896
|
valueSuggestions={resolvedValueSuggestions}
|
|
830
897
|
onFieldValueSelect={handleFieldValueSelectWithServiceResolve}
|
|
898
|
+
isAttributesLoading={attributesLoading}
|
|
899
|
+
isValuesLoading={attributeValuesLoading}
|
|
831
900
|
toolbar={<LogsViewerToolbar {...toolbarProps} />}
|
|
832
901
|
/>
|
|
833
902
|
</div>
|
|
@@ -17,10 +17,17 @@ export interface LogSearchBarProps {
|
|
|
17
17
|
value: string;
|
|
18
18
|
onChange: (value: string) => void;
|
|
19
19
|
onSubmit: () => void;
|
|
20
|
+
// Top-level field names (e.g. "severity", "service") — used as `field:value`.
|
|
20
21
|
suggestions?: Array<string> | undefined;
|
|
22
|
+
// Telemetry attribute keys (no leading `@`) — used as `@attr:value`.
|
|
23
|
+
attributeSuggestions?: Array<string> | undefined;
|
|
21
24
|
valueSuggestions?: Record<string, Array<string>> | undefined;
|
|
22
25
|
onFieldValueSelect?: ((fieldKey: string, value: string) => void) | undefined;
|
|
23
26
|
placeholder?: string | undefined;
|
|
27
|
+
// Loading state for `@attribute` autocomplete (initial fetch of keys).
|
|
28
|
+
isAttributesLoading?: boolean | undefined;
|
|
29
|
+
// Loading state for `@attribute:value` autocomplete (per-key value fetch).
|
|
30
|
+
isValuesLoading?: boolean | undefined;
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
export interface LogSearchBarRef {
|
|
@@ -67,31 +74,48 @@ const LogSearchBar: React.ForwardRefExoticComponent<
|
|
|
67
74
|
? normalizedWord.substring(colonIndex + 1)
|
|
68
75
|
: "";
|
|
69
76
|
|
|
77
|
+
/*
|
|
78
|
+
* `@` is the explicit trigger for attribute mode — only show attribute
|
|
79
|
+
* keys there. Without `@`, only show top-level field names. Mixing
|
|
80
|
+
* the two led to confusing dropdowns that rendered field names like
|
|
81
|
+
* "severity" as if they were attributes.
|
|
82
|
+
*/
|
|
83
|
+
const activeSuggestions: Array<string> = hasAtPrefix
|
|
84
|
+
? props.attributeSuggestions || []
|
|
85
|
+
: props.suggestions || [];
|
|
86
|
+
|
|
70
87
|
const filteredSuggestions: Array<string> = isValueMode
|
|
71
88
|
? getValueSuggestions(
|
|
72
89
|
fieldPrefix,
|
|
73
90
|
partialValue,
|
|
74
91
|
props.valueSuggestions || {},
|
|
75
92
|
)
|
|
76
|
-
:
|
|
93
|
+
: activeSuggestions.filter((s: string): boolean => {
|
|
77
94
|
if (!normalizedWord && !hasAtPrefix) {
|
|
78
95
|
return false;
|
|
79
96
|
}
|
|
80
|
-
// When just "@" is typed, show all suggestions
|
|
81
97
|
if (hasAtPrefix && normalizedWord.length === 0) {
|
|
82
98
|
return true;
|
|
83
99
|
}
|
|
84
|
-
|
|
85
|
-
const normalizedSuggestion: string = s.startsWith("@")
|
|
86
|
-
? s.substring(1).toLowerCase()
|
|
87
|
-
: s.toLowerCase();
|
|
88
|
-
return normalizedSuggestion.startsWith(normalizedWord.toLowerCase());
|
|
100
|
+
return s.toLowerCase().startsWith(normalizedWord.toLowerCase());
|
|
89
101
|
});
|
|
90
102
|
|
|
103
|
+
/*
|
|
104
|
+
* Show a loader inside the dropdown while the parent is fetching:
|
|
105
|
+
* - attribute keys: `@` was just typed but the keys haven't arrived
|
|
106
|
+
* - attribute values: `@key:` was typed but values for that key
|
|
107
|
+
* haven't arrived yet
|
|
108
|
+
*/
|
|
109
|
+
const isLoadingForCurrentMode: boolean = isValueMode
|
|
110
|
+
? Boolean(props.isValuesLoading)
|
|
111
|
+
: hasAtPrefix
|
|
112
|
+
? Boolean(props.isAttributesLoading)
|
|
113
|
+
: false;
|
|
114
|
+
|
|
91
115
|
const shouldShowSuggestions: boolean =
|
|
92
116
|
showSuggestions &&
|
|
93
117
|
isFocused &&
|
|
94
|
-
filteredSuggestions.length > 0 &&
|
|
118
|
+
(filteredSuggestions.length > 0 || isLoadingForCurrentMode) &&
|
|
95
119
|
(isValueMode ? true : currentWord.length > 0);
|
|
96
120
|
|
|
97
121
|
// Show help when focused, input is empty, and no suggestions visible
|
|
@@ -111,6 +135,7 @@ const LogSearchBar: React.ForwardRefExoticComponent<
|
|
|
111
135
|
if (e.key === "Enter") {
|
|
112
136
|
if (
|
|
113
137
|
shouldShowSuggestions &&
|
|
138
|
+
!isLoadingForCurrentMode &&
|
|
114
139
|
selectedSuggestionIndex >= 0 &&
|
|
115
140
|
selectedSuggestionIndex < filteredSuggestions.length
|
|
116
141
|
) {
|
|
@@ -119,13 +144,17 @@ const LogSearchBar: React.ForwardRefExoticComponent<
|
|
|
119
144
|
return;
|
|
120
145
|
}
|
|
121
146
|
|
|
122
|
-
// If in value mode with a typed value,
|
|
147
|
+
// If in value mode with a typed value, apply as a chip
|
|
123
148
|
if (
|
|
124
149
|
isValueMode &&
|
|
125
150
|
partialValue.length > 0 &&
|
|
126
151
|
props.onFieldValueSelect
|
|
127
152
|
) {
|
|
128
|
-
|
|
153
|
+
/*
|
|
154
|
+
* Prefer a match from the suggestion list (so casing matches
|
|
155
|
+
* what's actually in the data); otherwise accept the typed
|
|
156
|
+
* value as-is so users aren't blocked when no suggestion exists.
|
|
157
|
+
*/
|
|
129
158
|
const resolvedField: string =
|
|
130
159
|
FIELD_ALIAS_MAP[fieldPrefix] || fieldPrefix;
|
|
131
160
|
const availableValues: Array<string> =
|
|
@@ -137,25 +166,21 @@ const LogSearchBar: React.ForwardRefExoticComponent<
|
|
|
137
166
|
},
|
|
138
167
|
);
|
|
139
168
|
|
|
140
|
-
|
|
141
|
-
const resolvedMatch: string | undefined =
|
|
169
|
+
const resolvedMatch: string =
|
|
142
170
|
exactMatch ||
|
|
143
171
|
(filteredSuggestions.length === 1
|
|
144
|
-
? filteredSuggestions[0]
|
|
145
|
-
:
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
e.preventDefault();
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
172
|
+
? filteredSuggestions[0]!
|
|
173
|
+
: partialValue);
|
|
174
|
+
|
|
175
|
+
props.onFieldValueSelect(fieldPrefix, resolvedMatch);
|
|
176
|
+
const parts: Array<string> = props.value.split(/\s+/);
|
|
177
|
+
parts.pop();
|
|
178
|
+
const remaining: string = parts.join(" ");
|
|
179
|
+
props.onChange(remaining ? remaining + " " : "");
|
|
180
|
+
setShowSuggestions(false);
|
|
181
|
+
setShowHelp(false);
|
|
182
|
+
e.preventDefault();
|
|
183
|
+
return;
|
|
159
184
|
}
|
|
160
185
|
|
|
161
186
|
props.onSubmit();
|
|
@@ -170,7 +195,7 @@ const LogSearchBar: React.ForwardRefExoticComponent<
|
|
|
170
195
|
return;
|
|
171
196
|
}
|
|
172
197
|
|
|
173
|
-
if (!shouldShowSuggestions) {
|
|
198
|
+
if (!shouldShowSuggestions || isLoadingForCurrentMode) {
|
|
174
199
|
return;
|
|
175
200
|
}
|
|
176
201
|
|
|
@@ -197,6 +222,7 @@ const LogSearchBar: React.ForwardRefExoticComponent<
|
|
|
197
222
|
fieldPrefix,
|
|
198
223
|
partialValue,
|
|
199
224
|
props,
|
|
225
|
+
isLoadingForCurrentMode,
|
|
200
226
|
],
|
|
201
227
|
);
|
|
202
228
|
|
|
@@ -219,11 +245,16 @@ const LogSearchBar: React.ForwardRefExoticComponent<
|
|
|
219
245
|
return;
|
|
220
246
|
}
|
|
221
247
|
|
|
222
|
-
|
|
248
|
+
/*
|
|
249
|
+
* Field name mode: append colon (re-prefix `@` for attribute keys
|
|
250
|
+
* since they're stored without it)
|
|
251
|
+
*/
|
|
223
252
|
const parts: Array<string> = props.value.split(/\s+/);
|
|
224
253
|
|
|
225
254
|
if (parts.length > 0) {
|
|
226
|
-
parts[parts.length - 1] =
|
|
255
|
+
parts[parts.length - 1] = hasAtPrefix
|
|
256
|
+
? "@" + suggestion + ":"
|
|
257
|
+
: suggestion + ":";
|
|
227
258
|
}
|
|
228
259
|
|
|
229
260
|
props.onChange(parts.join(" "));
|
|
@@ -231,7 +262,7 @@ const LogSearchBar: React.ForwardRefExoticComponent<
|
|
|
231
262
|
setShowHelp(false);
|
|
232
263
|
inputRef.current?.focus();
|
|
233
264
|
},
|
|
234
|
-
[props, isValueMode, fieldPrefix],
|
|
265
|
+
[props, isValueMode, fieldPrefix, hasAtPrefix],
|
|
235
266
|
);
|
|
236
267
|
|
|
237
268
|
const handleExampleClick: (example: string) => void = useCallback(
|
|
@@ -262,6 +293,17 @@ const LogSearchBar: React.ForwardRefExoticComponent<
|
|
|
262
293
|
};
|
|
263
294
|
}, []);
|
|
264
295
|
|
|
296
|
+
const loadingMessage: string = isValueMode
|
|
297
|
+
? `Loading values for ${fieldPrefix}...`
|
|
298
|
+
: "Loading attributes...";
|
|
299
|
+
|
|
300
|
+
const emptyMessage: string | undefined =
|
|
301
|
+
isValueMode &&
|
|
302
|
+
!isLoadingForCurrentMode &&
|
|
303
|
+
filteredSuggestions.length === 0
|
|
304
|
+
? `No matching values — press Enter to filter by "${partialValue}"`
|
|
305
|
+
: undefined;
|
|
306
|
+
|
|
265
307
|
return (
|
|
266
308
|
<div ref={containerRef} className="relative">
|
|
267
309
|
<div
|
|
@@ -326,6 +368,10 @@ const LogSearchBar: React.ForwardRefExoticComponent<
|
|
|
326
368
|
selectedIndex={selectedSuggestionIndex}
|
|
327
369
|
onSelect={applySuggestion}
|
|
328
370
|
fieldContext={isValueMode ? fieldPrefix : undefined}
|
|
371
|
+
isAttributeMode={hasAtPrefix}
|
|
372
|
+
isLoading={isLoadingForCurrentMode}
|
|
373
|
+
loadingMessage={loadingMessage}
|
|
374
|
+
emptyMessage={emptyMessage}
|
|
329
375
|
/>
|
|
330
376
|
)}
|
|
331
377
|
|
|
@@ -5,6 +5,14 @@ export interface LogSearchSuggestionsProps {
|
|
|
5
5
|
selectedIndex: number;
|
|
6
6
|
onSelect: (suggestion: string) => void;
|
|
7
7
|
fieldContext?: string | undefined;
|
|
8
|
+
/*
|
|
9
|
+
* When true, items are attribute keys (rendered with `@`).
|
|
10
|
+
* When false, items are top-level field names (no prefix).
|
|
11
|
+
*/
|
|
12
|
+
isAttributeMode?: boolean | undefined;
|
|
13
|
+
isLoading?: boolean | undefined;
|
|
14
|
+
loadingMessage?: string | undefined;
|
|
15
|
+
emptyMessage?: string | undefined;
|
|
8
16
|
}
|
|
9
17
|
|
|
10
18
|
const MAX_VISIBLE_SUGGESTIONS: number = 8;
|
|
@@ -19,6 +27,39 @@ const LogSearchSuggestions: FunctionComponent<LogSearchSuggestionsProps> = (
|
|
|
19
27
|
|
|
20
28
|
return (
|
|
21
29
|
<div className="absolute left-0 right-0 top-full z-50 mt-1 max-h-56 overflow-y-auto rounded-lg border border-gray-200 bg-white py-1 shadow-lg">
|
|
30
|
+
{props.isLoading && (
|
|
31
|
+
<div className="flex w-full items-center px-3 py-2 text-left text-sm text-gray-500">
|
|
32
|
+
<svg
|
|
33
|
+
className="animate-spin -ml-0.5 mr-2 h-4 w-4 text-indigo-500"
|
|
34
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
35
|
+
fill="none"
|
|
36
|
+
viewBox="0 0 24 24"
|
|
37
|
+
aria-hidden="true"
|
|
38
|
+
>
|
|
39
|
+
<circle
|
|
40
|
+
className="opacity-25"
|
|
41
|
+
cx="12"
|
|
42
|
+
cy="12"
|
|
43
|
+
r="10"
|
|
44
|
+
stroke="currentColor"
|
|
45
|
+
strokeWidth="4"
|
|
46
|
+
></circle>
|
|
47
|
+
<path
|
|
48
|
+
className="opacity-75"
|
|
49
|
+
fill="currentColor"
|
|
50
|
+
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
|
|
51
|
+
></path>
|
|
52
|
+
</svg>
|
|
53
|
+
<span>{props.loadingMessage || "Loading..."}</span>
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
{!props.isLoading &&
|
|
57
|
+
visible.length === 0 &&
|
|
58
|
+
props.emptyMessage !== undefined && (
|
|
59
|
+
<div className="px-3 py-2 text-sm text-gray-400">
|
|
60
|
+
{props.emptyMessage}
|
|
61
|
+
</div>
|
|
62
|
+
)}
|
|
22
63
|
{visible.map((suggestion: string, index: number) => {
|
|
23
64
|
const isSelected: boolean = index === props.selectedIndex;
|
|
24
65
|
|
|
@@ -43,11 +84,13 @@ const LogSearchSuggestions: FunctionComponent<LogSearchSuggestionsProps> = (
|
|
|
43
84
|
</span>
|
|
44
85
|
<span className="font-mono">{suggestion}</span>
|
|
45
86
|
</>
|
|
46
|
-
) : (
|
|
87
|
+
) : props.isAttributeMode ? (
|
|
47
88
|
<>
|
|
48
89
|
<span className="font-mono text-xs text-indigo-400">@</span>
|
|
49
90
|
<span className="font-mono">{suggestion}</span>
|
|
50
91
|
</>
|
|
92
|
+
) : (
|
|
93
|
+
<span className="font-mono">{suggestion}</span>
|
|
51
94
|
)}
|
|
52
95
|
</button>
|
|
53
96
|
);
|