@parca/profile 0.16.409 → 0.16.410

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 CHANGED
@@ -3,6 +3,10 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [0.16.410](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.409...@parca/profile@0.16.410) (2024-07-11)
7
+
8
+ **Note:** Version bump only for package @parca/profile
9
+
6
10
  ## [0.16.409](https://github.com/parca-dev/parca/compare/@parca/profile@0.16.408...@parca/profile@0.16.409) (2024-07-10)
7
11
 
8
12
  **Note:** Version bump only for package @parca/profile
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ProfileMetricsGraph/index.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAC,QAAQ,EAAC,MAAM,0BAA0B,CAAC;AAGlD,OAAO,EAAW,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAY,MAAM,eAAe,CAAC;AACjG,OAAO,EACL,aAAa,EAKd,MAAM,mBAAmB,CAAC;AAI3B,OAAO,EAAyB,gBAAgB,EAAC,MAAM,IAAI,CAAC;AAI5D,UAAU,6BAA6B;IACrC,OAAO,EAAE,MAAM,CAAC;CACjB;AAaD,eAAO,MAAM,wBAAwB,gBAAe,6BAA6B,KAAG,GAAG,CAAC,OAMvF,CAAC;AAEF,UAAU,wBAAwB;IAChC,WAAW,EAAE,kBAAkB,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC7C,eAAe,EAAE,CACf,MAAM,EAAE;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,GAAG,KAAK,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,CAAC,KACvE,IAAI,CAAC;IACV,YAAY,EAAE,CACZ,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,KAAK,EAAE,EACf,eAAe,EAAE,MAAM,EACvB,QAAQ,EAAE,MAAM,KACb,IAAI,CAAC;IACV,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACpC,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,QAAQ,GAAG,IAAI,CAAC;CACxB;AAcD,eAAO,MAAM,aAAa,WAChB,kBAAkB,mBACT,MAAM,SAChB,MAAM,OACR,MAAM,UACJ,MAAM,EAAE,qBAEd,gBAgEF,CAAC;AAEF,QAAA,MAAM,mBAAmB,iHAUtB,wBAAwB,KAAG,GAAG,CAAC,OAoFjC,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ProfileMetricsGraph/index.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAC,QAAQ,EAAC,MAAM,0BAA0B,CAAC;AAGlD,OAAO,EAAW,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAY,MAAM,eAAe,CAAC;AACjG,OAAO,EACL,aAAa,EAKd,MAAM,mBAAmB,CAAC;AAI3B,OAAO,EAAyB,gBAAgB,EAAC,MAAM,IAAI,CAAC;AAQ5D,UAAU,6BAA6B;IACrC,OAAO,EAAE,MAAM,CAAC;CACjB;AAaD,eAAO,MAAM,wBAAwB,gBAAe,6BAA6B,KAAG,GAAG,CAAC,OAMvF,CAAC;AAEF,UAAU,wBAAwB;IAChC,WAAW,EAAE,kBAAkB,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC7C,eAAe,EAAE,CACf,MAAM,EAAE;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,GAAG,KAAK,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,CAAC,KACvE,IAAI,CAAC;IACV,YAAY,EAAE,CACZ,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,KAAK,EAAE,EACf,eAAe,EAAE,MAAM,EACvB,QAAQ,EAAE,MAAM,KACb,IAAI,CAAC;IACV,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACpC,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,QAAQ,GAAG,IAAI,CAAC;CACxB;AAYD,eAAO,MAAM,aAAa,WAChB,kBAAkB,mBACT,MAAM,SAChB,MAAM,OACR,MAAM,UACJ,MAAM,EAAE,qBAEd,gBA+CF,CAAC;AAEF,QAAA,MAAM,mBAAmB,iHAUtB,wBAAwB,KAAG,GAAG,CAAC,OA2GjC,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // Copyright 2022 The Parca Authors
3
3
  // Licensed under the Apache License, Version 2.0 (the "License");
4
4
  // you may not use this file except in compliance with the License.
