@parca/profile 0.13.11-alpha.1 → 0.14.1

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,21 +3,21 @@
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.11-alpha.1](https://github.com/parca-dev/parca/compare/ui-v0.13.10...ui-v0.13.11-alpha.1) (2022-06-24)
6
+ ## [0.14.1](https://github.com/parca-dev/parca/compare/ui-v0.13.14...ui-v0.14.1) (2022-07-25)
7
7
 
8
8
  **Note:** Version bump only for package @parca/profile
9
9
 
10
-
11
-
12
-
13
-
14
- ## [0.13.11-alpha.0](https://github.com/parca-dev/parca/compare/ui-v0.13.10...ui-v0.13.11-alpha.0) (2022-06-24)
10
+ # [0.14.0](https://github.com/parca-dev/parca/compare/ui-v0.13.14...ui-v0.14.0) (2022-07-25)
15
11
 
16
12
  **Note:** Version bump only for package @parca/profile
17
13
 
14
+ ## [0.13.15](https://github.com/parca-dev/parca/compare/ui-v0.13.14...ui-v0.13.15) (2022-07-12)
18
15
 
16
+ **Note:** Version bump only for package @parca/profile
19
17
 
18
+ ## [0.13.13](https://github.com/parca-dev/parca/compare/ui-v0.13.12...ui-v0.13.13) (2022-06-29)
20
19
 
20
+ **Note:** Version bump only for package @parca/profile
21
21
 
22
22
  ## [0.13.10](https://github.com/parca-dev/parca/compare/ui-v0.13.9...ui-v0.13.10) (2022-06-22)
23
23
 
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@parca/profile",
3
- "version": "0.13.11-alpha.1",
3
+ "version": "0.14.1",
4
4
  "description": "Profile viewing libraries",
5
5
  "dependencies": {
6
- "@parca/client": "^0.13.10",
7
- "@parca/dynamicsize": "^0.13.0",
8
- "@parca/parser": "^0.13.0",
9
- "d3-scale": "^4.0.2"
6
+ "@iconify/react": "^3.2.2",
7
+ "@parca/client": "^0.14.1",
8
+ "@parca/dynamicsize": "^0.14.1",
9
+ "@parca/parser": "^0.14.1",
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": "2a54d215b391188a43ea7d4dfec880742747ba6c"
24
+ "gitHead": "0521a654350bbd2f758b7e2695bac1001ca11a63"
23
25
  }
@@ -1,12 +1,14 @@
1
1
  import React, {useEffect, useState} from 'react';
2
2
  import {parseParams} from '@parca/functions';
3
3
  import {QueryServiceClient, QueryRequest_ReportType} from '@parca/client';
4
- import {Button, Card, useGrpcMetadata, useParcaTheme} from '@parca/components';
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
 
@@ -46,6 +48,11 @@ export const ProfileView = ({
46
48
  const metadata = useGrpcMetadata();
47
49
  const {loader} = useParcaTheme();
48
50
 
51
+ useEffect(() => {
52
+ // Reset the current path when the profile source changes
53
+ setCurPath([]);
54
+ }, [profileSource]);
55
+
49
56
  useEffect(() => {
50
57
  let showLoaderTimeout;
51
58
  if (isLoading && !isLoaderVisible) {
@@ -67,31 +74,18 @@ export const ProfileView = ({
67
74
  return <div className="p-10 flex justify-center">An error occurred: {error.message}</div>;
68
75
  }
69
76
 
70
- const downloadPProf = (e: React.MouseEvent<HTMLElement>) => {
77
+ const downloadPProf = async (e: React.MouseEvent<HTMLElement>) => {
71
78
  e.preventDefault();
72
79
 
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
- });
80
+ try {
81
+ const blob = await downloadPprof(profileSource.QueryRequest(), queryClient, metadata);
82
+ const link = document.createElement('a');
83
+ link.href = window.URL.createObjectURL(blob);
84
+ link.download = 'profile.pb.gz';
85
+ link.click();
86
+ } catch (error) {
87
+ console.error('Error while querying', error);
88
+ }
95
89
  };
96
90
 
97
91
  const resetIcicleGraph = () => setCurPath([]);
@@ -119,13 +113,18 @@ export const ProfileView = ({
119
113
  <Card.Body>
120
114
  <div className="flex py-3 w-full">
121
115
  <div className="w-2/5 flex space-x-4">
122
- <div>
116
+ <div className="flex space-x-1">
117
+ <ProfileShareButton
118
+ queryRequest={profileSource.QueryRequest()}
119
+ queryClient={queryClient}
120
+ />
121
+
123
122
  <Button color="neutral" onClick={downloadPProf}>
124
123
  Download pprof
125
124
  </Button>
126
125
  </div>
127
126
 
128
- {/* <SearchNodes /> */}
127
+ <SearchNodes />
129
128
  </div>
130
129
 
131
130
  <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
+ };