@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/GraphTooltipArrow/Content.js +9 -36
  3. package/dist/GraphTooltipArrow/DockedGraphTooltip/index.js +16 -25
  4. package/dist/GraphTooltipArrow/index.d.ts +2 -1
  5. package/dist/GraphTooltipArrow/index.js +3 -5
  6. package/dist/MetricsGraph/MetricsContextMenu/index.d.ts +16 -0
  7. package/dist/MetricsGraph/MetricsContextMenu/index.js +27 -0
  8. package/dist/MetricsGraph/MetricsTooltip/index.d.ts +1 -2
  9. package/dist/MetricsGraph/MetricsTooltip/index.js +3 -4
  10. package/dist/MetricsGraph/index.d.ts +9 -3
  11. package/dist/MetricsGraph/index.js +23 -17
  12. package/dist/ProfileIcicleGraph/IcicleGraphArrow/ContextMenu.d.ts +19 -0
  13. package/dist/ProfileIcicleGraph/IcicleGraphArrow/ContextMenu.js +75 -0
  14. package/dist/ProfileIcicleGraph/IcicleGraphArrow/IcicleGraphNodes.d.ts +3 -0
  15. package/dist/ProfileIcicleGraph/IcicleGraphArrow/IcicleGraphNodes.js +7 -8
  16. package/dist/ProfileIcicleGraph/IcicleGraphArrow/index.js +20 -3
  17. package/dist/ProfileMetricsGraph/index.d.ts +7 -1
  18. package/dist/ProfileMetricsGraph/index.js +1 -1
  19. package/dist/ProfileSelector/index.js +23 -4
  20. package/dist/styles.css +1 -1
  21. package/package.json +10 -9
  22. package/src/GraphTooltipArrow/Content.tsx +24 -112
  23. package/src/GraphTooltipArrow/DockedGraphTooltip/index.tsx +34 -128
  24. package/src/GraphTooltipArrow/index.tsx +4 -6
  25. package/src/MetricsGraph/MetricsContextMenu/index.tsx +81 -0
  26. package/src/MetricsGraph/MetricsTooltip/index.tsx +17 -21
  27. package/src/MetricsGraph/index.tsx +48 -27
  28. package/src/ProfileIcicleGraph/IcicleGraphArrow/ContextMenu.tsx +181 -0
  29. package/src/ProfileIcicleGraph/IcicleGraphArrow/IcicleGraphNodes.tsx +10 -5
  30. package/src/ProfileIcicleGraph/IcicleGraphArrow/index.tsx +63 -21
  31. package/src/ProfileMetricsGraph/index.tsx +4 -2
  32. package/src/ProfileSelector/index.tsx +27 -4
  33. 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 {useState} from 'react';
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 {Button, IconButton, useParcaContext} from '@parca/components';
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, enableSourcesView} = useParcaContext();
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) : 'unknown';
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
- <CopyToClipboard onCopy={onCopy} text={name}>
147
- <button className="cursor-pointer text-left">{name}</button>
148
- </CopyToClipboard>
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
- title="Cumulative"
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
- title="Address"
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) ?? 'Not available'}
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={truncateString(getLastItem(mappingBuildID) ?? 'Not available', 28)}
245
- onCopy={onCopy}
246
- copyText={mappingBuildID ?? 'Not available'}
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
- <span className="mx-2 block text-xs text-gray-500">
253
- {isCopied ? 'Copied!' : 'Hold shift and click on a value to copy.'}
254
- </span>
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 (isShiftDown) {
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, isShiftDown, x, y]);
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(function (label: Label) {
154
- return (
155
- <button
156
- key={label.name}
157
- type="button"
158
- 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"
159
- onClick={() => onLabelClick(label.name, label.value)}
160
- >
161
- <TextWithTooltip
162
- text={`${label.name}="${label.value}"`}
163
- maxTextLength={37}
164
- id={`tooltip-${label.name}-${label.value}`}
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, useKeyDown} from '@parca/components';
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
- onLabelClick: (labelName: string, labelValue: string) => void;
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
- onLabelClick,
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
- onLabelClick={onLabelClick}
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
- onLabelClick,
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
- // do not update position if shift is down because this means the user is locking the tooltip
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
- <MetricsTooltip
370
- x={pos[0] + margin}
371
- y={pos[1] + margin}
372
- highlighted={highlighted}
373
- onLabelClick={onLabelClick}
374
- contextElement={graph.current}
375
- sampleUnit={sampleUnit}
376
- delta={profile !== null ? profile?.query.profType.delta : false}
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`}