@parca/profile 0.19.109 → 0.19.111
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.
- package/CHANGELOG.md +8 -0
- package/dist/ProfileSelector/index.d.ts.map +1 -1
- package/dist/ProfileSelector/index.js +9 -1
- package/dist/ProfileSelector/useAutoQuerySelector.js +1 -1
- package/dist/ProfileView/components/ProfileFilters/useProfileFilters.d.ts +2 -0
- package/dist/ProfileView/components/ProfileFilters/useProfileFilters.d.ts.map +1 -1
- package/dist/ProfileView/components/ProfileFilters/useProfileFilters.js +3 -1
- package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.d.ts +1 -0
- package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.d.ts.map +1 -1
- package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.js +17 -3
- package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.d.ts +2 -0
- package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.d.ts.map +1 -0
- package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.js +541 -0
- package/dist/QueryControls/index.d.ts.map +1 -1
- package/dist/QueryControls/index.js +1 -3
- package/dist/SourceView/Highlighter.d.ts +5 -2
- package/dist/SourceView/Highlighter.d.ts.map +1 -1
- package/dist/SourceView/Highlighter.js +3 -2
- package/dist/SourceView/index.d.ts.map +1 -1
- package/dist/SourceView/index.js +40 -11
- package/dist/hooks/useLabels.d.ts.map +1 -1
- package/dist/hooks/useLabels.js +0 -7
- package/dist/hooks/useQueryState.d.ts +4 -0
- package/dist/hooks/useQueryState.d.ts.map +1 -1
- package/dist/hooks/useQueryState.js +29 -5
- package/dist/hooks/useQueryState.test.js +72 -8
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/package.json +3 -3
- package/src/ProfileSelector/index.tsx +14 -1
- package/src/ProfileSelector/useAutoQuerySelector.ts +1 -1
- package/src/ProfileView/components/ProfileFilters/useProfileFilters.ts +5 -1
- package/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.tsx +663 -0
- package/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.ts +24 -3
- package/src/QueryControls/index.tsx +1 -3
- package/src/SourceView/Highlighter.tsx +6 -5
- package/src/SourceView/index.tsx +45 -12
- package/src/hooks/useLabels.ts +0 -10
- package/src/hooks/useQueryState.test.tsx +90 -11
- package/src/hooks/useQueryState.ts +36 -4
- package/src/index.tsx +4 -0
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
// See the License for the specific language governing permissions and
|
|
12
12
|
// limitations under the License.
|
|
13
13
|
|
|
14
|
-
import {useMemo} from 'react';
|
|
14
|
+
import {useCallback, useMemo} from 'react';
|
|
15
15
|
|
|
16
|
-
import {useURLStateCustom, type ParamValueSetterCustom} from '@parca/components';
|
|
16
|
+
import {useURLStateBatch, useURLStateCustom, type ParamValueSetterCustom} from '@parca/components';
|
|
17
17
|
import {safeDecode} from '@parca/utilities';
|
|
18
18
|
|
|
19
19
|
import {isPresetKey} from './filterPresets';
|
|
@@ -140,10 +140,13 @@ export const decodeProfileFilters = (encoded: string): ProfileFilter[] => {
|
|
|
140
140
|
export const useProfileFiltersUrlState = (): {
|
|
141
141
|
appliedFilters: ProfileFilter[];
|
|
142
142
|
setAppliedFilters: ParamValueSetterCustom<ProfileFilter[]>;
|
|
143
|
+
forceApplyFilters: (filters: ProfileFilter[]) => void;
|
|
143
144
|
} => {
|
|
145
|
+
const batchUpdates = useURLStateBatch();
|
|
146
|
+
|
|
144
147
|
// Store applied filters in URL state for persistence using compact encoding
|
|
145
148
|
const [appliedFilters, setAppliedFilters] = useURLStateCustom<ProfileFilter[]>(
|
|
146
|
-
|
|
149
|
+
`profile_filters`,
|
|
147
150
|
{
|
|
148
151
|
parse: value => {
|
|
149
152
|
return decodeProfileFilters(value as string);
|
|
@@ -159,8 +162,26 @@ export const useProfileFiltersUrlState = (): {
|
|
|
159
162
|
return appliedFilters ?? [];
|
|
160
163
|
}, [appliedFilters]);
|
|
161
164
|
|
|
165
|
+
// Force apply filters (bypasses preserve-existing strategy)
|
|
166
|
+
const forceApplyFilters = useCallback(
|
|
167
|
+
(filters: ProfileFilter[]) => {
|
|
168
|
+
const validFilters = filters.filter(f => {
|
|
169
|
+
if (f.type != null && isPresetKey(f.type)) {
|
|
170
|
+
return f.value !== '' && f.type != null;
|
|
171
|
+
}
|
|
172
|
+
return f.value !== '' && f.type != null && f.field != null && f.matchType != null;
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
batchUpdates(() => {
|
|
176
|
+
setAppliedFilters(validFilters);
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
[batchUpdates, setAppliedFilters]
|
|
180
|
+
);
|
|
181
|
+
|
|
162
182
|
return {
|
|
163
183
|
appliedFilters: memoizedAppliedFilters,
|
|
164
184
|
setAppliedFilters,
|
|
185
|
+
forceApplyFilters,
|
|
165
186
|
};
|
|
166
187
|
};
|
|
@@ -177,9 +177,7 @@ export function QueryControls({
|
|
|
177
177
|
{viewComponent?.createViewComponent}
|
|
178
178
|
</div>
|
|
179
179
|
|
|
180
|
-
{viewComponent?.
|
|
181
|
-
viewComponent?.labelnames !== undefined &&
|
|
182
|
-
viewComponent?.labelnames.length >= 1 ? (
|
|
180
|
+
{viewComponent?.labelnames !== undefined && viewComponent?.labelnames.length >= 1 ? (
|
|
183
181
|
<ViewMatchers labelNames={viewComponent.labelnames} />
|
|
184
182
|
) : showAdvancedMode && advancedModeForQueryBrowser ? (
|
|
185
183
|
<MatchersInput
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
|
|
14
14
|
import {MouseEventHandler, useId, useMemo} from 'react';
|
|
15
15
|
|
|
16
|
-
import {Vector} from 'apache-arrow';
|
|
17
16
|
import cx from 'classnames';
|
|
18
17
|
import {scaleLinear} from 'd3-scale';
|
|
19
18
|
import {type createElementProps} from 'react-syntax-highlighter';
|
|
@@ -114,9 +113,10 @@ const charsToWidth = (chars: number): string => {
|
|
|
114
113
|
return charsToWidthMap[chars];
|
|
115
114
|
};
|
|
116
115
|
|
|
116
|
+
export type LineDataLookup = (lineNumber: number) => {cumulative: bigint; flat: bigint} | undefined;
|
|
117
|
+
|
|
117
118
|
export const profileAwareRenderer = (
|
|
118
|
-
|
|
119
|
-
flat: Vector | null,
|
|
119
|
+
getLineData: LineDataLookup,
|
|
120
120
|
total: bigint,
|
|
121
121
|
filtered: bigint,
|
|
122
122
|
onContextMenu: MouseEventHandler<HTMLDivElement>
|
|
@@ -135,6 +135,7 @@ export const profileAwareRenderer = (
|
|
|
135
135
|
const lineNumber: number = node.children[0].children[0].value as number;
|
|
136
136
|
const isCurrentLine = lineNumber >= startLine && lineNumber <= endLine;
|
|
137
137
|
node.children = node.children.slice(1);
|
|
138
|
+
const data = getLineData(lineNumber);
|
|
138
139
|
return (
|
|
139
140
|
<div className="flex gap-1" key={`${i}`}>
|
|
140
141
|
<div
|
|
@@ -161,11 +162,11 @@ export const profileAwareRenderer = (
|
|
|
161
162
|
/>
|
|
162
163
|
</div>
|
|
163
164
|
<LineProfileMetadata
|
|
164
|
-
value={cumulative
|
|
165
|
+
value={data?.cumulative ?? 0n}
|
|
165
166
|
total={total}
|
|
166
167
|
filtered={filtered}
|
|
167
168
|
/>
|
|
168
|
-
<LineProfileMetadata value={flat
|
|
169
|
+
<LineProfileMetadata value={data?.flat ?? 0n} total={total} filtered={filtered} />
|
|
169
170
|
<div
|
|
170
171
|
className={cx(
|
|
171
172
|
'w-full flex-grow-0 border-l border-gray-200 pl-1 dark:border-gray-700',
|
package/src/SourceView/index.tsx
CHANGED
|
@@ -22,7 +22,7 @@ import {SourceSkeleton, useParcaContext, useURLState, type ProfileData} from '@p
|
|
|
22
22
|
|
|
23
23
|
import {ExpandOnHover} from '../GraphTooltipArrow/ExpandOnHoverValue';
|
|
24
24
|
import {truncateStringReverse} from '../utils';
|
|
25
|
-
import {Highlighter, profileAwareRenderer} from './Highlighter';
|
|
25
|
+
import {Highlighter, profileAwareRenderer, type LineDataLookup} from './Highlighter';
|
|
26
26
|
import useLineRange from './useSelectedLineRange';
|
|
27
27
|
|
|
28
28
|
interface SourceViewProps {
|
|
@@ -59,31 +59,64 @@ export const SourceView = React.memo(function SourceView({
|
|
|
59
59
|
|
|
60
60
|
const {startLine, endLine} = useLineRange();
|
|
61
61
|
|
|
62
|
-
const
|
|
62
|
+
const sourceTable = useMemo(() => {
|
|
63
63
|
if (data === undefined) {
|
|
64
|
-
return
|
|
64
|
+
return null;
|
|
65
65
|
}
|
|
66
66
|
const table = tableFromIPC(data.record);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
return {
|
|
68
|
+
numRows: table.numRows,
|
|
69
|
+
lineNumbers: table.getChild('line_number'),
|
|
70
|
+
cumulative: table.getChild('cumulative'),
|
|
71
|
+
flat: table.getChild('flat'),
|
|
72
|
+
};
|
|
70
73
|
}, [data]);
|
|
71
74
|
|
|
75
|
+
const getLineData: LineDataLookup = useCallback(
|
|
76
|
+
(lineNumber: number) => {
|
|
77
|
+
if (sourceTable === null || sourceTable.lineNumbers === null) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
const {numRows, lineNumbers, cumulative, flat} = sourceTable;
|
|
81
|
+
|
|
82
|
+
let lo = 0;
|
|
83
|
+
let hi = numRows - 1;
|
|
84
|
+
while (lo <= hi) {
|
|
85
|
+
const mid = (lo + hi) >>> 1;
|
|
86
|
+
const midVal = Number(lineNumbers.get(mid));
|
|
87
|
+
if (midVal === lineNumber) {
|
|
88
|
+
return {
|
|
89
|
+
cumulative: (cumulative?.get(mid) as bigint) ?? 0n,
|
|
90
|
+
flat: (flat?.get(mid) as bigint) ?? 0n,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (midVal < lineNumber) {
|
|
94
|
+
lo = mid + 1;
|
|
95
|
+
} else {
|
|
96
|
+
hi = mid - 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
},
|
|
101
|
+
[sourceTable]
|
|
102
|
+
);
|
|
103
|
+
|
|
72
104
|
const getProfileDataForLine = useCallback(
|
|
73
105
|
(line: number, newLine: number): ProfileData | undefined => {
|
|
74
|
-
|
|
106
|
+
const data = getLineData(line);
|
|
107
|
+
if (data === undefined) {
|
|
75
108
|
return undefined;
|
|
76
109
|
}
|
|
77
|
-
if (cumulative
|
|
110
|
+
if (data.cumulative === 0n && data.flat === 0n) {
|
|
78
111
|
return undefined;
|
|
79
112
|
}
|
|
80
113
|
return {
|
|
81
114
|
line: newLine,
|
|
82
|
-
cumulative: Number(cumulative
|
|
83
|
-
flat: Number(flat
|
|
115
|
+
cumulative: Number(data.cumulative),
|
|
116
|
+
flat: Number(data.flat),
|
|
84
117
|
};
|
|
85
118
|
},
|
|
86
|
-
[
|
|
119
|
+
[getLineData]
|
|
87
120
|
);
|
|
88
121
|
|
|
89
122
|
const [selectedCode, profileData] = useMemo(() => {
|
|
@@ -155,7 +188,7 @@ export const SourceView = React.memo(function SourceView({
|
|
|
155
188
|
<Highlighter
|
|
156
189
|
file={sourceFileName as string}
|
|
157
190
|
content={data.source}
|
|
158
|
-
renderer={profileAwareRenderer(
|
|
191
|
+
renderer={profileAwareRenderer(getLineData, total, filtered, onContextMenu)}
|
|
159
192
|
/>
|
|
160
193
|
{sourceViewContextMenuItems.length > 0 ? (
|
|
161
194
|
<Menu id={MENU_ID}>
|
package/src/hooks/useLabels.ts
CHANGED
|
@@ -11,8 +11,6 @@
|
|
|
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
|
-
|
|
16
14
|
import {LabelsRequest, LabelsResponse, QueryServiceClient, ValuesRequest} from '@parca/client';
|
|
17
15
|
import {useGrpcMetadata} from '@parca/components';
|
|
18
16
|
import {millisToProtoTimestamp, sanitizeLabelValue} from '@parca/utilities';
|
|
@@ -70,10 +68,6 @@ export const useLabelNames = (
|
|
|
70
68
|
},
|
|
71
69
|
});
|
|
72
70
|
|
|
73
|
-
useEffect(() => {
|
|
74
|
-
console.log('Label names query result:', {data, error, isLoading});
|
|
75
|
-
}, [data, error, isLoading]);
|
|
76
|
-
|
|
77
71
|
return {
|
|
78
72
|
result: {response: data, error: error as Error},
|
|
79
73
|
loading: isLoading,
|
|
@@ -113,10 +107,6 @@ export const useLabelValues = (
|
|
|
113
107
|
},
|
|
114
108
|
});
|
|
115
109
|
|
|
116
|
-
useEffect(() => {
|
|
117
|
-
console.log('Label values query result:', {data, error, isLoading, labelName});
|
|
118
|
-
}, [data, error, isLoading, labelName]);
|
|
119
|
-
|
|
120
110
|
return {
|
|
121
111
|
result: {response: data ?? [], error: error as Error},
|
|
122
112
|
loading: isLoading,
|
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
// See the License for the specific language governing permissions and
|
|
12
12
|
// limitations under the License.
|
|
13
13
|
|
|
14
|
-
import {ReactNode} from 'react';
|
|
14
|
+
import {ReactNode, act} from 'react';
|
|
15
15
|
|
|
16
16
|
// eslint-disable-next-line import/named
|
|
17
|
-
import {
|
|
17
|
+
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
|
|
18
|
+
// eslint-disable-next-line import/named
|
|
19
|
+
import {renderHook, waitFor} from '@testing-library/react';
|
|
18
20
|
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
|
19
21
|
|
|
20
22
|
import {URLStateProvider} from '@parca/components';
|
|
@@ -99,14 +101,60 @@ vi.mock('../useSumBy', async () => {
|
|
|
99
101
|
};
|
|
100
102
|
});
|
|
101
103
|
|
|
104
|
+
// Track profile types loading state for tests
|
|
105
|
+
let mockProfileTypesLoading = false;
|
|
106
|
+
let mockProfileTypesData:
|
|
107
|
+
| {
|
|
108
|
+
types: Array<{
|
|
109
|
+
name: string;
|
|
110
|
+
sampleType: string;
|
|
111
|
+
sampleUnit: string;
|
|
112
|
+
periodType: string;
|
|
113
|
+
periodUnit: string;
|
|
114
|
+
delta: boolean;
|
|
115
|
+
}>;
|
|
116
|
+
}
|
|
117
|
+
| undefined;
|
|
118
|
+
|
|
119
|
+
// Mock useProfileTypes to control loading state in tests
|
|
120
|
+
vi.mock('../ProfileSelector', async () => {
|
|
121
|
+
const actual = await vi.importActual('../ProfileSelector');
|
|
122
|
+
return {
|
|
123
|
+
...actual,
|
|
124
|
+
useProfileTypes: () => ({
|
|
125
|
+
loading: mockProfileTypesLoading,
|
|
126
|
+
data: mockProfileTypesData,
|
|
127
|
+
error: null,
|
|
128
|
+
}),
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Helper to set profile types loading state for tests
|
|
133
|
+
const setProfileTypesLoading = (loading: boolean): void => {
|
|
134
|
+
mockProfileTypesLoading = loading;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const setProfileTypesData = (data: typeof mockProfileTypesData): void => {
|
|
138
|
+
mockProfileTypesData = data;
|
|
139
|
+
};
|
|
140
|
+
|
|
102
141
|
// Helper to create wrapper with URLStateProvider
|
|
103
142
|
const createWrapper = (
|
|
104
143
|
paramPreferences = {}
|
|
105
144
|
): (({children}: {children: ReactNode}) => JSX.Element) => {
|
|
145
|
+
const queryClient = new QueryClient({
|
|
146
|
+
defaultOptions: {
|
|
147
|
+
queries: {
|
|
148
|
+
retry: false,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
});
|
|
106
152
|
const Wrapper = ({children}: {children: ReactNode}): JSX.Element => (
|
|
107
|
-
<
|
|
108
|
-
{
|
|
109
|
-
|
|
153
|
+
<QueryClientProvider client={queryClient}>
|
|
154
|
+
<URLStateProvider navigateTo={mockNavigateTo} paramPreferences={paramPreferences}>
|
|
155
|
+
{children}
|
|
156
|
+
</URLStateProvider>
|
|
157
|
+
</QueryClientProvider>
|
|
110
158
|
);
|
|
111
159
|
Wrapper.displayName = 'URLStateProviderWrapper';
|
|
112
160
|
return Wrapper;
|
|
@@ -120,6 +168,9 @@ describe('useQueryState', () => {
|
|
|
120
168
|
writable: true,
|
|
121
169
|
});
|
|
122
170
|
mockLocation.search = '';
|
|
171
|
+
// Reset profile types mock state
|
|
172
|
+
setProfileTypesLoading(false);
|
|
173
|
+
setProfileTypesData(undefined);
|
|
123
174
|
});
|
|
124
175
|
|
|
125
176
|
describe('Basic functionality', () => {
|
|
@@ -127,7 +178,7 @@ describe('useQueryState', () => {
|
|
|
127
178
|
const {result} = renderHook(
|
|
128
179
|
() =>
|
|
129
180
|
useQueryState({
|
|
130
|
-
defaultExpression: 'process_cpu{}',
|
|
181
|
+
defaultExpression: 'process_cpu:cpu:nanoseconds:cpu:nanoseconds{}',
|
|
131
182
|
defaultTimeSelection: 'relative:hour|1',
|
|
132
183
|
defaultFrom: 1000,
|
|
133
184
|
defaultTo: 2000,
|
|
@@ -136,7 +187,7 @@ describe('useQueryState', () => {
|
|
|
136
187
|
);
|
|
137
188
|
|
|
138
189
|
const {querySelection} = result.current;
|
|
139
|
-
expect(querySelection.expression).toBe('process_cpu{}');
|
|
190
|
+
expect(querySelection.expression).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{}');
|
|
140
191
|
expect(querySelection.timeSelection).toBe('relative:hour|1');
|
|
141
192
|
// From/to should be calculated from the range
|
|
142
193
|
expect(querySelection.from).toBeDefined();
|
|
@@ -524,7 +575,9 @@ describe('useQueryState', () => {
|
|
|
524
575
|
});
|
|
525
576
|
|
|
526
577
|
describe('Edge cases', () => {
|
|
527
|
-
it('should handle invalid expression gracefully', () => {
|
|
578
|
+
it('should handle invalid expression gracefully and log warning', () => {
|
|
579
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
580
|
+
|
|
528
581
|
const {result} = renderHook(
|
|
529
582
|
() =>
|
|
530
583
|
useQueryState({
|
|
@@ -533,8 +586,33 @@ describe('useQueryState', () => {
|
|
|
533
586
|
{wrapper: createWrapper()}
|
|
534
587
|
);
|
|
535
588
|
|
|
536
|
-
// Should not throw error
|
|
589
|
+
// Should not throw error - invalid expressions are caught and logged
|
|
590
|
+
expect(() => result.current.querySelection).not.toThrow();
|
|
591
|
+
// Should fall back to empty expression
|
|
592
|
+
expect(result.current.querySelection.expression).toBe('invalid{{}expression');
|
|
593
|
+
// Should log a warning about the parse failure
|
|
594
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
595
|
+
'Failed to parse expression',
|
|
596
|
+
expect.objectContaining({
|
|
597
|
+
expression: 'invalid{{}expression',
|
|
598
|
+
})
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
consoleSpy.mockRestore();
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('should handle empty expression gracefully', () => {
|
|
605
|
+
const {result} = renderHook(
|
|
606
|
+
() =>
|
|
607
|
+
useQueryState({
|
|
608
|
+
defaultExpression: '',
|
|
609
|
+
}),
|
|
610
|
+
{wrapper: createWrapper()}
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
// Should not throw error with empty expression
|
|
537
614
|
expect(() => result.current.querySelection).not.toThrow();
|
|
615
|
+
expect(result.current.querySelection.expression).toBe('');
|
|
538
616
|
});
|
|
539
617
|
|
|
540
618
|
it('should clear merge params for non-delta profiles', async () => {
|
|
@@ -1191,7 +1269,8 @@ describe('useQueryState', () => {
|
|
|
1191
1269
|
});
|
|
1192
1270
|
|
|
1193
1271
|
it('should preserve other URL params when setting ProfileSelection', async () => {
|
|
1194
|
-
mockLocation.search =
|
|
1272
|
+
mockLocation.search =
|
|
1273
|
+
'?expression_a=process_cpu:cpu:nanoseconds:cpu:nanoseconds{}&other_param=value&unrelated=test';
|
|
1195
1274
|
|
|
1196
1275
|
const {result} = renderHook(() => useQueryState({suffix: '_a'}), {wrapper: createWrapper()});
|
|
1197
1276
|
|
|
@@ -1212,7 +1291,7 @@ describe('useQueryState', () => {
|
|
|
1212
1291
|
expect(params.selection_a).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{pod="test"}');
|
|
1213
1292
|
|
|
1214
1293
|
// Other params should be preserved
|
|
1215
|
-
expect(params.expression_a).toBe('process_cpu{}');
|
|
1294
|
+
expect(params.expression_a).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{}');
|
|
1216
1295
|
expect(params.other_param).toBe('value');
|
|
1217
1296
|
expect(params.unrelated).toBe('test');
|
|
1218
1297
|
});
|
|
@@ -29,6 +29,7 @@ interface UseQueryStateOptions {
|
|
|
29
29
|
defaultFrom?: number;
|
|
30
30
|
defaultTo?: number;
|
|
31
31
|
comparing?: boolean; // If true, don't auto-select for delta profiles
|
|
32
|
+
onProfileTypeChange?: () => void; // Called when profile type changes on commit, after reset
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
interface UseQueryStateReturn {
|
|
@@ -65,6 +66,10 @@ interface UseQueryStateReturn {
|
|
|
65
66
|
|
|
66
67
|
// parsed query
|
|
67
68
|
parsedQuery: Query | null;
|
|
69
|
+
|
|
70
|
+
setExpressionParam: (value: string | undefined) => void;
|
|
71
|
+
setSumByParam: (value: string | undefined) => void;
|
|
72
|
+
setGroupByParam: (value: string[] | undefined) => void;
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryStateReturn => {
|
|
@@ -76,6 +81,7 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
|
|
|
76
81
|
defaultFrom,
|
|
77
82
|
defaultTo,
|
|
78
83
|
comparing = false,
|
|
84
|
+
onProfileTypeChange,
|
|
79
85
|
} = options;
|
|
80
86
|
|
|
81
87
|
const batchUpdates = useURLStateBatch();
|
|
@@ -101,6 +107,10 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
|
|
|
101
107
|
|
|
102
108
|
const [sumByParam, setSumByParam] = useURLState<string>(`sum_by${suffix}`);
|
|
103
109
|
|
|
110
|
+
const [, setGroupByParam] = useURLState<string>('group_by', {
|
|
111
|
+
alwaysReturnArray: true,
|
|
112
|
+
});
|
|
113
|
+
|
|
104
114
|
const [mergeFrom, setMergeFromState] = useURLState<string>(`merge_from${suffix}`);
|
|
105
115
|
const [mergeTo, setMergeToState] = useURLState<string>(`merge_to${suffix}`);
|
|
106
116
|
|
|
@@ -121,7 +131,11 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
|
|
|
121
131
|
const draftQuery = useMemo(() => {
|
|
122
132
|
try {
|
|
123
133
|
return Query.parse(draftExpression ?? '');
|
|
124
|
-
} catch {
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.warn('Failed to parse draft expression', {
|
|
136
|
+
expression: draftExpression,
|
|
137
|
+
error: error instanceof Error ? error.message : String(error),
|
|
138
|
+
});
|
|
125
139
|
return Query.parse('');
|
|
126
140
|
}
|
|
127
141
|
}, [draftExpression]);
|
|
@@ -129,7 +143,11 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
|
|
|
129
143
|
const query = useMemo(() => {
|
|
130
144
|
try {
|
|
131
145
|
return Query.parse(expression ?? '');
|
|
132
|
-
} catch {
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.warn('Failed to parse expression', {
|
|
148
|
+
expression,
|
|
149
|
+
error: error instanceof Error ? error.message : String(error),
|
|
150
|
+
});
|
|
133
151
|
return Query.parse('');
|
|
134
152
|
}
|
|
135
153
|
}, [expression]);
|
|
@@ -346,6 +364,7 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
|
|
|
346
364
|
Query.parse(querySelection.expression).profileType().toString()
|
|
347
365
|
) {
|
|
348
366
|
resetStateOnProfileTypeChange();
|
|
367
|
+
onProfileTypeChange?.();
|
|
349
368
|
}
|
|
350
369
|
});
|
|
351
370
|
},
|
|
@@ -369,6 +388,7 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
|
|
|
369
388
|
setSelectionParam,
|
|
370
389
|
resetFlameGraphState,
|
|
371
390
|
resetStateOnProfileTypeChange,
|
|
391
|
+
onProfileTypeChange,
|
|
372
392
|
draftProfileType,
|
|
373
393
|
querySelection.expression,
|
|
374
394
|
]
|
|
@@ -426,7 +446,11 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
|
|
|
426
446
|
const draftParsedQuery = useMemo(() => {
|
|
427
447
|
try {
|
|
428
448
|
return Query.parse(draftSelection.expression ?? '');
|
|
429
|
-
} catch {
|
|
449
|
+
} catch (error) {
|
|
450
|
+
console.warn('Failed to parse draft selection expression', {
|
|
451
|
+
expression: draftSelection.expression,
|
|
452
|
+
error: error instanceof Error ? error.message : String(error),
|
|
453
|
+
});
|
|
430
454
|
return Query.parse('');
|
|
431
455
|
}
|
|
432
456
|
}, [draftSelection.expression]);
|
|
@@ -434,7 +458,11 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
|
|
|
434
458
|
const parsedQuery = useMemo(() => {
|
|
435
459
|
try {
|
|
436
460
|
return Query.parse(querySelection.expression ?? '');
|
|
437
|
-
} catch {
|
|
461
|
+
} catch (error) {
|
|
462
|
+
console.warn('Failed to parse query selection expression', {
|
|
463
|
+
expression: querySelection.expression,
|
|
464
|
+
error: error instanceof Error ? error.message : String(error),
|
|
465
|
+
});
|
|
438
466
|
return Query.parse('');
|
|
439
467
|
}
|
|
440
468
|
}, [querySelection.expression]);
|
|
@@ -466,5 +494,9 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState
|
|
|
466
494
|
|
|
467
495
|
draftParsedQuery,
|
|
468
496
|
parsedQuery,
|
|
497
|
+
|
|
498
|
+
setExpressionParam: setExpressionState,
|
|
499
|
+
setSumByParam,
|
|
500
|
+
setGroupByParam,
|
|
469
501
|
};
|
|
470
502
|
};
|
package/src/index.tsx
CHANGED
|
@@ -34,6 +34,8 @@ export * from './ProfileSource';
|
|
|
34
34
|
export {
|
|
35
35
|
convertToProtoFilters,
|
|
36
36
|
convertFromProtoFilters,
|
|
37
|
+
useProfileFilters,
|
|
38
|
+
type ProfileFilter,
|
|
37
39
|
} from './ProfileView/components/ProfileFilters/useProfileFilters';
|
|
38
40
|
export * from './ProfileView';
|
|
39
41
|
export * from './ProfileViewWithData';
|
|
@@ -57,6 +59,8 @@ export const DEFAULT_PROFILE_EXPLORER_PARAM_VALUES: ParamPreferences = {
|
|
|
57
59
|
},
|
|
58
60
|
};
|
|
59
61
|
|
|
62
|
+
export {useProfileTypes} from './ProfileSelector';
|
|
63
|
+
|
|
60
64
|
export {
|
|
61
65
|
ProfileExplorer,
|
|
62
66
|
ProfileTypeSelector,
|