@@ -11,14 +11,18 @@ import { jsx as _jsx } from "react/jsx-runtime";
11
11
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  // See the License for the specific language governing permissions and
13
13
  // limitations under the License.
14
- import { useEffect, useMemo, useState } from 'react';
14
+ import { useEffect, useMemo } from 'react';
15
15
  import { AnimatePresence, motion } from 'framer-motion';
16
16
  import { Duration, Timestamp } from '@parca/client';
17
17
  import { MetricsGraphSkeleton, useGrpcMetadata, useParcaContext, useURLState, } from '@parca/components';
18
18
  import { Query } from '@parca/parser';
19
19
  import { capitalizeOnlyFirstLetter, getStepDuration } from '@parca/utilities';
20
+ import { useLabelNames } from '../MatchersInput';
20
21
  import MetricsGraph from '../MetricsGraph';
21
22
  import { useMetricsGraphDimensions } from '../MetricsGraph/useMetricsGraphDimensions';
23
+ import useGrpcQuery from '../useGrpcQuery';
24
+ import { Toolbar } from './Toolbar';
25
+ import { DEFAULT_EMPTY_SUM_BY, useSumBy } from './useSumBy';
22
26
  const ErrorContent = ({ errorMessage }) => {
23
27
  return (_jsx("div", { className: "relative rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700", role: "alert", children: _jsx("span", { className: "block sm:inline", children: errorMessage }) }));
24
28
  };
@@ -33,14 +37,7 @@ const getStepCountFromScreenWidth = (pixelsPerPoint) => {
33
37
  width = width - (20 + 24 + 68) * 2;
34
38
  return Math.round(width / pixelsPerPoint);
35
39
  };
36
- const EMPTY_SUM_BY = [];
37
- export const useQueryRange = (client, queryExpression, start, end, sumBy = EMPTY_SUM_BY, skip = false) => {
38
- const [isLoading, setLoading] = useState(!skip);
39
- const [state, setState] = useState({
40
- response: null,
41
- isLoading,
42
- error: null,
43
- });
40
+ export const useQueryRange = (client, queryExpression, start, end, sumBy = DEFAULT_EMPTY_SUM_BY, skip = false) => {
44
41
  const metadata = useGrpcMetadata();
45
42
  const { navigateTo } = useParcaContext();
46
43
  const [stepCountStr, setStepCount] = useURLState({ param: 'step_count', navigateTo });
@@ -58,14 +55,11 @@ export const useQueryRange = (client, queryExpression, start, end, sumBy = EMPTY
58
55
  setStepCount(defaultStepCount.toString());
59
56
  }
60
57
  }, [stepCountStr, defaultStepCount, setStepCount]);
