@parca/profile 0.19.61 → 0.19.63
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/MatchersInput/SuggestionsList.d.ts +3 -1
- package/dist/MatchersInput/SuggestionsList.d.ts.map +1 -1
- package/dist/MatchersInput/SuggestionsList.js +48 -4
- package/dist/MatchersInput/index.d.ts +2 -0
- package/dist/MatchersInput/index.d.ts.map +1 -1
- package/dist/MatchersInput/index.js +21 -9
- package/dist/ProfileSelector/QueryControls.d.ts.map +1 -1
- package/dist/ProfileSelector/QueryControls.js +4 -1
- package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.d.ts +4 -0
- package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.d.ts.map +1 -1
- package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.js +2 -4
- package/dist/ProfileView/components/Toolbars/index.d.ts +4 -0
- package/dist/ProfileView/components/Toolbars/index.d.ts.map +1 -1
- package/dist/ProfileView/components/Toolbars/index.js +2 -2
- package/dist/ProfileView/hooks/useVisualizationState.d.ts +2 -0
- package/dist/ProfileView/hooks/useVisualizationState.d.ts.map +1 -1
- package/dist/ProfileView/hooks/useVisualizationState.js +19 -1
- package/dist/ProfileView/index.d.ts.map +1 -1
- package/dist/ProfileView/index.js +2 -2
- package/dist/SimpleMatchers/Select.d.ts +3 -0
- package/dist/SimpleMatchers/Select.d.ts.map +1 -1
- package/dist/SimpleMatchers/Select.js +30 -6
- package/dist/SimpleMatchers/index.d.ts +1 -0
- package/dist/SimpleMatchers/index.d.ts.map +1 -1
- package/dist/SimpleMatchers/index.js +92 -9
- package/dist/ViewMatchers/index.js +14 -14
- package/dist/contexts/MatchersInputLabelsContext.d.ts +2 -0
- package/dist/contexts/MatchersInputLabelsContext.d.ts.map +1 -1
- package/dist/contexts/MatchersInputLabelsContext.js +6 -2
- package/dist/contexts/SimpleMatchersLabelContext.d.ts +2 -0
- package/dist/contexts/SimpleMatchersLabelContext.d.ts.map +1 -1
- package/dist/contexts/SimpleMatchersLabelContext.js +24 -3
- package/dist/styles.css +1 -1
- package/dist/useGrpcQuery/index.d.ts +2 -1
- package/dist/useGrpcQuery/index.d.ts.map +1 -1
- package/dist/useGrpcQuery/index.js +2 -1
- package/package.json +5 -5
- package/src/MatchersInput/SuggestionsList.tsx +184 -39
- package/src/MatchersInput/index.tsx +28 -8
- package/src/ProfileSelector/QueryControls.tsx +5 -1
- package/src/ProfileView/components/Toolbars/MultiLevelDropdown.tsx +10 -5
- package/src/ProfileView/components/Toolbars/index.tsx +12 -0
- package/src/ProfileView/hooks/useVisualizationState.ts +34 -1
- package/src/ProfileView/index.tsx +7 -0
- package/src/SimpleMatchers/Select.tsx +132 -60
- package/src/SimpleMatchers/index.tsx +116 -12
- package/src/ViewMatchers/index.tsx +15 -15
- package/src/contexts/MatchersInputLabelsContext.tsx +16 -13
- package/src/contexts/SimpleMatchersLabelContext.tsx +39 -3
- package/src/useGrpcQuery/index.ts +3 -1
|
@@ -14,9 +14,12 @@
|
|
|
14
14
|
import {Fragment, useCallback, useEffect, useState} from 'react';
|
|
15
15
|
|
|
16
16
|
import {Transition} from '@headlessui/react';
|
|
17
|
+
import {Icon} from '@iconify/react';
|
|
18
|
+
import cx from 'classnames';
|
|
17
19
|
import {usePopper} from 'react-popper';
|
|
18
20
|
|
|
19
21
|
import {useParcaContext} from '@parca/components';
|
|
22
|
+
import {TEST_IDS, testId} from '@parca/test-utils';
|
|
20
23
|
|
|
21
24
|
import SuggestionItem from './SuggestionItem';
|
|
22
25
|
|
|
@@ -53,6 +56,8 @@ interface Props {
|
|
|
53
56
|
isLabelNamesLoading: boolean;
|
|
54
57
|
isLabelValuesLoading: boolean;
|
|
55
58
|
shouldTrimPrefix: boolean;
|
|
59
|
+
refetchLabelValues: () => void;
|
|
60
|
+
refetchLabelNames: () => void;
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
const LoadingSpinner = (): JSX.Element => {
|
|
@@ -61,6 +66,43 @@ const LoadingSpinner = (): JSX.Element => {
|
|
|
61
66
|
return <div className="pt-2 pb-4">{Spinner}</div>;
|
|
62
67
|
};
|
|
63
68
|
|
|
69
|
+
interface RefreshButtonProps {
|
|
70
|
+
onClick: () => void;
|
|
71
|
+
disabled: boolean;
|
|
72
|
+
title: string;
|
|
73
|
+
testId: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const RefreshButton = ({onClick, disabled, title, testId}: RefreshButtonProps): JSX.Element => {
|
|
77
|
+
return (
|
|
78
|
+
<div className="absolute w-full flex items-center justify-center bottom-0 px-3 py-2 bg-gray-50 dark:bg-gray-900">
|
|
79
|
+
<button
|
|
80
|
+
onClick={e => {
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
e.stopPropagation();
|
|
83
|
+
onClick();
|
|
84
|
+
}}
|
|
85
|
+
disabled={disabled}
|
|
86
|
+
className={cx(
|
|
87
|
+
'py-1 px-2 flex items-center gap-1 rounded-full transition-all duration-200 w-auto justify-center',
|
|
88
|
+
disabled
|
|
89
|
+
? 'cursor-wait opacity-50'
|
|
90
|
+
: 'hover:bg-gray-200 dark:hover:bg-gray-700 cursor-pointer'
|
|
91
|
+
)}
|
|
92
|
+
title={title}
|
|
93
|
+
type="button"
|
|
94
|
+
data-testid={testId}
|
|
95
|
+
>
|
|
96
|
+
<Icon
|
|
97
|
+
icon="system-uicons:reset"
|
|
98
|
+
className={cx('w-3 h-3 text-gray-500 dark:text-gray-400', disabled && 'animate-spin')}
|
|
99
|
+
/>
|
|
100
|
+
<span className="text-xs text-gray-500 dark:text-gray-400">Refresh results</span>
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
64
106
|
const transformLabelsForSuggestions = (labelNames: string, shouldTrimPrefix = false): string => {
|
|
65
107
|
const trimmedLabel = shouldTrimPrefix ? labelNames.split('.').pop() ?? labelNames : labelNames;
|
|
66
108
|
return trimmedLabel;
|
|
@@ -75,6 +117,8 @@ const SuggestionsList = ({
|
|
|
75
117
|
isLabelNamesLoading,
|
|
76
118
|
isLabelValuesLoading,
|
|
77
119
|
shouldTrimPrefix = false,
|
|
120
|
+
refetchLabelValues,
|
|
121
|
+
refetchLabelNames,
|
|
78
122
|
}: Props): JSX.Element => {
|
|
79
123
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
|
80
124
|
const {styles, attributes} = usePopper(inputRef, popperElement, {
|
|
@@ -82,6 +126,30 @@ const SuggestionsList = ({
|
|
|
82
126
|
});
|
|
83
127
|
const [highlightedSuggestionIndex, setHighlightedSuggestionIndex] = useState<number>(-1);
|
|
84
128
|
const [showSuggest, setShowSuggest] = useState(true);
|
|
129
|
+
const [isRefetchingValues, setIsRefetchingValues] = useState(false);
|
|
130
|
+
const [isRefetchingNames, setIsRefetchingNames] = useState(false);
|
|
131
|
+
|
|
132
|
+
const handleRefetchValues = useCallback(async () => {
|
|
133
|
+
if (isRefetchingValues) return;
|
|
134
|
+
|
|
135
|
+
setIsRefetchingValues(true);
|
|
136
|
+
try {
|
|
137
|
+
await refetchLabelValues();
|
|
138
|
+
} finally {
|
|
139
|
+
setIsRefetchingValues(false);
|
|
140
|
+
}
|
|
141
|
+
}, [refetchLabelValues, isRefetchingValues]);
|
|
142
|
+
|
|
143
|
+
const handleRefetchNames = useCallback(async () => {
|
|
144
|
+
if (isRefetchingNames) return;
|
|
145
|
+
|
|
146
|
+
setIsRefetchingNames(true);
|
|
147
|
+
try {
|
|
148
|
+
await refetchLabelNames();
|
|
149
|
+
} finally {
|
|
150
|
+
setIsRefetchingNames(false);
|
|
151
|
+
}
|
|
152
|
+
}, [refetchLabelNames, isRefetchingNames]);
|
|
85
153
|
|
|
86
154
|
const suggestionsLength =
|
|
87
155
|
suggestions.literals.length + suggestions.labelNames.length + suggestions.labelValues.length;
|
|
@@ -211,6 +279,12 @@ const SuggestionsList = ({
|
|
|
211
279
|
};
|
|
212
280
|
}, [inputRef, highlightedSuggestionIndex, suggestions, handleKeyPress, handleKeyDown]);
|
|
213
281
|
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
if (suggestionsLength > 0 && focusedInput) {
|
|
284
|
+
setShowSuggest(true);
|
|
285
|
+
}
|
|
286
|
+
}, [suggestionsLength, focusedInput]);
|
|
287
|
+
|
|
214
288
|
return (
|
|
215
289
|
<>
|
|
216
290
|
{suggestionsLength > 0 && (
|
|
@@ -231,43 +305,114 @@ const SuggestionsList = ({
|
|
|
231
305
|
style={{width: inputRef?.offsetWidth}}
|
|
232
306
|
className="absolute z-10 mt-1 max-h-[400px] overflow-auto rounded-md bg-gray-50 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-gray-900 sm:text-sm"
|
|
233
307
|
>
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
308
|
+
<div
|
|
309
|
+
className={cx('relative', {
|
|
310
|
+
'pb-12': suggestions.labelNames.length === 0 && suggestions.literals.length === 0,
|
|
311
|
+
})}
|
|
312
|
+
>
|
|
313
|
+
{isLabelNamesLoading ? (
|
|
314
|
+
<LoadingSpinner />
|
|
315
|
+
) : suggestions.literals.length === 0 && suggestions.labelValues.length === 0 ? (
|
|
316
|
+
<>
|
|
317
|
+
{suggestions.labelNames.length === 0 ? (
|
|
318
|
+
<div
|
|
319
|
+
className="px-4 py-3 text-sm text-gray-500 dark:text-gray-400 text-center"
|
|
320
|
+
{...testId(TEST_IDS.SUGGESTIONS_NO_RESULTS)}
|
|
321
|
+
>
|
|
322
|
+
No label names found
|
|
323
|
+
</div>
|
|
324
|
+
) : (
|
|
325
|
+
suggestions.labelNames.map((l, i) => (
|
|
326
|
+
<SuggestionItem
|
|
327
|
+
isHighlighted={highlightedSuggestionIndex === i}
|
|
328
|
+
onHighlight={() => setHighlightedSuggestionIndex(i)}
|
|
329
|
+
onApplySuggestion={() => applySuggestionWithIndex(i)}
|
|
330
|
+
onResetHighlight={() => resetHighlight()}
|
|
331
|
+
value={transformLabelsForSuggestions(l.value, shouldTrimPrefix)}
|
|
332
|
+
key={transformLabelsForSuggestions(l.value, shouldTrimPrefix)}
|
|
333
|
+
/>
|
|
334
|
+
))
|
|
335
|
+
)}
|
|
336
|
+
<RefreshButton
|
|
337
|
+
onClick={() => void handleRefetchNames()}
|
|
338
|
+
disabled={isRefetchingNames}
|
|
339
|
+
title="Refresh label names"
|
|
340
|
+
testId="suggestions-refresh-names-button"
|
|
341
|
+
/>
|
|
342
|
+
</>
|
|
343
|
+
) : (
|
|
344
|
+
<>
|
|
345
|
+
{suggestions.labelNames.map((l, i) => (
|
|
346
|
+
<SuggestionItem
|
|
347
|
+
isHighlighted={highlightedSuggestionIndex === i}
|
|
348
|
+
onHighlight={() => setHighlightedSuggestionIndex(i)}
|
|
349
|
+
onApplySuggestion={() => applySuggestionWithIndex(i)}
|
|
350
|
+
onResetHighlight={() => resetHighlight()}
|
|
351
|
+
value={transformLabelsForSuggestions(l.value, shouldTrimPrefix)}
|
|
352
|
+
key={transformLabelsForSuggestions(l.value, shouldTrimPrefix)}
|
|
353
|
+
/>
|
|
354
|
+
))}
|
|
355
|
+
</>
|
|
356
|
+
)}
|
|
357
|
+
|
|
358
|
+
{suggestions.literals.map((l, i) => (
|
|
359
|
+
<SuggestionItem
|
|
360
|
+
isHighlighted={highlightedSuggestionIndex === i + suggestions.labelNames.length}
|
|
361
|
+
onHighlight={() =>
|
|
362
|
+
setHighlightedSuggestionIndex(i + suggestions.labelNames.length)
|
|
363
|
+
}
|
|
364
|
+
onApplySuggestion={() =>
|
|
365
|
+
applySuggestionWithIndex(i + suggestions.labelNames.length)
|
|
366
|
+
}
|
|
367
|
+
onResetHighlight={() => resetHighlight()}
|
|
368
|
+
value={l.value}
|
|
369
|
+
key={l.value}
|
|
370
|
+
/>
|
|
371
|
+
))}
|
|
372
|
+
|
|
373
|
+
{isLabelValuesLoading ? (
|
|
374
|
+
<LoadingSpinner />
|
|
375
|
+
) : suggestions.labelNames.length === 0 && suggestions.literals.length === 0 ? (
|
|
376
|
+
<>
|
|
377
|
+
{suggestions.labelValues.length === 0 ? (
|
|
378
|
+
<div
|
|
379
|
+
className="px-4 py-3 text-sm text-gray-500 dark:text-gray-400 text-center"
|
|
380
|
+
{...testId(TEST_IDS.SUGGESTIONS_NO_RESULTS)}
|
|
381
|
+
>
|
|
382
|
+
No label values found
|
|
383
|
+
</div>
|
|
384
|
+
) : (
|
|
385
|
+
suggestions.labelValues.map((l, i) => (
|
|
386
|
+
<SuggestionItem
|
|
387
|
+
isHighlighted={
|
|
388
|
+
highlightedSuggestionIndex ===
|
|
389
|
+
i + suggestions.labelNames.length + suggestions.literals.length
|
|
390
|
+
}
|
|
391
|
+
onHighlight={() =>
|
|
392
|
+
setHighlightedSuggestionIndex(
|
|
393
|
+
i + suggestions.labelNames.length + suggestions.literals.length
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
onApplySuggestion={() =>
|
|
397
|
+
applySuggestionWithIndex(
|
|
398
|
+
i + suggestions.labelNames.length + suggestions.literals.length
|
|
399
|
+
)
|
|
400
|
+
}
|
|
401
|
+
onResetHighlight={() => resetHighlight()}
|
|
402
|
+
value={l.value}
|
|
403
|
+
key={l.value}
|
|
404
|
+
/>
|
|
405
|
+
))
|
|
406
|
+
)}
|
|
407
|
+
<RefreshButton
|
|
408
|
+
onClick={() => void handleRefetchValues()}
|
|
409
|
+
disabled={isRefetchingValues}
|
|
410
|
+
title="Refresh label values"
|
|
411
|
+
testId="suggestions-refresh-values-button"
|
|
246
412
|
/>
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
{suggestions.literals.map((l, i) => (
|
|
252
|
-
<SuggestionItem
|
|
253
|
-
isHighlighted={highlightedSuggestionIndex === i + suggestions.labelNames.length}
|
|
254
|
-
onHighlight={() =>
|
|
255
|
-
setHighlightedSuggestionIndex(i + suggestions.labelNames.length)
|
|
256
|
-
}
|
|
257
|
-
onApplySuggestion={() =>
|
|
258
|
-
applySuggestionWithIndex(i + suggestions.labelNames.length)
|
|
259
|
-
}
|
|
260
|
-
onResetHighlight={() => resetHighlight()}
|
|
261
|
-
value={l.value}
|
|
262
|
-
key={l.value}
|
|
263
|
-
/>
|
|
264
|
-
))}
|
|
265
|
-
|
|
266
|
-
{isLabelValuesLoading ? (
|
|
267
|
-
<LoadingSpinner />
|
|
268
|
-
) : (
|
|
269
|
-
<>
|
|
270
|
-
{suggestions.labelValues.map((l, i) => (
|
|
413
|
+
</>
|
|
414
|
+
) : (
|
|
415
|
+
suggestions.labelValues.map((l, i) => (
|
|
271
416
|
<SuggestionItem
|
|
272
417
|
isHighlighted={
|
|
273
418
|
highlightedSuggestionIndex ===
|
|
@@ -287,9 +432,9 @@ const SuggestionsList = ({
|
|
|
287
432
|
value={l.value}
|
|
288
433
|
key={l.value}
|
|
289
434
|
/>
|
|
290
|
-
))
|
|
291
|
-
|
|
292
|
-
|
|
435
|
+
))
|
|
436
|
+
)}
|
|
437
|
+
</div>
|
|
293
438
|
</div>
|
|
294
439
|
</Transition>
|
|
295
440
|
</div>
|
|
@@ -46,6 +46,7 @@ export interface ILabelNamesResult {
|
|
|
46
46
|
interface UseLabelNames {
|
|
47
47
|
result: ILabelNamesResult;
|
|
48
48
|
loading: boolean;
|
|
49
|
+
refetch: () => void;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
export const useLabelNames = (
|
|
@@ -57,7 +58,7 @@ export const useLabelNames = (
|
|
|
57
58
|
): UseLabelNames => {
|
|
58
59
|
const metadata = useGrpcMetadata();
|
|
59
60
|
|
|
60
|
-
const {data, isLoading, error} = useGrpcQuery<LabelsResponse>({
|
|
61
|
+
const {data, isLoading, error, refetch} = useGrpcQuery<LabelsResponse>({
|
|
61
62
|
key: ['labelNames', profileType, match?.join(','), start, end],
|
|
62
63
|
queryFn: async signal => {
|
|
63
64
|
const request: LabelsRequest = {match: match !== undefined ? match : []};
|
|
@@ -73,12 +74,17 @@ export const useLabelNames = (
|
|
|
73
74
|
},
|
|
74
75
|
options: {
|
|
75
76
|
enabled: profileType !== undefined && profileType !== '',
|
|
76
|
-
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
77
77
|
keepPreviousData: false,
|
|
78
78
|
},
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
return {
|
|
81
|
+
return {
|
|
82
|
+
result: {response: data, error: error as Error},
|
|
83
|
+
loading: isLoading,
|
|
84
|
+
refetch: () => {
|
|
85
|
+
void refetch();
|
|
86
|
+
},
|
|
87
|
+
};
|
|
82
88
|
};
|
|
83
89
|
|
|
84
90
|
interface UseLabelValues {
|
|
@@ -87,6 +93,7 @@ interface UseLabelValues {
|
|
|
87
93
|
error?: Error;
|
|
88
94
|
};
|
|
89
95
|
loading: boolean;
|
|
96
|
+
refetch: () => void;
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
export const useLabelValues = (
|
|
@@ -98,7 +105,7 @@ export const useLabelValues = (
|
|
|
98
105
|
): UseLabelValues => {
|
|
99
106
|
const metadata = useGrpcMetadata();
|
|
100
107
|
|
|
101
|
-
const {data, isLoading, error} = useGrpcQuery<string[]>({
|
|
108
|
+
const {data, isLoading, error, refetch} = useGrpcQuery<string[]>({
|
|
102
109
|
key: ['labelValues', labelName, profileType, start, end],
|
|
103
110
|
queryFn: async signal => {
|
|
104
111
|
const request: ValuesRequest = {labelName, match: [], profileType};
|
|
@@ -115,12 +122,17 @@ export const useLabelValues = (
|
|
|
115
122
|
profileType !== '' &&
|
|
116
123
|
labelName !== undefined &&
|
|
117
124
|
labelName !== '',
|
|
118
|
-
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
119
125
|
keepPreviousData: false,
|
|
120
126
|
},
|
|
121
127
|
});
|
|
122
128
|
|
|
123
|
-
return {
|
|
129
|
+
return {
|
|
130
|
+
result: {response: data ?? [], error: error as Error},
|
|
131
|
+
loading: isLoading,
|
|
132
|
+
refetch: () => {
|
|
133
|
+
void refetch();
|
|
134
|
+
},
|
|
135
|
+
};
|
|
124
136
|
};
|
|
125
137
|
|
|
126
138
|
export const useFetchUtilizationLabelValues = (
|
|
@@ -130,8 +142,10 @@ export const useFetchUtilizationLabelValues = (
|
|
|
130
142
|
const {data} = useQuery({
|
|
131
143
|
queryKey: ['utilizationLabelValues', labelName],
|
|
132
144
|
queryFn: async () => {
|
|
133
|
-
|
|
145
|
+
const result = await utilizationLabels?.utilizationFetchLabelValues?.(labelName);
|
|
146
|
+
return result ?? [];
|
|
134
147
|
},
|
|
148
|
+
enabled: utilizationLabels?.utilizationFetchLabelValues != null && labelName !== '',
|
|
135
149
|
});
|
|
136
150
|
|
|
137
151
|
return data ?? [];
|
|
@@ -155,6 +169,8 @@ const MatchersInput = ({
|
|
|
155
169
|
currentLabelName,
|
|
156
170
|
setCurrentLabelName,
|
|
157
171
|
shouldHandlePrefixes,
|
|
172
|
+
refetchLabelValues,
|
|
173
|
+
refetchLabelNames,
|
|
158
174
|
} = useLabels();
|
|
159
175
|
|
|
160
176
|
const value = currentQuery.matchersString();
|
|
@@ -325,8 +341,12 @@ const MatchersInput = ({
|
|
|
325
341
|
inputRef={inputRef.current}
|
|
326
342
|
runQuery={runQuery}
|
|
327
343
|
focusedInput={focusedInput}
|
|
328
|
-
isLabelValuesLoading={
|
|
344
|
+
isLabelValuesLoading={
|
|
345
|
+
isLabelValuesLoading && lastCompleted.type === 'literal' && lastCompleted.value !== ','
|
|
346
|
+
}
|
|
329
347
|
shouldTrimPrefix={shouldHandlePrefixes}
|
|
348
|
+
refetchLabelValues={refetchLabelValues}
|
|
349
|
+
refetchLabelNames={refetchLabelNames}
|
|
330
350
|
/>
|
|
331
351
|
</div>
|
|
332
352
|
);
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
// See the License for the specific language governing permissions and
|
|
12
12
|
// limitations under the License.
|
|
13
13
|
|
|
14
|
+
import {useState} from 'react';
|
|
15
|
+
|
|
14
16
|
import {Switch} from '@headlessui/react';
|
|
15
17
|
import {RpcError} from '@protobuf-ts/runtime-rpc';
|
|
16
18
|
import Select, {type SelectInstance} from 'react-select';
|
|
@@ -93,6 +95,7 @@ export function QueryControls({
|
|
|
93
95
|
profileTypesError,
|
|
94
96
|
}: QueryControlsProps): JSX.Element {
|
|
95
97
|
const {timezone} = useParcaContext();
|
|
98
|
+
const [searchExecutedTimestamp, setSearchExecutedTimestamp] = useState<number>(0);
|
|
96
99
|
|
|
97
100
|
return (
|
|
98
101
|
<div
|
|
@@ -180,7 +183,6 @@ export function QueryControls({
|
|
|
180
183
|
/>
|
|
181
184
|
) : (
|
|
182
185
|
<SimpleMatchers
|
|
183
|
-
key={query.toString()}
|
|
184
186
|
setMatchersString={setMatchersString}
|
|
185
187
|
runQuery={setQueryExpression}
|
|
186
188
|
currentQuery={query}
|
|
@@ -189,6 +191,7 @@ export function QueryControls({
|
|
|
189
191
|
queryClient={queryClient}
|
|
190
192
|
start={timeRangeSelection.getFromMs()}
|
|
191
193
|
end={timeRangeSelection.getToMs()}
|
|
194
|
+
searchExecutedTimestamp={searchExecutedTimestamp}
|
|
192
195
|
/>
|
|
193
196
|
)}
|
|
194
197
|
</div>
|
|
@@ -261,6 +264,7 @@ export function QueryControls({
|
|
|
261
264
|
disabled={searchDisabled}
|
|
262
265
|
onClick={(e: React.MouseEvent<HTMLElement>) => {
|
|
263
266
|
e.preventDefault();
|
|
267
|
+
setSearchExecutedTimestamp(Date.now());
|
|
264
268
|
setQueryExpression(true);
|
|
265
269
|
}}
|
|
266
270
|
id="h-matcher-search-button"
|
|
@@ -187,6 +187,10 @@ interface MultiLevelDropdownProps {
|
|
|
187
187
|
groupBy: string[];
|
|
188
188
|
toggleGroupBy: (key: string) => void;
|
|
189
189
|
isTableVizOnly: boolean;
|
|
190
|
+
alignFunctionName: string;
|
|
191
|
+
setAlignFunctionName: (align: string) => void;
|
|
192
|
+
colorBy: string;
|
|
193
|
+
setColorBy: (colorBy: string) => void;
|
|
190
194
|
}
|
|
191
195
|
|
|
192
196
|
const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
|
|
@@ -195,6 +199,10 @@ const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
|
|
|
195
199
|
groupBy,
|
|
196
200
|
toggleGroupBy,
|
|
197
201
|
isTableVizOnly,
|
|
202
|
+
alignFunctionName,
|
|
203
|
+
setAlignFunctionName,
|
|
204
|
+
colorBy,
|
|
205
|
+
setColorBy,
|
|
198
206
|
}) => {
|
|
199
207
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
200
208
|
const [shouldOpenLeft, setShouldOpenLeft] = useState(false);
|
|
@@ -202,7 +210,6 @@ const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
|
|
|
202
210
|
defaultValue: FIELD_FUNCTION_NAME,
|
|
203
211
|
});
|
|
204
212
|
const [colorStackLegend, setStoreColorStackLegend] = useURLState('color_stack_legend');
|
|
205
|
-
const [colorBy, setColorBy] = useURLState('color_by');
|
|
206
213
|
const [hiddenBinaries, setHiddenBinaries] = useURLState('hidden_binaries', {
|
|
207
214
|
defaultValue: [],
|
|
208
215
|
alwaysReturnArray: true,
|
|
@@ -212,9 +219,7 @@ const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
|
|
|
212
219
|
USER_PREFERENCES.FLAMEGRAPH_COLOR_PROFILE.key
|
|
213
220
|
);
|
|
214
221
|
const isColorStackLegendEnabled = colorStackLegend === 'true';
|
|
215
|
-
|
|
216
|
-
const [alignFunctionName, setAlignFunctionName] = useURLState('align_function_name');
|
|
217
|
-
const isLeftAligned = alignFunctionName === 'left' || alignFunctionName === undefined;
|
|
222
|
+
const isLeftAligned = alignFunctionName === 'left';
|
|
218
223
|
|
|
219
224
|
// By default, we want delta profiles (CPU) to be relatively compared.
|
|
220
225
|
// For non-delta profiles, like goroutines or memory, we want the profiles to be compared absolutely.
|
|
@@ -421,7 +426,7 @@ const MultiLevelDropdown: React.FC<MultiLevelDropdownProps> = ({
|
|
|
421
426
|
closeDropdown={close}
|
|
422
427
|
activeValueForSortBy={storeSortBy as string}
|
|
423
428
|
activeValueForColorBy={
|
|
424
|
-
colorBy === undefined || colorBy === '' ? 'binary' :
|
|
429
|
+
colorBy === undefined || colorBy === '' ? 'binary' : colorBy
|
|
425
430
|
}
|
|
426
431
|
activeValuesForLevel={groupBy}
|
|
427
432
|
renderAsDiv={item.renderAsDiv}
|
|
@@ -50,6 +50,10 @@ export interface VisualisationToolbarProps {
|
|
|
50
50
|
setGroupByLabels: (labels: string[]) => void;
|
|
51
51
|
showVisualizationSelector?: boolean;
|
|
52
52
|
sandwichFunctionName?: string;
|
|
53
|
+
alignFunctionName: string;
|
|
54
|
+
setAlignFunctionName: (align: string) => void;
|
|
55
|
+
colorBy: string;
|
|
56
|
+
setColorBy: (colorBy: string) => void;
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
export interface TableToolbarProps {
|
|
@@ -139,6 +143,10 @@ export const VisualisationToolbar: FC<VisualisationToolbarProps> = ({
|
|
|
139
143
|
total,
|
|
140
144
|
filtered,
|
|
141
145
|
showVisualizationSelector = true,
|
|
146
|
+
alignFunctionName,
|
|
147
|
+
setAlignFunctionName,
|
|
148
|
+
colorBy,
|
|
149
|
+
setColorBy,
|
|
142
150
|
}) => {
|
|
143
151
|
const {dashboardItems} = useDashboard();
|
|
144
152
|
|
|
@@ -183,6 +191,10 @@ export const VisualisationToolbar: FC<VisualisationToolbarProps> = ({
|
|
|
183
191
|
profileType={profileType}
|
|
184
192
|
onSelect={() => {}}
|
|
185
193
|
isTableVizOnly={isTableVizOnly}
|
|
194
|
+
alignFunctionName={alignFunctionName}
|
|
195
|
+
setAlignFunctionName={setAlignFunctionName}
|
|
196
|
+
colorBy={colorBy}
|
|
197
|
+
setColorBy={setColorBy}
|
|
186
198
|
/>
|
|
187
199
|
|
|
188
200
|
<ShareButton
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import {useCallback, useMemo} from 'react';
|
|
15
15
|
|
|
16
16
|
import {JSONParser, JSONSerializer, useURLState, useURLStateCustom} from '@parca/components';
|
|
17
|
+
import {USER_PREFERENCES, useUserPreference} from '@parca/hooks';
|
|
17
18
|
|
|
18
19
|
import {
|
|
19
20
|
FIELD_FUNCTION_FILE_NAME,
|
|
@@ -38,14 +39,28 @@ export const useVisualizationState = (): {
|
|
|
38
39
|
sandwichFunctionName: string | undefined;
|
|
39
40
|
setSandwichFunctionName: (sandwichFunctionName: string | undefined) => void;
|
|
40
41
|
resetSandwichFunctionName: () => void;
|
|
42
|
+
alignFunctionName: string;
|
|
43
|
+
setAlignFunctionName: (align: string) => void;
|
|
41
44
|
} => {
|
|
45
|
+
const [colorByPreference, setColorByPreference] = useUserPreference<string>(
|
|
46
|
+
USER_PREFERENCES.COLOR_BY.key
|
|
47
|
+
);
|
|
48
|
+
const [alignFunctionNamePreference, setAlignFunctionNamePreference] = useUserPreference<string>(
|
|
49
|
+
USER_PREFERENCES.ALIGN_FUNCTION_NAME.key
|
|
50
|
+
);
|
|
51
|
+
|
|
42
52
|
const [curPathArrow, setCurPathArrow] = useURLStateCustom<CurrentPathFrame[]>('cur_path', {
|
|
43
53
|
parse: JSONParser<CurrentPathFrame[]>,
|
|
44
54
|
stringify: JSONSerializer,
|
|
45
55
|
defaultValue: '[]',
|
|
46
56
|
});
|
|
47
57
|
const [colorStackLegend] = useURLState<string | undefined>('color_stack_legend');
|
|
48
|
-
const [colorBy,
|
|
58
|
+
const [colorBy, setStoreColorBy] = useURLState('color_by', {
|
|
59
|
+
defaultValue: colorByPreference,
|
|
60
|
+
});
|
|
61
|
+
const [alignFunctionName, setStoreAlignFunctionName] = useURLState('align_function_name', {
|
|
62
|
+
defaultValue: alignFunctionNamePreference,
|
|
63
|
+
});
|
|
49
64
|
const [groupBy, setStoreGroupBy] = useURLState<string[]>('group_by', {
|
|
50
65
|
defaultValue: [FIELD_FUNCTION_NAME],
|
|
51
66
|
alwaysReturnArray: true,
|
|
@@ -99,6 +114,22 @@ export const useVisualizationState = (): {
|
|
|
99
114
|
setSandwichFunctionName(undefined);
|
|
100
115
|
}, [setSandwichFunctionName]);
|
|
101
116
|
|
|
117
|
+
const setColorBy = useCallback(
|
|
118
|
+
(value: string): void => {
|
|
119
|
+
setStoreColorBy(value);
|
|
120
|
+
setColorByPreference(value);
|
|
121
|
+
},
|
|
122
|
+
[setStoreColorBy, setColorByPreference]
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const setAlignFunctionName = useCallback(
|
|
126
|
+
(value: string): void => {
|
|
127
|
+
setStoreAlignFunctionName(value);
|
|
128
|
+
setAlignFunctionNamePreference(value);
|
|
129
|
+
},
|
|
130
|
+
[setStoreAlignFunctionName, setAlignFunctionNamePreference]
|
|
131
|
+
);
|
|
132
|
+
|
|
102
133
|
return {
|
|
103
134
|
curPathArrow,
|
|
104
135
|
setCurPathArrow,
|
|
@@ -112,5 +143,7 @@ export const useVisualizationState = (): {
|
|
|
112
143
|
sandwichFunctionName,
|
|
113
144
|
setSandwichFunctionName,
|
|
114
145
|
resetSandwichFunctionName,
|
|
146
|
+
alignFunctionName: (alignFunctionName as string) ?? 'left',
|
|
147
|
+
setAlignFunctionName,
|
|
115
148
|
};
|
|
116
149
|
};
|
|
@@ -60,11 +60,14 @@ export const ProfileView = ({
|
|
|
60
60
|
setCurPathArrow,
|
|
61
61
|
colorStackLegend,
|
|
62
62
|
colorBy,
|
|
63
|
+
setColorBy,
|
|
63
64
|
groupBy,
|
|
64
65
|
toggleGroupBy,
|
|
65
66
|
setGroupByLabels,
|
|
66
67
|
sandwichFunctionName,
|
|
67
68
|
resetSandwichFunctionName,
|
|
69
|
+
alignFunctionName,
|
|
70
|
+
setAlignFunctionName,
|
|
68
71
|
} = useVisualizationState();
|
|
69
72
|
|
|
70
73
|
const {colorMappings} = useProfileMetadata({
|
|
@@ -148,6 +151,10 @@ export const ProfileView = ({
|
|
|
148
151
|
setGroupByLabels={setGroupByLabels}
|
|
149
152
|
showVisualizationSelector={showVisualizationSelector}
|
|
150
153
|
sandwichFunctionName={sandwichFunctionName}
|
|
154
|
+
alignFunctionName={alignFunctionName}
|
|
155
|
+
setAlignFunctionName={setAlignFunctionName}
|
|
156
|
+
colorBy={colorBy}
|
|
157
|
+
setColorBy={setColorBy}
|
|
151
158
|
/>
|
|
152
159
|
|
|
153
160
|
{isColorStackLegendEnabled && (
|