Skip to content

Commit 0f6799f

Browse files
authored
ui: Add a copy menu item to the Table right-click context menu (#5870)
* ui: Add a copy menu item to the Table right-click context menu * Add onRowContextMenu prop to Table component * Reorder values to be copied Reorders and updates the logic for mapping row values to context menu items, ensuring column visibility checks and value formatting are consistent. Improves clarity and maintainability by grouping related fields and updating label names. * satisfy linter
1 parent 13e826c commit 0f6799f

4 files changed

Lines changed: 214 additions & 72 deletions

File tree

ui/packages/shared/components/src/Table/index.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export interface RowRendererProps<TData> {
3838
usePointerCursor?: boolean;
3939
onRowClick?: (row: TData) => void;
4040
onRowDoubleClick?: (row: Row<TData>, rows: Array<Row<TData>>) => void;
41+
onRowContextMenu?: (e: React.MouseEvent, row: TData) => void;
4142
enableHighlighting?: boolean;
4243
shouldHighlightRow?: (row: TData) => boolean;
4344
rows: Array<Row<TData>>;
@@ -48,6 +49,7 @@ const DefaultRowRenderer = ({
4849
usePointerCursor,
4950
onRowClick,
5051
onRowDoubleClick,
52+
onRowContextMenu,
5153
enableHighlighting,
5254
shouldHighlightRow,
5355
rows,
@@ -62,6 +64,7 @@ const DefaultRowRenderer = ({
6264
)}
6365
onClick={onRowClick != null ? () => onRowClick(row.original) : undefined}
6466
onDoubleClick={onRowDoubleClick != null ? () => onRowDoubleClick(row, rows) : undefined}
67+
onContextMenu={onRowContextMenu != null ? e => onRowContextMenu(e, row.original) : undefined}
6568
style={
6669
enableHighlighting !== true || shouldHighlightRow === undefined
6770
? undefined
@@ -94,6 +97,7 @@ interface Props<TData> {
9497
columnVisibility?: VisibilityState;
9598
onRowClick?: (row: TData) => void;
9699
onRowDoubleClick?: (row: Row<TData>, rows: Array<Row<TData>>) => void;
100+
onRowContextMenu?: (e: React.MouseEvent, row: TData) => void;
97101
enableHighlighting?: boolean;
98102
shouldHighlightRow?: (row: TData) => boolean;
99103
usePointerCursor?: boolean;
@@ -120,6 +124,7 @@ const Table = <T,>({
120124
columnVisibility = {},
121125
onRowClick,
122126
onRowDoubleClick,
127+
onRowContextMenu,
123128
enableHighlighting = false,
124129
usePointerCursor = true,
125130
shouldHighlightRow,
@@ -323,6 +328,7 @@ const Table = <T,>({
323328
enableHighlighting={enableHighlighting}
324329
onRowDoubleClick={onRowDoubleClick}
325330
onRowClick={onRowClick}
331+
onRowContextMenu={onRowContextMenu}
326332
shouldHighlightRow={shouldHighlightRow}
327333
usePointerCursor={usePointerCursor}
328334
rows={rows}
@@ -336,6 +342,7 @@ const Table = <T,>({
336342
enableHighlighting={enableHighlighting}
337343
onRowDoubleClick={onRowDoubleClick}
338344
onRowClick={onRowClick}
345+
onRowContextMenu={onRowContextMenu}
339346
shouldHighlightRow={shouldHighlightRow}
340347
usePointerCursor={usePointerCursor}
341348
rows={rows}

ui/packages/shared/profile/src/Table/TableContextMenu.tsx

Lines changed: 169 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,35 @@
1313

1414
import {Icon} from '@iconify/react';
1515
import cx from 'classnames';
16-
import {Item, Menu} from 'react-contexify';
16+
import {Item, Menu, Submenu} from 'react-contexify';
1717

1818
import 'react-contexify/dist/ReactContexify.css';
1919

2020
import {useParcaContext, useURLState} from '@parca/components';
21+
import {valueFormatter} from '@parca/utilities';
2122

2223
import {type Row} from '.';
24+
import {getTextForCumulative} from '../ProfileFlameGraph/FlameGraphArrow/utils';
25+
import {truncateString} from '../utils';
26+
import {type ColumnName} from './utils/functions';
2327

2428
interface TableContextMenuProps {
2529
menuId: string;
2630
row: Row | null;
31+
unit?: string;
32+
total?: bigint;
33+
totalUnfiltered?: bigint;
34+
columnVisibility?: Record<ColumnName, boolean>;
2735
}
2836

29-
const TableContextMenu = ({menuId, row}: TableContextMenuProps): React.JSX.Element => {
37+
const TableContextMenu = ({
38+
menuId,
39+
row,
40+
unit,
41+
total,
42+
totalUnfiltered,
43+
columnVisibility,
44+
}: TableContextMenuProps): React.JSX.Element => {
3045
const [_, setSandwichFunctionName] = useURLState<string | undefined>('sandwich_function_name');
3146
const [dashboardItems, setDashboardItems] = useURLState<string[]>('dashboard_items', {
3247
alwaysReturnArray: true,
@@ -42,6 +57,133 @@ const TableContextMenu = ({menuId, row}: TableContextMenuProps): React.JSX.Eleme
4257
}
4358
};
4459

60+
const handleCopyItem = (text: string): void => {
61+
void navigator.clipboard.writeText(text);
62+
};
63+
64+
const isColumnVisible = (columnName: ColumnName): boolean => {
65+
return columnVisibility?.[columnName] ?? true;
66+
};
67+
68+
const valuesToCopy =
69+
row !== null
70+
? [
71+
...(isColumnVisible('flat')
72+
? [
73+
{
74+
id: 'Flat',
75+
value:
76+
total !== null &&
77+
total !== undefined &&
78+
totalUnfiltered !== null &&
79+
totalUnfiltered !== undefined
80+
? getTextForCumulative(row.flat, total, totalUnfiltered, unit ?? '')
81+
: valueFormatter(row.flat, unit ?? '', 1),
82+
},
83+
]
84+
: []),
85+
...(isColumnVisible('flatPercentage')
86+
? [
87+
{
88+
id: 'Flat (%)',
89+
value:
90+
total !== null &&
91+
total !== undefined &&
92+
totalUnfiltered !== null &&
93+
totalUnfiltered !== undefined
94+
? getTextForCumulative(row.flat, total, totalUnfiltered, unit ?? '')
95+
: valueFormatter(row.flat, unit ?? '', 1),
96+
},
97+
]
98+
: []),
99+
...(isColumnVisible('flatDiff')
100+
? [
101+
{
102+
id: 'Flat Diff',
103+
value: row.flatDiff !== 0n ? valueFormatter(row.flatDiff, unit ?? '', 1) : '',
104+
},
105+
]
106+
: []),
107+
...(isColumnVisible('flatDiffPercentage')
108+
? [
109+
{
110+
id: 'Flat Diff (%)',
111+
value: row.flatDiff !== 0n ? valueFormatter(row.flatDiff, unit ?? '', 1) : '',
112+
},
113+
]
114+
: []),
115+
...(isColumnVisible('cumulative')
116+
? [
117+
{
118+
id: 'Cumulative',
119+
value:
120+
total !== null &&
121+
total !== undefined &&
122+
totalUnfiltered !== null &&
123+
totalUnfiltered !== undefined
124+
? getTextForCumulative(row.cumulative, total, totalUnfiltered, unit ?? '')
125+
: valueFormatter(row.cumulative, unit ?? '', 1),
126+
},
127+
]
128+
: []),
129+
...(isColumnVisible('cumulativePercentage')
130+
? [
131+
{
132+
id: 'Cumulative (%)',
133+
value:
134+
total !== null &&
135+
total !== undefined &&
136+
totalUnfiltered !== null &&
137+
totalUnfiltered !== undefined
138+
? getTextForCumulative(row.cumulative, total, totalUnfiltered, unit ?? '')
139+
: valueFormatter(row.cumulative, unit ?? '', 1),
140+
},
141+
]
142+
: []),
143+
...(isColumnVisible('cumulativeDiff')
144+
? [
145+
{
146+
id: 'Cumulative Diff',
147+
value:
148+
row.cumulativeDiff !== 0n
149+
? valueFormatter(row.cumulativeDiff, unit ?? '', 1)
150+
: '',
151+
},
152+
]
153+
: []),
154+
...(isColumnVisible('cumulativeDiffPercentage')
155+
? [
156+
{
157+
id: 'Cumulative Diff (%)',
158+
value:
159+
row.cumulativeDiff !== 0n
160+
? valueFormatter(row.cumulativeDiff, unit ?? '', 1)
161+
: '',
162+
},
163+
]
164+
: []),
165+
...(isColumnVisible('name')
166+
? [
167+
{
168+
id: 'Name',
169+
value: row.name ?? '',
170+
},
171+
]
172+
: []),
173+
...(isColumnVisible('functionSystemName')
174+
? [{id: 'Function System Name', value: row.functionSystemName ?? ''}]
175+
: []),
176+
...(isColumnVisible('functionFileName')
177+
? [{id: 'Function File Name', value: row.functionFileName ?? ''}]
178+
: []),
179+
...(isColumnVisible('mappingFile')
180+
? [{id: 'Mapping File', value: row.mappingFile ?? ''}]
181+
: []),
182+
].flat()
183+
: [];
184+
185+
const nonEmptyValuesToCopy = valuesToCopy.filter(({value}) => value !== '');
186+
45187
const isMenuDisabled = row === null || enableSandwichView !== true;
46188

47189
return (
@@ -63,6 +205,31 @@ const TableContextMenu = ({menuId, row}: TableContextMenuProps): React.JSX.Eleme
63205
</div>
64206
</div>
65207
</Item>
208+
<Submenu
209+
label={
210+
<div className="flex w-full items-center gap-2">
211+
<Icon icon="ph:copy" />
212+
<div>Copy</div>
213+
</div>
214+
}
215+
disabled={row === null}
216+
>
217+
<div className="max-h-[300px] overflow-scroll">
218+
{nonEmptyValuesToCopy.map(({id, value}: {id: string; value: string}) => (
219+
<Item
220+
key={id}
221+
id={id}
222+
onClick={() => handleCopyItem(value)}
223+
className="dark:bg-gray-800"
224+
>
225+
<div className="flex flex-col dark:text-gray-300 hover:dark:text-gray-100">
226+
<div className="text-sm">{id}</div>
227+
<div className="text-xs">{truncateString(value, 30)}</div>
228+
</div>
229+
</Item>
230+
))}
231+
</div>
232+
</Submenu>
66233
</Menu>
67234
);
68235
};

ui/packages/shared/profile/src/Table/TableContextMenuWrapper.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@ import {forwardRef, useImperativeHandle, useState} from 'react';
1515

1616
import {type Row} from '.';
1717
import TableContextMenu from './TableContextMenu';
18+
import {type ColumnName} from './utils/functions';
1819

1920
interface TableContextMenuWrapperProps {
2021
menuId: string;
22+
unit?: string;
23+
total?: bigint;
24+
totalUnfiltered?: bigint;
25+
columnVisibility?: Record<ColumnName, boolean>;
2126
}
2227

2328
export interface TableContextMenuWrapperRef {
@@ -27,7 +32,7 @@ export interface TableContextMenuWrapperRef {
2732
const TableContextMenuWrapper = forwardRef<
2833
TableContextMenuWrapperRef,
2934
TableContextMenuWrapperProps
30-
>(({menuId}, ref) => {
35+
>(({menuId, unit, total, totalUnfiltered, columnVisibility}, ref) => {
3136
const [row, setRow] = useState<Row | null>(null);
3237

3338
useImperativeHandle(ref, () => ({
@@ -40,7 +45,16 @@ const TableContextMenuWrapper = forwardRef<
4045
},
4146
}));
4247

43-
return <TableContextMenu menuId={menuId} row={row} />;
48+
return (
49+
<TableContextMenu
50+
menuId={menuId}
51+
row={row}
52+
unit={unit}
53+
total={total}
54+
totalUnfiltered={totalUnfiltered}
55+
columnVisibility={columnVisibility}
56+
/>
57+
);
4458
});
4559

4660
TableContextMenuWrapper.displayName = 'TableContextMenuWrapper';

0 commit comments

Comments
 (0)