@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/MatchersInput/SuggestionsList.d.ts +2 -1
  3. package/dist/MatchersInput/SuggestionsList.d.ts.map +1 -1
  4. package/dist/MatchersInput/SuggestionsList.js +24 -3
  5. package/dist/MatchersInput/index.d.ts +1 -0
  6. package/dist/MatchersInput/index.d.ts.map +1 -1
  7. package/dist/MatchersInput/index.js +13 -7
  8. package/dist/ProfileSelector/QueryControls.d.ts.map +1 -1
  9. package/dist/ProfileSelector/QueryControls.js +4 -1
  10. package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.d.ts +4 -0
  11. package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.d.ts.map +1 -1
  12. package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.js +2 -4
  13. package/dist/ProfileView/components/Toolbars/index.d.ts +4 -0
  14. package/dist/ProfileView/components/Toolbars/index.d.ts.map +1 -1
  15. package/dist/ProfileView/components/Toolbars/index.js +2 -2
  16. package/dist/ProfileView/hooks/useVisualizationState.d.ts +2 -0
  17. package/dist/ProfileView/hooks/useVisualizationState.d.ts.map +1 -1
  18. package/dist/ProfileView/hooks/useVisualizationState.js +19 -1
  19. package/dist/ProfileView/index.d.ts.map +1 -1
  20. package/dist/ProfileView/index.js +2 -2
  21. package/dist/SimpleMatchers/Select.d.ts +3 -0
  22. package/dist/SimpleMatchers/Select.d.ts.map +1 -1
  23. package/dist/SimpleMatchers/Select.js +29 -6
  24. package/dist/SimpleMatchers/index.d.ts +1 -0
  25. package/dist/SimpleMatchers/index.d.ts.map +1 -1
  26. package/dist/SimpleMatchers/index.js +92 -9
  27. package/dist/ViewMatchers/index.js +14 -14
  28. package/dist/contexts/MatchersInputLabelsContext.d.ts +1 -0
  29. package/dist/contexts/MatchersInputLabelsContext.d.ts.map +1 -1
  30. package/dist/contexts/MatchersInputLabelsContext.js +3 -1
  31. package/dist/contexts/SimpleMatchersLabelContext.d.ts +1 -0
  32. package/dist/contexts/SimpleMatchersLabelContext.d.ts.map +1 -1
  33. package/dist/contexts/SimpleMatchersLabelContext.js +19 -2
  34. package/dist/styles.css +1 -1
  35. package/dist/useGrpcQuery/index.d.ts +2 -1
  36. package/dist/useGrpcQuery/index.d.ts.map +1 -1
  37. package/dist/useGrpcQuery/index.js +2 -1
  38. package/package.json +8 -8
  39. package/src/MatchersInput/SuggestionsList.tsx +119 -40
  40. package/src/MatchersInput/index.tsx +14 -5
  41. package/src/ProfileSelector/QueryControls.tsx +5 -1
  42. package/src/ProfileView/components/Toolbars/MultiLevelDropdown.tsx +10 -5
  43. package/src/ProfileView/components/Toolbars/index.tsx +12 -0
  44. package/src/ProfileView/hooks/useVisualizationState.ts +34 -1
  45. package/src/ProfileView/index.tsx +7 -0
  46. package/src/SimpleMatchers/Select.tsx +128 -60
  47. package/src/SimpleMatchers/index.tsx +109 -12
  48. package/src/ViewMatchers/index.tsx +15 -15
  49. package/src/contexts/MatchersInputLabelsContext.tsx +8 -7
  50. package/src/contexts/SimpleMatchersLabelContext.tsx +28 -2
  51. 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
- {searchable && (
269
- <div className="sticky z-10 top-[-5px] w-auto max-w-full">
270
- <div className="flex flex-col">
271
- {editable ? (
272
- <>
273
- <textarea
274
- ref={searchInputRef as React.LegacyRef<HTMLTextAreaElement>}
275
- 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]"
276
- placeholder="Type a RegEx to add"
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
- {editable && searchTerm.length > 0 && (
282
- <div className="p-2 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800">
283
- <Button
284
- variant="primary"
285
- className="w-full h-[30px]"
286
- onClick={() => {
287
- onSelection(searchTerm);
288
- setIsOpen(false);
289
- }}
290
- >
291
- Add
292
- </Button>
293
- </div>
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
- </div>
308
- )}
309
- {loading === true ? (
310
- <div className="w-[270px]">{loader}</div>
311
- ) : (
312
- groupedFilteredItems.map(group => (
313
- <>
314
- {groupedFilteredItems.length > 1 ? (
315
- <div className="pl-2">
316
- <DividerWithLabel label={group.type} />
317
- </div>
318
- ) : null}
319
- {group.values.map((item, index) => (
320
- <OptionItem
321
- key={item.key}
322
- item={item}
323
- index={index}
324
- optionRefs={optionRefs}
325
- focusedIndex={focusedIndex}
326
- selectedKey={selectedKey}
327
- handleSelection={handleSelection}
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
- const visibleRows = showAll ? queryRows : queryRows.slice(0, 3);
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={labelNameOptions}
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(row.labelValues)}
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-[300px]', {
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={() => setShowAll(!showAll)}
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
- useEffect(() => {
112
- const fetchAllLabelValues = async (): Promise<void> => {
113
- const newLabelValuesMap: Record<string, string[]> = {};
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
- for (const labelName of labelNames) {
117
- newIsLoading[labelName] = true;
118
- setIsLoading(prev => ({...prev, [labelName]: true}));
115
+ for (const labelName of labelNames) {
116
+ newIsLoading[labelName] = true;
117
+ setIsLoading(prev => ({...prev, [labelName]: true}));
119
118
 
120
- const values = await fetchLabelValues(labelName);
121
- newLabelValuesMap[labelName] = values;
122
- newIsLoading[labelName] = false;
123
- }
119
+ const values = await fetchLabelValues(labelName);
120
+ newLabelValuesMap[labelName] = values;
121
+ newIsLoading[labelName] = false;
122
+ }
124
123
 
125
- setLabelValuesMap(newLabelValuesMap);
126
- setIsLoading(newIsLoading);
127
- };
124
+ setLabelValuesMap(newLabelValuesMap);
125
+ setIsLoading(newIsLoading);
126
+ }, [labelNames, fetchLabelValues]);
128
127
 
128
+ useEffect(() => {
129
129
  void fetchAllLabelValues();
130
- }, [labelNames, fetchLabelValues]);
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 {result: labelValuesOriginal, loading: isLabelValuesLoading} = useLabelValues(
75
- queryClient,
76
- currentLabelName ?? '',
77
- profileType,
78
- start,
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
- return <LabelContext.Provider value={value}>{children}</LabelContext.Provider>;
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
  };