@parca/profile 0.19.94 → 0.19.102

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 (30) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/ProfileFlameGraph/FlameGraphArrow/ContextMenu.d.ts.map +1 -1
  3. package/dist/ProfileFlameGraph/FlameGraphArrow/ContextMenu.js +2 -0
  4. package/dist/ProfileSelector/MetricsGraphSection.d.ts +1 -1
  5. package/dist/ProfileSelector/MetricsGraphSection.d.ts.map +1 -1
  6. package/dist/ProfileSelector/MetricsGraphSection.js +1 -1
  7. package/dist/ProfileSelector/index.d.ts.map +1 -1
  8. package/dist/ProfileSelector/index.js +1 -2
  9. package/dist/ProfileView/hooks/useResetStateOnProfileTypeChange.d.ts.map +1 -1
  10. package/dist/ProfileView/hooks/useResetStateOnProfileTypeChange.js +8 -0
  11. package/dist/SimpleMatchers/Select.d.ts.map +1 -1
  12. package/dist/SimpleMatchers/Select.js +3 -3
  13. package/dist/hooks/useLabels.d.ts.map +1 -1
  14. package/dist/hooks/useLabels.js +4 -1
  15. package/dist/hooks/useQueryState.d.ts.map +1 -1
  16. package/dist/hooks/useQueryState.js +53 -23
  17. package/dist/hooks/useQueryState.test.js +32 -22
  18. package/dist/useSumBy.d.ts +10 -2
  19. package/dist/useSumBy.d.ts.map +1 -1
  20. package/dist/useSumBy.js +30 -7
  21. package/package.json +15 -10
  22. package/src/ProfileFlameGraph/FlameGraphArrow/ContextMenu.tsx +2 -0
  23. package/src/ProfileSelector/MetricsGraphSection.tsx +2 -2
  24. package/src/ProfileSelector/index.tsx +1 -5
  25. package/src/ProfileView/hooks/useResetStateOnProfileTypeChange.ts +8 -0
  26. package/src/SimpleMatchers/Select.tsx +3 -3
  27. package/src/hooks/useLabels.ts +5 -1
  28. package/src/hooks/useQueryState.test.tsx +41 -22
  29. package/src/hooks/useQueryState.ts +72 -31
  30. package/src/useSumBy.ts +58 -4
package/dist/useSumBy.js CHANGED
@@ -31,7 +31,7 @@ const getDefaultSumBy = (profile, labels) => {
31
31
  }
32
32
  return undefined;
33
33
  };
