@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/MatchersInput/SuggestionsList.d.ts +3 -1
  3. package/dist/MatchersInput/SuggestionsList.d.ts.map +1 -1
  4. package/dist/MatchersInput/SuggestionsList.js +48 -4
  5. package/dist/MatchersInput/index.d.ts +2 -0
  6. package/dist/MatchersInput/index.d.ts.map +1 -1
  7. package/dist/MatchersInput/index.js +21 -9
  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 +30 -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 +2 -0
  29. package/dist/contexts/MatchersInputLabelsContext.d.ts.map +1 -1
  30. package/dist/contexts/MatchersInputLabelsContext.js +6 -2
  31. package/dist/contexts/SimpleMatchersLabelContext.d.ts +2 -0
  32. package/dist/contexts/SimpleMatchersLabelContext.d.ts.map +1 -1
  33. package/dist/contexts/SimpleMatchersLabelContext.js +24 -3
  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 +5 -5
  39. package/src/MatchersInput/SuggestionsList.tsx +184 -39
  40. package/src/MatchersInput/index.tsx +28 -8
  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 +132 -60
  47. package/src/SimpleMatchers/index.tsx +116 -12
  48. package/src/ViewMatchers/index.tsx +15 -15
  49. package/src/contexts/MatchersInputLabelsContext.tsx +16 -13
  50. package/src/contexts/SimpleMatchersLabelContext.tsx +39 -3
  51. 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
- {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"
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
- {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>
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
- </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
- )}
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
- 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,75 @@ const SimpleMatchers = ({
195
203
  [setMatchersString]
196
204
  );
197
205
 
198
- const {labelNameOptions, isLoading: labelNamesLoading} = useLabels();
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={labelNameOptions}
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(row.labelValues)}
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-[300px]', {
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={() => setShowAll(!showAll)}
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
- 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,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 {result: labelNamesResponse, loading: isLabelNamesLoading} = useLabelNames(
60
- queryClient,
61
- profileType,
62
- start,
63
- end
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 {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,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 {loading, result} = useLabelNames(queryClient, profileType, start, end);
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
- return <LabelContext.Provider value={value}>{children}</LabelContext.Provider>;
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
  };