@parca/profile 0.16.269 → 0.16.274
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 +20 -0
- package/dist/GraphTooltipArrow/Content.js +9 -36
- package/dist/GraphTooltipArrow/DockedGraphTooltip/index.js +16 -25
- package/dist/GraphTooltipArrow/index.d.ts +2 -1
- package/dist/GraphTooltipArrow/index.js +3 -5
- package/dist/MetricsGraph/MetricsContextMenu/index.d.ts +16 -0
- package/dist/MetricsGraph/MetricsContextMenu/index.js +27 -0
- package/dist/MetricsGraph/MetricsTooltip/index.d.ts +1 -2
- package/dist/MetricsGraph/MetricsTooltip/index.js +3 -4
- package/dist/MetricsGraph/index.d.ts +9 -3
- package/dist/MetricsGraph/index.js +23 -17
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/ContextMenu.d.ts +19 -0
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/ContextMenu.js +75 -0
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/IcicleGraphNodes.d.ts +3 -0
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/IcicleGraphNodes.js +7 -8
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/index.js +20 -3
- package/dist/ProfileMetricsGraph/index.d.ts +7 -1
- package/dist/ProfileMetricsGraph/index.js +1 -1
- package/dist/ProfileSelector/index.js +23 -4
- package/dist/styles.css +1 -1
- package/package.json +10 -9
- package/src/GraphTooltipArrow/Content.tsx +24 -112
- package/src/GraphTooltipArrow/DockedGraphTooltip/index.tsx +34 -128
- package/src/GraphTooltipArrow/index.tsx +4 -6
- package/src/MetricsGraph/MetricsContextMenu/index.tsx +81 -0
- package/src/MetricsGraph/MetricsTooltip/index.tsx +17 -21
- package/src/MetricsGraph/index.tsx +48 -27
- package/src/ProfileIcicleGraph/IcicleGraphArrow/ContextMenu.tsx +181 -0
- package/src/ProfileIcicleGraph/IcicleGraphArrow/IcicleGraphNodes.tsx +10 -5
- package/src/ProfileIcicleGraph/IcicleGraphArrow/index.tsx +63 -21
- package/src/ProfileMetricsGraph/index.tsx +4 -2
- package/src/ProfileSelector/index.tsx +27 -4
- package/yarn-error.log +28744 -0
|
@@ -11,25 +11,18 @@
|
|
|
11
11
|
// See the License for the specific language governing permissions and
|
|
12
12
|
// limitations under the License.
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
|
|
14
|
+
import {Icon} from '@iconify/react';
|
|
16
15
|
import {Table} from 'apache-arrow';
|
|
17
16
|
import cx from 'classnames';
|
|
18
|
-
import {CopyToClipboard} from 'react-copy-to-clipboard';
|
|
19
|
-
import {Tooltip} from 'react-tooltip';
|
|
20
17
|
import {useWindowSize} from 'react-use';
|
|
21
18
|
|
|
22
|
-
import {
|
|
23
|
-
import {USER_PREFERENCES, useUserPreference} from '@parca/hooks';
|
|
19
|
+
import {useParcaContext} from '@parca/components';
|
|
24
20
|
import {getLastItem} from '@parca/utilities';
|
|
25
21
|
|
|
26
22
|
import {hexifyAddress, truncateString, truncateStringReverse} from '../../utils';
|
|
27
|
-
import {ExpandOnHover} from '../ExpandOnHoverValue';
|
|
28
23
|
import {useGraphTooltip} from '../useGraphTooltip';
|
|
29
24
|
import {useGraphTooltipMetaInfo} from '../useGraphTooltipMetaInfo';
|
|
30
25
|
|
|
31
|
-
let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
|
32
|
-
|
|
33
26
|
interface Props {
|
|
34
27
|
table: Table<any>;
|
|
35
28
|
unit: string;
|
|
@@ -42,28 +35,24 @@ interface Props {
|
|
|
42
35
|
const InfoSection = ({
|
|
43
36
|
title,
|
|
44
37
|
value,
|
|
45
|
-
onCopy,
|
|
46
|
-
copyText,
|
|
47
38
|
minWidth = '',
|
|
48
39
|
}: {
|
|
49
40
|
title: string;
|
|
50
41
|
value: string | JSX.Element;
|
|
51
|
-
copyText: string;
|
|
52
|
-
onCopy: () => void;
|
|
53
42
|
minWidth?: string;
|
|
54
43
|
}): JSX.Element => {
|
|
55
44
|
return (
|
|
56
45
|
<div className={cx('flex shrink-0 flex-col gap-1 p-2', {[minWidth]: minWidth != null})}>
|
|
57
46
|
<p className="text-sm font-medium leading-5 text-gray-500 dark:text-gray-400">{title}</p>
|
|
58
|
-
<div className="text-lg font-normal text-gray-900 dark:text-gray-50">
|
|
59
|
-
<CopyToClipboard onCopy={onCopy} text={copyText}>
|
|
60
|
-
<button>{value}</button>
|
|
61
|
-
</CopyToClipboard>
|
|
62
|
-
</div>
|
|
47
|
+
<div className="text-lg font-normal text-gray-900 dark:text-gray-50">{value}</div>
|
|
63
48
|
</div>
|
|
64
49
|
);
|
|
65
50
|
};
|
|
66
51
|
|
|
52
|
+
const NoData = (): React.JSX.Element => {
|
|
53
|
+
return <span className="rounded bg-gray-200 px-2 dark:bg-gray-800">Not available</span>;
|
|
54
|
+
};
|
|
55
|
+
|
|
67
56
|
export const DockedGraphTooltip = ({
|
|
68
57
|
table,
|
|
69
58
|
unit,
|
|
@@ -73,19 +62,9 @@ export const DockedGraphTooltip = ({
|
|
|
73
62
|
level,
|
|
74
63
|
}: Props): JSX.Element => {
|
|
75
64
|
let {width} = useWindowSize();
|
|
76
|
-
const {profileExplorer, navigateTo
|
|
65
|
+
const {profileExplorer, navigateTo} = useParcaContext();
|
|
77
66
|
const {PaddingX} = profileExplorer ?? {PaddingX: 0};
|
|
78
67
|
width = width - PaddingX - 24;
|
|
79
|
-
const [isCopied, setIsCopied] = useState<boolean>(false);
|
|
80
|
-
|
|
81
|
-
const onCopy = (): void => {
|
|
82
|
-
setIsCopied(true);
|
|
83
|
-
|
|
84
|
-
if (timeoutHandle !== null) {
|
|
85
|
-
clearTimeout(timeoutHandle);
|
|
86
|
-
}
|
|
87
|
-
timeoutHandle = setTimeout(() => setIsCopied(false), 3000);
|
|
88
|
-
};
|
|
89
68
|
|
|
90
69
|
const graphTooltipData = useGraphTooltip({
|
|
91
70
|
table,
|
|
@@ -100,16 +79,12 @@ export const DockedGraphTooltip = ({
|
|
|
100
79
|
labelPairs,
|
|
101
80
|
functionFilename,
|
|
102
81
|
file,
|
|
103
|
-
openFile,
|
|
104
|
-
isSourceAvailable,
|
|
105
82
|
locationAddress,
|
|
106
83
|
mappingFile,
|
|
107
84
|
mappingBuildID,
|
|
108
85
|
inlined,
|
|
109
86
|
} = useGraphTooltipMetaInfo({table, row: row ?? 0, navigateTo});
|
|
110
87
|
|
|
111
|
-
const [_, setIsDocked] = useUserPreference(USER_PREFERENCES.GRAPH_METAINFO_DOCKED.key);
|
|
112
|
-
|
|
113
88
|
if (graphTooltipData === null) {
|
|
114
89
|
return <></>;
|
|
115
90
|
}
|
|
@@ -127,9 +102,9 @@ export const DockedGraphTooltip = ({
|
|
|
127
102
|
)
|
|
128
103
|
);
|
|
129
104
|
|
|
105
|
+
const isMappingBuildIDAvailable = mappingBuildID !== null && mappingBuildID !== '';
|
|
130
106
|
const inlinedText = inlined === null ? 'merged' : inlined ? 'yes' : 'no';
|
|
131
|
-
const addressText = locationAddress !== 0n ? hexifyAddress(locationAddress) :
|
|
132
|
-
const fileText = functionFilename !== '' ? file : 'Not available';
|
|
107
|
+
const addressText = locationAddress !== 0n ? hexifyAddress(locationAddress) : <NoData />;
|
|
133
108
|
|
|
134
109
|
return (
|
|
135
110
|
<div
|
|
@@ -141,117 +116,48 @@ export const DockedGraphTooltip = ({
|
|
|
141
116
|
{row === 0 ? (
|
|
142
117
|
<p>root</p>
|
|
143
118
|
) : (
|
|
144
|
-
|
|
145
|
-
{name !== ''
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
{locationAddress !== 0n ? (
|
|
152
|
-
<CopyToClipboard onCopy={onCopy} text={hexifyAddress(locationAddress)}>
|
|
153
|
-
<button className="cursor-pointer text-left">
|
|
154
|
-
{hexifyAddress(locationAddress)}
|
|
155
|
-
</button>
|
|
156
|
-
</CopyToClipboard>
|
|
157
|
-
) : (
|
|
158
|
-
<p>unknown</p>
|
|
159
|
-
)}
|
|
160
|
-
</>
|
|
161
|
-
)}
|
|
162
|
-
</>
|
|
119
|
+
<p>
|
|
120
|
+
{name !== ''
|
|
121
|
+
? name
|
|
122
|
+
: locationAddress !== 0n
|
|
123
|
+
? hexifyAddress(locationAddress)
|
|
124
|
+
: 'unknown'}
|
|
125
|
+
</p>
|
|
163
126
|
)}
|
|
164
|
-
<IconButton
|
|
165
|
-
onClick={() => setIsDocked(false)}
|
|
166
|
-
icon="mdi:dock-window"
|
|
167
|
-
title="Undock MetaInfo Panel"
|
|
168
|
-
/>
|
|
169
127
|
</div>
|
|
170
128
|
<div className="flex justify-between gap-3">
|
|
171
|
-
<InfoSection
|
|
172
|
-
|
|
173
|
-
value={cumulativeText}
|
|
174
|
-
onCopy={onCopy}
|
|
175
|
-
copyText={cumulativeText}
|
|
176
|
-
minWidth="w-44"
|
|
177
|
-
/>
|
|
178
|
-
{diff !== 0n ? (
|
|
179
|
-
<InfoSection
|
|
180
|
-
title="Diff"
|
|
181
|
-
value={diffText}
|
|
182
|
-
onCopy={onCopy}
|
|
183
|
-
copyText={diffText}
|
|
184
|
-
minWidth="w-44"
|
|
185
|
-
/>
|
|
186
|
-
) : null}
|
|
129
|
+
<InfoSection title="Cumulative" value={cumulativeText} minWidth="w-44" />
|
|
130
|
+
{diff !== 0n ? <InfoSection title="Diff" value={diffText} minWidth="w-44" /> : null}
|
|
187
131
|
<InfoSection
|
|
188
132
|
title="File"
|
|
189
|
-
value={
|
|
190
|
-
<div className="flex gap-2">
|
|
191
|
-
<ExpandOnHover
|
|
192
|
-
value={fileText}
|
|
193
|
-
displayValue={truncateStringReverse(fileText, 45)}
|
|
194
|
-
/>
|
|
195
|
-
<div
|
|
196
|
-
className={cx('flex items-center gap-2', {
|
|
197
|
-
hidden: enableSourcesView === false || functionFilename === '',
|
|
198
|
-
})}
|
|
199
|
-
>
|
|
200
|
-
<div
|
|
201
|
-
data-tooltip-id="open-source-button-help"
|
|
202
|
-
data-tooltip-content="There is no source code uploaded for this build"
|
|
203
|
-
>
|
|
204
|
-
<Button
|
|
205
|
-
variant={'neutral'}
|
|
206
|
-
onClick={() => openFile()}
|
|
207
|
-
className="shrink-0"
|
|
208
|
-
disabled={!isSourceAvailable}
|
|
209
|
-
>
|
|
210
|
-
open
|
|
211
|
-
</Button>
|
|
212
|
-
</div>
|
|
213
|
-
{!isSourceAvailable ? <Tooltip id="open-source-button-help" /> : null}
|
|
214
|
-
</div>
|
|
215
|
-
</div>
|
|
216
|
-
}
|
|
217
|
-
onCopy={onCopy}
|
|
218
|
-
copyText={file}
|
|
133
|
+
value={functionFilename !== '' ? truncateStringReverse(file, 45) : <NoData />}
|
|
219
134
|
minWidth={'w-[460px]'}
|
|
220
135
|
/>
|
|
221
|
-
<InfoSection
|
|
222
|
-
|
|
223
|
-
value={addressText}
|
|
224
|
-
onCopy={onCopy}
|
|
225
|
-
copyText={addressText}
|
|
226
|
-
minWidth="w-44"
|
|
227
|
-
/>
|
|
228
|
-
<InfoSection
|
|
229
|
-
title="Inlined"
|
|
230
|
-
value={inlinedText}
|
|
231
|
-
onCopy={onCopy}
|
|
232
|
-
copyText={inlinedText}
|
|
233
|
-
minWidth="w-44"
|
|
234
|
-
/>
|
|
136
|
+
<InfoSection title="Address" value={addressText} minWidth="w-44" />
|
|
137
|
+
<InfoSection title="Inlined" value={inlinedText} minWidth="w-44" />
|
|
235
138
|
<InfoSection
|
|
236
139
|
title="Binary"
|
|
237
|
-
value={(mappingFile != null ? getLastItem(mappingFile) : null) ??
|
|
238
|
-
onCopy={onCopy}
|
|
239
|
-
copyText={mappingFile ?? 'Not available'}
|
|
140
|
+
value={(mappingFile != null ? getLastItem(mappingFile) : null) ?? <NoData />}
|
|
240
141
|
minWidth="w-44"
|
|
241
142
|
/>
|
|
242
143
|
<InfoSection
|
|
243
144
|
title="Build ID"
|
|
244
|
-
value={
|
|
245
|
-
|
|
246
|
-
|
|
145
|
+
value={
|
|
146
|
+
isMappingBuildIDAvailable ? (
|
|
147
|
+
<div>{truncateString(getLastItem(mappingBuildID) as string, 28)}</div>
|
|
148
|
+
) : (
|
|
149
|
+
<NoData />
|
|
150
|
+
)
|
|
151
|
+
}
|
|
247
152
|
/>
|
|
248
153
|
</div>
|
|
249
154
|
<div>
|
|
250
155
|
<div className="flex h-5 gap-1">{labels}</div>
|
|
251
156
|
</div>
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
157
|
+
</div>
|
|
158
|
+
<div className="flex w-full items-center gap-1 text-xs text-gray-500">
|
|
159
|
+
<Icon icon="iconoir:mouse-button-right" />
|
|
160
|
+
<div>Right click to show context menu</div>
|
|
255
161
|
</div>
|
|
256
162
|
</div>
|
|
257
163
|
);
|
|
@@ -16,8 +16,6 @@ import React, {useEffect, useState} from 'react';
|
|
|
16
16
|
import {pointer} from 'd3-selection';
|
|
17
17
|
import {usePopper} from 'react-popper';
|
|
18
18
|
|
|
19
|
-
import {useKeyDown} from '@parca/components';
|
|
20
|
-
|
|
21
19
|
interface GraphTooltipProps {
|
|
22
20
|
children: React.ReactNode;
|
|
23
21
|
x?: number;
|
|
@@ -25,6 +23,7 @@ interface GraphTooltipProps {
|
|
|
25
23
|
contextElement: Element | null;
|
|
26
24
|
isFixed?: boolean;
|
|
27
25
|
virtualContextElement?: boolean;
|
|
26
|
+
isContextMenuOpen?: boolean;
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
const virtualElement = {
|
|
@@ -61,6 +60,7 @@ const GraphTooltip = ({
|
|
|
61
60
|
contextElement,
|
|
62
61
|
isFixed = false,
|
|
63
62
|
virtualContextElement = true,
|
|
63
|
+
isContextMenuOpen = false,
|
|
64
64
|
}: GraphTooltipProps): React.JSX.Element => {
|
|
65
65
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
|
66
66
|
|
|
@@ -88,12 +88,10 @@ const GraphTooltip = ({
|
|
|
88
88
|
}
|
|
89
89
|
);
|
|
90
90
|
|
|
91
|
-
const {isShiftDown} = useKeyDown();
|
|
92
|
-
|
|
93
91
|
useEffect(() => {
|
|
94
92
|
if (contextElement === null) return;
|
|
95
93
|
const onMouseMove: EventListenerOrEventListenerObject = (e: Event) => {
|
|
96
|
-
if (
|
|
94
|
+
if (isContextMenuOpen) {
|
|
97
95
|
return;
|
|
98
96
|
}
|
|
99
97
|
|
|
@@ -117,7 +115,7 @@ const GraphTooltip = ({
|
|
|
117
115
|
return () => {
|
|
118
116
|
contextElement.removeEventListener('mousemove', onMouseMove);
|
|
119
117
|
};
|
|
120
|
-
}, [contextElement, popperProps,
|
|
118
|
+
}, [contextElement, popperProps, x, y, isContextMenuOpen]);
|
|
121
119
|
|
|
122
120
|
return isFixed ? (
|
|
123
121
|
<>{children}</>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Copyright 2022 The Parca Authors
|
|
2
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
// you may not use this file except in compliance with the License.
|
|
4
|
+
// You may obtain a copy of the License at
|
|
5
|
+
//
|
|
6
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
//
|
|
8
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
// See the License for the specific language governing permissions and
|
|
12
|
+
// limitations under the License.
|
|
13
|
+
|
|
14
|
+
import {Icon} from '@iconify/react';
|
|
15
|
+
import {Item, Menu, Submenu} from 'react-contexify';
|
|
16
|
+
|
|
17
|
+
import {Label} from '@parca/client';
|
|
18
|
+
|
|
19
|
+
import {HighlightedSeries} from '../';
|
|
20
|
+
|
|
21
|
+
interface MetricsContextMenuProps {
|
|
22
|
+
menuId: string;
|
|
23
|
+
onAddLabelMatcher: (
|
|
24
|
+
labels: {key: string; value: string} | Array<{key: string; value: string}>
|
|
25
|
+
) => void;
|
|
26
|
+
highlighted: HighlightedSeries | null;
|
|
27
|
+
trackVisibility: (isVisible: boolean) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const MetricsContextMenu = ({
|
|
31
|
+
menuId,
|
|
32
|
+
onAddLabelMatcher,
|
|
33
|
+
highlighted,
|
|
34
|
+
trackVisibility,
|
|
35
|
+
}: MetricsContextMenuProps): JSX.Element => {
|
|
36
|
+
const labels = highlighted?.labels.filter((label: Label) => label.name !== '__name__');
|
|
37
|
+
|
|
38
|
+
const handleFocusOnSingleSeries = (): void => {
|
|
39
|
+
const labelsToAdd = labels?.map((label: Label) => ({
|
|
40
|
+
key: label.name,
|
|
41
|
+
value: label.value,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
labelsToAdd !== undefined && onAddLabelMatcher(labelsToAdd);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Menu id={menuId} onVisibilityChange={trackVisibility}>
|
|
49
|
+
<Item id="focus-on-single-series" onClick={handleFocusOnSingleSeries}>
|
|
50
|
+
<div className="flex w-full items-center gap-2">
|
|
51
|
+
<Icon icon="ph:star" />
|
|
52
|
+
<div>Focus only on this series</div>
|
|
53
|
+
</div>
|
|
54
|
+
</Item>
|
|
55
|
+
<Submenu
|
|
56
|
+
label={
|
|
57
|
+
<div className="flex w-full items-center gap-2">
|
|
58
|
+
<Icon icon="material-symbols:add" />
|
|
59
|
+
<div>Add to query</div>
|
|
60
|
+
</div>
|
|
61
|
+
}
|
|
62
|
+
style={{maxHeight: '300px', overflow: 'scroll'}}
|
|
63
|
+
>
|
|
64
|
+
{labels?.map((label: Label) => (
|
|
65
|
+
<Item
|
|
66
|
+
key={label.name}
|
|
67
|
+
id={label.name}
|
|
68
|
+
onClick={() => onAddLabelMatcher({key: label.name, value: label.value})}
|
|
69
|
+
style={{maxWidth: '400px', overflow: 'hidden'}}
|
|
70
|
+
>
|
|
71
|
+
<div className="mr-3 inline-block rounded-lg bg-gray-200 px-2 py-1 text-xs font-bold text-gray-700 dark:bg-gray-700 dark:text-gray-400">
|
|
72
|
+
{`${label.name}="${label.value}"`}
|
|
73
|
+
</div>
|
|
74
|
+
</Item>
|
|
75
|
+
))}
|
|
76
|
+
</Submenu>
|
|
77
|
+
</Menu>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export default MetricsContextMenu;
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import {useEffect, useState} from 'react';
|
|
15
15
|
|
|
16
|
+
import {Icon} from '@iconify/react';
|
|
16
17
|
import type {VirtualElement} from '@popperjs/core';
|
|
17
18
|
import {usePopper} from 'react-popper';
|
|
18
19
|
|
|
@@ -27,7 +28,6 @@ interface Props {
|
|
|
27
28
|
x: number;
|
|
28
29
|
y: number;
|
|
29
30
|
highlighted: HighlightedSeries;
|
|
30
|
-
onLabelClick: (labelName: string, labelValue: string) => void;
|
|
31
31
|
contextElement: Element | null;
|
|
32
32
|
sampleUnit: string;
|
|
33
33
|
delta: boolean;
|
|
@@ -65,7 +65,6 @@ const MetricsTooltip = ({
|
|
|
65
65
|
x,
|
|
66
66
|
y,
|
|
67
67
|
highlighted,
|
|
68
|
-
onLabelClick,
|
|
69
68
|
contextElement,
|
|
70
69
|
sampleUnit,
|
|
71
70
|
delta,
|
|
@@ -150,26 +149,23 @@ const MetricsTooltip = ({
|
|
|
150
149
|
<span className="my-2 block text-gray-500">
|
|
151
150
|
{highlighted.labels
|
|
152
151
|
.filter((label: Label) => label.name !== '__name__')
|
|
153
|
-
.map(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
/>
|
|
166
|
-
</button>
|
|
167
|
-
);
|
|
168
|
-
})}
|
|
169
|
-
</span>
|
|
170
|
-
<span className="block text-xs text-gray-500">
|
|
171
|
-
Hold shift and click label to add to query.
|
|
152
|
+
.map((label: Label) => (
|
|
153
|
+
<div
|
|
154
|
+
key={label.name}
|
|
155
|
+
className="mr-3 inline-block rounded-lg bg-gray-200 px-2 py-1 text-xs font-bold text-gray-700 dark:bg-gray-700 dark:text-gray-400"
|
|
156
|
+
>
|
|
157
|
+
<TextWithTooltip
|
|
158
|
+
text={`${label.name}="${label.value}"`}
|
|
159
|
+
maxTextLength={37}
|
|
160
|
+
id={`tooltip-${label.name}-${label.value}`}
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
))}
|
|
172
164
|
</span>
|
|
165
|
+
<div className="flex w-full items-center gap-1 text-xs text-gray-500">
|
|
166
|
+
<Icon icon="iconoir:mouse-button-right" />
|
|
167
|
+
<div>Right click to add labels to query.</div>
|
|
168
|
+
</div>
|
|
173
169
|
</div>
|
|
174
170
|
</div>
|
|
175
171
|
</div>
|
|
@@ -11,14 +11,15 @@
|
|
|
11
11
|
// See the License for the specific language governing permissions and
|
|
12
12
|
// limitations under the License.
|
|
13
13
|
|
|
14
|
-
import React, {Fragment, useRef, useState} from 'react';
|
|
14
|
+
import React, {Fragment, useCallback, useRef, useState} from 'react';
|
|
15
15
|
|
|
16
16
|
import * as d3 from 'd3';
|
|
17
17
|
import {pointer} from 'd3-selection';
|
|
18
18
|
import throttle from 'lodash.throttle';
|
|
19
|
+
import {useContextMenu} from 'react-contexify';
|
|
19
20
|
|
|
20
21
|
import {Label, MetricsSample, MetricsSeries as MetricsSeriesPb} from '@parca/client';
|
|
21
|
-
import {DateTimeRange
|
|
22
|
+
import {DateTimeRange} from '@parca/components';
|
|
22
23
|
import {
|
|
23
24
|
formatDate,
|
|
24
25
|
formatForTimespan,
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
import {MergedProfileSelection} from '..';
|
|
30
31
|
import MetricsCircle from '../MetricsCircle';
|
|
31
32
|
import MetricsSeries from '../MetricsSeries';
|
|
33
|
+
import MetricsContextMenu from './MetricsContextMenu';
|
|
32
34
|
import MetricsTooltip from './MetricsTooltip';
|
|
33
35
|
|
|
34
36
|
interface Props {
|
|
@@ -37,7 +39,9 @@ interface Props {
|
|
|
37
39
|
to: number;
|
|
38
40
|
profile: MergedProfileSelection | null;
|
|
39
41
|
onSampleClick: (timestamp: number, value: number, labels: Label[]) => void;
|
|
40
|
-
|
|
42
|
+
addLabelMatcher: (
|
|
43
|
+
labels: {key: string; value: string} | Array<{key: string; value: string}>
|
|
44
|
+
) => void;
|
|
41
45
|
setTimeRange: (range: DateTimeRange) => void;
|
|
42
46
|
sampleUnit: string;
|
|
43
47
|
width?: number;
|
|
@@ -68,7 +72,7 @@ const MetricsGraph = ({
|
|
|
68
72
|
to,
|
|
69
73
|
profile,
|
|
70
74
|
onSampleClick,
|
|
71
|
-
|
|
75
|
+
addLabelMatcher,
|
|
72
76
|
setTimeRange,
|
|
73
77
|
sampleUnit,
|
|
74
78
|
width = 0,
|
|
@@ -82,7 +86,7 @@ const MetricsGraph = ({
|
|
|
82
86
|
to={to}
|
|
83
87
|
profile={profile}
|
|
84
88
|
onSampleClick={onSampleClick}
|
|
85
|
-
|
|
89
|
+
addLabelMatcher={addLabelMatcher}
|
|
86
90
|
setTimeRange={setTimeRange}
|
|
87
91
|
sampleUnit={sampleUnit}
|
|
88
92
|
width={width}
|
|
@@ -110,7 +114,7 @@ export const RawMetricsGraph = ({
|
|
|
110
114
|
to,
|
|
111
115
|
profile,
|
|
112
116
|
onSampleClick,
|
|
113
|
-
|
|
117
|
+
addLabelMatcher,
|
|
114
118
|
setTimeRange,
|
|
115
119
|
width,
|
|
116
120
|
height = 50,
|
|
@@ -122,8 +126,8 @@ export const RawMetricsGraph = ({
|
|
|
122
126
|
const [hovering, setHovering] = useState(false);
|
|
123
127
|
const [relPos, setRelPos] = useState(-1);
|
|
124
128
|
const [pos, setPos] = useState([0, 0]);
|
|
129
|
+
const [isContextMenuOpen, setIsContextMenuOpen] = useState<boolean>(false);
|
|
125
130
|
const metricPointRef = useRef(null);
|
|
126
|
-
const {isShiftDown} = useKeyDown();
|
|
127
131
|
|
|
128
132
|
// the time of the selected point is the start of the merge window
|
|
129
133
|
const time: number = parseFloat(profile?.HistoryParams().merge_from);
|
|
@@ -222,11 +226,6 @@ export const RawMetricsGraph = ({
|
|
|
222
226
|
const highlighted = getClosest();
|
|
223
227
|
|
|
224
228
|
const onMouseDown = (e: React.MouseEvent<SVGSVGElement | HTMLDivElement, MouseEvent>): void => {
|
|
225
|
-
// if shift is down, disable mouse behavior
|
|
226
|
-
if (isShiftDown) {
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
229
|
// only left mouse button
|
|
231
230
|
if (e.button !== 0) {
|
|
232
231
|
return;
|
|
@@ -257,10 +256,6 @@ export const RawMetricsGraph = ({
|
|
|
257
256
|
};
|
|
258
257
|
|
|
259
258
|
const onMouseUp = (e: React.MouseEvent<SVGSVGElement | HTMLDivElement, MouseEvent>): void => {
|
|
260
|
-
if (isShiftDown) {
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
259
|
setDragging(false);
|
|
265
260
|
|
|
266
261
|
if (relPos === -1) {
|
|
@@ -293,8 +288,7 @@ export const RawMetricsGraph = ({
|
|
|
293
288
|
const throttledSetPos = throttle(setPos, 20);
|
|
294
289
|
|
|
295
290
|
const onMouseMove = (e: React.MouseEvent<SVGSVGElement | HTMLDivElement, MouseEvent>): void => {
|
|
296
|
-
|
|
297
|
-
if (isShiftDown) {
|
|
291
|
+
if (isContextMenuOpen) {
|
|
298
292
|
return;
|
|
299
293
|
}
|
|
300
294
|
|
|
@@ -358,23 +352,49 @@ export const RawMetricsGraph = ({
|
|
|
358
352
|
|
|
359
353
|
const selected = findSelectedProfile();
|
|
360
354
|
|
|
355
|
+
const MENU_ID = 'metrics-context-menu';
|
|
356
|
+
|
|
357
|
+
const {show} = useContextMenu({
|
|
358
|
+
id: MENU_ID,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const displayMenu = useCallback(
|
|
362
|
+
(e: React.MouseEvent): void => {
|
|
363
|
+
show({
|
|
364
|
+
event: e,
|
|
365
|
+
});
|
|
366
|
+
},
|
|
367
|
+
[show]
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
const trackVisibility = (isVisible: boolean): void => {
|
|
371
|
+
setIsContextMenuOpen(isVisible);
|
|
372
|
+
};
|
|
373
|
+
|
|
361
374
|
return (
|
|
362
375
|
<>
|
|
376
|
+
<MetricsContextMenu
|
|
377
|
+
onAddLabelMatcher={addLabelMatcher}
|
|
378
|
+
menuId={MENU_ID}
|
|
379
|
+
highlighted={highlighted}
|
|
380
|
+
trackVisibility={trackVisibility}
|
|
381
|
+
/>
|
|
363
382
|
{highlighted != null && hovering && !dragging && pos[0] !== 0 && pos[1] !== 0 && (
|
|
364
383
|
<div
|
|
365
384
|
onMouseMove={onMouseMove}
|
|
366
385
|
onMouseEnter={() => setHovering(true)}
|
|
367
386
|
onMouseLeave={() => setHovering(false)}
|
|
368
387
|
>
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
388
|
+
{!isContextMenuOpen && (
|
|
389
|
+
<MetricsTooltip
|
|
390
|
+
x={pos[0] + margin}
|
|
391
|
+
y={pos[1] + margin}
|
|
392
|
+
highlighted={highlighted}
|
|
393
|
+
contextElement={graph.current}
|
|
394
|
+
sampleUnit={sampleUnit}
|
|
395
|
+
delta={profile !== null ? profile?.query.profType.delta : false}
|
|
396
|
+
/>
|
|
397
|
+
)}
|
|
378
398
|
</div>
|
|
379
399
|
)}
|
|
380
400
|
<div
|
|
@@ -383,6 +403,7 @@ export const RawMetricsGraph = ({
|
|
|
383
403
|
setHovering(true);
|
|
384
404
|
}}
|
|
385
405
|
onMouseLeave={() => setHovering(false)}
|
|
406
|
+
onContextMenu={displayMenu}
|
|
386
407
|
>
|
|
387
408
|
<svg
|
|
388
409
|
width={`${width}px`}
|