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