Skip to content

Commit 2c54935

Browse files
committed
Add a menu to copy the Marker Table as text
1 parent b0614b5 commit 2c54935

File tree

9 files changed

+346
-11
lines changed

9 files changed

+346
-11
lines changed

locales/en-US/app.ftl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,12 @@ MarkerContextMenu--select-the-sender-thread =
468468
MarkerFiltersContextMenu--drop-samples-outside-of-markers-matching =
469469
Drop samples outside of markers matching “<strong>{ $filter }</strong>”
470470
471+
MarkerCopyTableContextMenu--copy-table-as-plain =
472+
Copy marker table as plain text
473+
474+
MarkerCopyTableContextMenu--copy-table-as-markdown =
475+
Copy marker table as Markdown
476+
471477
## MarkerSettings
472478
## This is used in all panels related to markers.
473479

@@ -478,6 +484,9 @@ MarkerSettings--panel-search =
478484
MarkerSettings--marker-filters =
479485
.title = Marker Filters
480486
487+
MarkerSettings--copy-table =
488+
.title = Copy table as text
489+
481490
## MarkerSidebar
482491
## This is the sidebar component that is used in Marker Table panel.
483492

src/components/marker-table/index.tsx

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
} from '../../actions/profile-view';
2424
import { MarkerSettings } from '../shared/MarkerSettings';
2525
import { formatSeconds, formatTimestamp } from '../../utils/format-numbers';
26+
import copy from 'copy-to-clipboard';
2627

2728
import './index.css';
2829

@@ -71,6 +72,77 @@ class MarkerTree {
7172
this._getMarkerLabel = getMarkerLabel;
7273
}
7374

