@parca/profile 0.14.12 → 0.14.13
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 +6 -0
- package/package.json +2 -2
- package/src/IcicleGraph.tsx +2 -1
- package/src/ProfileIcicleGraph.tsx +1 -2
- package/src/ProfileView.tsx +94 -60
- package/src/ProfileViewWithData.tsx +66 -0
- package/src/TopTable.tsx +12 -32
- package/src/index.tsx +1 -0
- package/src/useDelayedLoader.ts +26 -0
- package/src/useQuery.tsx +13 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
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.14.13](https://github.com/parca-dev/parca/compare/ui-v0.14.12...ui-v0.14.13) (2022-08-03)
|
|
7
|
+
|
|
8
|
+
## [0.14.7](https://github.com/parca-dev/parca/compare/ui-v0.14.5...ui-v0.14.7) (2022-07-27)
|
|
9
|
+
|
|
10
|
+
**Note:** Version bump only for package @parca/profile
|
|
11
|
+
|
|
6
12
|
## [0.14.12](https://github.com/parca-dev/parca/compare/ui-v0.14.11...ui-v0.14.12) (2022-08-02)
|
|
7
13
|
|
|
8
14
|
**Note:** Version bump only for package @parca/profile
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parca/profile",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.13",
|
|
4
4
|
"description": "Profile viewing libraries",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@iconify/react": "^3.2.2",
|
|
@@ -22,5 +22,5 @@
|
|
|
22
22
|
"access": "public",
|
|
23
23
|
"registry": "https://registry.npmjs.org/"
|
|
24
24
|
},
|
|
25
|
-
"gitHead": "
|
|
25
|
+
"gitHead": "614acf00ec750eaee1d52b8e8a827c5c8d54ccfc"
|
|
26
26
|
}
|
package/src/IcicleGraph.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, {MouseEvent, useEffect, useRef, useState} from 'react';
|
|
2
|
+
|
|
2
3
|
import {throttle} from 'lodash';
|
|
3
4
|
import {pointer} from 'd3-selection';
|
|
4
5
|
import {scaleLinear} from 'd3-scale';
|
|
@@ -185,7 +186,7 @@ export function IcicleGraphNodes({
|
|
|
185
186
|
const onMouseLeave = () => setHoveringNode(undefined);
|
|
186
187
|
|
|
187
188
|
return (
|
|
188
|
-
<React.Fragment>
|
|
189
|
+
<React.Fragment key={`node-${key}`}>
|
|
189
190
|
<IcicleRect
|
|
190
191
|
key={`rect-${key}`}
|
|
191
192
|
x={xStart}
|
|
@@ -20,13 +20,12 @@ const ProfileIcicleGraph = ({
|
|
|
20
20
|
sampleUnit,
|
|
21
21
|
}: ProfileIcicleGraphProps) => {
|
|
22
22
|
const compareMode = useAppSelector(selectCompareMode);
|
|
23
|
+
const {ref, dimensions} = useContainerDimensions();
|
|
23
24
|
|
|
24
25
|
if (graph === undefined) return <div>no data...</div>;
|
|
25
26
|
const total = graph.total;
|
|
26
27
|
if (parseFloat(total) === 0) return <>Profile has no samples</>;
|
|
27
28
|
|
|
28
|
-
const {ref, dimensions} = useContainerDimensions();
|
|
29
|
-
|
|
30
29
|
return (
|
|
31
30
|
<>
|
|
32
31
|
{compareMode && <DiffLegend />}
|
package/src/ProfileView.tsx
CHANGED
|
@@ -1,22 +1,46 @@
|
|
|
1
|
-
import React, {useEffect, useState} from 'react';
|
|
1
|
+
import React, {useEffect, useMemo, useState} from 'react';
|
|
2
|
+
|
|
2
3
|
import {parseParams} from '@parca/functions';
|
|
3
|
-
import {QueryServiceClient,
|
|
4
|
+
import {QueryServiceClient, Flamegraph, Top} from '@parca/client';
|
|
4
5
|
import {Button, Card, SearchNodes, useGrpcMetadata, useParcaTheme} from '@parca/components';
|
|
5
6
|
|
|
6
7
|
import ProfileShareButton from './components/ProfileShareButton';
|
|
7
8
|
import ProfileIcicleGraph from './ProfileIcicleGraph';
|
|
8
9
|
import {ProfileSource} from './ProfileSource';
|
|
9
|
-
import {useQuery} from './useQuery';
|
|
10
10
|
import TopTable from './TopTable';
|
|
11
|
+
import useDelayedLoader from './useDelayedLoader';
|
|
11
12
|
import {downloadPprof} from './utils';
|
|
12
13
|
|
|
13
14
|
import './ProfileView.styles.css';
|
|
14
15
|
|
|
15
16
|
type NavigateFunction = (path: string, queryParams: any) => void;
|
|
16
17
|
|
|
18
|
+
interface FlamegraphData {
|
|
19
|
+
loading: boolean;
|
|
20
|
+
data?: Flamegraph;
|
|
21
|
+
error?: any;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface TopTableData {
|
|
25
|
+
loading: boolean;
|
|
26
|
+
data?: Top;
|
|
27
|
+
error?: any;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type VisualizationType = 'icicle' | 'table' | 'both';
|
|
31
|
+
|
|
32
|
+
interface ProfileVisState {
|
|
33
|
+
currentView: VisualizationType;
|
|
34
|
+
setCurrentView: (view: VisualizationType) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
17
37
|
interface ProfileViewProps {
|
|
18
|
-
|
|
19
|
-
|
|
38
|
+
flamegraphData?: FlamegraphData;
|
|
39
|
+
topTableData?: TopTableData;
|
|
40
|
+
sampleUnit: string;
|
|
41
|
+
profileVisState: ProfileVisState;
|
|
42
|
+
profileSource?: ProfileSource;
|
|
43
|
+
queryClient?: QueryServiceClient;
|
|
20
44
|
navigateTo?: NavigateFunction;
|
|
21
45
|
compare?: boolean;
|
|
22
46
|
}
|
|
@@ -29,22 +53,28 @@ function arrayEquals(a, b): boolean {
|
|
|
29
53
|
a.every((val, index) => val === b[index])
|
|
30
54
|
);
|
|
31
55
|
}
|
|
56
|
+
export const useProfileVisState = (): ProfileVisState => {
|
|
57
|
+
const router = parseParams(window.location.search);
|
|
58
|
+
const currentViewFromURL = router.currentProfileView as string;
|
|
59
|
+
const [currentView, setCurrentView] = useState<VisualizationType>(
|
|
60
|
+
(currentViewFromURL as VisualizationType) || 'icicle'
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return {currentView, setCurrentView};
|
|
64
|
+
};
|
|
32
65
|
|
|
33
66
|
export const ProfileView = ({
|
|
34
|
-
|
|
67
|
+
flamegraphData,
|
|
68
|
+
topTableData,
|
|
69
|
+
sampleUnit,
|
|
35
70
|
profileSource,
|
|
71
|
+
queryClient,
|
|
36
72
|
navigateTo,
|
|
73
|
+
profileVisState,
|
|
37
74
|
}: ProfileViewProps): JSX.Element => {
|
|
38
|
-
const router = parseParams(window.location.search);
|
|
39
|
-
const currentViewFromURL = router.currentProfileView as string;
|
|
40
75
|
const [curPath, setCurPath] = useState<string[]>([]);
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
queryClient,
|
|
44
|
-
profileSource,
|
|
45
|
-
QueryRequest_ReportType.FLAMEGRAPH_UNSPECIFIED
|
|
46
|
-
);
|
|
47
|
-
const [currentView, setCurrentView] = useState<string | undefined>(currentViewFromURL);
|
|
76
|
+
const {currentView, setCurrentView} = profileVisState;
|
|
77
|
+
|
|
48
78
|
const metadata = useGrpcMetadata();
|
|
49
79
|
const {loader} = useParcaTheme();
|
|
50
80
|
|
|
@@ -53,29 +83,39 @@ export const ProfileView = ({
|
|
|
53
83
|
setCurPath([]);
|
|
54
84
|
}, [profileSource]);
|
|
55
85
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
// if the request takes longer than half a second, show the loading icon
|
|
60
|
-
showLoaderTimeout = setTimeout(() => {
|
|
61
|
-
setIsLoaderVisible(true);
|
|
62
|
-
}, 500);
|
|
63
|
-
} else {
|
|
64
|
-
setIsLoaderVisible(false);
|
|
86
|
+
const isLoading = useMemo(() => {
|
|
87
|
+
if (currentView === 'icicle') {
|
|
88
|
+
return !!flamegraphData?.loading;
|
|
65
89
|
}
|
|
66
|
-
|
|
67
|
-
|
|
90
|
+
if (currentView === 'table') {
|
|
91
|
+
return !!topTableData?.loading;
|
|
92
|
+
}
|
|
93
|
+
if (currentView === 'both') {
|
|
94
|
+
return !!flamegraphData?.loading || !!topTableData?.loading;
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}, [currentView, flamegraphData?.loading, topTableData?.loading]);
|
|
98
|
+
|
|
99
|
+
const isLoaderVisible = useDelayedLoader(isLoading);
|
|
68
100
|
|
|
69
101
|
if (isLoaderVisible) {
|
|
70
102
|
return <>{loader}</>;
|
|
71
103
|
}
|
|
72
104
|
|
|
73
|
-
if (error
|
|
74
|
-
|
|
105
|
+
if (flamegraphData?.error != null) {
|
|
106
|
+
console.error('Error: ', flamegraphData?.error);
|
|
107
|
+
return (
|
|
108
|
+
<div className="p-10 flex justify-center">
|
|
109
|
+
An error occurred: {flamegraphData?.error.message}
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
75
112
|
}
|
|
76
113
|
|
|
77
114
|
const downloadPProf = async (e: React.MouseEvent<HTMLElement>) => {
|
|
78
115
|
e.preventDefault();
|
|
116
|
+
if (!profileSource || !queryClient) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
79
119
|
|
|
80
120
|
try {
|
|
81
121
|
const blob = await downloadPprof(profileSource.QueryRequest(), queryClient, metadata);
|
|
@@ -96,16 +136,18 @@ export const ProfileView = ({
|
|
|
96
136
|
}
|
|
97
137
|
};
|
|
98
138
|
|
|
99
|
-
const switchProfileView = (view:
|
|
139
|
+
const switchProfileView = (view: VisualizationType) => {
|
|
140
|
+
if (view == null) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
100
143
|
if (navigateTo === undefined) return;
|
|
101
144
|
|
|
102
145
|
setCurrentView(view);
|
|
146
|
+
const router = parseParams(window.location.search);
|
|
103
147
|
|
|
104
148
|
navigateTo('/', {...router, ...{currentProfileView: view}});
|
|
105
149
|
};
|
|
106
150
|
|
|
107
|
-
const sampleUnit = profileSource.ProfileType().sampleUnit;
|
|
108
|
-
|
|
109
151
|
return (
|
|
110
152
|
<>
|
|
111
153
|
<div className="py-3">
|
|
@@ -114,10 +156,12 @@ export const ProfileView = ({
|
|
|
114
156
|
<div className="flex py-3 w-full">
|
|
115
157
|
<div className="w-2/5 flex space-x-4">
|
|
116
158
|
<div className="flex space-x-1">
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
159
|
+
{profileSource && queryClient ? (
|
|
160
|
+
<ProfileShareButton
|
|
161
|
+
queryRequest={profileSource.QueryRequest()}
|
|
162
|
+
queryClient={queryClient}
|
|
163
|
+
/>
|
|
164
|
+
) : null}
|
|
121
165
|
|
|
122
166
|
<Button color="neutral" onClick={downloadPProf}>
|
|
123
167
|
Download pprof
|
|
@@ -149,7 +193,7 @@ export const ProfileView = ({
|
|
|
149
193
|
|
|
150
194
|
<Button
|
|
151
195
|
variant={`${currentView === 'both' ? 'primary' : 'neutral'}`}
|
|
152
|
-
className="items-center rounded-tl-none rounded-tr-none rounded-bl-none rounded-br-none border-l-0 border-r-0 w-auto px-8 whitespace-nowrap no-outline-on-buttons
|
|
196
|
+
className="items-center rounded-tl-none rounded-tr-none rounded-bl-none rounded-br-none border-l-0 border-r-0 w-auto px-8 whitespace-nowrap no-outline-on-buttons text-ellipsis"
|
|
153
197
|
onClick={() => switchProfileView('both')}
|
|
154
198
|
>
|
|
155
199
|
Both
|
|
@@ -166,45 +210,35 @@ export const ProfileView = ({
|
|
|
166
210
|
</div>
|
|
167
211
|
|
|
168
212
|
<div className="flex space-x-4 justify-between">
|
|
169
|
-
{currentView === 'icicle' &&
|
|
170
|
-
response !== null &&
|
|
171
|
-
response.report.oneofKind === 'flamegraph' && (
|
|
172
|
-
<div className="w-full">
|
|
173
|
-
<ProfileIcicleGraph
|
|
174
|
-
curPath={curPath}
|
|
175
|
-
setNewCurPath={setNewCurPath}
|
|
176
|
-
graph={response.report.flamegraph}
|
|
177
|
-
sampleUnit={sampleUnit}
|
|
178
|
-
/>
|
|
179
|
-
</div>
|
|
180
|
-
)}
|
|
181
|
-
|
|
182
|
-
{currentView === 'table' && (
|
|
213
|
+
{currentView === 'icicle' && flamegraphData?.data != null && (
|
|
183
214
|
<div className="w-full">
|
|
184
|
-
<
|
|
185
|
-
|
|
186
|
-
|
|
215
|
+
<ProfileIcicleGraph
|
|
216
|
+
curPath={curPath}
|
|
217
|
+
setNewCurPath={setNewCurPath}
|
|
218
|
+
graph={flamegraphData.data}
|
|
187
219
|
sampleUnit={sampleUnit}
|
|
188
220
|
/>
|
|
189
221
|
</div>
|
|
190
222
|
)}
|
|
191
223
|
|
|
224
|
+
{currentView === 'table' && topTableData != null && (
|
|
225
|
+
<div className="w-full">
|
|
226
|
+
<TopTable data={topTableData.data} sampleUnit={sampleUnit} />
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
|
|
192
230
|
{currentView === 'both' && (
|
|
193
231
|
<>
|
|
194
232
|
<div className="w-1/2">
|
|
195
|
-
<TopTable
|
|
196
|
-
queryClient={queryClient}
|
|
197
|
-
profileSource={profileSource}
|
|
198
|
-
sampleUnit={sampleUnit}
|
|
199
|
-
/>
|
|
233
|
+
<TopTable data={topTableData?.data} sampleUnit={sampleUnit} />
|
|
200
234
|
</div>
|
|
201
235
|
|
|
202
236
|
<div className="w-1/2">
|
|
203
|
-
{
|
|
237
|
+
{flamegraphData != null && (
|
|
204
238
|
<ProfileIcicleGraph
|
|
205
239
|
curPath={curPath}
|
|
206
240
|
setNewCurPath={setNewCurPath}
|
|
207
|
-
graph={
|
|
241
|
+
graph={flamegraphData.data}
|
|
208
242
|
sampleUnit={sampleUnit}
|
|
209
243
|
/>
|
|
210
244
|
)}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {QueryServiceClient, QueryRequest_ReportType} from '@parca/client';
|
|
2
|
+
|
|
3
|
+
import {useQuery} from './useQuery';
|
|
4
|
+
import {ProfileView, useProfileVisState} from './ProfileView';
|
|
5
|
+
import {ProfileSource} from './ProfileSource';
|
|
6
|
+
|
|
7
|
+
type NavigateFunction = (path: string, queryParams: any) => void;
|
|
8
|
+
|
|
9
|
+
interface ProfileViewWithDataProps {
|
|
10
|
+
queryClient: QueryServiceClient;
|
|
11
|
+
profileSource: ProfileSource;
|
|
12
|
+
navigateTo?: NavigateFunction;
|
|
13
|
+
compare?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const ProfileViewWithData = ({
|
|
17
|
+
queryClient,
|
|
18
|
+
profileSource,
|
|
19
|
+
navigateTo,
|
|
20
|
+
}: ProfileViewWithDataProps) => {
|
|
21
|
+
const profileVisState = useProfileVisState();
|
|
22
|
+
const {currentView} = profileVisState;
|
|
23
|
+
const {
|
|
24
|
+
isLoading: flamegraphLoading,
|
|
25
|
+
response: flamegraphResponse,
|
|
26
|
+
error: flamegraphError,
|
|
27
|
+
} = useQuery(queryClient, profileSource, QueryRequest_ReportType.FLAMEGRAPH_UNSPECIFIED, {
|
|
28
|
+
skip: currentView != 'icicle' && currentView != 'both',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
isLoading: topTableLoading,
|
|
33
|
+
response: topTableResponse,
|
|
34
|
+
error: topTableError,
|
|
35
|
+
} = useQuery(queryClient, profileSource, QueryRequest_ReportType.TOP, {
|
|
36
|
+
skip: currentView != 'table' && currentView != 'both',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const sampleUnit = profileSource.ProfileType().sampleUnit;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<ProfileView
|
|
43
|
+
flamegraphData={{
|
|
44
|
+
loading: flamegraphLoading,
|
|
45
|
+
data:
|
|
46
|
+
flamegraphResponse?.report.oneofKind === 'flamegraph'
|
|
47
|
+
? flamegraphResponse?.report?.flamegraph
|
|
48
|
+
: undefined,
|
|
49
|
+
error: flamegraphError,
|
|
50
|
+
}}
|
|
51
|
+
topTableData={{
|
|
52
|
+
loading: topTableLoading,
|
|
53
|
+
data:
|
|
54
|
+
topTableResponse?.report.oneofKind === 'top' ? topTableResponse.report.top : undefined,
|
|
55
|
+
error: topTableError,
|
|
56
|
+
}}
|
|
57
|
+
profileVisState={profileVisState}
|
|
58
|
+
sampleUnit={sampleUnit}
|
|
59
|
+
profileSource={profileSource}
|
|
60
|
+
queryClient={queryClient}
|
|
61
|
+
navigateTo={navigateTo}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export default ProfileViewWithData;
|
package/src/TopTable.tsx
CHANGED
|
@@ -1,21 +1,15 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import {getLastItem, valueFormatter, isSearchMatch} from '@parca/functions';
|
|
3
4
|
import {useAppSelector, selectCompareMode, selectSearchNodeString} from '@parca/store';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
QueryServiceClient,
|
|
7
|
-
QueryRequest_ReportType,
|
|
8
|
-
TopNodeMeta,
|
|
9
|
-
} from '@parca/client';
|
|
10
|
-
import {ProfileSource} from './ProfileSource';
|
|
11
|
-
import {useQuery} from './useQuery';
|
|
5
|
+
import {TopNodeMeta, Top} from '@parca/client';
|
|
6
|
+
|
|
12
7
|
import {hexifyAddress} from './utils';
|
|
13
8
|
|
|
14
9
|
import './TopTable.styles.css';
|
|
15
10
|
|
|
16
|
-
interface
|
|
17
|
-
|
|
18
|
-
profileSource: ProfileSource;
|
|
11
|
+
interface TopTableProps {
|
|
12
|
+
data?: Top;
|
|
19
13
|
sampleUnit: string;
|
|
20
14
|
}
|
|
21
15
|
|
|
@@ -29,21 +23,17 @@ const Arrow = ({direction}: {direction: string | undefined}) => {
|
|
|
29
23
|
width="11"
|
|
30
24
|
xmlns="http://www.w3.org/2000/svg"
|
|
31
25
|
>
|
|
32
|
-
<path
|
|
26
|
+
<path clipRule="evenodd" d="m.573997 0 5.000003 10 5-10h-9.999847z" fillRule="evenodd" />
|
|
33
27
|
</svg>
|
|
34
28
|
);
|
|
35
29
|
};
|
|
36
30
|
|
|
37
|
-
const useSortableData = (
|
|
38
|
-
response: QueryResponse | null,
|
|
39
|
-
config = {key: 'cumulative', direction: 'desc'}
|
|
40
|
-
) => {
|
|
31
|
+
const useSortableData = (top?: Top, config = {key: 'cumulative', direction: 'desc'}) => {
|
|
41
32
|
const [sortConfig, setSortConfig] = React.useState<{key: string; direction: string} | null>(
|
|
42
33
|
config
|
|
43
34
|
);
|
|
44
35
|
|
|
45
|
-
const rawTableReport =
|
|
46
|
-
response !== null && response.report.oneofKind === 'top' ? response.report.top.list : [];
|
|
36
|
+
const rawTableReport = top ? top.list : [];
|
|
47
37
|
|
|
48
38
|
const items = rawTableReport.map(node => ({
|
|
49
39
|
...node,
|
|
@@ -98,25 +88,15 @@ export const RowLabel = (meta: TopNodeMeta | undefined): string => {
|
|
|
98
88
|
return fallback === '' ? '<unknown>' : fallback;
|
|
99
89
|
};
|
|
100
90
|
|
|
101
|
-
export const TopTable = ({
|
|
102
|
-
|
|
103
|
-
profileSource,
|
|
104
|
-
sampleUnit,
|
|
105
|
-
}: ProfileViewProps): JSX.Element => {
|
|
106
|
-
const {response, error} = useQuery(queryClient, profileSource, QueryRequest_ReportType.TOP);
|
|
107
|
-
const {items, requestSort, sortConfig} = useSortableData(response);
|
|
91
|
+
export const TopTable = ({data: top, sampleUnit}: TopTableProps): JSX.Element => {
|
|
92
|
+
const {items, requestSort, sortConfig} = useSortableData(top);
|
|
108
93
|
const currentSearchString = useAppSelector(selectSearchNodeString);
|
|
109
94
|
|
|
110
95
|
const compareMode = useAppSelector(selectCompareMode);
|
|
111
96
|
|
|
112
97
|
const unit = sampleUnit;
|
|
113
98
|
|
|
114
|
-
|
|
115
|
-
return <div className="p-10 flex justify-center">An error occurred: {error.message}</div>;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const total =
|
|
119
|
-
response !== null && response.report.oneofKind === 'top' ? response.report.top.list.length : 0;
|
|
99
|
+
const total = top ? top.list.length : 0;
|
|
120
100
|
if (total === 0) return <>Profile has no samples</>;
|
|
121
101
|
|
|
122
102
|
const getClassNamesFor = name => {
|
package/src/index.tsx
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {useEffect, useState} from 'react';
|
|
2
|
+
|
|
3
|
+
interface DelayedLoaderOptions {
|
|
4
|
+
delay?: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const useDelayedLoader = (isLoading = false, options?: DelayedLoaderOptions) => {
|
|
8
|
+
const {delay = 500} = options || {};
|
|
9
|
+
const [isLoaderVisible, setIsLoaderVisible] = useState<boolean>(false);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
let showLoaderTimeout;
|
|
12
|
+
if (isLoading && !isLoaderVisible) {
|
|
13
|
+
// if the request takes longer than half a second, show the loading icon
|
|
14
|
+
showLoaderTimeout = setTimeout(() => {
|
|
15
|
+
setIsLoaderVisible(true);
|
|
16
|
+
}, delay);
|
|
17
|
+
} else {
|
|
18
|
+
setIsLoaderVisible(false);
|
|
19
|
+
}
|
|
20
|
+
return () => clearTimeout(showLoaderTimeout);
|
|
21
|
+
}, [isLoading]);
|
|
22
|
+
|
|
23
|
+
return isLoaderVisible;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default useDelayedLoader;
|
package/src/useQuery.tsx
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {useEffect, useState} from 'react';
|
|
2
|
+
|
|
2
3
|
import {QueryServiceClient, QueryResponse, QueryRequest_ReportType} from '@parca/client';
|
|
3
4
|
import {RpcError} from '@protobuf-ts/runtime-rpc';
|
|
4
5
|
import {useGrpcMetadata} from '@parca/components';
|
|
6
|
+
|
|
5
7
|
import {ProfileSource} from './ProfileSource';
|
|
6
8
|
|
|
7
9
|
export interface IQueryResult {
|
|
@@ -10,11 +12,17 @@ export interface IQueryResult {
|
|
|
10
12
|
isLoading: boolean;
|
|
11
13
|
}
|
|
12
14
|
|
|
15
|
+
interface UseQueryOptions {
|
|
16
|
+
skip?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
export const useQuery = (
|
|
14
20
|
client: QueryServiceClient,
|
|
15
21
|
profileSource: ProfileSource,
|
|
16
|
-
reportType: QueryRequest_ReportType
|
|
22
|
+
reportType: QueryRequest_ReportType,
|
|
23
|
+
options?: UseQueryOptions
|
|
17
24
|
): IQueryResult => {
|
|
25
|
+
const {skip = false} = options || {};
|
|
18
26
|
const [result, setResult] = useState<IQueryResult>({
|
|
19
27
|
response: null,
|
|
20
28
|
error: null,
|
|
@@ -23,6 +31,9 @@ export const useQuery = (
|
|
|
23
31
|
const metadata = useGrpcMetadata();
|
|
24
32
|
|
|
25
33
|
useEffect(() => {
|
|
34
|
+
if (skip) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
26
37
|
setResult({
|
|
27
38
|
response: null,
|
|
28
39
|
error: null,
|