@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
|
@@ -11,13 +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, {useEffect, useRef, useState} from 'react';
|
|
14
|
+
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
|
15
15
|
|
|
16
16
|
import {Icon} from '@iconify/react';
|
|
17
17
|
import cx from 'classnames';
|
|
18
18
|
import levenshtein from 'fast-levenshtein';
|
|
19
19
|
|
|
20
20
|
import {Button, DividerWithLabel, useParcaContext} from '@parca/components';
|
|
21
|
+
import {TEST_IDS, testId} from '@parca/test-utils/dist/test-ids';
|
|
21
22
|
|
|
22
23
|
export interface SelectElement {
|
|
23
24
|
active: JSX.Element;
|
|
@@ -55,6 +56,9 @@ interface CustomSelectProps {
|
|
|
55
56
|
searchable?: boolean;
|
|
56
57
|
onButtonClick?: () => void;
|
|
57
58
|
editable?: boolean;
|
|
59
|
+
refetchValues?: () => void;
|
|
60
|
+
showLoadingInButton?: boolean;
|
|
61
|
+
hasRefreshButton?: boolean;
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
const CustomSelect: React.FC<CustomSelectProps> = ({
|
|
@@ -73,16 +77,31 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
|
|
73
77
|
searchable = false,
|
|
74
78
|
onButtonClick,
|
|
75
79
|
editable = false,
|
|
80
|
+
refetchValues,
|
|
81
|
+
showLoadingInButton = false,
|
|
82
|
+
hasRefreshButton = false,
|
|
76
83
|
}) => {
|
|
77
84
|
const {loader} = useParcaContext();
|
|
78
85
|
const [isOpen, setIsOpen] = useState(false);
|
|
79
86
|
const [focusedIndex, setFocusedIndex] = useState(-1);
|
|
80
87
|
const [searchTerm, setSearchTerm] = useState('');
|
|
88
|
+
const [isRefetching, setIsRefetching] = useState(false);
|
|
81
89
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
82
90
|
const optionsRef = useRef<HTMLDivElement>(null);
|
|
83
91
|
const searchInputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null);
|
|
84
92
|
const optionRefs = useRef<Array<HTMLElement | null>>([]);
|
|
85
93
|
|
|
94
|
+
const handleRefetch = useCallback(async () => {
|
|
95
|
+
if (refetchValues == null || isRefetching) return;
|
|
96
|
+
|
|
97
|
+
setIsRefetching(true);
|
|
98
|
+
try {
|
|
99
|
+
await refetchValues();
|
|
100
|
+
} finally {
|
|
101
|
+
setIsRefetching(false);
|
|
102
|
+
}
|
|
103
|
+
}, [refetchValues, isRefetching]);
|
|
104
|
+
|
|
86
105
|
let items: TypedSelectItem[] = [];
|
|
87
106
|
if (itemsProp[0] != null && 'type' in itemsProp[0]) {
|
|
88
107
|
items = (itemsProp as GroupedSelectItem[]).flatMap(item =>
|
|
@@ -192,6 +211,18 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
|
|
192
211
|
'text-gray-100 dark:gray-900 bg-indigo-600 border-indigo-500 font-medium py-2 px-4';
|
|
193
212
|
|
|
194
213
|
const renderSelection = (selection: SelectItem | string | undefined): string | JSX.Element => {
|
|
214
|
+
if (showLoadingInButton && loading === true && selectedKey === '') {
|
|
215
|
+
return (
|
|
216
|
+
<span
|
|
217
|
+
className="flex items-center gap-2"
|
|
218
|
+
{...testId(TEST_IDS.LABEL_VALUE_LOADING_INDICATOR)}
|
|
219
|
+
>
|
|
220
|
+
<Icon icon="svg-spinners:ring-resize" className="w-4 h-4" />
|
|
221
|
+
<span>Loading...</span>
|
|
222
|
+
</span>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
195
226
|
if (editable) {
|
|
196
227
|
return typeof selection === 'string' && selection.length > 0 ? selection : placeholder;
|
|
197
228
|
} else {
|
|
@@ -265,71 +296,112 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
|
|
265
296
|
)}
|
|
266
297
|
role="listbox"
|
|
267
298
|
>
|
|
268
|
-
{
|
|
269
|
-
|
|
270
|
-
<div className="
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
299
|
+
<div className={cx('relative', {'pb-6': hasRefreshButton})}>
|
|
300
|
+
{searchable && (
|
|
301
|
+
<div className="sticky z-10 top-[-5px] w-auto max-w-full">
|
|
302
|
+
<div className="flex flex-col">
|
|
303
|
+
{editable ? (
|
|
304
|
+
<>
|
|
305
|
+
<textarea
|
|
306
|
+
ref={searchInputRef as React.LegacyRef<HTMLTextAreaElement>}
|
|
307
|
+
className="w-full px-4 py-2 text-sm border-b border-gray-200 rounded-none ring-0 outline-none bg-gray-50 dark:bg-gray-800 dark:text-white min-h-[50px]"
|
|
308
|
+
placeholder="Type a RegEx to add"
|
|
309
|
+
value={searchTerm}
|
|
310
|
+
onChange={e => setSearchTerm(e.target.value)}
|
|
311
|
+
onFocus={e => moveCaretToEnd(e)}
|
|
312
|
+
/>
|
|
313
|
+
{editable && searchTerm.length > 0 && (
|
|
314
|
+
<div className="p-2 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
|
|
315
|
+
<Button
|
|
316
|
+
variant="primary"
|
|
317
|
+
className="w-full h-[30px]"
|
|
318
|
+
onClick={() => {
|
|
319
|
+
onSelection(searchTerm);
|
|
320
|
+
setIsOpen(false);
|
|
321
|
+
}}
|
|
322
|
+
>
|
|
323
|
+
Add
|
|
324
|
+
</Button>
|
|
325
|
+
</div>
|
|
326
|
+
)}
|
|
327
|
+
</>
|
|
328
|
+
) : (
|
|
329
|
+
<input
|
|
330
|
+
ref={searchInputRef as React.LegacyRef<HTMLInputElement>}
|
|
331
|
+
type="text"
|
|
332
|
+
className="w-full px-4 h-[45px] text-sm border-none rounded-none ring-0 outline-none bg-gray-50 dark:bg-gray-800 dark:text-white"
|
|
333
|
+
placeholder="Search..."
|
|
277
334
|
value={searchTerm}
|
|
278
335
|
onChange={e => setSearchTerm(e.target.value)}
|
|
279
|
-
onFocus={e => moveCaretToEnd(e)}
|
|
280
336
|
/>
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
337
|
+
)}
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
)}
|
|
341
|
+
{refetchValues !== undefined && loading !== true && (
|
|
342
|
+
<div className="absolute w-full flex items-center justify-center bottom-0 px-3 bg-gray-50 dark:bg-gray-900">
|
|
343
|
+
<button
|
|
344
|
+
onClick={e => {
|
|
345
|
+
e.preventDefault();
|
|
346
|
+
e.stopPropagation();
|
|
347
|
+
void handleRefetch();
|
|
348
|
+
}}
|
|
349
|
+
disabled={isRefetching}
|
|
350
|
+
className={cx(
|
|
351
|
+
'py-1 px-2 flex items-center gap-1 rounded-full transition-all duration-200 w-auto justify-center',
|
|
352
|
+
isRefetching
|
|
353
|
+
? 'cursor-wait opacity-50'
|
|
354
|
+
: 'hover:bg-gray-200 dark:hover:bg-gray-700 cursor-pointer'
|
|
355
|
+
)}
|
|
356
|
+
title="Refresh label values"
|
|
357
|
+
type="button"
|
|
358
|
+
{...testId(TEST_IDS.LABEL_VALUE_REFRESH_BUTTON)}
|
|
359
|
+
>
|
|
360
|
+
<Icon
|
|
361
|
+
icon="system-uicons:reset"
|
|
362
|
+
className={cx(
|
|
363
|
+
'w-3 h-3 text-gray-500 dark:text-gray-400',
|
|
364
|
+
isRefetching && 'animate-spin'
|
|
294
365
|
)}
|
|
295
|
-
</>
|
|
296
|
-
) : (
|
|
297
|
-
<input
|
|
298
|
-
ref={searchInputRef as React.LegacyRef<HTMLInputElement>}
|
|
299
|
-
type="text"
|
|
300
|
-
className="w-full px-4 h-[45px] text-sm border-none rounded-none ring-0 outline-none bg-gray-50 dark:bg-gray-800 dark:text-white"
|
|
301
|
-
placeholder="Search..."
|
|
302
|
-
value={searchTerm}
|
|
303
|
-
onChange={e => setSearchTerm(e.target.value)}
|
|
304
366
|
/>
|
|
305
|
-
|
|
367
|
+
<span className="text-xs text-gray-500 dark:text-gray-400">Refresh results</span>
|
|
368
|
+
</button>
|
|
306
369
|
</div>
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
370
|
+
)}
|
|
371
|
+
{loading === true ? (
|
|
372
|
+
<div className="w-[270px]">{loader}</div>
|
|
373
|
+
) : groupedFilteredItems.length === 0 ? (
|
|
374
|
+
<div
|
|
375
|
+
className="px-4 py-3 text-sm text-gray-500 dark:text-gray-400 text-center"
|
|
376
|
+
{...testId(TEST_IDS.LABEL_VALUE_NO_RESULTS)}
|
|
377
|
+
>
|
|
378
|
+
No values found
|
|
379
|
+
</div>
|
|
380
|
+
) : (
|
|
381
|
+
groupedFilteredItems.map(group => (
|
|
382
|
+
<>
|
|
383
|
+
{groupedFilteredItems.length > 1 &&
|
|
384
|
+
groupedFilteredItems.every(g => g.type !== '') &&
|
|
385
|
+
group.type !== '' ? (
|
|
386
|
+
<div className="pl-2">
|
|
387
|
+
<DividerWithLabel label={group.type} />
|
|
388
|
+
</div>
|
|
389
|
+
) : null}
|
|
390
|
+
{group.values.map((item, index) => (
|
|
391
|
+
<OptionItem
|
|
392
|
+
key={item.key}
|
|
393
|
+
item={item}
|
|
394
|
+
index={index}
|
|
395
|
+
optionRefs={optionRefs}
|
|
396
|
+
focusedIndex={focusedIndex}
|
|
397
|
+
selectedKey={selectedKey}
|
|
398
|
+
handleSelection={handleSelection}
|
|
399
|
+
/>
|
|
400
|
+
))}
|
|
401
|
+
</>
|
|
402
|
+
))
|
|
403
|
+
)}
|
|
404
|
+
</div>
|
|
333
405
|
</div>
|
|
334
406
|
)}
|
|
335
407
|
</div>
|
|
@@ -36,6 +36,7 @@ interface Props {
|
|
|
36
36
|
queryBrowserRef: React.RefObject<HTMLDivElement>;
|
|
37
37
|
start?: number;
|
|
38
38
|
end?: number;
|
|
39
|
+
searchExecutedTimestamp?: number;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
interface QueryRow {
|
|
@@ -121,6 +122,7 @@ const SimpleMatchers = ({
|
|
|
121
122
|
queryBrowserRef,
|
|
122
123
|
start,
|
|
123
124
|
end,
|
|
125
|
+
searchExecutedTimestamp,
|
|
124
126
|
}: Props): JSX.Element => {
|
|
125
127
|
const utilizationLabels = useUtilizationLabels();
|
|
126
128
|
const [queryRows, setQueryRows] = useState<QueryRow[]>([
|
|
@@ -130,8 +132,17 @@ const SimpleMatchers = ({
|
|
|
130
132
|
const metadata = useGrpcMetadata();
|
|
131
133
|
|
|
132
134
|
const [showAll, setShowAll] = useState(false);
|
|
135
|
+
const [isActivelyEditing, setIsActivelyEditing] = useState(false);
|
|
133
136
|
|
|
134
|
-
|
|
137
|
+
// Reset editing mode when search is executed
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (searchExecutedTimestamp !== undefined && searchExecutedTimestamp > 0) {
|
|
140
|
+
setIsActivelyEditing(false);
|
|
141
|
+
setShowAll(false);
|
|
142
|
+
}
|
|
143
|
+
}, [searchExecutedTimestamp]);
|
|
144
|
+
|
|
145
|
+
const visibleRows = showAll || isActivelyEditing ? queryRows : queryRows.slice(0, 3);
|
|
135
146
|
const hiddenRowsCount = queryRows.length - 3;
|
|
136
147
|
|
|
137
148
|
const maxWidthInPixels = `max-w-[${queryBrowserRef.current?.offsetWidth.toString() as string}px]`;
|
|
@@ -163,9 +174,6 @@ const SimpleMatchers = ({
|
|
|
163
174
|
).response;
|
|
164
175
|
const sanitizedValues = sanitizeLabelValue(response.labelValues);
|
|
165
176
|
return sanitizedValues;
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
169
177
|
}
|
|
170
178
|
);
|
|
171
179
|
return values;
|
|
@@ -195,7 +203,75 @@ const SimpleMatchers = ({
|
|
|
195
203
|
[setMatchersString]
|
|
196
204
|
);
|
|
197
205
|
|
|
198
|
-
const {
|
|
206
|
+
const {
|
|
207
|
+
labelNameOptions,
|
|
208
|
+
isLoading: labelNamesLoading,
|
|
209
|
+
refetchLabelValues,
|
|
210
|
+
refetchLabelNames,
|
|
211
|
+
} = useLabels();
|
|
212
|
+
|
|
213
|
+
// Helper to ensure selected label name is in the options (for page load before API returns)
|
|
214
|
+
const getLabelNameOptionsWithSelected = useCallback(
|
|
215
|
+
(selectedLabelName: string): typeof labelNameOptions => {
|
|
216
|
+
if (selectedLabelName === '') return labelNameOptions;
|
|
217
|
+
|
|
218
|
+
// Check if the selected label name exists in any group
|
|
219
|
+
const existsInOptions = labelNameOptions.some(group =>
|
|
220
|
+
group.values.some(item => item.key === selectedLabelName)
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
if (existsInOptions) return labelNameOptions;
|
|
224
|
+
|
|
225
|
+
// Add it temporarily to a group with type '' (matching non-matching labels group)
|
|
226
|
+
if (labelNameOptions.length === 0) {
|
|
227
|
+
return [
|
|
228
|
+
{
|
|
229
|
+
type: '',
|
|
230
|
+
values: [
|
|
231
|
+
{
|
|
232
|
+
key: selectedLabelName,
|
|
233
|
+
element: {active: <>{selectedLabelName}</>, expanded: <>{selectedLabelName}</>},
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Find the group with type '' (non-matching labels) or create it
|
|
241
|
+
const emptyTypeGroupIndex = labelNameOptions.findIndex(group => group.type === '');
|
|
242
|
+
|
|
243
|
+
if (emptyTypeGroupIndex !== -1) {
|
|
244
|
+
// Add to existing '' type group
|
|
245
|
+
const updatedOptions = [...labelNameOptions];
|
|
246
|
+
updatedOptions[emptyTypeGroupIndex] = {
|
|
247
|
+
...updatedOptions[emptyTypeGroupIndex],
|
|
248
|
+
values: [
|
|
249
|
+
...updatedOptions[emptyTypeGroupIndex].values,
|
|
250
|
+
{
|
|
251
|
+
key: selectedLabelName,
|
|
252
|
+
element: {active: <>{selectedLabelName}</>, expanded: <>{selectedLabelName}</>},
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
};
|
|
256
|
+
return updatedOptions;
|
|
257
|
+
} else {
|
|
258
|
+
// Create new '' type group
|
|
259
|
+
return [
|
|
260
|
+
...labelNameOptions,
|
|
261
|
+
{
|
|
262
|
+
type: '',
|
|
263
|
+
values: [
|
|
264
|
+
{
|
|
265
|
+
key: selectedLabelName,
|
|
266
|
+
element: {active: <>{selectedLabelName}</>, expanded: <>{selectedLabelName}</>},
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
},
|
|
270
|
+
];
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
[labelNameOptions]
|
|
274
|
+
);
|
|
199
275
|
|
|
200
276
|
const fetchLabelValuesUnified = useCallback(
|
|
201
277
|
async (labelName: string): Promise<string[]> => {
|
|
@@ -290,19 +366,29 @@ const SimpleMatchers = ({
|
|
|
290
366
|
|
|
291
367
|
const handleUpdateRow = useCallback(
|
|
292
368
|
(index: number, field: keyof QueryRow, value: string) => {
|
|
369
|
+
// Only set actively editing if not already showing all
|
|
370
|
+
// If showAll is true, user has explicitly expanded, so keep that state
|
|
371
|
+
if (!showAll) {
|
|
372
|
+
setIsActivelyEditing(true);
|
|
373
|
+
}
|
|
293
374
|
void updateRow(index, field, value);
|
|
294
375
|
},
|
|
295
|
-
[updateRow]
|
|
376
|
+
[updateRow, showAll]
|
|
296
377
|
);
|
|
297
378
|
|
|
298
379
|
const addNewRow = useCallback((): void => {
|
|
380
|
+
// Only set actively editing if not already showing all label names and values
|
|
381
|
+
// If showAll is true, user has explicitly expanded, so keep that state
|
|
382
|
+
if (!showAll) {
|
|
383
|
+
setIsActivelyEditing(true);
|
|
384
|
+
}
|
|
299
385
|
const newRows = [
|
|
300
386
|
...queryRows,
|
|
301
387
|
{labelName: '', operator: '=', labelValue: '', labelValues: [], isLoading: false},
|
|
302
388
|
];
|
|
303
389
|
setQueryRows(newRows);
|
|
304
390
|
updateMatchersString(newRows);
|
|
305
|
-
}, [queryRows, updateMatchersString]);
|
|
391
|
+
}, [queryRows, updateMatchersString, showAll]);
|
|
306
392
|
|
|
307
393
|
const removeRow = useCallback(
|
|
308
394
|
(index: number): void => {
|
|
@@ -362,7 +448,7 @@ const SimpleMatchers = ({
|
|
|
362
448
|
{visibleRows.map((row, index) => (
|
|
363
449
|
<div key={index} className="flex items-center" {...testId(TEST_IDS.SIMPLE_MATCHER_ROW)}>
|
|
364
450
|
<Select
|
|
365
|
-
items={
|
|
451
|
+
items={getLabelNameOptionsWithSelected(row.labelName)}
|
|
366
452
|
onSelection={value => handleUpdateRow(index, 'labelName', value)}
|
|
367
453
|
placeholder="Select label name"
|
|
368
454
|
selectedKey={row.labelName}
|
|
@@ -370,6 +456,8 @@ const SimpleMatchers = ({
|
|
|
370
456
|
loading={labelNamesLoading}
|
|
371
457
|
searchable={true}
|
|
372
458
|
{...testId(TEST_IDS.LABEL_NAME_SELECT)}
|
|
459
|
+
refetchValues={refetchLabelNames}
|
|
460
|
+
hasRefreshButton={true}
|
|
373
461
|
/>
|
|
374
462
|
<Select
|
|
375
463
|
items={operatorOptions}
|
|
@@ -379,12 +467,16 @@ const SimpleMatchers = ({
|
|
|
379
467
|
{...testId(TEST_IDS.OPERATOR_SELECT)}
|
|
380
468
|
/>
|
|
381
469
|
<Select
|
|
382
|
-
items={transformLabelsForSelect(
|
|
470
|
+
items={transformLabelsForSelect(
|
|
471
|
+
row.labelValue !== '' && !row.labelValues.includes(row.labelValue)
|
|
472
|
+
? [...row.labelValues, row.labelValue]
|
|
473
|
+
: row.labelValues
|
|
474
|
+
)}
|
|
383
475
|
onSelection={value => handleUpdateRow(index, 'labelValue', value)}
|
|
384
476
|
placeholder="Select label value"
|
|
385
477
|
selectedKey={row.labelValue}
|
|
386
478
|
className="rounded-none ring-0 focus:ring-0 outline-none max-w-48"
|
|
387
|
-
optionsClassname={cx('max-w-[
|
|
479
|
+
optionsClassname={cx('max-w-[600px]', {
|
|
388
480
|
'w-[300px]': isRowRegex(row),
|
|
389
481
|
})}
|
|
390
482
|
searchable={true}
|
|
@@ -393,6 +485,9 @@ const SimpleMatchers = ({
|
|
|
393
485
|
onButtonClick={() => handleLabelValueClick(index)}
|
|
394
486
|
editable={isRowRegex(row)}
|
|
395
487
|
{...testId(TEST_IDS.LABEL_VALUE_SELECT)}
|
|
488
|
+
refetchValues={refetchLabelValues}
|
|
489
|
+
showLoadingInButton={true}
|
|
490
|
+
hasRefreshButton={true}
|
|
396
491
|
/>
|
|
397
492
|
<button
|
|
398
493
|
onClick={() => removeRow(index)}
|
|
@@ -406,9 +501,18 @@ const SimpleMatchers = ({
|
|
|
406
501
|
</div>
|
|
407
502
|
))}
|
|
408
503
|
|
|
409
|
-
{queryRows.length > 3 && (
|
|
504
|
+
{queryRows.length > 3 && !isActivelyEditing && (
|
|
410
505
|
<button
|
|
411
|
-
onClick={() =>
|
|
506
|
+
onClick={() => {
|
|
507
|
+
if (showAll) {
|
|
508
|
+
// Clicking "Show less" - collapse and stop editing mode
|
|
509
|
+
setShowAll(false);
|
|
510
|
+
setIsActivelyEditing(false);
|
|
511
|
+
} else {
|
|
512
|
+
// Clicking "Show more" - just expand
|
|
513
|
+
setShowAll(true);
|
|
514
|
+
}
|
|
515
|
+
}}
|
|
412
516
|
className="mr-2 px-3 py-1 text-sm font-medium text-gray-700 dark:text-gray-200 bg-gray-100 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:bg-gray-900"
|
|
413
517
|
{...testId(showAll ? TEST_IDS.SHOW_LESS_BUTTON : TEST_IDS.SHOW_MORE_BUTTON)}
|
|
414
518
|
>
|
|
@@ -108,26 +108,26 @@ const ViewMatchers: React.FC<Props> = ({
|
|
|
108
108
|
[queryClient, metadata, profileType, start, end]
|
|
109
109
|
);
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
const newIsLoading: Record<string, boolean> = {};
|
|
111
|
+
const fetchAllLabelValues = useCallback(async (): Promise<void> => {
|
|
112
|
+
const newLabelValuesMap: Record<string, string[]> = {};
|
|
113
|
+
const newIsLoading: Record<string, boolean> = {};
|
|
115
114
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
for (const labelName of labelNames) {
|
|
116
|
+
newIsLoading[labelName] = true;
|
|
117
|
+
setIsLoading(prev => ({...prev, [labelName]: true}));
|
|
119
118
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
const values = await fetchLabelValues(labelName);
|
|
120
|
+
newLabelValuesMap[labelName] = values;
|
|
121
|
+
newIsLoading[labelName] = false;
|
|
122
|
+
}
|
|
124
123
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
setLabelValuesMap(newLabelValuesMap);
|
|
125
|
+
setIsLoading(newIsLoading);
|
|
126
|
+
}, [labelNames, fetchLabelValues]);
|
|
128
127
|
|
|
128
|
+
useEffect(() => {
|
|
129
129
|
void fetchAllLabelValues();
|
|
130
|
-
}, [
|
|
130
|
+
}, [fetchAllLabelValues]);
|
|
131
131
|
|
|
132
132
|
const updateMatcherString = useCallback(() => {
|
|
133
133
|
const matcherParts = Object.entries(selectionsRef.current)
|
|
@@ -32,6 +32,8 @@ interface LabelsContextType {
|
|
|
32
32
|
currentLabelName: string | null;
|
|
33
33
|
setCurrentLabelName: (name: string | null) => void;
|
|
34
34
|
shouldHandlePrefixes: boolean;
|
|
35
|
+
refetchLabelValues: () => void;
|
|
36
|
+
refetchLabelNames: () => void;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
const LabelsContext = createContext<LabelsContextType | null>(null);
|
|
@@ -56,12 +58,11 @@ export function LabelsProvider({
|
|
|
56
58
|
const [currentLabelName, setCurrentLabelName] = React.useState<string | null>(null);
|
|
57
59
|
const utilizationLabels = useUtilizationLabels();
|
|
58
60
|
|
|
59
|
-
const {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
);
|
|
61
|
+
const {
|
|
62
|
+
result: labelNamesResponse,
|
|
63
|
+
loading: isLabelNamesLoading,
|
|
64
|
+
refetch: refetchLabelNames,
|
|
65
|
+
} = useLabelNames(queryClient, profileType, start, end);
|
|
65
66
|
|
|
66
67
|
const labelNamesFromAPI = useMemo(() => {
|
|
67
68
|
return (labelNamesResponse.error === undefined || labelNamesResponse.error == null) &&
|
|
@@ -71,13 +72,11 @@ export function LabelsProvider({
|
|
|
71
72
|
: [];
|
|
72
73
|
}, [labelNamesResponse]);
|
|
73
74
|
|
|
74
|
-
const {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
end
|
|
80
|
-
);
|
|
75
|
+
const {
|
|
76
|
+
result: labelValuesOriginal,
|
|
77
|
+
loading: isLabelValuesLoading,
|
|
78
|
+
refetch: refetchLabelValues,
|
|
79
|
+
} = useLabelValues(queryClient, currentLabelName ?? '', profileType, start, end);
|
|
81
80
|
|
|
82
81
|
const utilizationLabelValues = useFetchUtilizationLabelValues(
|
|
83
82
|
currentLabelName ?? '',
|
|
@@ -114,6 +113,8 @@ export function LabelsProvider({
|
|
|
114
113
|
currentLabelName,
|
|
115
114
|
setCurrentLabelName,
|
|
116
115
|
shouldHandlePrefixes,
|
|
116
|
+
refetchLabelValues,
|
|
117
|
+
refetchLabelNames,
|
|
117
118
|
}),
|
|
118
119
|
[
|
|
119
120
|
labelNames,
|
|
@@ -123,6 +124,8 @@ export function LabelsProvider({
|
|
|
123
124
|
isLabelValuesLoading,
|
|
124
125
|
currentLabelName,
|
|
125
126
|
shouldHandlePrefixes,
|
|
127
|
+
refetchLabelValues,
|
|
128
|
+
refetchLabelNames,
|
|
126
129
|
]
|
|
127
130
|
);
|
|
128
131
|
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
// See the License for the specific language governing permissions and
|
|
12
12
|
// limitations under the License.
|
|
13
13
|
|
|
14
|
-
import {createContext, useContext, useMemo} from 'react';
|
|
14
|
+
import {createContext, useCallback, useContext, useMemo} from 'react';
|
|
15
|
+
|
|
16
|
+
import {useQueryClient} from '@tanstack/react-query';
|
|
15
17
|
|
|
16
18
|
import {QueryServiceClient} from '@parca/client';
|
|
17
19
|
|
|
@@ -29,6 +31,8 @@ interface LabelContextValue {
|
|
|
29
31
|
labelNameOptions: LabelNameSection[];
|
|
30
32
|
isLoading: boolean;
|
|
31
33
|
error: Error | null;
|
|
34
|
+
refetchLabelValues: () => void;
|
|
35
|
+
refetchLabelNames: () => void;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
const LabelContext = createContext<LabelContextValue | null>(null);
|
|
@@ -53,8 +57,13 @@ export function LabelProvider({
|
|
|
53
57
|
start,
|
|
54
58
|
end,
|
|
55
59
|
}: LabelProviderProps): JSX.Element {
|
|
60
|
+
const reactQueryClient = useQueryClient();
|
|
56
61
|
const utilizationLabelResponse = useUtilizationLabels();
|
|
57
|
-
const {
|
|
62
|
+
const {
|
|
63
|
+
loading,
|
|
64
|
+
result,
|
|
65
|
+
refetch: refetchLabelNamesQuery,
|
|
66
|
+
} = useLabelNames(queryClient, profileType, start, end);
|
|
58
67
|
|
|
59
68
|
const profileValues = useMemo(() => {
|
|
60
69
|
const profileLabelNames =
|
|
@@ -131,7 +140,34 @@ export function LabelProvider({
|
|
|
131
140
|
};
|
|
132
141
|
}, [profileValues, utilizationValues, labelNameFromMatchers]);
|
|
133
142
|
|
|
134
|
-
|
|
143
|
+
const refetchLabelValues = useCallback(() => {
|
|
144
|
+
void reactQueryClient.refetchQueries({
|
|
145
|
+
predicate: query => {
|
|
146
|
+
const key = query.queryKey;
|
|
147
|
+
return (
|
|
148
|
+
Array.isArray(key) &&
|
|
149
|
+
key.length === 4 &&
|
|
150
|
+
typeof key[0] === 'string' &&
|
|
151
|
+
key[1] === profileType
|
|
152
|
+
);
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}, [reactQueryClient, profileType]);
|
|
156
|
+
|
|
157
|
+
const refetchLabelNames = useCallback(() => {
|
|
158
|
+
refetchLabelNamesQuery();
|
|
159
|
+
}, [refetchLabelNamesQuery]);
|
|
160
|
+
|
|
161
|
+
const contextValue = useMemo(
|
|
162
|
+
() => ({
|
|
163
|
+
...value,
|
|
164
|
+
refetchLabelValues,
|
|
165
|
+
refetchLabelNames,
|
|
166
|
+
}),
|
|
167
|
+
[value, refetchLabelValues, refetchLabelNames]
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
return <LabelContext.Provider value={contextValue}>{children}</LabelContext.Provider>;
|
|
135
171
|
}
|
|
136
172
|
|
|
137
173
|
export function useLabels(): LabelContextValue {
|
|
@@ -21,13 +21,14 @@ interface Props<IRes> {
|
|
|
21
21
|
staleTime?: number | undefined;
|
|
22
22
|
retry?: number | boolean;
|
|
23
23
|
keepPreviousData?: boolean | undefined;
|
|
24
|
+
cacheTime?: number | undefined;
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
const useGrpcQuery = <IRes>({
|
|
28
29
|
key,
|
|
29
30
|
queryFn,
|
|
30
|
-
options: {enabled = true, staleTime, retry, keepPreviousData} = {},
|
|
31
|
+
options: {enabled = true, staleTime, retry, keepPreviousData, cacheTime} = {},
|
|
31
32
|
}: Props<IRes>): UseQueryResult<IRes> => {
|
|
32
33
|
return useQuery<IRes>(
|
|
33
34
|
key,
|
|
@@ -39,6 +40,7 @@ const useGrpcQuery = <IRes>({
|
|
|
39
40
|
staleTime,
|
|
40
41
|
retry,
|
|
41
42
|
keepPreviousData,
|
|
43
|
+
cacheTime,
|
|
42
44
|
}
|
|
43
45
|
);
|
|
44
46
|
};
|