75+
copyTable = (format: string) => {
76+
const lines = [];
77+
78+
const startLabel = 'Start';
79+
const durationLabel = 'Duration';
80+
const nameLabel = 'Name';
81+
const detailsLabel = 'Details';
82+
83+
const header = [startLabel, durationLabel, nameLabel, detailsLabel];
84+
85+
let maxStartLength = startLabel.length;
86+
let maxDurationLength = durationLabel.length;
87+
let maxNameLength = nameLabel.length;
88+
89+
for (const index of this.getRoots()) {
90+
const data = this.getDisplayData(index);
91+
const duration = data.duration ?? '';
92+
93+
maxStartLength = Math.max(data.start.length, maxStartLength);
94+
maxDurationLength = Math.max(duration.length, maxDurationLength);
95+
maxNameLength = Math.max(data.name.length, maxNameLength);
96+
97+
lines.push([
98+
data.start,
99+
duration.replace(/μ/g, 'u'),
100+
data.name,
101+
data.details,
102+
]);
103+
}
104+
105+
let text = '';
106+
if (format === 'plain') {
107+
const formatter = ([start, duration, name, details]: string[]) => {
108+
const line = [
109+
start.padStart(maxStartLength, ' '),
110+
duration.padStart(maxDurationLength, ' '),
111+
name.padStart(maxNameLength, ' '),
112+
];
113+
if (details) {
114+
line.push(details);
115+
}
116+
return line.join(' ');
117+
};
118+
119+
text += formatter(header) + '\n' + lines.map(formatter).join('\n');
120+
} else if (format === 'markdown') {
121+
const formatter = ([start, duration, name, details]: string[]) => {
122+
const line = [
123+
start.padStart(maxStartLength, ' '),
124+
duration.padStart(maxDurationLength, ' '),
125+
name.padStart(maxNameLength, ' '),
126+
details,
127+
];
128+
return '| ' + line.join(' | ') + ' |';
129+
};
130+
const sep =
131+
'|' +
132+
[
133+
'-'.repeat(maxStartLength + 1) + ':',
134+
'-'.repeat(maxDurationLength + 1) + ':',
135+
'-'.repeat(maxNameLength + 1) + ':',
136+
'-'.repeat(9),
137+
].join('|') +
138+
'|';
139+
text =
140+
formatter(header) + '\n' + sep + '\n' + lines.map(formatter).join('\n');
141+
}
142+
143+
copy(text);
144+
};
145+
74146
getRoots(): MarkerIndex[] {
75147
return this._markerIndexes;
76148
}
@@ -263,7 +335,7 @@ class MarkerTableImpl extends PureComponent<Props> {
263335
role="tabpanel"
264336
aria-labelledby="marker-table-tab-button"
265337
>
266-
<MarkerSettings />
338+
<MarkerSettings copyTable={tree.copyTable} />
267339
{markerIndexes.length === 0 ? (
268340
<MarkerTableEmptyReasons />
269341
) : (
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
import { PureComponent } from 'react';
5+
import { MenuItem } from '@firefox-devtools/react-contextmenu';
6+
import { Localized } from '@fluent/react';
7+
8+
import { ContextMenu } from './ContextMenu';
9+
import explicitConnect from 'firefox-profiler/utils/connect';
10+
11+
import type { ConnectedProps } from 'firefox-profiler/utils/connect';
12+
13+
type OwnProps = {
14+
readonly onShow: () => void;
15+
readonly onHide: () => void;
16+
readonly onCopy: (format: string) => void;
17+
};
18+
19+
type Props = ConnectedProps<OwnProps, {}, {}>;
20+
21+
class MarkerCopyTableContextMenuImpl extends PureComponent<Props> {
22+
copyAsPlain = () => {
23+
const { onCopy } = this.props;
24+
onCopy('plain');
25+
};
26+
27+
copyAsMarkdown = () => {
28+
const { onCopy } = this.props;
29+
onCopy('markdown');
30+
};
31+
32+
override render() {
33+
const { onShow, onHide } = this.props;
34+
return (
35+
<ContextMenu
36+
id="MarkerCopyTableContextMenu"
37+
className="markerCopyTableContextMenu"
38+
onShow={onShow}
39+
onHide={onHide}
40+
>
41+
<MenuItem onClick={this.copyAsPlain}>
42+
<Localized id="MarkerCopyTableContextMenu--copy-table-as-plain">
43+
Copy marker table as plain text
44+
</Localized>
45+
</MenuItem>
46+
<MenuItem onClick={this.copyAsMarkdown}>
47+
<Localized id="MarkerCopyTableContextMenu--copy-table-as-markdown">
48+
Copy marker table as Markdown
49+
</Localized>
50+
</MenuItem>
51+
</ContextMenu>
52+
);
53+
}
54+
}
55+
56+
export const MarkerCopyTableContextMenu = explicitConnect<OwnProps, {}, {}>({
57+
component: MarkerCopyTableContextMenuImpl,
58+
});

src/components/shared/MarkerSettings.css

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,30 @@
1313
white-space: nowrap;
1414
}
1515

16-
.filterMarkersButton {
16+
.filterMarkersButton,
17+
.copyTableButton {
1718
position: relative;
1819
width: 24px;
1920
height: 24px;
2021
flex: none;
2122
padding-right: 30px;
2223
margin: 0 4px;
23-
background-image: url(../../../res/img/svg/filter.svg);
2424
background-position: 4px center;
2525
background-repeat: no-repeat;
2626
}
2727

28+
.filterMarkersButton {
29+
background-image: url(../../../res/img/svg/filter.svg);
30+
}
31+
32+
.copyTableButton {
33+
margin: 0 16px 0 4px;
34+
background-image: url(../../../res/img/svg/copy-dark.svg);
35+
}
36+
2837
/* This is the dropdown arrow on the right of the button. */
29-
.filterMarkersButton::after {
38+
.filterMarkersButton::after,
39+
.copyTableButton::after {
3040
position: absolute;
3141
top: 2px;
3242
right: 2px;

src/components/shared/MarkerSettings.tsx

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,17 @@ import { getProfileUsesMultipleStackTypes } from 'firefox-profiler/selectors/pro
1414
import { PanelSearch } from './PanelSearch';
1515
import { StackImplementationSetting } from 'firefox-profiler/components/shared/StackImplementationSetting';
1616
import { MarkerFiltersContextMenu } from './MarkerFiltersContextMenu';
17+
import { MarkerCopyTableContextMenu } from './MarkerCopyTableContextMenu';
1718

1819
import type { ConnectedProps } from 'firefox-profiler/utils/connect';
1920

2021
import 'firefox-profiler/components/shared/PanelSettingsList.css';
2122
import './MarkerSettings.css';
2223

24+
type OwnProps = {
25+
readonly copyTable?: (format: string) => void;
26+
};
27+
2328
type StateProps = {
2429
readonly searchString: string;
2530
readonly allowSwitchingStackType: boolean;
@@ -29,7 +34,7 @@ type DispatchProps = {
2934
readonly changeMarkersSearchString: typeof changeMarkersSearchString;
3035
};
3136

32-
type Props = ConnectedProps<{}, StateProps, DispatchProps>;
37+
type Props = ConnectedProps<OwnProps, StateProps, DispatchProps>;
3338

3439
type State = {
3540
readonly isMarkerFiltersMenuVisible: boolean;
@@ -39,12 +44,16 @@ type State = {
3944
// Otherwise, if we check this in onClick event, the state will always be
4045
// `false` since the library already hid it on mousedown.
4146
readonly isFilterMenuVisibleOnMouseDown: boolean;
47+
readonly isMarkerCopyTableMenuVisible: boolean;
48+
readonly isCopyTableMenuVisibleOnMouseDown: boolean;
4249
};
4350

4451
class MarkerSettingsImpl extends PureComponent<Props, State> {
4552
override state = {
4653
isMarkerFiltersMenuVisible: false,
4754
isFilterMenuVisibleOnMouseDown: false,
55+
isMarkerCopyTableMenuVisible: false,
56+
isCopyTableMenuVisibleOnMouseDown: false,
4857
};
4958

5059
_onSearch = (value: string) => {
@@ -72,6 +81,36 @@ class MarkerSettingsImpl extends PureComponent<Props, State> {
7281
});
7382
};
7483

84+
_onClickToggleCopyTableMenu = (event: React.MouseEvent<HTMLElement>) => {
85+
const { isCopyTableMenuVisibleOnMouseDown } = this.state;
86+
if (isCopyTableMenuVisibleOnMouseDown) {
87+
// Do nothing as we would like to hide the menu if the menu was already visible on mouse down.
88+
return;
89+
}
90+
91+
const rect = event.currentTarget.getBoundingClientRect();
92+
// FIXME: Currently we assume that the context menu is 250px wide, but ideally
93+
// we should get the real width. It's not so easy though, because the context
94+
// menu is not rendered yet.
95+
const isRightAligned = rect.right > window.innerWidth - 250;
96+
97+
showMenu({
98+
data: null,
99+
id: 'MarkerCopyTableContextMenu',
100+
position: { x: isRightAligned ? rect.right : rect.left, y: rect.bottom },
101+
target: event.target,
102+
});
103+
};
104+
105+
_onCopyTable = (format: string) => {
106+
const { copyTable } = this.props;
107+
if (!copyTable) {
108+
return;
109+
}
110+
111+
copyTable(format);
112+
};
113+
75114
_onShowFiltersContextMenu = () => {
76115
this.setState({ isMarkerFiltersMenuVisible: true });
77116
};
@@ -80,15 +119,30 @@ class MarkerSettingsImpl extends PureComponent<Props, State> {
80119
this.setState({ isMarkerFiltersMenuVisible: false });
81120
};
82121

122+
_onShowCopyTableContextMenu = () => {
123+
this.setState({ isMarkerCopyTableMenuVisible: true });
124+
};
125+
126+
_onHideCopyTableContextMenu = () => {
127+
this.setState({ isMarkerCopyTableMenuVisible: false });
128+
};
129+
83130
_onMouseDownToggleFilterButton = () => {
84131
this.setState((state) => ({
85132
isFilterMenuVisibleOnMouseDown: state.isMarkerFiltersMenuVisible,
86133
}));
87134
};
88135

136+
_onMouseDownToggleCopyTableMenu = () => {
137+
this.setState((state) => ({
138+
isCopyTableMenuVisibleOnMouseDown: state.isMarkerCopyTableMenuVisible,
139+
}));
140+
};
141+
89142
override render() {
90-
const { searchString, allowSwitchingStackType } = this.props;
91-
const { isMarkerFiltersMenuVisible } = this.state;
143+
const { searchString, allowSwitchingStackType, copyTable } = this.props;
144+
const { isMarkerFiltersMenuVisible, isMarkerCopyTableMenuVisible } =
145+
this.state;
92146

93147
return (
94148
<div className="markerSettings">
@@ -99,6 +153,26 @@ class MarkerSettingsImpl extends PureComponent<Props, State> {
99153
</li>
100154
) : null}
101155
</ul>
156+
{copyTable ? (
157+
<Localized id="MarkerSettings--copy-table" attrs={{ title: true }}>
158+
<button
159+
className={classNames(
160+
'copyTableButton',
161+
'photon-button',
162+
'photon-button-ghost',
163+
{
164+
'photon-button-ghost--checked': isMarkerCopyTableMenuVisible,
165+
}
166+
)}
167+
title="Copy table as text"
168+
type="button"
169+
onClick={this._onClickToggleCopyTableMenu}
170+
onMouseDown={this._onMouseDownToggleCopyTableMenu}
171+
/>
172+
</Localized>
173+
) : (
174+
''
175+
)}
102176
<Localized
103177
id="MarkerSettings--panel-search"
104178
attrs={{ label: true, title: true }}
@@ -132,12 +206,21 @@ class MarkerSettingsImpl extends PureComponent<Props, State> {
132206
onShow={this._onShowFiltersContextMenu}
133207
onHide={this._onHideFiltersContextMenu}
134208
/>
209+
<MarkerCopyTableContextMenu
210+
onShow={this._onShowCopyTableContextMenu}
211+
onHide={this._onHideCopyTableContextMenu}
212+
onCopy={this._onCopyTable}
213+
/>
135214
</div>
136215
);
137216
}
138217
}
139218

140-
export const MarkerSettings = explicitConnect<{}, StateProps, DispatchProps>({
219+
export const MarkerSettings = explicitConnect<
220+
OwnProps,
221+
StateProps,
222+
DispatchProps
223+
>({
141224
mapStateToProps: (state) => ({
142225
searchString: getMarkersSearchString(state),
143226
allowSwitchingStackType: getProfileUsesMultipleStackTypes(state),

0 commit comments

Comments
 (0)