@parca/profile 0.13.11-alpha.0 → 0.13.15
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,13 +3,13 @@
|
|
|
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.13.
|
|
6
|
+
## [0.13.15](https://github.com/parca-dev/parca/compare/ui-v0.13.14...ui-v0.13.15) (2022-07-12)
|
|
7
7
|
|
|
8
8
|
**Note:** Version bump only for package @parca/profile
|
|
9
9
|
|
|
10
|
+
## [0.13.13](https://github.com/parca-dev/parca/compare/ui-v0.13.12...ui-v0.13.13) (2022-06-29)
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
**Note:** Version bump only for package @parca/profile
|
|
13
13
|
|
|
14
14
|
## [0.13.10](https://github.com/parca-dev/parca/compare/ui-v0.13.9...ui-v0.13.10) (2022-06-22)
|
|
15
15
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parca/profile",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.15",
|
|
4
4
|
"description": "Profile viewing libraries",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@
|
|
6
|
+
"@iconify/react": "^3.2.2",
|
|
7
|
+
"@parca/client": "^0.13.15",
|
|
7
8
|
"@parca/dynamicsize": "^0.13.0",
|
|
8
|
-
"@parca/parser": "^0.13.
|
|
9
|
-
"d3-scale": "^4.0.2"
|
|
9
|
+
"@parca/parser": "^0.13.13",
|
|
10
|
+
"d3-scale": "^4.0.2",
|
|
11
|
+
"react-copy-to-clipboard": "^5.1.0"
|
|
10
12
|
},
|
|
11
13
|
"main": "src/index.tsx",
|
|
12
14
|
"scripts": {
|
|
@@ -19,5 +21,5 @@
|
|
|
19
21
|
"access": "public",
|
|
20
22
|
"registry": "https://registry.npmjs.org/"
|
|
21
23
|
},
|
|
22
|
-
"gitHead": "
|
|
24
|
+
"gitHead": "f069ec81f844a6b5bdac6530f5e8eaaee26265aa"
|
|
23
25
|
}
|
package/src/ProfileView.tsx
CHANGED
|
@@ -3,10 +3,12 @@ import {parseParams} from '@parca/functions';
|
|
|
3
3
|
import {QueryServiceClient, QueryRequest_ReportType} from '@parca/client';
|
|
4
4
|
import {Button, Card, SearchNodes, useGrpcMetadata, useParcaTheme} from '@parca/components';
|
|
5
5
|
|
|
6
|
+
import ProfileShareButton from './components/ProfileShareButton';
|
|
6
7
|
import ProfileIcicleGraph from './ProfileIcicleGraph';
|
|
7
8
|
import {ProfileSource} from './ProfileSource';
|
|
8
9
|
import {useQuery} from './useQuery';
|
|
9
10
|
import TopTable from './TopTable';
|
|
11
|
+
import {downloadPprof} from './utils';
|
|
10
12
|
|
|
11
13
|
import './ProfileView.styles.css';
|
|
12
14
|
|
|
@@ -67,31 +69,18 @@ export const ProfileView = ({
|
|
|
67
69
|
return <div className="p-10 flex justify-center">An error occurred: {error.message}</div>;
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
const downloadPProf = (e: React.MouseEvent<HTMLElement>) => {
|
|
72
|
+
const downloadPProf = async (e: React.MouseEvent<HTMLElement>) => {
|
|
71
73
|
e.preventDefault();
|
|
72
74
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
.
|
|
81
|
-
|
|
82
|
-
console.log('Expected pprof report, got:', response.report.oneofKind);
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
const blob = new Blob([response.report.pprof], {type: 'application/octet-stream'});
|
|
86
|
-
|
|
87
|
-
const link = document.createElement('a');
|
|
88
|
-
link.href = window.URL.createObjectURL(blob);
|
|
89
|
-
link.download = 'profile.pb.gz';
|
|
90
|
-
link.click();
|
|
91
|
-
})
|
|
92
|
-
.catch(error => {
|
|
93
|
-
console.error('Error while querying', error);
|
|
94
|
-
});
|
|
75
|
+
try {
|
|
76
|
+
const blob = await downloadPprof(profileSource.QueryRequest(), queryClient, metadata);
|
|
77
|
+
const link = document.createElement('a');
|
|
78
|
+
link.href = window.URL.createObjectURL(blob);
|
|
79
|
+
link.download = 'profile.pb.gz';
|
|
80
|
+
link.click();
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('Error while querying', error);
|
|
83
|
+
}
|
|
95
84
|
};
|
|
96
85
|
|
|
97
86
|
const resetIcicleGraph = () => setCurPath([]);
|
|
@@ -119,13 +108,18 @@ export const ProfileView = ({
|
|
|
119
108
|
<Card.Body>
|
|
120
109
|
<div className="flex py-3 w-full">
|
|
121
110
|
<div className="w-2/5 flex space-x-4">
|
|
122
|
-
<div>
|
|
111
|
+
<div className="flex space-x-1">
|
|
112
|
+
<ProfileShareButton
|
|
113
|
+
queryRequest={profileSource.QueryRequest()}
|
|
114
|
+
queryClient={queryClient}
|
|
115
|
+
/>
|
|
116
|
+
|
|
123
117
|
<Button color="neutral" onClick={downloadPProf}>
|
|
124
118
|
Download pprof
|
|
125
119
|
</Button>
|
|
126
120
|
</div>
|
|
127
121
|
|
|
128
|
-
|
|
122
|
+
<SearchNodes />
|
|
129
123
|
</div>
|
|
130
124
|
|
|
131
125
|
<div className="flex ml-auto">
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {useState} from 'react';
|
|
2
|
+
import cx from 'classnames';
|
|
3
|
+
import {Icon} from '@iconify/react';
|
|
4
|
+
import {Button} from '@parca/components';
|
|
5
|
+
import {CopyToClipboard} from 'react-copy-to-clipboard';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
value: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
|
13
|
+
|
|
14
|
+
const ResultBox = ({value, className = ''}: Props) => {
|
|
15
|
+
const [isCopied, setIsCopied] = useState<boolean>(false);
|
|
16
|
+
|
|
17
|
+
const onCopy = () => {
|
|
18
|
+
setIsCopied(true);
|
|
19
|
+
(window.document?.activeElement as HTMLElement)?.blur();
|
|
20
|
+
if (timeoutHandle != null) {
|
|
21
|
+
clearTimeout(timeoutHandle);
|
|
22
|
+
}
|
|
23
|
+
timeoutHandle = setTimeout(() => setIsCopied(false), 3000);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className={cx('flex flex-row w-full', {[className]: className?.length > 0})}>
|
|
28
|
+
<span className="flex justify-center items-center border border-r-0 w-16 rounded-l">
|
|
29
|
+
<Icon icon="ant-design:link-outlined" />
|
|
30
|
+
</span>
|
|
31
|
+
<input
|
|
32
|
+
type="text"
|
|
33
|
+
className="border text-sm bg-inherit w-full px-1 py-2 flex-grow"
|
|
34
|
+
value={value}
|
|
35
|
+
readOnly
|
|
36
|
+
/>
|
|
37
|
+
<CopyToClipboard text={value} onCopy={onCopy}>
|
|
38
|
+
<Button
|
|
39
|
+
variant="link"
|
|
40
|
+
className="border border-l-0 w-fit whitespace-nowrap p-4 items-center !text-indigo-600 dark:!text-indigo-400 rounded-none rounded-r"
|
|
41
|
+
>
|
|
42
|
+
{isCopied ? 'Copied!' : 'Copy Link'}
|
|
43
|
+
</Button>
|
|
44
|
+
</CopyToClipboard>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default ResultBox;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {useState} from 'react';
|
|
2
|
+
import {Button, Modal} from '@parca/components';
|
|
3
|
+
import {Icon} from '@iconify/react';
|
|
4
|
+
import {QueryRequest, QueryServiceClient} from '@parca/client';
|
|
5
|
+
import ResultBox from './ResultBox';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
queryRequest: QueryRequest;
|
|
9
|
+
queryClient: QueryServiceClient;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ProfileShareModalProps {
|
|
13
|
+
queryRequest: QueryRequest;
|
|
14
|
+
queryClient: QueryServiceClient;
|
|
15
|
+
isOpen: boolean;
|
|
16
|
+
closeModal: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const ProfileShareModal = ({
|
|
20
|
+
isOpen,
|
|
21
|
+
closeModal,
|
|
22
|
+
queryRequest,
|
|
23
|
+
queryClient,
|
|
24
|
+
}: ProfileShareModalProps) => {
|
|
25
|
+
const [isShared, setIsShared] = useState(false);
|
|
26
|
+
const [loading, setLoading] = useState<boolean>(false);
|
|
27
|
+
const [error, setError] = useState<string>('');
|
|
28
|
+
const [description, setDescription] = useState<string>('');
|
|
29
|
+
const [sharedLink, setSharedLink] = useState<string>('');
|
|
30
|
+
const isFormDataValid = () => true;
|
|
31
|
+
|
|
32
|
+
const handleSubmit: () => void = async () => {
|
|
33
|
+
try {
|
|
34
|
+
setLoading(true);
|
|
35
|
+
const {response} = await queryClient.shareProfile({queryRequest, description});
|
|
36
|
+
setSharedLink(response.link);
|
|
37
|
+
setLoading(false);
|
|
38
|
+
setIsShared(true);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error(err);
|
|
41
|
+
setLoading(false);
|
|
42
|
+
setError(err.toString());
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const onClose = () => {
|
|
47
|
+
setLoading(false);
|
|
48
|
+
setError('');
|
|
49
|
+
setDescription('');
|
|
50
|
+
setIsShared(false);
|
|
51
|
+
closeModal();
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Modal isOpen={isOpen} closeModal={onClose} title="Share Profile" className="w-[420px]">
|
|
56
|
+
<form className="py-2">
|
|
57
|
+
<p className="text-sm text-gray-500 dark:text-gray-300">
|
|
58
|
+
Note: Shared profiles can be accessed by anyone with the link, even from people outside
|
|
59
|
+
your organisation.
|
|
60
|
+
</p>
|
|
61
|
+
{!isShared || error?.length > 0 ? (
|
|
62
|
+
<>
|
|
63
|
+
<p className="text-sm text-gray-500 dark:text-gray-300 mt-3 mb-2">
|
|
64
|
+
Enter a description (optional)
|
|
65
|
+
</p>
|
|
66
|
+
<textarea
|
|
67
|
+
className="border w-full text-gray-500 dark:text-gray-300 bg-inherit text-sm px-2 py-2"
|
|
68
|
+
value={description}
|
|
69
|
+
onChange={e => setDescription(e.target.value)}
|
|
70
|
+
></textarea>
|
|
71
|
+
<Button
|
|
72
|
+
className="w-fit mt-4"
|
|
73
|
+
onClick={e => {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
handleSubmit();
|
|
76
|
+
}}
|
|
77
|
+
disabled={loading || !isFormDataValid()}
|
|
78
|
+
type="submit"
|
|
79
|
+
>
|
|
80
|
+
{loading ? 'Sharing' : 'Share'}
|
|
81
|
+
</Button>
|
|
82
|
+
{error !== '' ? <p>Something went wrong please try again</p> : null}
|
|
83
|
+
</>
|
|
84
|
+
) : (
|
|
85
|
+
<>
|
|
86
|
+
<ResultBox value={sharedLink} className="mt-4" />
|
|
87
|
+
<div className="flex justify-center mt-8">
|
|
88
|
+
<Button variant="neutral" className="w-fit" onClick={onClose}>
|
|
89
|
+
Close
|
|
90
|
+
</Button>
|
|
91
|
+
</div>
|
|
92
|
+
</>
|
|
93
|
+
)}
|
|
94
|
+
</form>
|
|
95
|
+
</Modal>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const ProfileShareButton = ({queryRequest, queryClient}: Props) => {
|
|
100
|
+
const [isOpen, setIsOpen] = useState<boolean>(false);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<>
|
|
104
|
+
<Button color="neutral" className="w-fit" onClick={() => setIsOpen(true)}>
|
|
105
|
+
<Icon icon="ei:share-apple" width={20} />
|
|
106
|
+
</Button>
|
|
107
|
+
<ProfileShareModal
|
|
108
|
+
isOpen={isOpen}
|
|
109
|
+
closeModal={() => setIsOpen(false)}
|
|
110
|
+
queryRequest={queryRequest}
|
|
111
|
+
queryClient={queryClient}
|
|
112
|
+
/>
|
|
113
|
+
</>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export default ProfileShareButton;
|
package/src/utils.ts
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
|
+
import {QueryRequest, QueryRequest_ReportType, QueryServiceClient} from '@parca/client';
|
|
2
|
+
import {RpcMetadata} from '@protobuf-ts/runtime-rpc';
|
|
3
|
+
|
|
1
4
|
export const hexifyAddress = (address?: string): string => {
|
|
2
5
|
if (address == null) {
|
|
3
6
|
return '';
|
|
4
7
|
}
|
|
5
8
|
return `0x${parseInt(address, 10).toString(16)}`;
|
|
6
9
|
};
|
|
10
|
+
|
|
11
|
+
export const downloadPprof = async (
|
|
12
|
+
request: QueryRequest,
|
|
13
|
+
queryClient: QueryServiceClient,
|
|
14
|
+
metadata: RpcMetadata
|
|
15
|
+
) => {
|
|
16
|
+
const req = {
|
|
17
|
+
...request,
|
|
18
|
+
reportType: QueryRequest_ReportType.PPROF,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const {response} = await queryClient.query(req, {meta: metadata});
|
|
22
|
+
if (response.report.oneofKind !== 'pprof') {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Expected pprof report, got: ${
|
|
25
|
+
response.report.oneofKind !== undefined ? response.report.oneofKind : 'undefined'
|
|
26
|
+
}`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
const blob = new Blob([response.report.pprof], {type: 'application/octet-stream'});
|
|
30
|
+
return blob;
|
|
31
|
+
};
|