34
- export const useSumBySelection = (profileType, labelNamesLoading, labels, { defaultValue, } = {}) => {
34
+ export const useSumBySelection = (profileType, labelNamesLoading, labels, draftSumBy, { defaultValue, } = {}) => {
35
35
  const [userSelectedSumBy, setUserSelectedSumBy] = useState(profileType != null ? { [profileType.toString()]: defaultValue } : {});
36
36
  // Update userSelectedSumBy when defaultValue changes (e.g., during navigation)
37
37
  useEffect(() => {
@@ -57,9 +57,15 @@ export const useSumBySelection = (profileType, labelNamesLoading, labels, { defa
57
57
  // Store the last valid sumBy value to return during loading
58
58
  const lastValidSumByRef = useRef(DEFAULT_EMPTY_SUM_BY);
59
59
  const sumBy = useMemo(() => {
60
- // If loading, return the last valid value to prevent input from blanking
61
- if (labelNamesLoading && lastValidSumByRef.current !== DEFAULT_EMPTY_SUM_BY) {
62
- return lastValidSumByRef.current;
60
+ if (labelNamesLoading) {
61
+ // For smoother UX, return draftSumBy first if available during loading
62
+ // as this must be recently computed with the draft time range labels.
63
+ if (draftSumBy !== undefined) {
64
+ return draftSumBy;
65
+ }
66
+ if (lastValidSumByRef.current == null) {
67
+ return lastValidSumByRef.current;
68
+ }
63
69
  }
64
70
  let result = userSelectedSumBy[profileType?.toString() ?? ''] ?? defaultSumBy ?? DEFAULT_EMPTY_SUM_BY;
65
71
  if (profileType?.delta !== true) {
@@ -68,7 +74,7 @@ export const useSumBySelection = (profileType, labelNamesLoading, labels, { defa
68
74
  // Store the computed value for next loading state
69
75
  lastValidSumByRef.current = result;
70
76
  return result;
71
- }, [userSelectedSumBy, profileType, defaultSumBy, labelNamesLoading]);
77
+ }, [userSelectedSumBy, profileType, defaultSumBy, labelNamesLoading, draftSumBy]);
72
78
  return [
73
79
  sumBy,
74
80
  setSumBy,
@@ -118,15 +124,32 @@ export const sumByToParam = (sumBy) => {
118
124
  return sumBy;
119
125
  };
120
126
  // Combined hook that handles all sumBy logic: fetching labels, computing defaults, and managing selection
121
- export const useSumBy = (queryClient, profileType, timeRange, defaultValue) => {
127
+ export const useSumBy = (queryClient, profileType, timeRange, draftProfileType, draftTimeRange, defaultValue) => {
122
128
  const { loading: labelNamesLoading, result } = useLabelNames(queryClient, profileType?.toString() ?? '', timeRange.getFromMs(), timeRange.getToMs());
129
+ const { draftSumBy, setDraftSumBy, isDraftSumByLoading } = useDraftSumBy(queryClient, draftProfileType, draftTimeRange, defaultValue);
123
130
  const labels = useMemo(() => {
124
131
  return result.response?.labelNames === undefined ? [] : result.response.labelNames;
125
132
  }, [result]);
126
- const [sumBySelection, setSumByInternal, { isLoading }] = useSumBySelection(profileType, labelNamesLoading, labels, { defaultValue });
133
+ const [sumBySelection, setSumByInternal, { isLoading }] = useSumBySelection(profileType, labelNamesLoading, labels, draftSumBy, { defaultValue });
127
134
  return {
128
135
  sumBy: sumBySelection,
129
136
  setSumBy: setSumByInternal,
130
137
  isLoading,
138
+ draftSumBy,
139
+ setDraftSumBy,
140
+ isDraftSumByLoading,
141
+ };
142
+ };
143
+ export const useDraftSumBy = (queryClient, profileType, timeRange, defaultValue) => {
144
+ const [draftSumBy, setDraftSumBy] = useState(defaultValue);
145
+ const { loading: labelNamesLoading, result } = useLabelNames(queryClient, profileType?.toString() ?? '', timeRange.getFromMs(), timeRange.getToMs());
146
+ const labels = useMemo(() => {
147
+ return result.response?.labelNames === undefined ? [] : result.response.labelNames;
148
+ }, [result]);
149
+ const { defaultSumBy, isLoading } = useDefaultSumBy(profileType, labelNamesLoading, labels);
150
+ return {
151
+ draftSumBy: draftSumBy ?? defaultSumBy ?? DEFAULT_EMPTY_SUM_BY,
152
+ setDraftSumBy: setDraftSumBy,
153
+ isDraftSumByLoading: isLoading,
131
154
  };
132
155
  };
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@parca/profile",
3
- "version": "0.19.94",
3
+ "version": "0.19.102",
4
4
  "description": "Profile viewing libraries",
5
5
  "dependencies": {
6
6
  "@floating-ui/react": "^0.27.12",
7
7
  "@headlessui/react": "^1.7.19",
8
8
  "@iconify/react": "^4.0.0",
9
- "@parca/client": "0.17.11",
10
- "@parca/components": "0.16.386",
11
- "@parca/dynamicsize": "0.16.67",
12
- "@parca/hooks": "0.0.111",
13
- "@parca/icons": "0.16.74",
14
- "@parca/parser": "0.16.81",
15
- "@parca/store": "0.16.194",
9
+ "@parca/client": "0.17.16",
10
+ "@parca/components": "0.16.392",
11
+ "@parca/dynamicsize": "0.16.72",
12
+ "@parca/hooks": "0.0.116",
13
+ "@parca/icons": "0.16.79",
14
+ "@parca/parser": "0.16.86",
15
+ "@parca/store": "0.16.199",
16
16
  "@parca/test-utils": "0.0.17",
17
- "@parca/utilities": "0.0.117",
17
+ "@parca/utilities": "0.0.122",
18
18
  "@popperjs/core": "^2.11.8",
19
19
  "@protobuf-ts/runtime-rpc": "^2.5.0",
20
20
  "@storybook/preview-api": "^8.4.3",
@@ -75,9 +75,14 @@
75
75
  "keywords": [],
76
76
  "author": "",
77
77
  "license": "ISC",
78
+ "repository": {
79
+ "type": "git",
80
+ "url": "https://github.com/parca-dev/parca",
81
+ "directory": "ui/packages/shared/profile"
82
+ },
78
83
  "publishConfig": {
79
84
  "access": "public",
80
85
  "registry": "https://registry.npmjs.org/"
81
86
  },
82
- "gitHead": "876c0c10323fe16e6412180c163078a17f23c444"
87
+ "gitHead": "71d719bdbc5b9591b427c668b4ab9685eeb44b4a"
83
88
  }
@@ -194,11 +194,13 @@ const ContextMenu = ({
194
194
 
195
195
  if (dashboardItems.includes('sandwich')) {
196
196
  setSandwichFunctionName(functionName);
197
+ hideMenu();
197
198
  return;
198
199
  }
199
200
 
200
201
  setSandwichFunctionName(functionName);
201
202
  setDashboardItems([...dashboardItems, 'sandwich']);
203
+ hideMenu();
202
204
  }}
203
205
  disabled={functionName === '' || functionName == null}
204
206
  >
@@ -29,7 +29,7 @@ interface MetricsGraphSectionProps {
29
29
  querySelection: QuerySelection;
30
30
  profileSelection: ProfileSelection | null;
31
31
  comparing: boolean;
32
- sumBy: string[] | null;
32
+ sumBy: string[] | undefined;
33
33
  defaultSumByLoading: boolean;
34
34
  queryClient: QueryServiceClient;
35
35
  queryExpressionString: string;
@@ -170,7 +170,7 @@ export function MetricsGraphSection({
170
170
  to={querySelection.to}
171
171
  profile={profileSelection}
172
172
  comparing={comparing}
173
- sumBy={querySelection.sumBy ?? sumBy ?? []}
173
+ sumBy={sumBy ?? []}
174
174
  sumByLoading={defaultSumByLoading}
175
175
  setTimeRange={handleTimeRangeChange}
176
176
  addLabelMatcher={addLabelMatcher}
@@ -209,10 +209,6 @@ const ProfileSelector = ({
209
209
  const currentTo = timeRangeSelection.getToMs(true);
210
210
  const currentRangeKey = timeRangeSelection.getRangeKey();
211
211
  // Commit with refreshed time range
212
- console.log(
213
- '[draftExpression] setQueryExpression: committing with refreshed time range:',
214
- draftSelection.expression
215
- );
216
212
  commitDraft({
217
213
  from: currentFrom,
218
214
  to: currentTo,
@@ -332,7 +328,7 @@ const ProfileSelector = ({
332
328
  querySelection={querySelection}
333
329
  profileSelection={profileSelection}
334
330
  comparing={comparing}
335
- sumBy={querySelection.sumBy ?? []}
331
+ sumBy={querySelection.sumBy}
336
332
  defaultSumByLoading={sumByLoading}
337
333
  queryClient={queryClient}
338
334
  queryExpressionString={queryExpressionString}
@@ -18,6 +18,8 @@ import {useProfileFilters} from '../components/ProfileFilters/useProfileFilters'
18
18
  export const useResetStateOnProfileTypeChange = (): (() => void) => {
19
19
  const [groupBy, setGroupBy] = useURLState('group_by');
20
20
  const [curPath, setCurPath] = useURLState('cur_path');
21
+ const [sumByA, setSumByA] = useURLState('sum_by_a');
22
+ const [sumByB, setSumByB] = useURLState('sum_by_b');
21
23
  const {resetFilters} = useProfileFilters();
22
24
  const [sandwichFunctionName, setSandwichFunctionName] = useURLState('sandwich_function_name');
23
25
  const batchUpdates = useURLStateBatch();
@@ -34,6 +36,12 @@ export const useResetStateOnProfileTypeChange = (): (() => void) => {
34
36
  if (sandwichFunctionName !== undefined) {
35
37
  setSandwichFunctionName(undefined);
36
38
  }
39
+ if (sumByA !== undefined) {
40
+ setSumByA(undefined);
41
+ }
42
+ if (sumByB !== undefined) {
43
+ setSumByB(undefined);
44
+ }
37
45
 
38
46
  resetFilters();
39
47
  });
@@ -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, {useCallback, useEffect, useRef, useState} from 'react';
14
+ import React, {Fragment, useCallback, useEffect, useRef, useState} from 'react';
15
15
 
16
16
  import {Icon} from '@iconify/react';
17
17
  import cx from 'classnames';
@@ -350,7 +350,7 @@ const CustomSelect: React.FC<CustomSelectProps & Record<string, any>> = ({
350
350
  </div>
351
351
  ) : (
352
352
  groupedFilteredItems.map(group => (
353
- <>
353
+ <Fragment key={group.type}>
354
354
  {groupedFilteredItems.length > 1 &&
355
355
  groupedFilteredItems.every(g => g.type !== '') &&
356
356
  group.type !== '' ? (
@@ -369,7 +369,7 @@ const CustomSelect: React.FC<CustomSelectProps & Record<string, any>> = ({
369
369
  handleSelection={handleSelection}
370
370
  />
371
371
  ))}
372
- </>
372
+ </Fragment>
373
373
  ))
374
374
  )}
375
375
  </div>
@@ -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 {useEffect} from 'react';
15
+
14
16
  import {LabelsRequest, LabelsResponse, QueryServiceClient, ValuesRequest} from '@parca/client';
15
17
  import {useGrpcMetadata} from '@parca/components';
16
18
  import {millisToProtoTimestamp, sanitizeLabelValue} from '@parca/utilities';
@@ -68,7 +70,9 @@ export const useLabelNames = (
68
70
  },
69
71
  });
70
72
 
71
- console.log('Label names query result:', {data, error, isLoading});
73
+ useEffect(() => {
74
+ console.log('Label names query result:', {data, error, isLoading});
75
+ }, [data, error, isLoading]);
72
76
 
73
77
  return {
74
78
  result: {response: data, error: error as Error},
@@ -69,16 +69,33 @@ vi.mock('@parca/components/src/hooks/URLState/utils', async () => {
69
69
  };
70
70
  });
71
71
 
72
- // Mock useSumBy to return the sumBy from URL params or undefined
72
+ // Mock useSumBy with stateful behavior using React's useState
73
73
  vi.mock('../useSumBy', async () => {
74
74
  const actual = await vi.importActual('../useSumBy');
75
+ const react = await import('react');
76
+
75
77
  return {
76
78
  ...actual,
77
- useSumBy: (_queryClient: any, _profileType: any, _timeRange: any, defaultValue: any) => ({
78
- sumBy: defaultValue,
79
- setSumBy: vi.fn(),
80
- isLoading: false,
81
- }),
79
+ useSumBy: (
80
+ _queryClient: any,
81
+ _profileType: any,
82
+ _timeRange: any,
83
+ _draftProfileType: any,
84
+ _draftTimeRange: any,
85
+ defaultValue: any
86
+ ) => {
87
+ const [draftSumBy, setDraftSumBy] = react.useState<string[] | undefined>(defaultValue);
88
+ const [sumBy, setSumBy] = react.useState<string[] | undefined>(defaultValue);
89
+
90
+ return {
91
+ sumBy,
92
+ setSumBy,
93
+ isLoading: false,
94
+ draftSumBy,
95
+ setDraftSumBy,
96
+ isDraftSumByLoading: false,
97
+ };
98
+ },
82
99
  };
83
100
  });
84
101
 
@@ -207,7 +224,9 @@ describe('useQueryState', () => {
207
224
  it('should update sumBy', async () => {
208
225
  const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
209
226
 
227
+ // sumBy only applies to delta profiles, so we need to set one first
210
228
  act(() => {
229
+ result.current.setDraftExpression('process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}');
211
230
  result.current.setDraftSumBy(['namespace', 'container']);
212
231
  });
213
232
 
@@ -256,15 +275,15 @@ describe('useQueryState', () => {
256
275
  const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
257
276
 
258
277
  act(() => {
259
- // Update multiple draft values
260
- result.current.setDraftExpression('memory:inuse_space:bytes:space:bytes{}');
278
+ // Update multiple draft values (using delta profile since sumBy only applies to delta)
279
+ result.current.setDraftExpression('memory:alloc_space:bytes:space:bytes:delta{}');
261
280
  result.current.setDraftTimeRange(7000, 8000, 'relative:minute|30');
262
281
  result.current.setDraftSumBy(['pod', 'node']);
263
282
  });
264
283
 
265
284
  // All drafts should be updated
266
285
  expect(result.current.draftSelection.expression).toBe(
267
- 'memory:inuse_space:bytes:space:bytes{}'
286
+ 'memory:alloc_space:bytes:space:bytes:delta{}'
268
287
  );
269
288
  expect(result.current.draftSelection.from).toBe(7000);
270
289
  expect(result.current.draftSelection.to).toBe(8000);
@@ -278,7 +297,7 @@ describe('useQueryState', () => {
278
297
  // Should only navigate once for all updates
279
298
  expect(mockNavigateTo).toHaveBeenCalledTimes(1);
280
299
  const [, params] = mockNavigateTo.mock.calls[0];
281
- expect(params.expression).toBe('memory:inuse_space:bytes:space:bytes{}');
300
+ expect(params.expression).toBe('memory:alloc_space:bytes:space:bytes:delta{}');
282
301
  expect(params.from).toBe('7000');
283
302
  expect(params.to).toBe('8000');
284
303
  expect(params.time_selection).toBe('relative:minute|30');
@@ -430,9 +449,9 @@ describe('useQueryState', () => {
430
449
  it('should handle _b suffix correctly', async () => {
431
450
  const {result} = renderHook(() => useQueryState({suffix: '_b'}), {wrapper: createWrapper()});
432
451
 
433
- // Update draft state
452
+ // Update draft state (using delta profile since sumBy only applies to delta)
434
453
  act(() => {
435
- result.current.setDraftExpression('memory:inuse_space:bytes:space:bytes{}');
454
+ result.current.setDraftExpression('memory:alloc_space:bytes:space:bytes:delta{}');
436
455
  result.current.setDraftTimeRange(3333, 4444, 'relative:hour|2');
437
456
  result.current.setDraftSumBy(['label_b']);
438
457
  });
@@ -445,7 +464,7 @@ describe('useQueryState', () => {
445
464
  await waitFor(() => {
446
465
  expect(mockNavigateTo).toHaveBeenCalled();
447
466
  const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
448
- expect(params.expression_b).toBe('memory:inuse_space:bytes:space:bytes{}');
467
+ expect(params.expression_b).toBe('memory:alloc_space:bytes:space:bytes:delta{}');
449
468
  expect(params.from_b).toBe('3333');
450
469
  expect(params.to_b).toBe('4444');
451
470
  expect(params.sum_by_b).toBe('label_b');
@@ -457,9 +476,9 @@ describe('useQueryState', () => {
457
476
  it('should not update URL until commit', async () => {
458
477
  const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
459
478
 
460
- // Make multiple draft changes
479
+ // Make multiple draft changes (using delta profile since sumBy only applies to delta)
461
480
  act(() => {
462
- result.current.setDraftExpression('memory:inuse_space:bytes:space:bytes{}');
481
+ result.current.setDraftExpression('memory:alloc_space:bytes:space:bytes:delta{}');
463
482
  result.current.setDraftTimeRange(5000, 6000, 'relative:hour|3');
464
483
  result.current.setDraftSumBy(['namespace', 'pod']);
465
484
  });
@@ -476,7 +495,7 @@ describe('useQueryState', () => {
476
495
  await waitFor(() => {
477
496
  expect(mockNavigateTo).toHaveBeenCalledTimes(1);
478
497
  const [, params] = mockNavigateTo.mock.calls[0];
479
- expect(params.expression).toBe('memory:inuse_space:bytes:space:bytes{}');
498
+ expect(params.expression).toBe('memory:alloc_space:bytes:space:bytes:delta{}');
480
499
  expect(params.from).toBe('5000');
481
500
  expect(params.to).toBe('6000');
482
501
  expect(params.sum_by).toBe('namespace,pod');
@@ -737,17 +756,17 @@ describe('useQueryState', () => {
737
756
 
738
757
  describe('State persistence after page reload', () => {
739
758
  it('should retain committed values after page reload simulation', async () => {
740
- // Initial state
759
+ // Initial state (using delta profile since sumBy only applies to delta)
741
760
  mockLocation.search =
742
- '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds{}&from=1000&to=2000';
761
+ '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}&from=1000&to=2000';
743
762
 
744
763
  const {result: result1, unmount} = renderHook(() => useQueryState(), {
745
764
  wrapper: createWrapper(),
746
765
  });
747
766
 
748
- // User makes changes to draft
767
+ // User makes changes to draft (using delta profile since sumBy only applies to delta)
749
768
  act(() => {
750
- result1.current.setDraftExpression('memory:inuse_space:bytes:space:bytes{}');
769
+ result1.current.setDraftExpression('memory:alloc_space:bytes:space:bytes:delta{}');
751
770
  result1.current.setDraftTimeRange(5000, 6000, 'relative:minute|15');
752
771
  result1.current.setDraftSumBy(['namespace', 'pod']);
753
772
  });
@@ -786,7 +805,7 @@ describe('useQueryState', () => {
786
805
 
787
806
  // Verify state is loaded from URL after "reload"
788
807
  expect(result2.current.querySelection.expression).toBe(
789
- 'memory:inuse_space:bytes:space:bytes{}'
808
+ 'memory:alloc_space:bytes:space:bytes:delta{}'
790
809
  );
791
810
  expect(result2.current.querySelection.from).toBe(5000);
792
811
  expect(result2.current.querySelection.to).toBe(6000);
@@ -795,7 +814,7 @@ describe('useQueryState', () => {
795
814
 
796
815
  // Draft should be synced with URL state on page load
797
816
  expect(result2.current.draftSelection.expression).toBe(
798
- 'memory:inuse_space:bytes:space:bytes{}'
817
+ 'memory:alloc_space:bytes:space:bytes:delta{}'
799
818
  );
800
819
  expect(result2.current.draftSelection.from).toBe(5000);
801
820
  expect(result2.current.draftSelection.to).toBe(6000);
@@ -19,7 +19,8 @@ import {Query} from '@parca/parser';
19
19
  import {QuerySelection} from '../ProfileSelector';
20
20
  import {ProfileSelection, ProfileSelectionFromParams, ProfileSource} from '../ProfileSource';
21
21
  import {useResetFlameGraphState} from '../ProfileView/hooks/useResetFlameGraphState';
22
- import {sumByToParam, useSumBy, useSumByFromParams} from '../useSumBy';
22
+ import {useResetStateOnProfileTypeChange} from '../ProfileView/hooks/useResetStateOnProfileTypeChange';
23
+ import {DEFAULT_EMPTY_SUM_BY, sumByToParam, useSumBy, useSumByFromParams} from '../useSumBy';
23
24
 
24
25
  interface UseQueryStateOptions {
25
26
  suffix?: '_a' | '_b'; // For comparison mode
@@ -79,6 +80,7 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
79
80
 
80
81
  const batchUpdates = useURLStateBatch();
81
82
  const resetFlameGraphState = useResetFlameGraphState();
83
+ const resetStateOnProfileTypeChange = useResetStateOnProfileTypeChange();
82
84
 
83
85
  // URL state hooks with appropriate suffixes
84
86
  const [expression, setExpressionState] = useURLState<string>(`expression${suffix}`, {
@@ -115,29 +117,6 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
115
117
  const [draftTimeSelection, setDraftTimeSelection] = useState<string>(
116
118
  timeSelection ?? defaultTimeSelection
117
119
  );
118
- const [draftSumBy, setDraftSumBy] = useState<string[] | undefined>(sumBy);
119
-
120
- // Sync draft state with URL state when URL changes externally
121
- useEffect(() => {
122
- setDraftExpression(expression ?? defaultExpression);
123
- }, [expression, defaultExpression]);
124
-
125
- useEffect(() => {
126
- setDraftFrom(from ?? defaultFrom?.toString() ?? '');
127
- }, [from, defaultFrom]);
128
-
129
- useEffect(() => {
130
- setDraftTo(to ?? defaultTo?.toString() ?? '');
131
- }, [to, defaultTo]);
132
-
133
- useEffect(() => {
134
- setDraftTimeSelection(timeSelection ?? defaultTimeSelection);
135
- }, [timeSelection, defaultTimeSelection]);
136
-
137
- useEffect(() => {
138
- setDraftSumBy(sumBy);
139
- }, [sumBy]);
140
-
141
120
  // Parse the draft query to extract profile information
142
121
  const draftQuery = useMemo(() => {
143
122
  try {
@@ -147,8 +126,16 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
147
126
  }
148
127
  }, [draftExpression]);
149
128
 
129
+ const query = useMemo(() => {
130
+ try {
131
+ return Query.parse(expression ?? '');
132
+ } catch {
133
+ return Query.parse('');
134
+ }
135
+ }, [expression]);
150
136
  const draftProfileType = useMemo(() => draftQuery.profileType(), [draftQuery]);
151
137
  const draftProfileName = useMemo(() => draftQuery.profileName(), [draftQuery]);
138
+ const profileType = useMemo(() => query.profileType(), [query]);
152
139
 
153
140
  // Compute draft time range for label fetching
154
141
  const draftTimeRange = useMemo(() => {
@@ -159,13 +146,50 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
159
146
  );
160
147
  }, [draftTimeSelection, draftFrom, draftTo, defaultTimeSelection, defaultFrom, defaultTo]);
161
148
  // Use combined sumBy hook for fetching labels and computing defaults (based on committed state)
162
- const {sumBy: computedSumByFromURL, isLoading: sumBySelectionLoading} = useSumBy(
149
+ const {
150
+ sumBy: computedSumByFromURL,
151
+ isLoading: sumBySelectionLoading,
152
+ draftSumBy,
153
+ setDraftSumBy,
154
+ isDraftSumByLoading,
155
+ } = useSumBy(
163
156
  queryClient,
157
+ profileType?.profileName !== '' ? profileType : draftProfileType,
158
+ draftTimeRange,
164
159
  draftProfileType,
165
160
  draftTimeRange,
166
161
  sumBy
167
162
  );
168
163
 
164
+ // Sync draft state with URL state when URL changes externally
165
+ useEffect(() => {
166
+ setDraftExpression(expression ?? defaultExpression);
167
+ }, [expression, defaultExpression]);
168
+
169
+ useEffect(() => {
170
+ setDraftFrom(from ?? defaultFrom?.toString() ?? '');
171
+ }, [from, defaultFrom]);
172
+
173
+ useEffect(() => {
174
+ setDraftTo(to ?? defaultTo?.toString() ?? '');
175
+ }, [to, defaultTo]);
176
+
177
+ useEffect(() => {
178
+ setDraftTimeSelection(timeSelection ?? defaultTimeSelection);
179
+ }, [timeSelection, defaultTimeSelection]);
180
+
181
+ useEffect(() => {
182
+ setDraftSumBy(sumBy);
183
+ }, [sumBy, setDraftSumBy]);
184
+
185
+ // Sync computed sumBy to URL if URL doesn't already have a value
186
+ // to ensure the shared URL can always pick it up.
187
+ useEffect(() => {
188
+ if (sumByParam === undefined && computedSumByFromURL !== undefined && !sumBySelectionLoading) {
189
+ setSumByParam(sumByToParam(computedSumByFromURL));
190
+ }
191
+ }, [sumByParam, computedSumByFromURL, sumBySelectionLoading, setSumByParam]);
192
+
169
193
  // Construct the QuerySelection object (committed state from URL)
170
194
  const querySelection: QuerySelection = useMemo(() => {
171
195
  const range = DateTimeRange.fromRangeKey(
@@ -285,12 +309,16 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
285
309
  setFromState(finalFrom);
286
310
  setToState(finalTo);
287
311
  setTimeSelectionState(finalTimeSelection);
288
- setSumByParam(sumByToParam(draftSumBy));
289
312
 
290
313
  // Auto-calculate merge parameters for delta profiles
291
314
  // Parse the final expression to check if it's a delta profile
292
315
  const finalQuery = Query.parse(finalExpression);
293
316
  const isDelta = finalQuery.profileType().delta;
317
+ if (isDelta) {
318
+ setSumByParam(sumByToParam(draftSumBy));
319
+ } else {
320
+ setSumByParam(DEFAULT_EMPTY_SUM_BY);
321
+ }
294
322
 
295
323
  if (isDelta && finalFrom !== '' && finalTo !== '') {
296
324
  const fromMs = parseInt(finalFrom);
@@ -313,6 +341,12 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
313
341
  setSelectionParam(undefined);
314
342
  }
315
343
  resetFlameGraphState();
344
+ if (
345
+ draftProfileType.toString() !==
346
+ Query.parse(querySelection.expression).profileType().toString()
347
+ ) {
348
+ resetStateOnProfileTypeChange();
349
+ }
316
350
  });
317
351
  },
318
352
  [
@@ -334,6 +368,9 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
334
368
  setMergeToState,
335
369
  setSelectionParam,
336
370
  resetFlameGraphState,
371
+ resetStateOnProfileTypeChange,
372
+ draftProfileType,
373
+ querySelection.expression,
337
374
  ]
338
375
  );
339
376
 
@@ -346,9 +383,12 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
346
383
  []
347
384
  );
348
385
 
349
- const setDraftSumByCallback = useCallback((newSumBy: string[] | undefined) => {
350
- setDraftSumBy(newSumBy);
351
- }, []);
386
+ const setDraftSumByCallback = useCallback(
387
+ (newSumBy: string[] | undefined) => {
388
+ setDraftSumBy(newSumBy);
389
+ },
390
+ [setDraftSumBy]
391
+ );
352
392
 
353
393
  const setDraftProfileName = useCallback(
354
394
  (newProfileName: string) => {
@@ -357,9 +397,10 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
357
397
  const [newQuery, changed] = draftQuery.setProfileName(newProfileName);
358
398
  if (changed) {
359
399
  setDraftExpression(newQuery.toString());
400
+ setDraftSumBy(undefined);
360
401
  }
361
402
  },
362
- [draftQuery]
403
+ [draftQuery, setDraftSumBy]
363
404
  );
364
405
 
365
406
  const setDraftMatchers = useCallback(
@@ -421,7 +462,7 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
421
462
  setProfileSelection,
422
463
 
423
464
  // Loading state
424
- sumByLoading: sumBySelectionLoading,
465
+ sumByLoading: isDraftSumByLoading || sumBySelectionLoading,
425
466
 
426
467
  draftParsedQuery,
427
468
  parsedQuery,