Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions locales/en-US/app.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,16 @@ MarkerContextMenu--select-the-sender-thread =
MarkerFiltersContextMenu--drop-samples-outside-of-markers-matching =
Drop samples outside of markers matching “<strong>{ $filter }</strong>”

## MarkerCopyTableContextMenu
## This is the menu when the copy icon is clicked in Marker Chart and Marker
## Table panels.

MarkerCopyTableContextMenu--copy-table-as-plain =
Copy marker table as plain text

MarkerCopyTableContextMenu--copy-table-as-markdown =
Copy marker table as Markdown

## MarkerSettings
## This is used in all panels related to markers.

Expand All @@ -478,6 +488,9 @@ MarkerSettings--panel-search =
MarkerSettings--marker-filters =
.title = Marker Filters

MarkerSettings--copy-table =
.title = Copy table as text

## MarkerSidebar
## This is the sidebar component that is used in Marker Table panel.

Expand Down
101 changes: 100 additions & 1 deletion src/components/marker-table/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from '../../actions/profile-view';
import { MarkerSettings } from '../shared/MarkerSettings';
import { formatSeconds, formatTimestamp } from '../../utils/format-numbers';
import copy from 'copy-to-clipboard';

import './index.css';

Expand All @@ -48,6 +49,8 @@ type MarkerDisplayData = {
details: string;
};

function assertExhaustiveCheck(_param: never) {}

class MarkerTree {
_getMarker: (param: MarkerIndex) => Marker;
_markerIndexes: MarkerIndex[];
Expand All @@ -71,6 +74,102 @@ class MarkerTree {
this._getMarkerLabel = getMarkerLabel;
}

copyTable = (
format: 'plain' | 'markdown',
onWarning: (message: string) => void
) => {
const lines = [];

const startLabel = 'Start';
const durationLabel = 'Duration';
const nameLabel = 'Name';
const detailsLabel = 'Details';

const header = [startLabel, durationLabel, nameLabel, detailsLabel];

let maxStartLength = startLabel.length;
let maxDurationLength = durationLabel.length;
let maxNameLength = nameLabel.length;

const MAX_COPY_ROWS = 10000;

let roots = this.getRoots();
if (roots.length > MAX_COPY_ROWS) {
onWarning(
`The number of rows hits the limit: ${roots.length} > ${MAX_COPY_ROWS}`
);
roots = roots.slice(0, MAX_COPY_ROWS);
}

for (const index of roots) {
const data = this.getDisplayData(index);
const duration = data.duration ?? '';

maxStartLength = Math.max(data.start.length, maxStartLength);
maxDurationLength = Math.max(duration.length, maxDurationLength);
maxNameLength = Math.max(data.name.length, maxNameLength);

lines.push([
data.start,
// Use "u" instead, to make the table aligned with fixed-width text.
duration.replace(/μ/g, 'u'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we replace μ here? Are unicode chars a problem?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because "μ" is wider than other ASCII characters (at least for me) and that breaks the alignment of the table

data.name,
data.details,
]);
}

let text = '';
switch (format) {
case 'plain': {
const formatter = ([start, duration, name, details]: string[]) => {
const line = [
start.padStart(maxStartLength, ' '),
duration.padStart(maxDurationLength, ' '),
name.padStart(maxNameLength, ' '),
];
if (details) {
line.push(details);
}
return line.join(' ');
};

text += formatter(header) + '\n' + lines.map(formatter).join('\n');
break;
}
case 'markdown': {
const formatter = ([start, duration, name, details]: string[]) => {
const line = [
start.padStart(maxStartLength, ' '),
duration.padStart(maxDurationLength, ' '),
name.padStart(maxNameLength, ' '),
details,
];
return '| ' + line.join(' | ') + ' |';
};
const sep =
'|' +
[
'-'.repeat(maxStartLength + 1) + ':',
'-'.repeat(maxDurationLength + 1) + ':',
'-'.repeat(maxNameLength + 1) + ':',
'-'.repeat(9),
].join('|') +
'|';
text =
formatter(header) +
'\n' +
sep +
'\n' +
lines.map(formatter).join('\n');
break;
}
default:
assertExhaustiveCheck(format);
}

copy(text);
};

getRoots(): MarkerIndex[] {
return this._markerIndexes;
}
Expand Down Expand Up @@ -263,7 +362,7 @@ class MarkerTableImpl extends PureComponent<Props> {
role="tabpanel"
aria-labelledby="marker-table-tab-button"
>
<MarkerSettings />
<MarkerSettings copyTable={tree.copyTable} />
{markerIndexes.length === 0 ? (
<MarkerTableEmptyReasons />
) : (
Expand Down
58 changes: 58 additions & 0 deletions src/components/shared/MarkerCopyTableContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { PureComponent } from 'react';
import { MenuItem } from '@firefox-devtools/react-contextmenu';
import { Localized } from '@fluent/react';

import { ContextMenu } from './ContextMenu';
import explicitConnect from 'firefox-profiler/utils/connect';

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

type OwnProps = {
readonly onShow: () => void;
readonly onHide: () => void;
readonly onCopy: (format: 'plain' | 'markdown') => void;
};

type Props = ConnectedProps<OwnProps, {}, {}>;

class MarkerCopyTableContextMenuImpl extends PureComponent<Props> {
copyAsPlain = () => {
const { onCopy } = this.props;
onCopy('plain');
};

copyAsMarkdown = () => {
const { onCopy } = this.props;
onCopy('markdown');
};

override render() {
const { onShow, onHide } = this.props;
return (
<ContextMenu
id="MarkerCopyTableContextMenu"
className="markerCopyTableContextMenu"
onShow={onShow}
onHide={onHide}
>
<MenuItem onClick={this.copyAsPlain}>
<Localized id="MarkerCopyTableContextMenu--copy-table-as-plain">
Copy marker table as plain text
</Localized>
</MenuItem>
<MenuItem onClick={this.copyAsMarkdown}>
<Localized id="MarkerCopyTableContextMenu--copy-table-as-markdown">
Copy marker table as Markdown
</Localized>
</MenuItem>
</ContextMenu>
);
}
}

export const MarkerCopyTableContextMenu = explicitConnect<OwnProps, {}, {}>({
component: MarkerCopyTableContextMenuImpl,
});
36 changes: 33 additions & 3 deletions src/components/shared/MarkerSettings.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,30 @@
white-space: nowrap;
}

.filterMarkersButton {
.filterMarkersButton,
.copyTableButton {
position: relative;
width: 24px;
height: 24px;
flex: none;
padding-right: 30px;
margin: 0 4px;
background-image: url(../../../res/img/svg/filter.svg);
background-position: 4px center;
background-repeat: no-repeat;
}

.filterMarkersButton {
background-image: url(../../../res/img/svg/filter.svg);
}

.copyTableButton {
margin-right: 16px;
background-image: url(../../../res/img/svg/copy-dark.svg);
}

/* This is the dropdown arrow on the right of the button. */
.filterMarkersButton::after {
.filterMarkersButton::after,
.copyTableButton::after {
position: absolute;
top: 2px;
right: 2px;
Expand All @@ -40,3 +50,23 @@
color: var(--grey-90);
content: '';
}

.copyTableButtonWarningWrapper {
/* Position */
position: fixed;
z-index: 4;
top: 0;
right: 0;
left: 0;

/* Box */
display: flex;

/* Other */
pointer-events: none;
}

.copyTableButtonWarning {
margin: 0 auto;
pointer-events: auto;
}
Loading