@oneuptime/common 10.0.86 → 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
|
@@ -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
|
);
|
|
@@ -9,6 +9,8 @@ export interface LogsFilterCardProps {
|
|
|
9
9
|
onSearchSubmit: () => void;
|
|
10
10
|
valueSuggestions?: Record<string, Array<string>> | undefined;
|
|
11
11
|
onFieldValueSelect?: ((fieldKey: string, value: string) => void) | undefined;
|
|
12
|
+
isAttributesLoading?: boolean | undefined;
|
|
13
|
+
isValuesLoading?: boolean | undefined;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
const LogsFilterCard: React.ForwardRefExoticComponent<
|
|
@@ -18,14 +20,11 @@ const LogsFilterCard: React.ForwardRefExoticComponent<
|
|
|
18
20
|
props: LogsFilterCardProps,
|
|
19
21
|
ref: React.Ref<LogSearchBarRef>,
|
|
20
22
|
): ReactElement => {
|
|
21
|
-
const
|
|
23
|
+
const fieldSuggestions: Array<string> = [
|
|
22
24
|
"severity",
|
|
23
25
|
"service",
|
|
24
26
|
"trace",
|
|
25
27
|
"span",
|
|
26
|
-
...props.logAttributes.map((attr: string) => {
|
|
27
|
-
return `@${attr}`;
|
|
28
|
-
}),
|
|
29
28
|
];
|
|
30
29
|
|
|
31
30
|
return (
|
|
@@ -36,9 +35,12 @@ const LogsFilterCard: React.ForwardRefExoticComponent<
|
|
|
36
35
|
value={props.searchQuery}
|
|
37
36
|
onChange={props.onSearchQueryChange}
|
|
38
37
|
onSubmit={props.onSearchSubmit}
|
|
39
|
-
suggestions={
|
|
38
|
+
suggestions={fieldSuggestions}
|
|
39
|
+
attributeSuggestions={props.logAttributes}
|
|
40
40
|
valueSuggestions={props.valueSuggestions}
|
|
41
41
|
onFieldValueSelect={props.onFieldValueSelect}
|
|
42
|
+
isAttributesLoading={props.isAttributesLoading}
|
|
43
|
+
isValuesLoading={props.isValuesLoading}
|
|
42
44
|
/>
|
|
43
45
|
</div>
|
|
44
46
|
<div>{props.toolbar}</div>
|
|
@@ -42,6 +42,7 @@ export interface TelemetryViewerProps<T> {
|
|
|
42
42
|
onSearchSubmit: () => void;
|
|
43
43
|
searchPlaceholder?: string | undefined;
|
|
44
44
|
searchSuggestions?: Array<string> | undefined;
|
|
45
|
+
searchAttributeSuggestions?: Array<string> | undefined;
|
|
45
46
|
searchValueSuggestions?: Record<string, Array<string>> | undefined;
|
|
46
47
|
searchFieldAliasMap?: Record<string, string> | undefined;
|
|
47
48
|
onSearchFieldValueSelect?:
|
|
@@ -50,6 +51,8 @@ export interface TelemetryViewerProps<T> {
|
|
|
50
51
|
searchHelpRows?: Array<SearchHelpRow> | undefined;
|
|
51
52
|
searchHelpCombinedExample?: string | undefined;
|
|
52
53
|
searchBarRef?: React.Ref<TelemetrySearchBarRef> | undefined;
|
|
54
|
+
searchAttributesLoading?: boolean | undefined;
|
|
55
|
+
searchValuesLoading?: boolean | undefined;
|
|
53
56
|
|
|
54
57
|
// -- Toolbar: time --
|
|
55
58
|
timeRange: RangeStartAndEndDateTime;
|
|
@@ -118,11 +121,14 @@ function TelemetryViewerInner<T>(props: TelemetryViewerProps<T>): ReactElement {
|
|
|
118
121
|
onSubmit={props.onSearchSubmit}
|
|
119
122
|
placeholder={props.searchPlaceholder}
|
|
120
123
|
suggestions={props.searchSuggestions}
|
|
124
|
+
attributeSuggestions={props.searchAttributeSuggestions}
|
|
121
125
|
valueSuggestions={props.searchValueSuggestions}
|
|
122
126
|
fieldAliasMap={props.searchFieldAliasMap}
|
|
123
127
|
onFieldValueSelect={props.onSearchFieldValueSelect}
|
|
124
128
|
helpRows={props.searchHelpRows}
|
|
125
129
|
helpCombinedExample={props.searchHelpCombinedExample}
|
|
130
|
+
isAttributesLoading={props.searchAttributesLoading}
|
|
131
|
+
isValuesLoading={props.searchValuesLoading}
|
|
126
132
|
/>
|
|
127
133
|
</div>
|
|
128
134
|
|
|
@@ -18,8 +18,17 @@ export interface TelemetrySearchBarProps {
|
|
|
18
18
|
value: string;
|
|
19
19
|
onChange: (value: string) => void;
|
|
20
20
|
onSubmit: () => void;
|
|
21
|
-
|
|
21
|
+
/*
|
|
22
|
+
* Top-level field names like "service", "name" — used as `field:value`
|
|
23
|
+
* (no @). Shown when the user types regular text.
|
|
24
|
+
*/
|
|
22
25
|
suggestions?: Array<string> | undefined;
|
|
26
|
+
/*
|
|
27
|
+
* Telemetry attribute keys like "host.name", "container.id" — used as
|
|
28
|
+
* `@attr:value`. Shown when the user types `@`. Pass plain keys, not
|
|
29
|
+
* pre-prefixed with `@` — the bar adds it on submit.
|
|
30
|
+
*/
|
|
31
|
+
attributeSuggestions?: Array<string> | undefined;
|
|
23
32
|
// field → allowed value completions (resolved field keys).
|
|
24
33
|
valueSuggestions?: Record<string, Array<string>> | undefined;
|
|
25
34
|
// Called when the user picks a concrete field:value chip from the dropdown.
|
|
@@ -30,6 +39,10 @@ export interface TelemetrySearchBarProps {
|
|
|
30
39
|
// Rows rendered in the help popover when the bar is empty + focused.
|
|
31
40
|
helpRows?: Array<SearchHelpRow> | undefined;
|
|
32
41
|
helpCombinedExample?: string | undefined;
|
|
42
|
+
// Loading state for `@attribute` autocomplete (initial fetch of keys).
|
|
43
|
+
isAttributesLoading?: boolean | undefined;
|
|
44
|
+
// Loading state for `@attribute:value` autocomplete (per-key value fetch).
|
|
45
|
+
isValuesLoading?: boolean | undefined;
|
|
33
46
|
}
|
|
34
47
|
|
|
35
48
|
export interface TelemetrySearchBarRef {
|
|
@@ -79,6 +92,17 @@ const TelemetrySearchBar: React.ForwardRefExoticComponent<
|
|
|
79
92
|
? normalizedWord.substring(colonIndex + 1)
|
|
80
93
|
: "";
|
|
81
94
|
|
|
95
|
+
/*
|
|
96
|
+
* Pick the suggestion list based on whether the user typed `@`.
|
|
97
|
+
* `@` is the explicit trigger for attribute mode — only show attribute
|
|
98
|
+
* keys there. Without `@`, only show top-level field names. Mixing the
|
|
99
|
+
* two led to confusing dropdowns that rendered field names like "name"
|
|
100
|
+
* as if they were attributes.
|
|
101
|
+
*/
|
|
102
|
+
const activeSuggestions: Array<string> = hasAtPrefix
|
|
103
|
+
? props.attributeSuggestions || []
|
|
104
|
+
: props.suggestions || [];
|
|
105
|
+
|
|
82
106
|
const filteredSuggestions: Array<string> = isValueMode
|
|
83
107
|
? getValueSuggestions(
|
|
84
108
|
fieldPrefix,
|
|
@@ -86,23 +110,32 @@ const TelemetrySearchBar: React.ForwardRefExoticComponent<
|
|
|
86
110
|
props.valueSuggestions || {},
|
|
87
111
|
fieldAliasMap,
|
|
88
112
|
)
|
|
89
|
-
:
|
|
113
|
+
: activeSuggestions.filter((s: string): boolean => {
|
|
90
114
|
if (!normalizedWord && !hasAtPrefix) {
|
|
91
115
|
return false;
|
|
92
116
|
}
|
|
93
117
|
if (hasAtPrefix && normalizedWord.length === 0) {
|
|
94
118
|
return true;
|
|
95
119
|
}
|
|
96
|
-
|
|
97
|
-
? s.substring(1).toLowerCase()
|
|
98
|
-
: s.toLowerCase();
|
|
99
|
-
return normalizedSuggestion.startsWith(normalizedWord.toLowerCase());
|
|
120
|
+
return s.toLowerCase().startsWith(normalizedWord.toLowerCase());
|
|
100
121
|
});
|
|
101
122
|
|
|
123
|
+
/*
|
|
124
|
+
* Show a loader inside the dropdown while the parent is fetching:
|
|
125
|
+
* - attribute keys: `@` was just typed but the keys haven't arrived
|
|
126
|
+
* - attribute values: `@key:` was typed but values for that key
|
|
127
|
+
* haven't arrived yet
|
|
128
|
+
*/
|
|
129
|
+
const isLoadingForCurrentMode: boolean = isValueMode
|
|
130
|
+
? Boolean(props.isValuesLoading)
|
|
131
|
+
: hasAtPrefix
|
|
132
|
+
? Boolean(props.isAttributesLoading)
|
|
133
|
+
: false;
|
|
134
|
+
|
|
102
135
|
const shouldShowSuggestions: boolean =
|
|
103
136
|
showSuggestions &&
|
|
104
137
|
isFocused &&
|
|
105
|
-
filteredSuggestions.length > 0 &&
|
|
138
|
+
(filteredSuggestions.length > 0 || isLoadingForCurrentMode) &&
|
|
106
139
|
(isValueMode ? true : currentWord.length > 0);
|
|
107
140
|
|
|
108
141
|
const shouldShowHelp: boolean =
|
|
@@ -123,6 +156,7 @@ const TelemetrySearchBar: React.ForwardRefExoticComponent<
|
|
|
123
156
|
if (e.key === "Enter") {
|
|
124
157
|
if (
|
|
125
158
|
shouldShowSuggestions &&
|
|
159
|
+
!isLoadingForCurrentMode &&
|
|
126
160
|
selectedSuggestionIndex >= 0 &&
|
|
127
161
|
selectedSuggestionIndex < filteredSuggestions.length
|
|
128
162
|
) {
|
|
@@ -136,6 +170,11 @@ const TelemetrySearchBar: React.ForwardRefExoticComponent<
|
|
|
136
170
|
partialValue.length > 0 &&
|
|
137
171
|
props.onFieldValueSelect
|
|
138
172
|
) {
|
|
173
|
+
/*
|
|
174
|
+
* Prefer a match from the suggestion list (so casing matches
|
|
175
|
+
* what's actually in the data); otherwise accept the typed
|
|
176
|
+
* value as-is so users aren't blocked when no suggestion exists.
|
|
177
|
+
*/
|
|
139
178
|
const resolvedField: string =
|
|
140
179
|
fieldAliasMap[fieldPrefix] || fieldPrefix;
|
|
141
180
|
const availableValues: Array<string> =
|
|
@@ -147,23 +186,21 @@ const TelemetrySearchBar: React.ForwardRefExoticComponent<
|
|
|
147
186
|
},
|
|
148
187
|
);
|
|
149
188
|
|
|
150
|
-
const resolvedMatch: string
|
|
189
|
+
const resolvedMatch: string =
|
|
151
190
|
exactMatch ||
|
|
152
191
|
(filteredSuggestions.length === 1
|
|
153
|
-
? filteredSuggestions[0]
|
|
154
|
-
:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
192
|
+
? filteredSuggestions[0]!
|
|
193
|
+
: partialValue);
|
|
194
|
+
|
|
195
|
+
props.onFieldValueSelect(fieldPrefix, resolvedMatch);
|
|
196
|
+
const parts: Array<string> = props.value.split(/\s+/);
|
|
197
|
+
parts.pop();
|
|
198
|
+
const remaining: string = parts.join(" ");
|
|
199
|
+
props.onChange(remaining ? remaining + " " : "");
|
|
200
|
+
setShowSuggestions(false);
|
|
201
|
+
setShowHelp(false);
|
|
202
|
+
e.preventDefault();
|
|
203
|
+
return;
|
|
167
204
|
}
|
|
168
205
|
|
|
169
206
|
props.onSubmit();
|
|
@@ -178,7 +215,7 @@ const TelemetrySearchBar: React.ForwardRefExoticComponent<
|
|
|
178
215
|
return;
|
|
179
216
|
}
|
|
180
217
|
|
|
181
|
-
if (!shouldShowSuggestions) {
|
|
218
|
+
if (!shouldShowSuggestions || isLoadingForCurrentMode) {
|
|
182
219
|
return;
|
|
183
220
|
}
|
|
184
221
|
|
|
@@ -206,6 +243,7 @@ const TelemetrySearchBar: React.ForwardRefExoticComponent<
|
|
|
206
243
|
partialValue,
|
|
207
244
|
props,
|
|
208
245
|
fieldAliasMap,
|
|
246
|
+
isLoadingForCurrentMode,
|
|
209
247
|
],
|
|
210
248
|
);
|
|
211
249
|
|
|
@@ -229,7 +267,13 @@ const TelemetrySearchBar: React.ForwardRefExoticComponent<
|
|
|
229
267
|
const parts: Array<string> = props.value.split(/\s+/);
|
|
230
268
|
|
|
231
269
|
if (parts.length > 0) {
|
|
232
|
-
|
|
270
|
+
/*
|
|
271
|
+
* Attribute suggestions are stored without `@`; add it back
|
|
272
|
+
* when filling the bar so the parser recognizes it as an attribute.
|
|
273
|
+
*/
|
|
274
|
+
parts[parts.length - 1] = hasAtPrefix
|
|
275
|
+
? "@" + suggestion + ":"
|
|
276
|
+
: suggestion + ":";
|
|
233
277
|
}
|
|
234
278
|
|
|
235
279
|
props.onChange(parts.join(" "));
|
|
@@ -237,7 +281,7 @@ const TelemetrySearchBar: React.ForwardRefExoticComponent<
|
|
|
237
281
|
setShowHelp(false);
|
|
238
282
|
inputRef.current?.focus();
|
|
239
283
|
},
|
|
240
|
-
[props, isValueMode, fieldPrefix],
|
|
284
|
+
[props, isValueMode, fieldPrefix, hasAtPrefix],
|
|
241
285
|
);
|
|
242
286
|
|
|
243
287
|
const handleExampleClick: (example: string) => void = useCallback(
|
|
@@ -268,6 +312,17 @@ const TelemetrySearchBar: React.ForwardRefExoticComponent<
|
|
|
268
312
|
};
|
|
269
313
|
}, []);
|
|
270
314
|
|
|
315
|
+
const loadingMessage: string = isValueMode
|
|
316
|
+
? `Loading values for ${fieldPrefix}...`
|
|
317
|
+
: "Loading attributes...";
|
|
318
|
+
|
|
319
|
+
const emptyMessage: string | undefined =
|
|
320
|
+
isValueMode &&
|
|
321
|
+
!isLoadingForCurrentMode &&
|
|
322
|
+
filteredSuggestions.length === 0
|
|
323
|
+
? `No matching values — press Enter to filter by "${partialValue}"`
|
|
324
|
+
: undefined;
|
|
325
|
+
|
|
271
326
|
return (
|
|
272
327
|
<div ref={containerRef} className="relative">
|
|
273
328
|
<div
|
|
@@ -329,6 +384,10 @@ const TelemetrySearchBar: React.ForwardRefExoticComponent<
|
|
|
329
384
|
selectedIndex={selectedSuggestionIndex}
|
|
330
385
|
onSelect={applySuggestion}
|
|
331
386
|
fieldContext={isValueMode ? fieldPrefix : undefined}
|
|
387
|
+
isAttributeMode={hasAtPrefix}
|
|
388
|
+
isLoading={isLoadingForCurrentMode}
|
|
389
|
+
loadingMessage={loadingMessage}
|
|
390
|
+
emptyMessage={emptyMessage}
|
|
332
391
|
/>
|
|
333
392
|
)}
|
|
334
393
|
|