@parca/profile 0.16.73 → 0.16.75
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/Callgraph/index.js +1 -1
- 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 +70 -183
- package/dist/MetricsCircle/index.d.ts +1 -0
- package/dist/MetricsGraph/index.d.ts +1 -0
- 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/ProfileMetricsGraph/index.js +1 -1
- 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/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 +3 -2
- package/dist/ProfileViewWithData.d.ts +2 -1
- package/dist/ProfileViewWithData.js +1 -1
- 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/components/ProfileShareButton/index.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/styles.css +1 -1
- package/dist/useGrpcQuery/index.d.ts +11 -0
- package/dist/useGrpcQuery/index.js +64 -0
- package/dist/useQuery.js +59 -23
- package/dist/utils.js +1 -1
- package/package.json +7 -5
- package/src/MatchersInput/SuggestionsList.tsx +291 -0
- package/src/MatchersInput/index.tsx +78 -285
- package/src/useGrpcQuery/index.ts +42 -0
- package/src/useQuery.tsx +16 -27
|
@@ -11,15 +11,14 @@
|
|
|
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 {Transition} from '@headlessui/react';
|
|
14
|
+
import React, {useState, useEffect, useMemo, useRef} from 'react';
|
|
16
15
|
import {Query} from '@parca/parser';
|
|
17
16
|
import {LabelsResponse, QueryServiceClient} from '@parca/client';
|
|
18
|
-
import
|
|
17
|
+
import TextareaAutosize from 'react-textarea-autosize';
|
|
19
18
|
import cx from 'classnames';
|
|
20
19
|
|
|
21
|
-
import {
|
|
22
|
-
import
|
|
20
|
+
import SuggestionsList, {Suggestion, Suggestions} from './SuggestionsList';
|
|
21
|
+
import {useGrpcMetadata} from '@parca/components';
|
|
23
22
|
|
|
24
23
|
interface MatchersInputProps {
|
|
25
24
|
queryClient: QueryServiceClient;
|
|
@@ -56,58 +55,23 @@ export const useLabelNames = (client: QueryServiceClient): UseLabelNames => {
|
|
|
56
55
|
return {result, loading};
|
|
57
56
|
};
|
|
58
57
|
|
|
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
58
|
const MatchersInput = ({
|
|
84
59
|
queryClient,
|
|
85
60
|
setMatchersString,
|
|
86
61
|
runQuery,
|
|
87
62
|
currentQuery,
|
|
88
63
|
}: MatchersInputProps): JSX.Element => {
|
|
89
|
-
const
|
|
64
|
+
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
|
90
65
|
const [focusedInput, setFocusedInput] = useState(false);
|
|
91
|
-
const [showSuggest, setShowSuggest] = useState(true);
|
|
92
|
-
const [highlightedSuggestionIndex, setHighlightedSuggestionIndex] = useState(-1);
|
|
93
66
|
const [labelValuesLoading, setLabelValuesLoading] = useState(false);
|
|
94
67
|
const [lastCompleted, setLastCompleted] = useState<Suggestion>(new Suggestion('', '', ''));
|
|
95
|
-
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
|
96
68
|
const [labelValues, setLabelValues] = useState<string[] | null>(null);
|
|
97
69
|
const [currentLabelName, setCurrentLabelName] = useState<string | null>(null);
|
|
98
|
-
const {styles, attributes} = usePopper(inputRef, popperElement, {
|
|
99
|
-
placement: 'bottom-start',
|
|
100
|
-
});
|
|
101
70
|
const metadata = useGrpcMetadata();
|
|
102
|
-
const {loader: Spinner} = useParcaContext();
|
|
103
71
|
|
|
104
72
|
const {loading: labelNamesLoading, result} = useLabelNames(queryClient);
|
|
105
73
|
const {response: labelNamesResponse, error: labelNamesError} = result;
|
|
106
74
|
|
|
107
|
-
const LoadingSpinner = (): JSX.Element => {
|
|
108
|
-
return <div className="pt-2 pb-4">{Spinner}</div>;
|
|
109
|
-
};
|
|
110
|
-
|
|
111
75
|
useEffect(() => {
|
|
112
76
|
if (currentLabelName !== null) {
|
|
113
77
|
const call = queryClient.values({labelName: currentLabelName, match: []}, {meta: metadata});
|
|
@@ -122,180 +86,93 @@ const MatchersInput = ({
|
|
|
122
86
|
}
|
|
123
87
|
}, [currentLabelName, queryClient, metadata]);
|
|
124
88
|
|
|
125
|
-
const labelNames =
|
|
126
|
-
(labelNamesError === undefined || labelNamesError == null) &&
|
|
127
|
-
|
|
128
|
-
|
|
89
|
+
const labelNames = useMemo(() => {
|
|
90
|
+
return (labelNamesError === undefined || labelNamesError == null) &&
|
|
91
|
+
labelNamesResponse !== undefined &&
|
|
92
|
+
labelNamesResponse != null
|
|
129
93
|
? labelNamesResponse.labelNames.filter(e => e !== '__name__')
|
|
130
94
|
: [];
|
|
95
|
+
}, [labelNamesError, labelNamesResponse]);
|
|
131
96
|
|
|
132
97
|
const value = currentQuery.matchersString();
|
|
133
98
|
|
|
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
|
-
});
|
|
99
|
+
const suggestionSections = useMemo(() => {
|
|
100
|
+
const suggestionSections = new Suggestions();
|
|
101
|
+
Query.suggest(`${currentQuery.profileName()}{${value}`).forEach(function (s) {
|
|
102
|
+
// Skip suggestions that we just completed. This really only works,
|
|
103
|
+
// because we know the language is not repetitive. For a language that
|
|
104
|
+
// has a repeating word, this would not work.
|
|
105
|
+
if (lastCompleted !== null && lastCompleted.type === s.type) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
159
108
|
|
|
160
|
-
|
|
161
|
-
|
|
109
|
+
// Need to figure out if any literal suggestions make sense, but a
|
|
110
|
+
// closing bracket doesn't in the guided query experience because all
|
|
111
|
+
// we have the user do is type the matchers.
|
|
112
|
+
if (s.type === 'literal' && s.value !== '}') {
|
|
113
|
+
suggestionSections.literals.push({
|
|
162
114
|
type: s.type,
|
|
163
115
|
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;
|
|
116
|
+
value: s.value,
|
|
117
|
+
});
|
|
173
118
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
119
|
+
if (s.type === 'labelName') {
|
|
120
|
+
const inputValue = s.typeahead.trim().toLowerCase();
|
|
121
|
+
const inputLength = inputValue.length;
|
|
122
|
+
const matches = labelNames.filter(function (label) {
|
|
123
|
+
return label.toLowerCase().slice(0, inputLength) === inputValue;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
matches.forEach(m =>
|
|
127
|
+
suggestionSections.labelNames.push({
|
|
128
|
+
type: s.type,
|
|
129
|
+
typeahead: s.typeahead,
|
|
130
|
+
value: m,
|
|
131
|
+
})
|
|
132
|
+
);
|
|
185
133
|
}
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
134
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
135
|
+
if (s.type === 'labelValue') {
|
|
136
|
+
if (currentLabelName === null || s.labelName !== currentLabelName) {
|
|
137
|
+
setCurrentLabelName(s.labelName);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (labelValues !== null) {
|
|
142
|
+
labelValues
|
|
143
|
+
.filter(v => v.slice(0, s.typeahead.length) === s.typeahead)
|
|
144
|
+
.forEach(v =>
|
|
145
|
+
suggestionSections.labelValues.push({
|
|
146
|
+
type: s.type,
|
|
147
|
+
typeahead: s.typeahead,
|
|
148
|
+
value: v,
|
|
149
|
+
})
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
return suggestionSections;
|
|
155
|
+
}, [currentQuery, lastCompleted, labelNames, labelValues, currentLabelName, value]);
|
|
193
156
|
|
|
194
|
-
const resetHighlight = (): void => setHighlightedSuggestionIndex(-1);
|
|
195
157
|
const resetLastCompleted = (): void => setLastCompleted(new Suggestion('', '', ''));
|
|
196
158
|
|
|
197
|
-
const onChange = (e: React.ChangeEvent<
|
|
159
|
+
const onChange = (e: React.ChangeEvent<HTMLTextAreaElement>): void => {
|
|
198
160
|
const newValue = e.target.value;
|
|
199
161
|
setMatchersString(newValue);
|
|
200
162
|
resetLastCompleted();
|
|
201
|
-
resetHighlight();
|
|
202
163
|
};
|
|
203
164
|
|
|
204
165
|
const complete = (suggestion: Suggestion): string => {
|
|
205
166
|
return value.slice(0, value.length - suggestion.typeahead.length) + suggestion.value;
|
|
206
167
|
};
|
|
207
168
|
|
|
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);
|
|
169
|
+
const applySuggestion = (suggestion: Suggestion): void => {
|
|
241
170
|
const newValue = complete(suggestion);
|
|
242
|
-
resetHighlight();
|
|
243
171
|
setLastCompleted(suggestion);
|
|
244
172
|
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();
|
|
173
|
+
if (inputRef.current !== null) {
|
|
174
|
+
inputRef.current.value = newValue;
|
|
175
|
+
inputRef.current.focus();
|
|
299
176
|
}
|
|
300
177
|
};
|
|
301
178
|
|
|
@@ -305,18 +182,16 @@ const MatchersInput = ({
|
|
|
305
182
|
|
|
306
183
|
const unfocus = (): void => {
|
|
307
184
|
setFocusedInput(false);
|
|
308
|
-
resetHighlight();
|
|
309
185
|
};
|
|
310
186
|
|
|
311
187
|
const profileSelected = currentQuery.profileName() === '';
|
|
312
188
|
|
|
313
189
|
return (
|
|
314
190
|
<div className="font-mono flex-1 w-full block">
|
|
315
|
-
<
|
|
316
|
-
ref={
|
|
317
|
-
type="text"
|
|
191
|
+
<TextareaAutosize
|
|
192
|
+
ref={inputRef}
|
|
318
193
|
className={cx(
|
|
319
|
-
'bg-
|
|
194
|
+
'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
195
|
profileSelected && 'cursor-not-allowed'
|
|
321
196
|
)}
|
|
322
197
|
placeholder={
|
|
@@ -328,8 +203,6 @@ const MatchersInput = ({
|
|
|
328
203
|
value={value}
|
|
329
204
|
onBlur={unfocus}
|
|
330
205
|
onFocus={focus}
|
|
331
|
-
onKeyPress={handleKeyPress}
|
|
332
|
-
onKeyDown={handleKeyDown}
|
|
333
206
|
disabled={profileSelected} // Disable input if no profile has been selected
|
|
334
207
|
title={
|
|
335
208
|
profileSelected
|
|
@@ -337,95 +210,15 @@ const MatchersInput = ({
|
|
|
337
210
|
: 'filter profiles... eg. node="test"'
|
|
338
211
|
}
|
|
339
212
|
/>
|
|
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
|
-
)}
|
|
213
|
+
<SuggestionsList
|
|
214
|
+
isLabelNamesLoading={labelNamesLoading}
|
|
215
|
+
suggestions={suggestionSections}
|
|
216
|
+
applySuggestion={applySuggestion}
|
|
217
|
+
inputRef={inputRef.current}
|
|
218
|
+
runQuery={runQuery}
|
|
219
|
+
focusedInput={focusedInput}
|
|
220
|
+
isLabelValuesLoading={labelValuesLoading && lastCompleted.type === 'literal'}
|
|
221
|
+
/>
|
|
429
222
|
</div>
|
|
430
223
|
);
|
|
431
224
|
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Copyright 2022 The Parca Authors
|
|
2
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
// you may not use this file except in compliance with the License.
|
|
4
|
+
// You may obtain a copy of the License at
|
|
5
|
+
//
|
|
6
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
//
|
|
8
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
// See the License for the specific language governing permissions and
|
|
12
|
+
// limitations under the License.
|
|
13
|
+
|
|
14
|
+
import {useQuery, UseQueryResult} from 'react-query';
|
|
15
|
+
|
|
16
|
+
interface Props<IRes> {
|
|
17
|
+
key: string | any[];
|
|
18
|
+
queryFn: () => Promise<IRes>;
|
|
19
|
+
options?: {
|
|
20
|
+
enabled?: boolean | undefined;
|
|
21
|
+
staleTime?: number | undefined;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const useGrpcQuery = <IRes>({
|
|
26
|
+
key,
|
|
27
|
+
queryFn,
|
|
28
|
+
options: {enabled = true, staleTime} = {},
|
|
29
|
+
}: Props<IRes>): UseQueryResult<IRes> => {
|
|
30
|
+
return useQuery<IRes>(
|
|
31
|
+
key,
|
|
32
|
+
async () => {
|
|
33
|
+
return await queryFn();
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
enabled,
|
|
37
|
+
staleTime,
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default useGrpcQuery;
|
package/src/useQuery.tsx
CHANGED
|
@@ -11,13 +11,12 @@
|
|
|
11
11
|
// See the License for the specific language governing permissions and
|
|
12
12
|
// limitations under the License.
|
|
13
13
|
|
|
14
|
-
import {useEffect, useState} from 'react';
|
|
15
|
-
|
|
16
14
|
import {QueryServiceClient, QueryResponse, QueryRequest_ReportType} from '@parca/client';
|
|
17
15
|
import {RpcError} from '@protobuf-ts/runtime-rpc';
|
|
18
16
|
import {useGrpcMetadata} from '@parca/components';
|
|
19
17
|
|
|
20
18
|
import {ProfileSource} from './ProfileSource';
|
|
19
|
+
import useGrpcQuery from './useGrpcQuery';
|
|
21
20
|
|
|
22
21
|
export interface IQueryResult {
|
|
23
22
|
response: QueryResponse | null;
|
|
@@ -36,31 +35,21 @@ export const useQuery = (
|
|
|
36
35
|
options?: UseQueryOptions
|
|
37
36
|
): IQueryResult => {
|
|
38
37
|
const {skip = false} = options ?? {};
|
|
39
|
-
const [result, setResult] = useState<IQueryResult>({
|
|
40
|
-
response: null,
|
|
41
|
-
error: null,
|
|
42
|
-
isLoading: false,
|
|
43
|
-
});
|
|
44
38
|
const metadata = useGrpcMetadata();
|
|
39
|
+
const {data, isLoading, error} = useGrpcQuery<QueryResponse | undefined>({
|
|
40
|
+
key: ['query', profileSource, reportType],
|
|
41
|
+
queryFn: async () => {
|
|
42
|
+
const req = profileSource.QueryRequest();
|
|
43
|
+
req.reportType = reportType;
|
|
44
|
+
|
|
45
|
+
const {response} = await client.query(req, {meta: metadata});
|
|
46
|
+
return response;
|
|
47
|
+
},
|
|
48
|
+
options: {
|
|
49
|
+
enabled: !skip,
|
|
50
|
+
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
51
|
+
},
|
|
52
|
+
});
|
|
45
53
|
|
|
46
|
-
|
|
47
|
-
if (skip) {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
setResult({
|
|
51
|
-
response: null,
|
|
52
|
-
error: null,
|
|
53
|
-
isLoading: true,
|
|
54
|
-
});
|
|
55
|
-
const req = profileSource.QueryRequest();
|
|
56
|
-
req.reportType = reportType;
|
|
57
|
-
|
|
58
|
-
const call = client.query(req, {meta: metadata});
|
|
59
|
-
|
|
60
|
-
call.response
|
|
61
|
-
.then(response => setResult({response, error: null, isLoading: false}))
|
|
62
|
-
.catch(error => setResult({error, response: null, isLoading: false}));
|
|
63
|
-
}, [skip, client, profileSource, metadata, reportType]);
|
|
64
|
-
|
|
65
|
-
return result;
|
|
54
|
+
return {isLoading, error: error as RpcError | null, response: data ?? null};
|
|
66
55
|
};
|