@parca/profile 0.16.74 → 0.16.76
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/CHANGELOG.md +8 -0
- package/dist/Callgraph/index.d.ts +1 -0
- package/dist/GraphTooltip/index.d.ts +1 -0
- package/dist/IcicleGraph.d.ts +1 -0
- package/dist/MatchersInput/SuggestionItem.d.ts +1 -0
- package/dist/MatchersInput/SuggestionsList.d.ts +24 -0
- package/dist/MatchersInput/SuggestionsList.js +162 -0
- package/dist/MatchersInput/index.d.ts +1 -0
- package/dist/MatchersInput/index.js +75 -185
- package/dist/MetricsCircle/index.d.ts +1 -0
- package/dist/MetricsGraph/index.d.ts +1 -0
- package/dist/MetricsGraph/index.js +3 -2
- package/dist/MetricsSeries/index.d.ts +1 -0
- package/dist/ProfileExplorer/ProfileExplorerCompare.d.ts +1 -0
- package/dist/ProfileExplorer/ProfileExplorerSingle.d.ts +1 -0
- package/dist/ProfileExplorer/index.d.ts +1 -0
- package/dist/ProfileIcicleGraph.d.ts +1 -0
- package/dist/ProfileMetricsGraph/index.d.ts +1 -0
- package/dist/ProfileSelector/CompareButton.d.ts +1 -0
- package/dist/ProfileSelector/MergeButton.d.ts +1 -0
- package/dist/ProfileSelector/index.d.ts +1 -0
- package/dist/ProfileSelector/index.js +4 -1
- package/dist/ProfileSource.d.ts +1 -0
- package/dist/ProfileTypeSelector/index.d.ts +1 -0
- package/dist/ProfileView/FilterByFunctionButton.d.ts +1 -0
- package/dist/ProfileView/index.d.ts +1 -0
- package/dist/ProfileViewWithData.d.ts +1 -0
- package/dist/TopTable.d.ts +1 -0
- package/dist/components/DiffLegend.d.ts +1 -0
- package/dist/components/ProfileShareButton/ResultBox.d.ts +1 -0
- package/dist/components/ProfileShareButton/index.d.ts +1 -0
- package/dist/styles.css +1 -1
- package/package.json +7 -6
- package/src/MatchersInput/SuggestionsList.tsx +291 -0
- package/src/MatchersInput/index.tsx +85 -287
- package/src/MetricsGraph/index.tsx +6 -2
- package/src/ProfileSelector/index.tsx +4 -1
|
@@ -11,15 +11,16 @@
|
|
|
11
11
|
// See the License for the specific language governing permissions and
|
|
12
12
|
// limitations under the License.
|
|
13
13
|
|
|
14
|
-
import React, {
|
|
15
|
-
import
|
|
14
|
+
import React, {useState, useEffect, useMemo, useRef} from 'react';
|
|
15
|
+
import TextareaAutosize from 'react-textarea-autosize';
|
|
16
|
+
import cx from 'classnames';
|
|
17
|
+
|
|
18
|
+
import {useGrpcMetadata} from '@parca/components';
|
|
19
|
+
import {sanitizeLabelValue} from '@parca/functions';
|
|
16
20
|
import {Query} from '@parca/parser';
|
|
17
21
|
import {LabelsResponse, QueryServiceClient} from '@parca/client';
|
|
18
|
-
import {usePopper} from 'react-popper';
|
|
19
|
-
import cx from 'classnames';
|
|
20
22
|
|
|
21
|
-
import {
|
|
22
|
-
import SuggestionItem from './SuggestionItem';
|
|
23
|
+
import SuggestionsList, {Suggestion, Suggestions} from './SuggestionsList';
|
|
23
24
|
|
|
24
25
|
interface MatchersInputProps {
|
|
25
26
|
queryClient: QueryServiceClient;
|
|
@@ -56,58 +57,23 @@ export const useLabelNames = (client: QueryServiceClient): UseLabelNames => {
|
|
|
56
57
|
return {result, loading};
|
|
57
58
|
};
|
|
58
59
|
|
|
59
|
-
class Suggestion {
|
|
60
|
-
type: string;
|
|
61
|
-
typeahead: string;
|
|
62
|
-
value: string;
|
|
63
|
-
|
|
64
|
-
constructor(type: string, typeahead: string, value: string) {
|
|
65
|
-
this.type = type;
|
|
66
|
-
this.typeahead = typeahead;
|
|
67
|
-
this.value = value;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
class Suggestions {
|
|
72
|
-
literals: Suggestion[];
|
|
73
|
-
labelNames: Suggestion[];
|
|
74
|
-
labelValues: Suggestion[];
|
|
75
|
-
|
|
76
|
-
constructor() {
|
|
77
|
-
this.literals = [];
|
|
78
|
-
this.labelNames = [];
|
|
79
|
-
this.labelValues = [];
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
60
|
const MatchersInput = ({
|
|
84
61
|
queryClient,
|
|
85
62
|
setMatchersString,
|
|
86
63
|
runQuery,
|
|
87
64
|
currentQuery,
|
|
88
65
|
}: MatchersInputProps): JSX.Element => {
|
|
89
|
-
const
|
|
66
|
+
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
|
90
67
|
const [focusedInput, setFocusedInput] = useState(false);
|
|
91
|
-
const [showSuggest, setShowSuggest] = useState(true);
|
|
92
|
-
const [highlightedSuggestionIndex, setHighlightedSuggestionIndex] = useState(-1);
|
|
93
68
|
const [labelValuesLoading, setLabelValuesLoading] = useState(false);
|
|
94
69
|
const [lastCompleted, setLastCompleted] = useState<Suggestion>(new Suggestion('', '', ''));
|
|
95
|
-
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
|
96
70
|
const [labelValues, setLabelValues] = useState<string[] | null>(null);
|
|
97
71
|
const [currentLabelName, setCurrentLabelName] = useState<string | null>(null);
|
|
98
|
-
const {styles, attributes} = usePopper(inputRef, popperElement, {
|
|
99
|
-
placement: 'bottom-start',
|
|
100
|
-
});
|
|
101
72
|
const metadata = useGrpcMetadata();
|
|
102
|
-
const {loader: Spinner} = useParcaContext();
|
|
103
73
|
|
|
104
74
|
const {loading: labelNamesLoading, result} = useLabelNames(queryClient);
|
|
105
75
|
const {response: labelNamesResponse, error: labelNamesError} = result;
|
|
106
76
|
|
|
107
|
-
const LoadingSpinner = (): JSX.Element => {
|
|
108
|
-
return <div className="pt-2 pb-4">{Spinner}</div>;
|
|
109
|
-
};
|
|
110
|
-
|
|
111
77
|
useEffect(() => {
|
|
112
78
|
if (currentLabelName !== null) {
|
|
113
79
|
const call = queryClient.values({labelName: currentLabelName, match: []}, {meta: metadata});
|
|
@@ -115,187 +81,103 @@ const MatchersInput = ({
|
|
|
115
81
|
|
|
116
82
|
call.response
|
|
117
83
|
.then(response => {
|
|
118
|
-
|
|
84
|
+
// replace single `\` in the `labelValues` string with doubles `\\` if available.
|
|
85
|
+
const newValues = sanitizeLabelValue(response.labelValues);
|
|
86
|
+
|
|
87
|
+
setLabelValues(newValues);
|
|
119
88
|
})
|
|
120
89
|
.catch(() => setLabelValues(null))
|
|
121
90
|
.finally(() => setLabelValuesLoading(false));
|
|
122
91
|
}
|
|
123
92
|
}, [currentLabelName, queryClient, metadata]);
|
|
124
93
|
|
|
125
|
-
const labelNames =
|
|
126
|
-
(labelNamesError === undefined || labelNamesError == null) &&
|
|
127
|
-
|
|
128
|
-
|
|
94
|
+
const labelNames = useMemo(() => {
|
|
95
|
+
return (labelNamesError === undefined || labelNamesError == null) &&
|
|
96
|
+
labelNamesResponse !== undefined &&
|
|
97
|
+
labelNamesResponse != null
|
|
129
98
|
? labelNamesResponse.labelNames.filter(e => e !== '__name__')
|
|
130
99
|
: [];
|
|
100
|
+
}, [labelNamesError, labelNamesResponse]);
|
|
131
101
|
|
|
132
102
|
const value = currentQuery.matchersString();
|
|
133
103
|
|
|
134
|
-
const suggestionSections =
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
// Need to figure out if any literal suggestions make sense, but a
|
|
144
|
-
// closing bracket doesn't in the guided query experience because all
|
|
145
|
-
// we have the user do is type the matchers.
|
|
146
|
-
if (s.type === 'literal' && s.value !== '}') {
|
|
147
|
-
suggestionSections.literals.push({
|
|
148
|
-
type: s.type,
|
|
149
|
-
typeahead: s.typeahead,
|
|
150
|
-
value: s.value,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
if (s.type === 'labelName') {
|
|
154
|
-
const inputValue = s.typeahead.trim().toLowerCase();
|
|
155
|
-
const inputLength = inputValue.length;
|
|
156
|
-
const matches = labelNames.filter(function (label) {
|
|
157
|
-
return label.toLowerCase().slice(0, inputLength) === inputValue;
|
|
158
|
-
});
|
|
104
|
+
const suggestionSections = useMemo(() => {
|
|
105
|
+
const suggestionSections = new Suggestions();
|
|
106
|
+
Query.suggest(`${currentQuery.profileName()}{${value}`).forEach(function (s) {
|
|
107
|
+
// Skip suggestions that we just completed. This really only works,
|
|
108
|
+
// because we know the language is not repetitive. For a language that
|
|
109
|
+
// has a repeating word, this would not work.
|
|
110
|
+
if (lastCompleted !== null && lastCompleted.type === s.type) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
159
113
|
|
|
160
|
-
|
|
161
|
-
|
|
114
|
+
// Need to figure out if any literal suggestions make sense, but a
|
|
115
|
+
// closing bracket doesn't in the guided query experience because all
|
|
116
|
+
// we have the user do is type the matchers.
|
|
117
|
+
if (s.type === 'literal' && s.value !== '}') {
|
|
118
|
+
suggestionSections.literals.push({
|
|
162
119
|
type: s.type,
|
|
163
120
|
typeahead: s.typeahead,
|
|
164
|
-
value:
|
|
165
|
-
})
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (s.type === 'labelValue') {
|
|
170
|
-
if (currentLabelName === null || s.labelName !== currentLabelName) {
|
|
171
|
-
setCurrentLabelName(s.labelName);
|
|
172
|
-
return;
|
|
121
|
+
value: s.value,
|
|
122
|
+
});
|
|
173
123
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
124
|
+
if (s.type === 'labelName') {
|
|
125
|
+
const inputValue = s.typeahead.trim().toLowerCase();
|
|
126
|
+
const inputLength = inputValue.length;
|
|
127
|
+
const matches = labelNames.filter(function (label) {
|
|
128
|
+
return label.toLowerCase().slice(0, inputLength) === inputValue;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
matches.forEach(m =>
|
|
132
|
+
suggestionSections.labelNames.push({
|
|
133
|
+
type: s.type,
|
|
134
|
+
typeahead: s.typeahead,
|
|
135
|
+
value: m,
|
|
136
|
+
})
|
|
137
|
+
);
|
|
185
138
|
}
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
139
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
140
|
+
if (s.type === 'labelValue') {
|
|
141
|
+
if (currentLabelName === null || s.labelName !== currentLabelName) {
|
|
142
|
+
setCurrentLabelName(s.labelName);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (labelValues !== null) {
|
|
147
|
+
labelValues
|
|
148
|
+
.filter(v => v.slice(0, s.typeahead.length) === s.typeahead)
|
|
149
|
+
.forEach(v =>
|
|
150
|
+
suggestionSections.labelValues.push({
|
|
151
|
+
type: s.type,
|
|
152
|
+
typeahead: s.typeahead,
|
|
153
|
+
value: v,
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
return suggestionSections;
|
|
160
|
+
}, [currentQuery, lastCompleted, labelNames, labelValues, currentLabelName, value]);
|
|
193
161
|
|
|
194
|
-
const resetHighlight = (): void => setHighlightedSuggestionIndex(-1);
|
|
195
162
|
const resetLastCompleted = (): void => setLastCompleted(new Suggestion('', '', ''));
|
|
196
163
|
|
|
197
|
-
const onChange = (e: React.ChangeEvent<
|
|
164
|
+
const onChange = (e: React.ChangeEvent<HTMLTextAreaElement>): void => {
|
|
198
165
|
const newValue = e.target.value;
|
|
199
166
|
setMatchersString(newValue);
|
|
200
167
|
resetLastCompleted();
|
|
201
|
-
resetHighlight();
|
|
202
168
|
};
|
|
203
169
|
|
|
204
170
|
const complete = (suggestion: Suggestion): string => {
|
|
205
171
|
return value.slice(0, value.length - suggestion.typeahead.length) + suggestion.value;
|
|
206
172
|
};
|
|
207
173
|
|
|
208
|
-
const
|
|
209
|
-
if (index < suggestionSections.labelNames.length) {
|
|
210
|
-
return suggestionSections.labelNames[index];
|
|
211
|
-
}
|
|
212
|
-
if (index < suggestionSections.labelNames.length + suggestionSections.literals.length) {
|
|
213
|
-
return suggestionSections.literals[index - suggestionSections.labelNames.length];
|
|
214
|
-
}
|
|
215
|
-
return suggestionSections.labelValues[
|
|
216
|
-
index - suggestionSections.labelNames.length - suggestionSections.literals.length
|
|
217
|
-
];
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
const highlightNext = (): void => {
|
|
221
|
-
const nextIndex = highlightedSuggestionIndex + 1;
|
|
222
|
-
if (nextIndex === suggestionsLength) {
|
|
223
|
-
resetHighlight();
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
setHighlightedSuggestionIndex(nextIndex);
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
const highlightPrevious = (): void => {
|
|
230
|
-
if (highlightedSuggestionIndex === -1) {
|
|
231
|
-
// Didn't select anything, so starting at the bottom.
|
|
232
|
-
setHighlightedSuggestionIndex(suggestionsLength - 1);
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
setHighlightedSuggestionIndex(highlightedSuggestionIndex - 1);
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
const applySuggestion = (suggestionIndex: number): void => {
|
|
240
|
-
const suggestion = getSuggestion(suggestionIndex);
|
|
174
|
+
const applySuggestion = (suggestion: Suggestion): void => {
|
|
241
175
|
const newValue = complete(suggestion);
|
|
242
|
-
resetHighlight();
|
|
243
176
|
setLastCompleted(suggestion);
|
|
244
177
|
setMatchersString(newValue);
|
|
245
|
-
if (inputRef !== null) {
|
|
246
|
-
inputRef.value = newValue;
|
|
247
|
-
inputRef.focus();
|
|
248
|
-
}
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
const applyHighlightedSuggestion = (): void => {
|
|
252
|
-
applySuggestion(highlightedSuggestionIndex);
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
const handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>): void => {
|
|
256
|
-
// If there is a highlighted suggestion and enter is hit, we complete
|
|
257
|
-
// with the highlighted suggestion.
|
|
258
|
-
if (highlightedSuggestionIndex >= 0 && event.key === 'Enter') {
|
|
259
|
-
applyHighlightedSuggestion();
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// If no suggestions is highlighted and we hit enter, we run the query,
|
|
263
|
-
// and hide suggestions until another actions enables them again.
|
|
264
|
-
if (highlightedSuggestionIndex === -1 && event.key === 'Enter') {
|
|
265
|
-
setShowSuggest(false);
|
|
266
|
-
runQuery();
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
setShowSuggest(true);
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {
|
|
274
|
-
// Don't need to handle any key interactions if no suggestions there.
|
|
275
|
-
if (suggestionsLength === 0) {
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Handle tabbing through suggestions.
|
|
280
|
-
if (event.key === 'Tab' && suggestionsLength > 0) {
|
|
281
|
-
event.preventDefault();
|
|
282
|
-
if (event.shiftKey) {
|
|
283
|
-
// Shift + tab goes up.
|
|
284
|
-
highlightPrevious();
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
// Just tab goes down.
|
|
288
|
-
highlightNext();
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Up arrow highlights previous suggestions.
|
|
292
|
-
if (event.key === 'ArrowUp') {
|
|
293
|
-
highlightPrevious();
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Down arrow highlights next suggestions.
|
|
297
|
-
if (event.key === 'ArrowDown') {
|
|
298
|
-
highlightNext();
|
|
178
|
+
if (inputRef.current !== null) {
|
|
179
|
+
inputRef.current.value = newValue;
|
|
180
|
+
inputRef.current.focus();
|
|
299
181
|
}
|
|
300
182
|
};
|
|
301
183
|
|
|
@@ -305,18 +187,16 @@ const MatchersInput = ({
|
|
|
305
187
|
|
|
306
188
|
const unfocus = (): void => {
|
|
307
189
|
setFocusedInput(false);
|
|
308
|
-
resetHighlight();
|
|
309
190
|
};
|
|
310
191
|
|
|
311
192
|
const profileSelected = currentQuery.profileName() === '';
|
|
312
193
|
|
|
313
194
|
return (
|
|
314
195
|
<div className="font-mono flex-1 w-full block">
|
|
315
|
-
<
|
|
316
|
-
ref={
|
|
317
|
-
type="text"
|
|
196
|
+
<TextareaAutosize
|
|
197
|
+
ref={inputRef}
|
|
318
198
|
className={cx(
|
|
319
|
-
'bg-
|
|
199
|
+
'bg-gray-50 dark:bg-gray-900 focus:ring-indigo-800 flex-1 block w-full px-2 py-2 text-sm outline-none rounded',
|
|
320
200
|
profileSelected && 'cursor-not-allowed'
|
|
321
201
|
)}
|
|
322
202
|
placeholder={
|
|
@@ -328,8 +208,6 @@ const MatchersInput = ({
|
|
|
328
208
|
value={value}
|
|
329
209
|
onBlur={unfocus}
|
|
330
210
|
onFocus={focus}
|
|
331
|
-
onKeyPress={handleKeyPress}
|
|
332
|
-
onKeyDown={handleKeyDown}
|
|
333
211
|
disabled={profileSelected} // Disable input if no profile has been selected
|
|
334
212
|
title={
|
|
335
213
|
profileSelected
|
|
@@ -337,95 +215,15 @@ const MatchersInput = ({
|
|
|
337
215
|
: 'filter profiles... eg. node="test"'
|
|
338
216
|
}
|
|
339
217
|
/>
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
as={Fragment}
|
|
350
|
-
leave="transition ease-in duration-100"
|
|
351
|
-
leaveFrom="opacity-100"
|
|
352
|
-
leaveTo="opacity-0"
|
|
353
|
-
>
|
|
354
|
-
<div
|
|
355
|
-
style={{width: inputRef?.offsetWidth}}
|
|
356
|
-
className="absolute z-10 max-h-[400px] mt-1 bg-gray-50 dark:bg-gray-900 shadow-lg rounded-md text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
|
|
357
|
-
>
|
|
358
|
-
{labelNamesLoading ? (
|
|
359
|
-
<LoadingSpinner />
|
|
360
|
-
) : (
|
|
361
|
-
<>
|
|
362
|
-
{suggestionSections.labelNames.map((l, i) => (
|
|
363
|
-
<SuggestionItem
|
|
364
|
-
isHighlighted={highlightedSuggestionIndex === i}
|
|
365
|
-
onHighlight={() => setHighlightedSuggestionIndex(i)}
|
|
366
|
-
onApplySuggestion={() => applySuggestion(i)}
|
|
367
|
-
onResetHighlight={() => resetHighlight()}
|
|
368
|
-
value={l.value}
|
|
369
|
-
key={l.value}
|
|
370
|
-
/>
|
|
371
|
-
))}
|
|
372
|
-
</>
|
|
373
|
-
)}
|
|
374
|
-
|
|
375
|
-
{suggestionSections.literals.map((l, i) => (
|
|
376
|
-
<SuggestionItem
|
|
377
|
-
isHighlighted={
|
|
378
|
-
highlightedSuggestionIndex === i + suggestionSections.labelNames.length
|
|
379
|
-
}
|
|
380
|
-
onHighlight={() =>
|
|
381
|
-
setHighlightedSuggestionIndex(i + suggestionSections.labelNames.length)
|
|
382
|
-
}
|
|
383
|
-
onApplySuggestion={() =>
|
|
384
|
-
applySuggestion(i + suggestionSections.labelNames.length)
|
|
385
|
-
}
|
|
386
|
-
onResetHighlight={() => resetHighlight()}
|
|
387
|
-
value={l.value}
|
|
388
|
-
key={l.value}
|
|
389
|
-
/>
|
|
390
|
-
))}
|
|
391
|
-
|
|
392
|
-
{labelValuesLoading && lastCompleted.type === 'literal' ? (
|
|
393
|
-
<LoadingSpinner />
|
|
394
|
-
) : (
|
|
395
|
-
<>
|
|
396
|
-
{suggestionSections.labelValues.map((l, i) => (
|
|
397
|
-
<SuggestionItem
|
|
398
|
-
isHighlighted={
|
|
399
|
-
highlightedSuggestionIndex ===
|
|
400
|
-
i +
|
|
401
|
-
suggestionSections.labelNames.length +
|
|
402
|
-
suggestionSections.literals.length
|
|
403
|
-
}
|
|
404
|
-
onHighlight={() =>
|
|
405
|
-
setHighlightedSuggestionIndex(
|
|
406
|
-
i +
|
|
407
|
-
suggestionSections.labelNames.length +
|
|
408
|
-
suggestionSections.literals.length
|
|
409
|
-
)
|
|
410
|
-
}
|
|
411
|
-
onApplySuggestion={() =>
|
|
412
|
-
applySuggestion(
|
|
413
|
-
i +
|
|
414
|
-
suggestionSections.labelNames.length +
|
|
415
|
-
suggestionSections.literals.length
|
|
416
|
-
)
|
|
417
|
-
}
|
|
418
|
-
onResetHighlight={() => resetHighlight()}
|
|
419
|
-
value={l.value}
|
|
420
|
-
key={l.value}
|
|
421
|
-
/>
|
|
422
|
-
))}
|
|
423
|
-
</>
|
|
424
|
-
)}
|
|
425
|
-
</div>
|
|
426
|
-
</Transition>
|
|
427
|
-
</div>
|
|
428
|
-
)}
|
|
218
|
+
<SuggestionsList
|
|
219
|
+
isLabelNamesLoading={labelNamesLoading}
|
|
220
|
+
suggestions={suggestionSections}
|
|
221
|
+
applySuggestion={applySuggestion}
|
|
222
|
+
inputRef={inputRef.current}
|
|
223
|
+
runQuery={runQuery}
|
|
224
|
+
focusedInput={focusedInput}
|
|
225
|
+
isLabelValuesLoading={labelValuesLoading && lastCompleted.type === 'literal'}
|
|
226
|
+
/>
|
|
429
227
|
</div>
|
|
430
228
|
);
|
|
431
229
|
};
|
|
@@ -21,7 +21,7 @@ import throttle from 'lodash.throttle';
|
|
|
21
21
|
import {MetricsSeries as MetricsSeriesPb, MetricsSample, Label} from '@parca/client';
|
|
22
22
|
import {usePopper} from 'react-popper';
|
|
23
23
|
import type {VirtualElement} from '@popperjs/core';
|
|
24
|
-
import {valueFormatter, formatDate} from '@parca/functions';
|
|
24
|
+
import {valueFormatter, formatDate, sanitizeHighlightedValues} from '@parca/functions';
|
|
25
25
|
import {DateTimeRange} from '@parca/components';
|
|
26
26
|
import {useContainerDimensions} from '@parca/dynamicsize';
|
|
27
27
|
import useIsShiftDown from '@parca/components/src/hooks/useIsShiftDown';
|
|
@@ -362,7 +362,11 @@ export const RawMetricsGraph = ({
|
|
|
362
362
|
|
|
363
363
|
const openClosestProfile = (): void => {
|
|
364
364
|
if (highlighted != null) {
|
|
365
|
-
onSampleClick(
|
|
365
|
+
onSampleClick(
|
|
366
|
+
Math.round(highlighted.timestamp),
|
|
367
|
+
highlighted.value,
|
|
368
|
+
sanitizeHighlightedValues(highlighted.labels) // When a user clicks on any sample in the graph, replace single `\` in the `labelValues` string with doubles `\\` if available.
|
|
369
|
+
);
|
|
366
370
|
}
|
|
367
371
|
};
|
|
368
372
|
|
|
@@ -138,7 +138,10 @@ const ProfileSelector = ({
|
|
|
138
138
|
};
|
|
139
139
|
|
|
140
140
|
const addLabelMatcher = (key: string, value: string): void => {
|
|
141
|
-
|
|
141
|
+
// When a user clicks on a label on the metrics graph tooltip,
|
|
142
|
+
// replace single `\` in the `value` string with doubles `\\` if available.
|
|
143
|
+
const newValue = value.includes('\\') ? value.replaceAll('\\', '\\\\') : value;
|
|
144
|
+
const [newQuery, changed] = Query.parse(queryExpressionString).setMatcher(key, newValue);
|
|
142
145
|
if (changed) {
|
|
143
146
|
setNewQueryExpression(newQuery.toString(), false);
|
|
144
147
|
}
|