@parca/profile 0.19.60 → 0.19.62
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 +2 -1
- package/dist/MatchersInput/SuggestionsList.d.ts.map +1 -1
- package/dist/MatchersInput/SuggestionsList.js +24 -3
- package/dist/MatchersInput/index.d.ts +1 -0
- package/dist/MatchersInput/index.d.ts.map +1 -1
- package/dist/MatchersInput/index.js +13 -7
- 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 +29 -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 +1 -0
- package/dist/contexts/MatchersInputLabelsContext.d.ts.map +1 -1
- package/dist/contexts/MatchersInputLabelsContext.js +3 -1
- package/dist/contexts/SimpleMatchersLabelContext.d.ts +1 -0
- package/dist/contexts/SimpleMatchersLabelContext.d.ts.map +1 -1
- package/dist/contexts/SimpleMatchersLabelContext.js +19 -2
- 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 +8 -8
- package/src/MatchersInput/SuggestionsList.tsx +119 -40
- package/src/MatchersInput/index.tsx +14 -5
- 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 +128 -60
- package/src/SimpleMatchers/index.tsx +109 -12
- package/src/ViewMatchers/index.tsx +15 -15
- package/src/contexts/MatchersInputLabelsContext.tsx +8 -7
- package/src/contexts/SimpleMatchersLabelContext.tsx +28 -2
- package/src/useGrpcQuery/index.ts +3 -1
|
@@ -11,7 +11,7 @@
|
|
|
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';
|
|
@@ -55,6 +55,9 @@ interface CustomSelectProps {
|
|
|
55
55
|
searchable?: boolean;
|
|
56
56
|
onButtonClick?: () => void;
|
|
57
57
|
editable?: boolean;
|
|
58
|
+
refetchLabelValues?: () => void;
|
|
59
|
+
showLoadingInButton?: boolean;
|
|
60
|
+
hasRefreshButton?: boolean;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
const CustomSelect: React.FC<CustomSelectProps> = ({
|
|
@@ -73,16 +76,31 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
|
|
73
76
|
searchable = false,
|
|
74
77
|
onButtonClick,
|
|
75
78
|
editable = false,
|
|
79
|
+
refetchLabelValues,
|
|
80
|
+
showLoadingInButton = false,
|
|
81
|
+
hasRefreshButton = false,
|
|
76
82
|
}) => {
|
|
77
83
|
const {loader} = useParcaContext();
|
|
78
84
|
const [isOpen, setIsOpen] = useState(false);
|
|
79
85
|
const [focusedIndex, setFocusedIndex] = useState(-1);
|
|
80
86
|
const [searchTerm, setSearchTerm] = useState('');
|
|
87
|
+
const [isRefetching, setIsRefetching] = useState(false);
|
|
81
88
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
82
89
|
const optionsRef = useRef<HTMLDivElement>(null);
|
|
83
90
|
const searchInputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null);
|
|
84
91
|
const optionRefs = useRef<Array<HTMLElement | null>>([]);
|
|
85
92
|
|
|
93
|
+
const handleRefetch = useCallback(async () => {
|
|
94
|
+
if (refetchLabelValues == null || isRefetching) return;
|
|
95
|
+
|
|
96
|
+
setIsRefetching(true);
|
|
97
|
+
try {
|
|
98
|
+
await refetchLabelValues();
|
|
99
|
+
} finally {
|
|
100
|
+
setIsRefetching(false);
|
|
101
|
+
}
|
|
102
|
+
}, [refetchLabelValues, isRefetching]);
|
|
103
|
+
|
|
86
104
|
let items: TypedSelectItem[] = [];
|
|
87
105
|
if (itemsProp[0] != null && 'type' in itemsProp[0]) {
|
|
88
106
|
items = (itemsProp as GroupedSelectItem[]).flatMap(item =>
|
|
@@ -192,6 +210,15 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
|
|
192
210
|
'text-gray-100 dark:gray-900 bg-indigo-600 border-indigo-500 font-medium py-2 px-4';
|
|
193
211
|
|
|
194
212
|
const renderSelection = (selection: SelectItem | string | undefined): string | JSX.Element => {
|
|
213
|
+
if (showLoadingInButton && loading === true && selectedKey === '') {
|
|
214
|
+
return (
|
|
215
|
+
<span className="flex items-center gap-2" data-testid="label-value-loading-indicator">
|
|
216
|
+
<Icon icon="svg-spinners:ring-resize" className="w-4 h-4" />
|
|
217
|
+
<span>Loading...</span>
|
|
218
|
+
</span>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
195
222
|
if (editable) {
|
|
196
223
|
return typeof selection === 'string' && selection.length > 0 ? selection : placeholder;
|
|
197
224
|
} else {
|
|
@@ -265,71 +292,112 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
|
|
265
292
|
)}
|
|
266
293
|
role="listbox"
|
|
267
294
|
>
|
|
268
|
-
{
|
|
269
|
-
|
|
270
|
-
<div className="
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
295
|
+
<div className={cx('relative', {'pb-6': hasRefreshButton})}>
|
|
296
|
+
{searchable && (
|
|
297
|
+
<div className="sticky z-10 top-[-5px] w-auto max-w-full">
|
|
298
|
+
<div className="flex flex-col">
|
|
299
|
+
{editable ? (
|
|
300
|
+
<>
|
|
301
|
+
<textarea
|
|
302
|
+
ref={searchInputRef as React.LegacyRef<HTMLTextAreaElement>}
|
|
303
|
+
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]"
|
|
304
|
+
placeholder="Type a RegEx to add"
|
|
305
|
+
value={searchTerm}
|
|
306
|
+
onChange={e => setSearchTerm(e.target.value)}
|
|
307
|
+
onFocus={e => moveCaretToEnd(e)}
|
|
308
|
+
/>
|
|
309
|
+
{editable && searchTerm.length > 0 && (
|
|
310
|
+
<div className="p-2 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
|
|
311
|
+
<Button
|
|
312
|
+
variant="primary"
|
|
313
|
+
className="w-full h-[30px]"
|
|
314
|
+
onClick={() => {
|
|
315
|
+
onSelection(searchTerm);
|
|
316
|
+
setIsOpen(false);
|
|
317
|
+
}}
|
|
318
|
+
>
|
|
319
|
+
Add
|
|
320
|
+
</Button>
|
|
321
|
+
</div>
|
|
322
|
+
)}
|
|
323
|
+
</>
|
|
324
|
+
) : (
|
|
325
|
+
<input
|
|
326
|
+
ref={searchInputRef as React.LegacyRef<HTMLInputElement>}
|
|
327
|
+
type="text"
|
|
328
|
+
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"
|
|
329
|
+
placeholder="Search..."
|
|
277
330
|
value={searchTerm}
|
|
278
331
|
onChange={e => setSearchTerm(e.target.value)}
|
|
279
|
-
onFocus={e => moveCaretToEnd(e)}
|
|
280
332
|
/>
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
333
|
+
)}
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
)}
|
|
337
|
+
{refetchLabelValues !== undefined && loading !== true && (
|
|
338
|
+
<div className="absolute w-full bottom-0 px-3 bg-gray-50 dark:bg-gray-800">
|
|
339
|
+
<button
|
|
340
|
+
onClick={e => {
|
|
341
|
+
e.preventDefault();
|
|
342
|
+
e.stopPropagation();
|
|
343
|
+
void handleRefetch();
|
|
344
|
+
}}
|
|
345
|
+
disabled={isRefetching}
|
|
346
|
+
className={cx(
|
|
347
|
+
'p-1 flex items-center gap-1 rounded-full transition-all duration-200 w-full justify-center',
|
|
348
|
+
isRefetching
|
|
349
|
+
? 'cursor-wait opacity-50'
|
|
350
|
+
: 'hover:bg-gray-200 dark:hover:bg-gray-700 cursor-pointer'
|
|
351
|
+
)}
|
|
352
|
+
title="Refresh label values"
|
|
353
|
+
type="button"
|
|
354
|
+
data-testid="label-value-refresh-button"
|
|
355
|
+
>
|
|
356
|
+
<Icon
|
|
357
|
+
icon="system-uicons:reset"
|
|
358
|
+
className={cx(
|
|
359
|
+
'w-3 h-3 text-gray-500 dark:text-gray-400',
|
|
360
|
+
isRefetching && 'animate-spin'
|
|
294
361
|
)}
|
|
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
362
|
/>
|
|
305
|
-
|
|
363
|
+
<span className="text-xs text-gray-500 dark:text-gray-400">Refresh results</span>
|
|
364
|
+
</button>
|
|
306
365
|
</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
|
-
|
|
366
|
+
)}
|
|
367
|
+
{loading === true ? (
|
|
368
|
+
<div className="w-[270px]">{loader}</div>
|
|
369
|
+
) : groupedFilteredItems.length === 0 ? (
|
|
370
|
+
<div
|
|
371
|
+
className="px-4 py-3 text-sm text-gray-500 dark:text-gray-400 text-center"
|
|
372
|
+
data-testid="label-value-no-results"
|
|
373
|
+
>
|
|
374
|
+
No values found
|
|
375
|
+
</div>
|
|
376
|
+
) : (
|
|
377
|
+
groupedFilteredItems.map(group => (
|
|
378
|
+
<>
|
|
379
|
+
{groupedFilteredItems.length > 1 &&
|
|
380
|
+
groupedFilteredItems.every(g => g.type !== '') &&
|
|
381
|
+
group.type !== '' ? (
|
|
382
|
+
<div className="pl-2">
|
|
383
|
+
<DividerWithLabel label={group.type} />
|
|
384
|
+
</div>
|
|
385
|
+
) : null}
|
|
386
|
+
{group.values.map((item, index) => (
|
|
387
|
+
<OptionItem
|
|
388
|
+
key={item.key}
|
|
389
|
+
item={item}
|
|
390
|
+
index={index}
|
|
391
|
+
optionRefs={optionRefs}
|
|
392
|
+
focusedIndex={focusedIndex}
|
|
393
|
+
selectedKey={selectedKey}
|
|
394
|
+
handleSelection={handleSelection}
|
|
395
|
+
/>
|
|
396
|
+
))}
|
|
397
|
+
</>
|
|
398
|
+
))
|
|
399
|
+
)}
|
|
400
|
+
</div>
|
|
333
401
|
</div>
|
|
334
402
|
)}
|
|
335
403
|
</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,70 @@ const SimpleMatchers = ({
|
|
|
195
203
|
[setMatchersString]
|
|
196
204
|
);
|
|
197
205
|
|
|
198
|
-
const {labelNameOptions, isLoading: labelNamesLoading} = useLabels();
|
|
206
|
+
const {labelNameOptions, isLoading: labelNamesLoading, refetchLabelValues} = useLabels();
|
|
207
|
+
|
|
208
|
+
// Helper to ensure selected label name is in the options (for page load before API returns)
|
|
209
|
+
const getLabelNameOptionsWithSelected = useCallback(
|
|
210
|
+
(selectedLabelName: string): typeof labelNameOptions => {
|
|
211
|
+
if (selectedLabelName === '') return labelNameOptions;
|
|
212
|
+
|
|
213
|
+
// Check if the selected label name exists in any group
|
|
214
|
+
const existsInOptions = labelNameOptions.some(group =>
|
|
215
|
+
group.values.some(item => item.key === selectedLabelName)
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
if (existsInOptions) return labelNameOptions;
|
|
219
|
+
|
|
220
|
+
// Add it temporarily to a group with type '' (matching non-matching labels group)
|
|
221
|
+
if (labelNameOptions.length === 0) {
|
|
222
|
+
return [
|
|
223
|
+
{
|
|
224
|
+
type: '',
|
|
225
|
+
values: [
|
|
226
|
+
{
|
|
227
|
+
key: selectedLabelName,
|
|
228
|
+
element: {active: <>{selectedLabelName}</>, expanded: <>{selectedLabelName}</>},
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
},
|
|
232
|
+
];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Find the group with type '' (non-matching labels) or create it
|
|
236
|
+
const emptyTypeGroupIndex = labelNameOptions.findIndex(group => group.type === '');
|
|
237
|
+
|
|
238
|
+
if (emptyTypeGroupIndex !== -1) {
|
|
239
|
+
// Add to existing '' type group
|
|
240
|
+
const updatedOptions = [...labelNameOptions];
|
|
241
|
+
updatedOptions[emptyTypeGroupIndex] = {
|
|
242
|
+
...updatedOptions[emptyTypeGroupIndex],
|
|
243
|
+
values: [
|
|
244
|
+
...updatedOptions[emptyTypeGroupIndex].values,
|
|
245
|
+
{
|
|
246
|
+
key: selectedLabelName,
|
|
247
|
+
element: {active: <>{selectedLabelName}</>, expanded: <>{selectedLabelName}</>},
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
};
|
|
251
|
+
return updatedOptions;
|
|
252
|
+
} else {
|
|
253
|
+
// Create new '' type group
|
|
254
|
+
return [
|
|
255
|
+
...labelNameOptions,
|
|
256
|
+
{
|
|
257
|
+
type: '',
|
|
258
|
+
values: [
|
|
259
|
+
{
|
|
260
|
+
key: selectedLabelName,
|
|
261
|
+
element: {active: <>{selectedLabelName}</>, expanded: <>{selectedLabelName}</>},
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
},
|
|
265
|
+
];
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
[labelNameOptions]
|
|
269
|
+
);
|
|
199
270
|
|
|
200
271
|
const fetchLabelValuesUnified = useCallback(
|
|
201
272
|
async (labelName: string): Promise<string[]> => {
|
|
@@ -290,19 +361,29 @@ const SimpleMatchers = ({
|
|
|
290
361
|
|
|
291
362
|
const handleUpdateRow = useCallback(
|
|
292
363
|
(index: number, field: keyof QueryRow, value: string) => {
|
|
364
|
+
// Only set actively editing if not already showing all
|
|
365
|
+
// If showAll is true, user has explicitly expanded, so keep that state
|
|
366
|
+
if (!showAll) {
|
|
367
|
+
setIsActivelyEditing(true);
|
|
368
|
+
}
|
|
293
369
|
void updateRow(index, field, value);
|
|
294
370
|
},
|
|
295
|
-
[updateRow]
|
|
371
|
+
[updateRow, showAll]
|
|
296
372
|
);
|
|
297
373
|
|
|
298
374
|
const addNewRow = useCallback((): void => {
|
|
375
|
+
// Only set actively editing if not already showing all label names and values
|
|
376
|
+
// If showAll is true, user has explicitly expanded, so keep that state
|
|
377
|
+
if (!showAll) {
|
|
378
|
+
setIsActivelyEditing(true);
|
|
379
|
+
}
|
|
299
380
|
const newRows = [
|
|
300
381
|
...queryRows,
|
|
301
382
|
{labelName: '', operator: '=', labelValue: '', labelValues: [], isLoading: false},
|
|
302
383
|
];
|
|
303
384
|
setQueryRows(newRows);
|
|
304
385
|
updateMatchersString(newRows);
|
|
305
|
-
}, [queryRows, updateMatchersString]);
|
|
386
|
+
}, [queryRows, updateMatchersString, showAll]);
|
|
306
387
|
|
|
307
388
|
const removeRow = useCallback(
|
|
308
389
|
(index: number): void => {
|
|
@@ -362,7 +443,7 @@ const SimpleMatchers = ({
|
|
|
362
443
|
{visibleRows.map((row, index) => (
|
|
363
444
|
<div key={index} className="flex items-center" {...testId(TEST_IDS.SIMPLE_MATCHER_ROW)}>
|
|
364
445
|
<Select
|
|
365
|
-
items={
|
|
446
|
+
items={getLabelNameOptionsWithSelected(row.labelName)}
|
|
366
447
|
onSelection={value => handleUpdateRow(index, 'labelName', value)}
|
|
367
448
|
placeholder="Select label name"
|
|
368
449
|
selectedKey={row.labelName}
|
|
@@ -379,12 +460,16 @@ const SimpleMatchers = ({
|
|
|
379
460
|
{...testId(TEST_IDS.OPERATOR_SELECT)}
|
|
380
461
|
/>
|
|
381
462
|
<Select
|
|
382
|
-
items={transformLabelsForSelect(
|
|
463
|
+
items={transformLabelsForSelect(
|
|
464
|
+
row.labelValue !== '' && !row.labelValues.includes(row.labelValue)
|
|
465
|
+
? [...row.labelValues, row.labelValue]
|
|
466
|
+
: row.labelValues
|
|
467
|
+
)}
|
|
383
468
|
onSelection={value => handleUpdateRow(index, 'labelValue', value)}
|
|
384
469
|
placeholder="Select label value"
|
|
385
470
|
selectedKey={row.labelValue}
|
|
386
471
|
className="rounded-none ring-0 focus:ring-0 outline-none max-w-48"
|
|
387
|
-
optionsClassname={cx('max-w-[
|
|
472
|
+
optionsClassname={cx('max-w-[600px]', {
|
|
388
473
|
'w-[300px]': isRowRegex(row),
|
|
389
474
|
})}
|
|
390
475
|
searchable={true}
|
|
@@ -393,6 +478,9 @@ const SimpleMatchers = ({
|
|
|
393
478
|
onButtonClick={() => handleLabelValueClick(index)}
|
|
394
479
|
editable={isRowRegex(row)}
|
|
395
480
|
{...testId(TEST_IDS.LABEL_VALUE_SELECT)}
|
|
481
|
+
refetchLabelValues={refetchLabelValues}
|
|
482
|
+
showLoadingInButton={true}
|
|
483
|
+
hasRefreshButton={true}
|
|
396
484
|
/>
|
|
397
485
|
<button
|
|
398
486
|
onClick={() => removeRow(index)}
|
|
@@ -406,9 +494,18 @@ const SimpleMatchers = ({
|
|
|
406
494
|
</div>
|
|
407
495
|
))}
|
|
408
496
|
|
|
409
|
-
{queryRows.length > 3 && (
|
|
497
|
+
{queryRows.length > 3 && !isActivelyEditing && (
|
|
410
498
|
<button
|
|
411
|
-
onClick={() =>
|
|
499
|
+
onClick={() => {
|
|
500
|
+
if (showAll) {
|
|
501
|
+
// Clicking "Show less" - collapse and stop editing mode
|
|
502
|
+
setShowAll(false);
|
|
503
|
+
setIsActivelyEditing(false);
|
|
504
|
+
} else {
|
|
505
|
+
// Clicking "Show more" - just expand
|
|
506
|
+
setShowAll(true);
|
|
507
|
+
}
|
|
508
|
+
}}
|
|
412
509
|
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
510
|
{...testId(showAll ? TEST_IDS.SHOW_LESS_BUTTON : TEST_IDS.SHOW_MORE_BUTTON)}
|
|
414
511
|
>
|
|
@@ -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,7 @@ interface LabelsContextType {
|
|
|
32
32
|
currentLabelName: string | null;
|
|
33
33
|
setCurrentLabelName: (name: string | null) => void;
|
|
34
34
|
shouldHandlePrefixes: boolean;
|
|
35
|
+
refetchLabelValues: () => void;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
const LabelsContext = createContext<LabelsContextType | null>(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,7 @@ export function LabelsProvider({
|
|
|
114
113
|
currentLabelName,
|
|
115
114
|
setCurrentLabelName,
|
|
116
115
|
shouldHandlePrefixes,
|
|
116
|
+
refetchLabelValues,
|
|
117
117
|
}),
|
|
118
118
|
[
|
|
119
119
|
labelNames,
|
|
@@ -123,6 +123,7 @@ export function LabelsProvider({
|
|
|
123
123
|
isLabelValuesLoading,
|
|
124
124
|
currentLabelName,
|
|
125
125
|
shouldHandlePrefixes,
|
|
126
|
+
refetchLabelValues,
|
|
126
127
|
]
|
|
127
128
|
);
|
|
128
129
|
|
|
@@ -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,7 @@ interface LabelContextValue {
|
|
|
29
31
|
labelNameOptions: LabelNameSection[];
|
|
30
32
|
isLoading: boolean;
|
|
31
33
|
error: Error | null;
|
|
34
|
+
refetchLabelValues: () => void;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
const LabelContext = createContext<LabelContextValue | null>(null);
|
|
@@ -53,6 +56,7 @@ export function LabelProvider({
|
|
|
53
56
|
start,
|
|
54
57
|
end,
|
|
55
58
|
}: LabelProviderProps): JSX.Element {
|
|
59
|
+
const reactQueryClient = useQueryClient();
|
|
56
60
|
const utilizationLabelResponse = useUtilizationLabels();
|
|
57
61
|
const {loading, result} = useLabelNames(queryClient, profileType, start, end);
|
|
58
62
|
|
|
@@ -131,7 +135,29 @@ export function LabelProvider({
|
|
|
131
135
|
};
|
|
132
136
|
}, [profileValues, utilizationValues, labelNameFromMatchers]);
|
|
133
137
|
|
|
134
|
-
|
|
138
|
+
const refetchLabelValues = useCallback(() => {
|
|
139
|
+
void reactQueryClient.refetchQueries({
|
|
140
|
+
predicate: query => {
|
|
141
|
+
const key = query.queryKey;
|
|
142
|
+
return (
|
|
143
|
+
Array.isArray(key) &&
|
|
144
|
+
key.length === 4 &&
|
|
145
|
+
typeof key[0] === 'string' &&
|
|
146
|
+
key[1] === profileType
|
|
147
|
+
);
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
}, [reactQueryClient, profileType]);
|
|
151
|
+
|
|
152
|
+
const contextValue = useMemo(
|
|
153
|
+
() => ({
|
|
154
|
+
...value,
|
|
155
|
+
refetchLabelValues,
|
|
156
|
+
}),
|
|
157
|
+
[value, refetchLabelValues]
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
return <LabelContext.Provider value={contextValue}>{children}</LabelContext.Provider>;
|
|
135
161
|
}
|
|
136
162
|
|
|
137
163
|
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
|
};
|