@parca/profile 0.13.13 → 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,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.13.15](https://github.com/parca-dev/parca/compare/ui-v0.13.14...ui-v0.13.15) (2022-07-12)
7
+
8
+ **Note:** Version bump only for package @parca/profile
9
+
6
10
  ## [0.13.13](https://github.com/parca-dev/parca/compare/ui-v0.13.12...ui-v0.13.13) (2022-06-29)
7
11
 
8
12
  **Note:** Version bump only for package @parca/profile
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@parca/profile",
3
- "version": "0.13.13",
3
+ "version": "0.13.15",
4
4
  "description": "Profile viewing libraries",
5
5
  "dependencies": {
6
- "@parca/client": "^0.13.13",
6
+ "@iconify/react": "^3.2.2",
7
+ "@parca/client": "^0.13.15",
7
8
  "@parca/dynamicsize": "^0.13.0",
8
9
  "@parca/parser": "^0.13.13",
9
- "d3-scale": "^4.0.2"
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": "843f14d7aba3e60c38a040ddbe59d98d84648a41"
24
+ "gitHead": "f069ec81f844a6b5bdac6530f5e8eaaee26265aa"
23
25
  }
@@ -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
- const req = {
74
- ...profileSource.QueryRequest(),
75
- reportType: QueryRequest_ReportType.PPROF,
76
- };
77
-
78
- queryClient
79
- .query(req, {meta: metadata})
80
- .response.then(response => {
81
- if (response.report.oneofKind !== 'pprof') {
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,7 +108,12 @@ 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>
@@ -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
+ };