61
- useEffect(() => {
62
- void (async () => {
63
- if (skip) {
64
- return;
65
- }
66
- setLoading(true);
58
+ const { data, isLoading, error } = useGrpcQuery({
59
+ key: ['query-range', queryExpression, start, end, sumBy.join(','), stepCount, metadata],
60
+ queryFn: async () => {
67
61
  const stepDuration = getStepDuration(start, end, stepCount);
68
- const call = client.queryRange({
62
+ const { response } = await client.queryRange({
69
63
  query: queryExpression,
70
64
  start: Timestamp.fromDate(new Date(start)),
71
65
  end: Timestamp.fromDate(new Date(end)),
@@ -73,22 +67,20 @@ export const useQueryRange = (client, queryExpression, start, end, sumBy = EMPTY
73
67
  limit: 0,
74
68
  sumBy,
75
69
  }, { meta: metadata });
76
- call.response
77
- .then(response => {
78
- setState({ response, isLoading: false, error: null });
79
- setLoading(false);
80
- return null;
81
- })
82
- .catch(error => {
83
- setState({ response: null, isLoading: false, error });
84
- setLoading(false);
85
- });
86
- })();
87
- }, [client, queryExpression, start, end, metadata, sumBy, skip, stepCount]);
88
- return { ...state, isLoading };
70
+ return response;
71
+ },
72
+ options: {
73
+ retry: false,
74
+ enabled: !skip && sumBy !== DEFAULT_EMPTY_SUM_BY,
75
+ staleTime: 1000 * 60 * 5, // 5 minutes
76
+ },
77
+ });
78
+ return { isLoading, error: error, response: data ?? null };
89
79
  };
90
80
  const ProfileMetricsGraph = ({ queryClient, queryExpression, profile, from, to, setTimeRange, addLabelMatcher, onPointClick, comparing = false, }) => {
91
- const { isLoading: metricsGraphLoading, response, error, } = useQueryRange(queryClient, queryExpression, from, to);
81
+ const { loading: labelNamesLoading, result: labelNamesResult } = useLabelNames(queryClient, profile?.ProfileSource()?.ProfileType()?.toString() ?? '', from, to);
82
+ const [sumBy, setSumBy] = useSumBy(profile?.ProfileSource()?.ProfileType(), labelNamesResult.response?.labelNames);
83
+ const { isLoading: metricsGraphLoading, response, error, } = useQueryRange(queryClient, queryExpression, from, to, sumBy, labelNamesLoading);
92
84
  const { onError, perf, authenticationErrorMessage, isDarkMode } = useParcaContext();
93
85
  const { width, height, margin, heightStyle } = useMetricsGraphDimensions(comparing);
94
86
  useEffect(() => {
@@ -104,8 +96,9 @@ const ProfileMetricsGraph = ({ queryClient, queryExpression, profile, from, to,
104
96
  }, [perf, response]);
105
97
  const series = response?.series;
106
98
  const dataAvailable = series !== null && series !== undefined && series?.length > 0;
107
- if (metricsGraphLoading) {
108
- return _jsx(MetricsGraphSkeleton, { heightStyle: heightStyle, isDarkMode: isDarkMode });
99
+ const loading = metricsGraphLoading || labelNamesLoading;
100
+ if (!labelNamesLoading && labelNamesResult?.error?.message != null) {
101
+ return (_jsx(ErrorContent, { errorMessage: capitalizeOnlyFirstLetter(labelNamesResult.error.message) }));
109
102
  }
110
103
  if (!metricsGraphLoading && error !== null) {
111
104
  if (authenticationErrorMessage !== undefined && error.code === 'UNAUTHENTICATED') {
@@ -113,19 +106,17 @@ const ProfileMetricsGraph = ({ queryClient, queryExpression, profile, from, to,
113
106
  }
114
107
  return _jsx(ErrorContent, { errorMessage: capitalizeOnlyFirstLetter(error.message) });
115
108
  }
109
+ let sampleUnit = '';
116
110
  if (dataAvailable) {
117
- const handleSampleClick = (timestamp, _value, labels, duration) => {
118
- onPointClick(timestamp, labels, queryExpression, duration);
119
- };
120
- let sampleUnit = '';
121
111
  if (series.every((val, i, arr) => val?.sampleType?.unit === arr[0]?.sampleType?.unit)) {
122
112
  sampleUnit = series[0]?.sampleType?.unit ?? '';
123
113
  }
124
114
  if (sampleUnit === '') {
125
115
  sampleUnit = Query.parse(queryExpression).profileType().sampleUnit;
126
116
  }
127
- return (_jsx(AnimatePresence, { children: _jsx(motion.div, { className: "h-full w-full relative", initial: { display: 'none', opacity: 0 }, animate: { display: 'block', opacity: 1 }, transition: { duration: 0.5 }, children: _jsx(MetricsGraph, { data: series, from: from, to: to, profile: profile, setTimeRange: setTimeRange, onSampleClick: handleSampleClick, addLabelMatcher: addLabelMatcher, sampleUnit: sampleUnit, height: height, width: width, margin: margin }) }, "metrics-graph-loaded") }));
128
117
  }
129
- return _jsx(ProfileMetricsEmptyState, { message: "No data found. Try a different query." });
118
+ return (_jsx(AnimatePresence, { children: _jsxs(motion.div, { className: "h-full w-full relative", initial: { display: 'none', opacity: 0 }, animate: { display: 'block', opacity: 1 }, transition: { duration: 0.5 }, children: [_jsx(Toolbar, { sumBy: sumBy, setSumBy: setSumBy, labels: labelNamesResult.response?.labelNames ?? [] }), loading ? (_jsx(MetricsGraphSkeleton, { heightStyle: heightStyle, isDarkMode: isDarkMode })) : dataAvailable ? (_jsx(MetricsGraph, { data: series, from: from, to: to, profile: profile, setTimeRange: setTimeRange, onSampleClick: (timestamp, _value, labels, duration) => {
119
+ onPointClick(timestamp, labels, queryExpression, duration);
120
+ }, addLabelMatcher: addLabelMatcher, sampleUnit: sampleUnit, height: height, width: width, margin: margin })) : (_jsx(ProfileMetricsEmptyState, { message: "No data found. Try a different query." }))] }, "metrics-graph-loaded") }));
130
121
  };
131
122
  export default ProfileMetricsGraph;
@@ -1,3 +1,4 @@
1
1
  import { ProfileType } from '@parca/parser';
2
+ export declare const DEFAULT_EMPTY_SUM_BY: string[];
2
3
  export declare const useSumBy: (profileType: ProfileType | undefined, labels: string[] | undefined) => [string[], (labels: string[]) => void];
3
4
  //# sourceMappingURL=useSumBy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useSumBy.d.ts","sourceRoot":"","sources":["../../src/ProfileMetricsGraph/useSumBy.ts"],"names":[],"mappings":"AAeA,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AA+B1C,eAAO,MAAM,QAAQ,gBACN,WAAW,GAAG,SAAS,UAC5B,MAAM,EAAE,GAAG,SAAS,KAC3B,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAoBvC,CAAC"}
1
+ {"version":3,"file":"useSumBy.d.ts","sourceRoot":"","sources":["../../src/ProfileMetricsGraph/useSumBy.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAE1C,eAAO,MAAM,oBAAoB,EAAE,MAAM,EAAO,CAAC;AA6BjD,eAAO,MAAM,QAAQ,gBACN,WAAW,GAAG,SAAS,UAC5B,MAAM,EAAE,GAAG,SAAS,KAC3B,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CA+EvC,CAAC"}
@@ -10,8 +10,9 @@
10
10
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
- import { useEffect, useState } from 'react';
14
- const DEFAULT_EMPTY_SUM_BY = [];
13
+ import { useCallback, useEffect, useMemo, useState } from 'react';
14
+ import { useParcaContext, useURLState } from '@parca/components';
15
+ export const DEFAULT_EMPTY_SUM_BY = [];
15
16
  const getDefaultSumBy = (profile, labels) => {
16
17
  if (profile === undefined || labels === undefined) {
17
18
  return undefined;
@@ -31,17 +32,59 @@ const getDefaultSumBy = (profile, labels) => {
31
32
  return undefined;
32
33
  };
33
34
  export const useSumBy = (profileType, labels) => {
34
- const [userSelectedSumBy, setUserSelectedSumBy] = useState(undefined);
35
+ const { navigateTo } = useParcaContext();
36
+ const [userSelectedSumByParam, setUserSelectedSumByParam] = useURLState({
37
+ param: 'sum_by',
38
+ navigateTo,
39
+ });
40
+ console.log('labels', labels);
41
+ console.log('userSelectedSumByParam', userSelectedSumByParam);
42
+ const userSelectedSumBy = useMemo(() => {
43
+ if (userSelectedSumByParam?.length === 0) {
44
+ return undefined;
45
+ }
46
+ if (userSelectedSumByParam === '__none__') {
47
+ return [];
48
+ }
49
+ if (typeof userSelectedSumByParam === 'string') {
50
+ return [userSelectedSumByParam];
51
+ }
52
+ return userSelectedSumByParam;
53
+ }, [userSelectedSumByParam]);
54
+ console.log('userSelectedSumBy', userSelectedSumBy);
55
+ const setUserSelectedSumBy = useCallback((sumBy) => {
56
+ if (sumBy.length === 0) {
57
+ setUserSelectedSumByParam('__none__');
58
+ return;
59
+ }
60
+ if (sumBy.length === 1) {
61
+ // Handle this separately to take care of the empty string scenario
62
+ setUserSelectedSumByParam(sumBy[0]);
63
+ return;
64
+ }
65
+ setUserSelectedSumByParam(sumBy);
66
+ }, [setUserSelectedSumByParam]);
35
67
  const [defaultSumBy, setDefaultSumBy] = useState(getDefaultSumBy(profileType, labels));
36
68
  useEffect(() => {
37
69
  setDefaultSumBy(getDefaultSumBy(profileType, labels));
38
70
  }, [profileType, labels]);
39
71
  useEffect(() => {
40
- if (profileType === undefined) {
72
+ if (profileType === undefined || labels === undefined) {
73
+ return;
74
+ }
75
+ if (userSelectedSumBy !== undefined && userSelectedSumBy.length === 0) {
76
+ // User has explicitly selected no sumBy, so don't reset it
41
77
  return;
42
78
  }
79
+ if (userSelectedSumBy !== undefined && userSelectedSumBy.length > 0) {
80
+ // If any of the user selected sumBy is present in the labels, then don't reset it
81
+ if (userSelectedSumBy.some(sumBy => labels?.includes(sumBy))) {
82
+ return;
83
+ }
84
+ }
43
85
  // Reset user selected sumBy if profile type changes
44
- setUserSelectedSumBy(undefined);
45
- }, [profileType]);
86
+ setUserSelectedSumBy(['']);
87
+ // eslint-disable-next-line react-hooks/exhaustive-deps
88
+ }, [profileType, labels]);
46
89
  return [userSelectedSumBy ?? defaultSumBy ?? DEFAULT_EMPTY_SUM_BY, setUserSelectedSumBy];
47
90
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parca/profile",
3
- "version": "0.16.409",
3
+ "version": "0.16.410",
4
4
  "description": "Profile viewing libraries",
5
5
  "dependencies": {
6
6
  "@headlessui/react": "^1.7.19",
@@ -73,5 +73,5 @@
73
73
  "access": "public",
74
74
  "registry": "https://registry.npmjs.org/"
75
75
  },
76
- "gitHead": "897320af0eaf5565a62ff1ad05b463b426a915fa"
76
+ "gitHead": "7fc4b0d3e5a29c4a79f8762185ec8b874dfdef2c"
77
77
  }
@@ -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 {useEffect, useMemo, useState} from 'react';
14
+ import {useEffect, useMemo} from 'react';
15
15
 
16
16
  import {RpcError} from '@protobuf-ts/runtime-rpc';
17
17
  import {AnimatePresence, motion} from 'framer-motion';
@@ -28,8 +28,12 @@ import {Query} from '@parca/parser';
28
28
  import {capitalizeOnlyFirstLetter, getStepDuration} from '@parca/utilities';
29
29
 
30
30
  import {MergedProfileSelection, ProfileSelection} from '..';
31
+ import {useLabelNames} from '../MatchersInput';
31
32
  import MetricsGraph from '../MetricsGraph';
32
33
  import {useMetricsGraphDimensions} from '../MetricsGraph/useMetricsGraphDimensions';
34
+ import useGrpcQuery from '../useGrpcQuery';
35
+ import {Toolbar} from './Toolbar';
36
+ import {DEFAULT_EMPTY_SUM_BY, useSumBy} from './useSumBy';
33
37
 
34
38
  interface ProfileMetricsEmptyStateProps {
35
39
  message: string;
@@ -89,26 +93,17 @@ const getStepCountFromScreenWidth = (pixelsPerPoint: number): number => {
89
93
  return Math.round(width / pixelsPerPoint);
90
94
  };
91
95
 
92
- const EMPTY_SUM_BY: string[] = [];
93
-
94
96
  export const useQueryRange = (
95
97
  client: QueryServiceClient,
96
98
  queryExpression: string,
97
99
  start: number,
98
100
  end: number,
99
- sumBy: string[] = EMPTY_SUM_BY,
101
+ sumBy: string[] = DEFAULT_EMPTY_SUM_BY,
100
102
  skip = false
101
103
  ): IQueryRangeState => {
102
- const [isLoading, setLoading] = useState<boolean>(!skip);
103
- const [state, setState] = useState<IQueryRangeState>({
104
- response: null,
105
- isLoading,
106
- error: null,
107
- });
108
104
  const metadata = useGrpcMetadata();
109
105
  const {navigateTo} = useParcaContext();
110
106
  const [stepCountStr, setStepCount] = useURLState({param: 'step_count', navigateTo});
111
-
112
107
  const defaultStepCount = useMemo(() => {
113
108
  return getStepCountFromScreenWidth(10);
114
109
  }, []);
@@ -127,16 +122,11 @@ export const useQueryRange = (
127
122
  }
128
123
  }, [stepCountStr, defaultStepCount, setStepCount]);
129
124
 
130
- useEffect(() => {
131
- void (async () => {
132
- if (skip) {
133
- return;
134
- }
135
-
136
- setLoading(true);
137
-
125
+ const {data, isLoading, error} = useGrpcQuery<QueryRangeResponse | undefined>({
126
+ key: ['query-range', queryExpression, start, end, sumBy.join(','), stepCount, metadata],
127
+ queryFn: async () => {
138
128
  const stepDuration = getStepDuration(start, end, stepCount);
139
- const call = client.queryRange(
129
+ const {response} = await client.queryRange(
140
130
  {
141
131
  query: queryExpression,
142
132
  start: Timestamp.fromDate(new Date(start)),
@@ -147,21 +137,16 @@ export const useQueryRange = (
147
137
  },
148
138
  {meta: metadata}
149
139
  );
140
+ return response;
141
+ },
142
+ options: {
143
+ retry: false,
144
+ enabled: !skip && sumBy !== DEFAULT_EMPTY_SUM_BY,
145
+ staleTime: 1000 * 60 * 5, // 5 minutes
146
+ },
147
+ });
150
148
 
151
- call.response
152
- .then(response => {
153
- setState({response, isLoading: false, error: null});
154
- setLoading(false);
155
- return null;
156
- })
157
- .catch(error => {
158
- setState({response: null, isLoading: false, error});
159
- setLoading(false);
160
- });
161
- })();
162
- }, [client, queryExpression, start, end, metadata, sumBy, skip, stepCount]);
163
-
164
- return {...state, isLoading};
149
+ return {isLoading, error: error as RpcError | null, response: data ?? null};
165
150
  };
166
151
 
167
152
  const ProfileMetricsGraph = ({
@@ -175,11 +160,22 @@ const ProfileMetricsGraph = ({
175
160
  onPointClick,
176
161
  comparing = false,
177
162
  }: ProfileMetricsGraphProps): JSX.Element => {
163
+ const {loading: labelNamesLoading, result: labelNamesResult} = useLabelNames(
164
+ queryClient,
165
+ profile?.ProfileSource()?.ProfileType()?.toString() ?? '',
166
+ from,
167
+ to
168
+ );
169
+ const [sumBy, setSumBy] = useSumBy(
170
+ profile?.ProfileSource()?.ProfileType(),
171
+ labelNamesResult.response?.labelNames
172
+ );
173
+
178
174
  const {
179
175
  isLoading: metricsGraphLoading,
180
176
  response,
181
177
  error,
182
- } = useQueryRange(queryClient, queryExpression, from, to);
178
+ } = useQueryRange(queryClient, queryExpression, from, to, sumBy, labelNamesLoading);
183
179
  const {onError, perf, authenticationErrorMessage, isDarkMode} = useParcaContext();
184
180
  const {width, height, margin, heightStyle} = useMetricsGraphDimensions(comparing);
185
181
 
@@ -200,8 +196,12 @@ const ProfileMetricsGraph = ({
200
196
  const series = response?.series;
201
197
  const dataAvailable = series !== null && series !== undefined && series?.length > 0;
202
198
 
203
- if (metricsGraphLoading) {
204
- return <MetricsGraphSkeleton heightStyle={heightStyle} isDarkMode={isDarkMode} />;
199
+ const loading = metricsGraphLoading || labelNamesLoading;
200
+
201
+ if (!labelNamesLoading && labelNamesResult?.error?.message != null) {
202
+ return (
203
+ <ErrorContent errorMessage={capitalizeOnlyFirstLetter(labelNamesResult.error.message)} />
204
+ );
205
205
  }
206
206
 
207
207
  if (!metricsGraphLoading && error !== null) {
@@ -212,52 +212,60 @@ const ProfileMetricsGraph = ({
212
212
  return <ErrorContent errorMessage={capitalizeOnlyFirstLetter(error.message)} />;
213
213
  }
214
214
 
215
- if (dataAvailable) {
216
- const handleSampleClick = (
217
- timestamp: number,
218
- _value: number,
219
- labels: Label[],
220
- duration: number
221
- ): void => {
222
- onPointClick(timestamp, labels, queryExpression, duration);
223
- };
215
+ let sampleUnit = '';
224
216
 
225
- let sampleUnit = '';
217
+ if (dataAvailable) {
226
218
  if (series.every((val, i, arr) => val?.sampleType?.unit === arr[0]?.sampleType?.unit)) {
227
219
  sampleUnit = series[0]?.sampleType?.unit ?? '';
228
220
  }
229
221
  if (sampleUnit === '') {
230
222
  sampleUnit = Query.parse(queryExpression).profileType().sampleUnit;
231
223
  }
224
+ }
232
225
 
233
- return (
234
- <AnimatePresence>
235
- <motion.div
236
- className="h-full w-full relative"
237
- key="metrics-graph-loaded"
238
- initial={{display: 'none', opacity: 0}}
239
- animate={{display: 'block', opacity: 1}}
240
- transition={{duration: 0.5}}
241
- >
226
+ return (
227
+ <AnimatePresence>
228
+ <motion.div
229
+ className="h-full w-full relative"
230
+ key="metrics-graph-loaded"
231
+ initial={{display: 'none', opacity: 0}}
232
+ animate={{display: 'block', opacity: 1}}
233
+ transition={{duration: 0.5}}
234
+ >
235
+ <Toolbar
236
+ sumBy={sumBy}
237
+ setSumBy={setSumBy}
238
+ labels={labelNamesResult.response?.labelNames ?? []}
239
+ />
240
+ {loading ? (
241
+ <MetricsGraphSkeleton heightStyle={heightStyle} isDarkMode={isDarkMode} />
242
+ ) : dataAvailable ? (
242
243
  <MetricsGraph
243
244
  data={series}
244
245
  from={from}
245
246
  to={to}
246
247
  profile={profile as MergedProfileSelection}
247
248
  setTimeRange={setTimeRange}
248
- onSampleClick={handleSampleClick}
249
+ onSampleClick={(
250
+ timestamp: number,
251
+ _value: number,
252
+ labels: Label[],
253
+ duration: number
254
+ ): void => {
255
+ onPointClick(timestamp, labels, queryExpression, duration);
256
+ }}
249
257
  addLabelMatcher={addLabelMatcher}
250
258
  sampleUnit={sampleUnit}
251
259
  height={height}
252
260
  width={width}
253
261
  margin={margin}
254
262
  />
255
- </motion.div>
256
- </AnimatePresence>
257
- );
258
- }
259
-
260
- return <ProfileMetricsEmptyState message="No data found. Try a different query." />;
263
+ ) : (
264
+ <ProfileMetricsEmptyState message="No data found. Try a different query." />
265
+ )}
266
+ </motion.div>
267
+ </AnimatePresence>
268
+ );
261
269
  };
262
270
 
263
271
  export default ProfileMetricsGraph;
@@ -11,11 +11,12 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
- import {useEffect, useState} from 'react';
14
+ import {useCallback, useEffect, useMemo, useState} from 'react';
15
15
 
16
+ import {useParcaContext, useURLState} from '@parca/components';
16
17
  import {ProfileType} from '@parca/parser';
17
18
 
18
- const DEFAULT_EMPTY_SUM_BY: string[] = [];
19
+ export const DEFAULT_EMPTY_SUM_BY: string[] = [];
19
20
 
20
21
  const getDefaultSumBy = (
21
22
  profile: ProfileType | undefined,
@@ -48,7 +49,52 @@ export const useSumBy = (
48
49
  profileType: ProfileType | undefined,
49
50
  labels: string[] | undefined
50
51
  ): [string[], (labels: string[]) => void] => {
51
- const [userSelectedSumBy, setUserSelectedSumBy] = useState<string[] | undefined>(undefined);
52
+ const {navigateTo} = useParcaContext();
53
+ const [userSelectedSumByParam, setUserSelectedSumByParam] = useURLState({
54
+ param: 'sum_by',
55
+ navigateTo,
56
+ });
57
+
58
+ console.log('labels', labels);
59
+
60
+ console.log('userSelectedSumByParam', userSelectedSumByParam);
61
+
62
+ const userSelectedSumBy = useMemo<string[] | undefined>(() => {
63
+ if (userSelectedSumByParam?.length === 0) {
64
+ return undefined;
65
+ }
66
+
67
+ if (userSelectedSumByParam === '__none__') {
68
+ return [];
69
+ }
70
+
71
+ if (typeof userSelectedSumByParam === 'string') {
72
+ return [userSelectedSumByParam];
73
+ }
74
+
75
+ return userSelectedSumByParam;
76
+ }, [userSelectedSumByParam]);
77
+
78
+ console.log('userSelectedSumBy', userSelectedSumBy);
79
+
80
+ const setUserSelectedSumBy = useCallback(
81
+ (sumBy: string[]) => {
82
+ if (sumBy.length === 0) {
83
+ setUserSelectedSumByParam('__none__');
84
+ return;
85
+ }
86
+
87
+ if (sumBy.length === 1) {
88
+ // Handle this separately to take care of the empty string scenario
89
+ setUserSelectedSumByParam(sumBy[0]);
90
+ return;
91
+ }
92
+
93
+ setUserSelectedSumByParam(sumBy);
94
+ },
95
+ [setUserSelectedSumByParam]
96
+ );
97
+
52
98
  const [defaultSumBy, setDefaultSumBy] = useState<string[] | undefined>(
53
99
  getDefaultSumBy(profileType, labels)
54
100
  );
@@ -58,13 +104,27 @@ export const useSumBy = (
58
104
  }, [profileType, labels]);
59
105
 
60
106
  useEffect(() => {
61
- if (profileType === undefined) {
107
+ if (profileType === undefined || labels === undefined) {
62
108
  return;
63
109
  }
64
110
 
111
+ if (userSelectedSumBy !== undefined && userSelectedSumBy.length === 0) {
112
+ // User has explicitly selected no sumBy, so don't reset it
113
+ return;
114
+ }
115
+
116
+ if (userSelectedSumBy !== undefined && userSelectedSumBy.length > 0) {
117
+ // If any of the user selected sumBy is present in the labels, then don't reset it
118
+ if (userSelectedSumBy.some(sumBy => labels?.includes(sumBy))) {
119
+ return;
120
+ }
121
+ }
122
+
65
123
  // Reset user selected sumBy if profile type changes
66
- setUserSelectedSumBy(undefined);
67
- }, [profileType]);
124
+ setUserSelectedSumBy(['']);
125
+
126
+ // eslint-disable-next-line react-hooks/exhaustive-deps
127
+ }, [profileType, labels]);
68
128
 
69
129
  return [userSelectedSumBy ?? defaultSumBy ?? DEFAULT_EMPTY_SUM_BY, setUserSelectedSumBy];
70
130
  };