Skip to content

Commit 5c6a55c

Browse files
ui: Query selector state refactoring and batchUpdates support in URLState (#6033)
* Query selector state refactoring and batchUpdates support in URLState * Linter and type fixes * [pre-commit.ci lite] apply automatic fixes * Compare mode and metrics graph selection bug fix * [pre-commit.ci lite] apply automatic fixes * test failures fixed * [pre-commit.ci lite] apply automatic fixes * Linter fix * Removed the todo comment as this defaultValues is needed in the hook * [pre-commit.ci lite] apply automatic fixes * Fix the 'focus only on this series' not working * SumBy not blanking if it already has a value * SumBy state moved into QueryState hook * Skip waiting for the label names query for the metrics graph when the sum by is already set in the url * Fixed the condition * [pre-commit.ci lite] apply automatic fixes * test fix * Linter fix * Test fixes * [pre-commit.ci lite] apply automatic fixes * URL compare bug fix * Sumby value copied over to compare mode * [pre-commit.ci lite] apply automatic fixes --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
1 parent 1e7632a commit 5c6a55c

File tree

28 files changed

+3069
-717
lines changed

28 files changed

+3069
-717
lines changed

ui/packages/app/web/src/components/ui/Navbar.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ const Navbar = () => {
5858
const queryParams = new URLSearchParams(location.search);
5959
const expressionA = queryParams.get('expression_a');
6060
const expressionB = queryParams.get('expression_b');
61-
const comparing = queryParams.get('comparing');
61+
const compareA = queryParams.get('compare_a');
62+
const compareB = queryParams.get('compare_b');
6263

6364
const queryParamsURL = parseParams(window.location.search);
6465

@@ -72,6 +73,7 @@ const Navbar = () => {
7273
dashboard_items,
7374
selection_a,
7475
expression_a,
76+
sum_by_a,
7577
} = queryParamsURL;
7678

7779
const isComparePage = expressionA !== null && expressionB !== null;
@@ -87,9 +89,13 @@ const Navbar = () => {
8789
const compareExplanation =
8890
'Compare two profiles and see the relative difference between them more clearly.';
8991

90-
const isCurrentPage = (item: {label: string; href: string; external: boolean}) =>
91-
(item.href === 'compare' && (isComparePage || comparing === 'true')) ||
92-
(!isComparePage && comparing !== 'true' && location.pathname === item.href);
92+
const isCurrentPage = (item: {label: string; href: string; external: boolean}) => {
93+
const isCompareMode = compareA === 'true' || compareB === 'true';
94+
return (
95+
(item.href === 'compare' && (isComparePage || isCompareMode)) ||
96+
(!isComparePage && !isCompareMode && location.pathname === item.href)
97+
);
98+
};
9399

94100
const navigateTo = useCallback(
95101
(path: string, queryParams: any, options?: {replace?: boolean}) => {
@@ -107,18 +113,25 @@ const Navbar = () => {
107113
const queryToBePassed =
108114
expression_a === undefined
109115
? {
110-
comparing: 'true',
116+
compare_a: 'true',
117+
compare_b: 'true',
111118
}
112119
: {
113-
comparing: 'true',
120+
compare_a: 'true',
121+
compare_b: 'true',
114122
dashboard_items: dashboard_items,
115123
expression_a: expression_a,
124+
expression_b: expression_a,
116125
from_a: from_a,
126+
from_b: from_a,
117127
to_a: to_a,
128+
to_b: to_a,
118129
time_selection_a: time_selection_a,
130+
time_selection_b: time_selection_a,
119131
selection_a: selection_a,
120132
merge_from_a: merge_from_a,
121133
merge_to_a: merge_to_a,
134+
...(sum_by_a !== undefined && sum_by_a !== '' ? {sum_by_a, sum_by_b: sum_by_a} : {}),
122135
};
123136

124137
return (

ui/packages/app/web/src/pages/index.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@
1414
import {useCallback} from 'react';
1515

1616
import {GrpcWebFetchTransport} from '@protobuf-ts/grpcweb-transport';
17-
import {useLocation, useNavigate} from 'react-router-dom';
17+
import {useNavigate} from 'react-router-dom';
1818

1919
import {QueryServiceClient} from '@parca/client';
2020
import {ParcaContextProvider, Spinner, URLStateProvider} from '@parca/components';
2121
import {DEFAULT_PROFILE_EXPLORER_PARAM_VALUES, ProfileExplorer} from '@parca/profile';
2222
import {selectDarkMode, useAppSelector} from '@parca/store';
23-
import {convertToQueryParams, parseParams} from '@parca/utilities';
23+
import {convertToQueryParams} from '@parca/utilities';
2424

2525
const apiEndpoint = import.meta.env.VITE_API_ENDPOINT;
2626

@@ -31,7 +31,6 @@ const queryClient = new QueryServiceClient(
3131
);
3232

3333
const Profiles = () => {
34-
const location = useLocation();
3534
const navigate = useNavigate();
3635
const isDarkMode = useAppSelector(selectDarkMode);
3736

@@ -47,10 +46,11 @@ const Profiles = () => {
4746
[navigate]
4847
);
4948

50-
const queryParams = parseParams(location.search);
51-
5249
return (
53-
<URLStateProvider navigateTo={navigateTo} defaultValues={DEFAULT_PROFILE_EXPLORER_PARAM_VALUES}>
50+
<URLStateProvider
51+
navigateTo={navigateTo}
52+
paramPreferences={DEFAULT_PROFILE_EXPLORER_PARAM_VALUES}
53+
>
5454
<ParcaContextProvider
5555
value={{
5656
Spinner,
@@ -61,11 +61,7 @@ const Profiles = () => {
6161
}}
6262
>
6363
<div className="bg-white dark:bg-gray-900 p-3">
64-
<ProfileExplorer
65-
queryClient={queryClient}
66-
queryParams={queryParams}
67-
navigateTo={navigateTo}
68-
/>
64+
<ProfileExplorer queryClient={queryClient} navigateTo={navigateTo} />
6965
</div>
7066
</ParcaContextProvider>
7167
</URLStateProvider>
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# URLState Hook Usage Guide
2+
3+
The `useURLState` hook provides a simple way to sync component state with URL query parameters. It now includes built-in batching support for efficient URL updates.
4+
5+
## Basic Usage
6+
7+
```tsx
8+
import {useURLState} from '@parca/components';
9+
10+
function MyComponent() {
11+
const [colorBy, setColorBy] = useURLState('color_by', {
12+
defaultValue: 'function',
13+
});
14+
15+
const [groupBy, setGroupBy] = useURLState<string[]>('group_by', {
16+
defaultValue: ['function_name'],
17+
alwaysReturnArray: true,
18+
});
19+
20+
// Use the state values and setters as normal
21+
return (
22+
<div>
23+
<button onClick={() => setColorBy('filename')}>Change Color</button>
24+
</div>
25+
);
26+
}
27+
```
28+
29+
## Batching Multiple Updates
30+
31+
When you need to update multiple URL parameters simultaneously, use `useURLStateBatch` to ensure a single URL update:
32+
33+
```tsx
34+
import {useURLState, useURLStateBatch} from '@parca/components';
35+
36+
function ProfileFilters() {
37+
const [colorBy, setColorBy] = useURLState('color_by');
38+
const [groupBy, setGroupBy] = useURLState<string[]>('group_by', {
39+
alwaysReturnArray: true,
40+
});
41+
const [view, setView] = useURLState('view');
42+
43+
// Get the batch function
44+
const batchUpdates = useURLStateBatch();
45+
46+
const handleComplexFilterChange = () => {
47+
// Batch multiple URL updates into a single navigation
48+
batchUpdates(() => {
49+
setColorBy('filename');
50+
setGroupBy(['function_name', 'filename']);
51+
setView('table');
52+
});
53+
// Results in ONE URL update instead of three!
54+
};
55+
56+
return <button onClick={handleComplexFilterChange}>Apply All Filters</button>;
57+
}
58+
```
59+
60+
## Key Features
61+
62+
### Automatic URL Synchronization
63+
64+
- All URL updates are now handled centrally by the `URLStateProvider`
65+
- Individual hooks only manage state; URL sync happens automatically
66+
- Built-in debouncing prevents excessive URL updates
67+
68+
### Batching Support
69+
70+
- Use `batchUpdates` to group multiple parameter changes
71+
- Prevents multiple browser history entries
72+
- Improves performance for complex state updates
73+
- Essential for maintaining URL coherence when multiple related parameters change
74+
75+
## Migration from Direct Navigation
76+
77+
Previously, the ProfileSelector component managed URL updates directly:
78+
79+
With the new approach, you can use individual `useURLState` hooks with batching:
80+
81+
```tsx
82+
// New approach - automatic URL sync with batching
83+
const [expression, setExpression] = useURLState('expression_a');
84+
const [from, setFrom] = useURLState('from_a');
85+
const [to, setTo] = useURLState('to_a');
86+
const batchUpdates = useURLStateBatch();
87+
88+
const selectQuery = (q: QuerySelection): void => {
89+
batchUpdates(() => {
90+
setExpression(q.expression);
91+
setFrom(q.from.toString());
92+
setTo(q.to.toString());
93+
// All updates result in a single URL change
94+
});
95+
};
96+
```
97+
98+
## Benefits
99+
100+
1. **Simpler Code**: No need to manually construct URL parameter objects
101+
2. **Better Performance**: Batching prevents multiple rapid URL updates
102+
3. **Cleaner History**: One history entry instead of multiple for related changes
103+
4. **Type Safety**: Each parameter is individually typed
104+
5. **Easier Testing**: URL synchronization logic is centralized and testable

0 commit comments

Comments
 (0)