@parca/profile 0.19.34 → 0.19.36

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 CHANGED
@@ -3,6 +3,14 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [0.19.36](https://github.com/parca-dev/parca/compare/@parca/profile@0.19.35...@parca/profile@0.19.36) (2025-08-15)
7
+
8
+ **Note:** Version bump only for package @parca/profile
9
+
10
+ ## [0.19.35](https://github.com/parca-dev/parca/compare/@parca/profile@0.19.34...@parca/profile@0.19.35) (2025-08-12)
11
+
12
+ **Note:** Version bump only for package @parca/profile
13
+
6
14
  ## [0.19.34](https://github.com/parca-dev/parca/compare/@parca/profile@0.19.33...@parca/profile@0.19.34) (2025-08-06)
7
15
 
8
16
  **Note:** Version bump only for package @parca/profile
@@ -1,5 +1,8 @@
1
1
  import { type ProfileFilter } from './useProfileFilters';
2
2
  export declare const isFilterComplete: (filter: ProfileFilter) => boolean;
3
- declare const ProfileFilters: () => JSX.Element;
3
+ export interface ProfileFiltersProps {
4
+ readOnly?: boolean;
5
+ }
6
+ declare const ProfileFilters: ({ readOnly }?: ProfileFiltersProps) => JSX.Element;
4
7
  export default ProfileFilters;
5
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/ProfileView/components/ProfileFilters/index.tsx"],"names":[],"mappings":"AAsBA,OAAO,EAAoB,KAAK,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAE1E,eAAO,MAAM,gBAAgB,GAAI,QAAQ,aAAa,KAAG,OASxD,CAAC;AAyIF,QAAA,MAAM,cAAc,QAAO,GAAG,CAAC,OAmL9B,CAAC;AAEF,eAAe,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/ProfileView/components/ProfileFilters/index.tsx"],"names":[],"mappings":"AAsBA,OAAO,EAAoB,KAAK,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAE1E,eAAO,MAAM,gBAAgB,GAAI,QAAQ,aAAa,KAAG,OASxD,CAAC;AAyIF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,QAAA,MAAM,cAAc,GAAI,eAAoB,mBAAwB,KAAG,GAAG,CAAC,OAsM1E,CAAC;AAEF,eAAe,cAAc,CAAC"}
@@ -139,7 +139,7 @@ const numberMatchTypeItems = [
139
139
  },
140
140
  },
141
141
  ];
142
- const ProfileFilters = () => {
142
+ const ProfileFilters = ({ readOnly = false } = {}) => {
143
143
  const { profileSource } = useProfileViewContext();
144
144
  const currentProfileType = profileSource?.ProfileType()?.toString();
145
145
  const filterTypeItems = getFilterTypeItems(currentProfileType);
@@ -158,7 +158,7 @@ const ProfileFilters = () => {
158
158
  const isNumberField = filter.field === 'address' || filter.field === 'line_number';
159
159
  const matchTypeItems = isNumberField ? numberMatchTypeItems : stringMatchTypeItems;
160
160
  const isPresetFilter = filter.type != null && isPresetKey(filter.type);
161
- return (_jsxs("div", { className: "flex items-center gap-0", children: [_jsx(Select, { items: filterTypeItems, selectedKey: filter.type, placeholder: "Select Filter", onSelection: key => {
161
+ return (_jsxs("div", { className: "flex items-center gap-0", children: [_jsx(Select, { items: filterTypeItems, selectedKey: filter.type, placeholder: "Select Filter", disabled: readOnly, onSelection: key => {
162
162
  // Check if this is a preset selection
163
163
  if (isPresetKey(key)) {
164
164
  const preset = getPresetByKey(key);
@@ -190,7 +190,7 @@ const ProfileFilters = () => {
190
190
  });
191
191
  }
192
192
  }
193
- }, className: cx('rounded-l-md pr-1 gap-0 focus:z-50 focus:relative focus:outline-1', isPresetFilter ? 'rounded-r-none border-r-0' : 'rounded-r-none', filter.type != null ? 'border-r-0 w-auto' : 'w-32') }), filter.type != null && !isPresetFilter && (_jsxs(_Fragment, { children: [_jsx(Select, { items: fieldItems, selectedKey: filter.field ?? '', onSelection: key => {
193
+ }, className: cx('gap-0 focus:z-50 focus:relative focus:outline-1', readOnly ? '' : 'pr-1', readOnly && isPresetFilter ? 'rounded-md' : 'rounded-l-md rounded-r-none', !readOnly && (isPresetFilter ? 'rounded-r-none border-r-0' : 'rounded-r-none'), readOnly ? 'w-auto' : filter.type != null ? 'border-r-0 w-auto' : 'w-32'), hideCaretDropdown: readOnly }), filter.type != null && !isPresetFilter && (_jsxs(_Fragment, { children: [_jsx(Select, { items: fieldItems, selectedKey: filter.field ?? '', disabled: readOnly, onSelection: key => {
194
194
  const newField = key;
195
195
  const isNewFieldNumber = newField === 'address' || newField === 'line_number';
196
196
  const isCurrentFieldNumber = filter.field === 'address' || filter.field === 'line_number';
@@ -203,7 +203,7 @@ const ProfileFilters = () => {
203
203
  else {
204
204
  updateFilter(filter.id, { field: newField });
205
205
  }
206
- }, className: "rounded-none border-r-0 w-32 pr-1 gap-0 focus:z-50 focus:relative focus:outline-1" }), _jsx(Select, { items: matchTypeItems, selectedKey: filter.matchType ?? '', onSelection: key => updateFilter(filter.id, { matchType: key }), className: "rounded-none border-r-0 pr-1 gap-0 focus:z-50 focus:relative focus:outline-1" }), _jsx(Input, { placeholder: "Value", value: filter.value, onChange: e => updateFilter(filter.id, { value: e.target.value }), onKeyDown: handleKeyDown, className: "rounded-none w-36 text-sm focus:outline-1" })] })), _jsx(Button, { variant: "neutral", onClick: () => {
206
+ }, className: cx('rounded-none border-r-0 w-32 gap-0 focus:z-50 focus:relative focus:outline-1', readOnly ? '' : 'pr-1'), hideCaretDropdown: readOnly }), _jsx(Select, { items: matchTypeItems, selectedKey: filter.matchType ?? '', disabled: readOnly, onSelection: key => updateFilter(filter.id, { matchType: key }), className: cx('rounded-none border-r-0 gap-0 focus:z-50 focus:relative focus:outline-1', readOnly ? '' : 'pr-1'), hideCaretDropdown: readOnly }), _jsx(Input, { placeholder: "Value", value: filter.value, disabled: readOnly, onChange: e => updateFilter(filter.id, { value: e.target.value }), onKeyDown: handleKeyDown, className: "rounded-none w-36 text-sm focus:outline-1" })] })), !readOnly && (_jsx(Button, { variant: "neutral", onClick: () => {
207
207
  // If we're displaying local filters and this is the last one, reset everything
208
208
  if (localFilters.length > 0 && localFilters.length === 1) {
209
209
  resetFilters();
@@ -216,7 +216,9 @@ const ProfileFilters = () => {
216
216
  else {
217
217
  removeFilter(filter.id);
218
218
  }
219
- }, className: cx('h-[38px] p-3', filter.type != null ? 'rounded-none rounded-r-md' : 'rounded-l-none rounded-r-md'), children: _jsx(Icon, { icon: "mdi:close", className: "h-4 w-4" }) })] }, filter.id));
220
- }), localFilters.length > 0 && (_jsx(Button, { variant: "neutral", onClick: addFilter, className: "p-3 h-[38px]", children: _jsx(Icon, { icon: "mdi:filter-plus-outline", className: "h-4 w-4" }) })), localFilters.length === 0 && (appliedFilters?.length ?? 0) === 0 && (_jsxs(Button, { variant: "neutral", onClick: addFilter, className: "flex items-center gap-2", children: [_jsx(Icon, { icon: "mdi:filter-outline", className: "h-4 w-4" }), _jsx("span", { children: "Filter" })] }))] }), localFilters.length > 0 && (_jsx(Button, { variant: "primary", onClick: onApplyFilters, disabled: !hasUnsavedChanges || !localFilters.some(isFilterComplete), className: cx('flex items-center gap-2 sticky top-0'), children: _jsx("span", { children: "Apply" }) }))] }));
219
+ }, className: cx('h-[38px] p-3', filter.type != null
220
+ ? 'rounded-none rounded-r-md'
221
+ : 'rounded-l-none rounded-r-md'), children: _jsx(Icon, { icon: "mdi:close", className: "h-4 w-4" }) }))] }, filter.id));
222
+ }), !readOnly && localFilters.length > 0 && (_jsx(Button, { variant: "neutral", onClick: addFilter, className: "p-3 h-[38px]", children: _jsx(Icon, { icon: "mdi:filter-plus-outline", className: "h-4 w-4" }) })), !readOnly && localFilters.length === 0 && (appliedFilters?.length ?? 0) === 0 && (_jsxs(Button, { variant: "neutral", onClick: addFilter, className: "flex items-center gap-2", children: [_jsx(Icon, { icon: "mdi:filter-outline", className: "h-4 w-4" }), _jsx("span", { children: "Filter" })] }))] }), !readOnly && localFilters.length > 0 && (_jsx(Button, { variant: "primary", onClick: onApplyFilters, disabled: !hasUnsavedChanges || !localFilters.some(isFilterComplete), className: cx('flex items-center gap-2 sticky top-0'), children: _jsx("span", { children: "Apply" }) }))] }));
221
223
  };
222
224
  export default ProfileFilters;
@@ -1,9 +1,14 @@
1
1
  import 'react-contexify/dist/ReactContexify.css';
2
2
  import { type Row } from '.';
3
+ import { type ColumnName } from './utils/functions';
3
4
  interface TableContextMenuProps {
4
5
  menuId: string;
5
6
  row: Row | null;
7
+ unit?: string;
8
+ total?: bigint;
9
+ totalUnfiltered?: bigint;
10
+ columnVisibility?: Record<ColumnName, boolean>;
6
11
  }
7
- declare const TableContextMenu: ({ menuId, row }: TableContextMenuProps) => React.JSX.Element;
12
+ declare const TableContextMenu: ({ menuId, row, unit, total, totalUnfiltered, columnVisibility, }: TableContextMenuProps) => React.JSX.Element;
8
13
  export default TableContextMenu;
9
14
  //# sourceMappingURL=TableContextMenu.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TableContextMenu.d.ts","sourceRoot":"","sources":["../../src/Table/TableContextMenu.tsx"],"names":[],"mappings":"AAiBA,OAAO,yCAAyC,CAAC;AAIjD,OAAO,EAAC,KAAK,GAAG,EAAC,MAAM,GAAG,CAAC;AAE3B,UAAU,qBAAqB;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,GAAG,GAAG,IAAI,CAAC;CACjB;AAED,QAAA,MAAM,gBAAgB,GAAI,iBAAe,qBAAqB,KAAG,KAAK,CAAC,GAAG,CAAC,OAuC1E,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"TableContextMenu.d.ts","sourceRoot":"","sources":["../../src/Table/TableContextMenu.tsx"],"names":[],"mappings":"AAiBA,OAAO,yCAAyC,CAAC;AAKjD,OAAO,EAAC,KAAK,GAAG,EAAC,MAAM,GAAG,CAAC;AAG3B,OAAO,EAAC,KAAK,UAAU,EAAC,MAAM,mBAAmB,CAAC;AAElD,UAAU,qBAAqB;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,GAAG,GAAG,IAAI,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;CAChD;AAED,QAAA,MAAM,gBAAgB,GAAI,kEAOvB,qBAAqB,KAAG,KAAK,CAAC,GAAG,CAAC,OA+LpC,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
@@ -13,10 +13,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
13
  // limitations under the License.
14
14
  import { Icon } from '@iconify/react';
15
15
  import cx from 'classnames';
16
- import { Item, Menu } from 'react-contexify';
16
+ import { Item, Menu, Submenu } from 'react-contexify';
17
17
  import 'react-contexify/dist/ReactContexify.css';
18
18
  import { useParcaContext, useURLState } from '@parca/components';
19
- const TableContextMenu = ({ menuId, row }) => {
19
+ import { valueFormatter } from '@parca/utilities';
20
+ import { getTextForCumulative } from '../ProfileFlameGraph/FlameGraphArrow/utils';
21
+ import { truncateString } from '../utils';
22
+ const TableContextMenu = ({ menuId, row, unit, total, totalUnfiltered, columnVisibility, }) => {
20
23
  const [_, setSandwichFunctionName] = useURLState('sandwich_function_name');
21
24
  const [dashboardItems, setDashboardItems] = useURLState('dashboard_items', {
22
25
  alwaysReturnArray: true,
@@ -30,9 +33,125 @@ const TableContextMenu = ({ menuId, row }) => {
30
33
  }
31
34
  }
32
35
  };
36
+ const handleCopyItem = (text) => {
37
+ void navigator.clipboard.writeText(text);
38
+ };
39
+ const isColumnVisible = (columnName) => {
40
+ return columnVisibility?.[columnName] ?? true;
41
+ };
42
+ const valuesToCopy = row !== null
43
+ ? [
44
+ ...(isColumnVisible('flat')
45
+ ? [
46
+ {
47
+ id: 'Flat',
48
+ value: total !== null &&
49
+ total !== undefined &&
50
+ totalUnfiltered !== null &&
51
+ totalUnfiltered !== undefined
52
+ ? getTextForCumulative(row.flat, total, totalUnfiltered, unit ?? '')
53
+ : valueFormatter(row.flat, unit ?? '', 1),
54
+ },
55
+ ]
56
+ : []),
57
+ ...(isColumnVisible('flatPercentage')
58
+ ? [
59
+ {
60
+ id: 'Flat (%)',
61
+ value: total !== null &&
62
+ total !== undefined &&
63
+ totalUnfiltered !== null &&
64
+ totalUnfiltered !== undefined
65
+ ? getTextForCumulative(row.flat, total, totalUnfiltered, unit ?? '')
66
+ : valueFormatter(row.flat, unit ?? '', 1),
67
+ },
68
+ ]
69
+ : []),
70
+ ...(isColumnVisible('flatDiff')
71
+ ? [
72
+ {
73
+ id: 'Flat Diff',
74
+ value: row.flatDiff !== 0n ? valueFormatter(row.flatDiff, unit ?? '', 1) : '',
75
+ },
76
+ ]
77
+ : []),
78
+ ...(isColumnVisible('flatDiffPercentage')
79
+ ? [
80
+ {
81
+ id: 'Flat Diff (%)',
82
+ value: row.flatDiff !== 0n ? valueFormatter(row.flatDiff, unit ?? '', 1) : '',
83
+ },
84
+ ]
85
+ : []),
86
+ ...(isColumnVisible('cumulative')
87
+ ? [
88
+ {
89
+ id: 'Cumulative',
90
+ value: total !== null &&
91
+ total !== undefined &&
92
+ totalUnfiltered !== null &&
93
+ totalUnfiltered !== undefined
94
+ ? getTextForCumulative(row.cumulative, total, totalUnfiltered, unit ?? '')
95
+ : valueFormatter(row.cumulative, unit ?? '', 1),
96
+ },
97
+ ]
98
+ : []),
99
+ ...(isColumnVisible('cumulativePercentage')
100
+ ? [
101
+ {
102
+ id: 'Cumulative (%)',
103
+ value: total !== null &&
104
+ total !== undefined &&
105
+ totalUnfiltered !== null &&
106
+ totalUnfiltered !== undefined
107
+ ? getTextForCumulative(row.cumulative, total, totalUnfiltered, unit ?? '')
108
+ : valueFormatter(row.cumulative, unit ?? '', 1),
109
+ },
110
+ ]
111
+ : []),
112
+ ...(isColumnVisible('cumulativeDiff')
113
+ ? [
114
+ {
115
+ id: 'Cumulative Diff',
116
+ value: row.cumulativeDiff !== 0n
117
+ ? valueFormatter(row.cumulativeDiff, unit ?? '', 1)
118
+ : '',
119
+ },
120
+ ]
121
+ : []),
122
+ ...(isColumnVisible('cumulativeDiffPercentage')
123
+ ? [
124
+ {
125
+ id: 'Cumulative Diff (%)',
126
+ value: row.cumulativeDiff !== 0n
127
+ ? valueFormatter(row.cumulativeDiff, unit ?? '', 1)
128
+ : '',
129
+ },
130
+ ]
131
+ : []),
132
+ ...(isColumnVisible('name')
133
+ ? [
134
+ {
135
+ id: 'Name',
136
+ value: row.name ?? '',
137
+ },
138
+ ]
139
+ : []),
140
+ ...(isColumnVisible('functionSystemName')
141
+ ? [{ id: 'Function System Name', value: row.functionSystemName ?? '' }]
142
+ : []),
143
+ ...(isColumnVisible('functionFileName')
144
+ ? [{ id: 'Function File Name', value: row.functionFileName ?? '' }]
145
+ : []),
146
+ ...(isColumnVisible('mappingFile')
147
+ ? [{ id: 'Mapping File', value: row.mappingFile ?? '' }]
148
+ : []),
149
+ ].flat()
150
+ : [];
151
+ const nonEmptyValuesToCopy = valuesToCopy.filter(({ value }) => value !== '');
33
152
  const isMenuDisabled = row === null || enableSandwichView !== true;
34
- return (_jsx(Menu, { id: menuId, theme: isDarkMode ? 'dark' : '', className: cx(dashboardItems.includes('sandwich') ? 'min-w-[350px] w-[350px]' : 'min-w-[260px] w-[260px]'), children: _jsx(Item, { id: "sandwich-view", onClick: onSandwichViewSelect, disabled: isMenuDisabled, children: _jsxs("div", { className: "flex w-full items-center gap-2", children: [_jsx(Icon, { icon: "tdesign:sandwich-filled" }), _jsxs("div", { className: "relative", children: [dashboardItems.includes('sandwich')
35
- ? 'Focus sandwich on this frame.'
36
- : 'Show in sandwich', _jsx("span", { className: "absolute top-[-2px] text-xs lowercase text-red-500", children: "\u00A0alpha" })] })] }) }) }));
153
+ return (_jsxs(Menu, { id: menuId, theme: isDarkMode ? 'dark' : '', className: cx(dashboardItems.includes('sandwich') ? 'min-w-[350px] w-[350px]' : 'min-w-[260px] w-[260px]'), children: [_jsx(Item, { id: "sandwich-view", onClick: onSandwichViewSelect, disabled: isMenuDisabled, children: _jsxs("div", { className: "flex w-full items-center gap-2", children: [_jsx(Icon, { icon: "tdesign:sandwich-filled" }), _jsxs("div", { className: "relative", children: [dashboardItems.includes('sandwich')
154
+ ? 'Focus sandwich on this frame.'
155
+ : 'Show in sandwich', _jsx("span", { className: "absolute top-[-2px] text-xs lowercase text-red-500", children: "\u00A0alpha" })] })] }) }), _jsx(Submenu, { label: _jsxs("div", { className: "flex w-full items-center gap-2", children: [_jsx(Icon, { icon: "ph:copy" }), _jsx("div", { children: "Copy" })] }), disabled: row === null, children: _jsx("div", { className: "max-h-[300px] overflow-scroll", children: nonEmptyValuesToCopy.map(({ id, value }) => (_jsx(Item, { id: id, onClick: () => handleCopyItem(value), className: "dark:bg-gray-800", children: _jsxs("div", { className: "flex flex-col dark:text-gray-300 hover:dark:text-gray-100", children: [_jsx("div", { className: "text-sm", children: id }), _jsx("div", { className: "text-xs", children: truncateString(value, 30) })] }) }, id))) }) })] }));
37
156
  };
38
157
  export default TableContextMenu;
@@ -1,6 +1,11 @@
1
1
  import { type Row } from '.';
2
+ import { type ColumnName } from './utils/functions';
2
3
  interface TableContextMenuWrapperProps {
3
4
  menuId: string;
5
+ unit?: string;
6
+ total?: bigint;
7
+ totalUnfiltered?: bigint;
8
+ columnVisibility?: Record<ColumnName, boolean>;
4
9
  }
5
10
  export interface TableContextMenuWrapperRef {
6
11
  setRow: (row: Row | null, callback?: () => void) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"TableContextMenuWrapper.d.ts","sourceRoot":"","sources":["../../src/Table/TableContextMenuWrapper.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAC,KAAK,GAAG,EAAC,MAAM,GAAG,CAAC;AAG3B,UAAU,4BAA4B;IACpC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,EAAE,QAAQ,CAAC,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;CAC1D;AAED,QAAA,MAAM,uBAAuB,qIAiB3B,CAAC;AAIH,eAAe,uBAAuB,CAAC"}
1
+ {"version":3,"file":"TableContextMenuWrapper.d.ts","sourceRoot":"","sources":["../../src/Table/TableContextMenuWrapper.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAC,KAAK,GAAG,EAAC,MAAM,GAAG,CAAC;AAE3B,OAAO,EAAC,KAAK,UAAU,EAAC,MAAM,mBAAmB,CAAC;AAElD,UAAU,4BAA4B;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;CAChD;AAED,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,EAAE,QAAQ,CAAC,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;CAC1D;AAED,QAAA,MAAM,uBAAuB,qIA0B3B,CAAC;AAIH,eAAe,uBAAuB,CAAC"}
@@ -13,7 +13,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
13
13
  // limitations under the License.
14
14
  import { forwardRef, useImperativeHandle, useState } from 'react';
15
15
  import TableContextMenu from './TableContextMenu';
16
- const TableContextMenuWrapper = forwardRef(({ menuId }, ref) => {
16
+ const TableContextMenuWrapper = forwardRef(({ menuId, unit, total, totalUnfiltered, columnVisibility }, ref) => {
17
17
  const [row, setRow] = useState(null);
18
18
  useImperativeHandle(ref, () => ({
19
19
  setRow: (newRow, callback) => {
@@ -24,7 +24,7 @@ const TableContextMenuWrapper = forwardRef(({ menuId }, ref) => {
24
24
  }
25
25
  },
26
26
  }));
27
- return _jsx(TableContextMenu, { menuId: menuId, row: row });
27
+ return (_jsx(TableContextMenu, { menuId: menuId, row: row, unit: unit, total: total, totalUnfiltered: totalUnfiltered, columnVisibility: columnVisibility }));
28
28
  });
29
29
  TableContextMenuWrapper.displayName = 'TableContextMenuWrapper';
30
30
  export default TableContextMenuWrapper;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/Table/index.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAgD,MAAM,OAAO,CAAC;AAarE,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAS1C,OAAO,EAAC,OAAO,EAAmC,MAAM,mBAAmB,CAAC;AAE5E,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AACjD,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AACzD,eAAO,MAAM,mBAAmB,kBAAkB,CAAC;AACnD,eAAO,MAAM,0BAA0B,yBAAyB,CAAC;AACjE,eAAO,MAAM,wBAAwB,uBAAuB,CAAC;AAC7D,eAAO,MAAM,UAAU,SAAS,CAAC;AACjC,eAAO,MAAM,eAAe,cAAc,CAAC;AAC3C,eAAO,MAAM,gBAAgB,eAAe,CAAC;AAC7C,eAAO,MAAM,qBAAqB,oBAAoB,CAAC;AACvD,eAAO,MAAM,aAAa,YAAY,CAAC;AACvC,eAAO,MAAM,aAAa,YAAY,CAAC;AAEvC,MAAM,MAAM,GAAG,GAAG,OAAO,CAAC;AAE1B,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,OAAO,KAAK,IAAI,CAAC;IACxD,YAAY,EAAE,OAAO,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;CACjC;AAED,eAAO,MAAM,KAAK,wCAoPhB,CAAC;AAEH,eAAe,KAAK,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/Table/index.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAgD,MAAM,OAAO,CAAC;AAarE,OAAO,EAAC,WAAW,EAAC,MAAM,eAAe,CAAC;AAS1C,OAAO,EAAC,OAAO,EAAmC,MAAM,mBAAmB,CAAC;AAE5E,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AACjD,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AACzD,eAAO,MAAM,mBAAmB,kBAAkB,CAAC;AACnD,eAAO,MAAM,0BAA0B,yBAAyB,CAAC;AACjE,eAAO,MAAM,wBAAwB,uBAAuB,CAAC;AAC7D,eAAO,MAAM,UAAU,SAAS,CAAC;AACjC,eAAO,MAAM,eAAe,cAAc,CAAC;AAC3C,eAAO,MAAM,gBAAgB,eAAe,CAAC;AAC7C,eAAO,MAAM,qBAAqB,oBAAoB,CAAC;AACvD,eAAO,MAAM,aAAa,YAAY,CAAC;AACvC,eAAO,MAAM,aAAa,YAAY,CAAC;AAEvC,MAAM,MAAM,GAAG,GAAG,OAAO,CAAC;AAE1B,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,OAAO,KAAK,IAAI,CAAC;IACxD,YAAY,EAAE,OAAO,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;CACjC;AAED,eAAO,MAAM,KAAK,wCAsMhB,CAAC;AAEH,eAAe,KAAK,CAAC"}
@@ -91,6 +91,14 @@ export const Table = React.memo(function Table({ data, total, filtered, profileT
91
91
  }
92
92
  selectSpan(row.name);
93
93
  }, [selectSpan, dashboardItems.length]);
94
+ const onRowContextMenu = useCallback((e, row) => {
95
+ e.preventDefault();
96
+ contextMenuRef.current?.setRow(row, () => {
97
+ show({
98
+ event: e,
99
+ });
100
+ });
101
+ }, [show]);
94
102
  const rows = useMemo(() => {
95
103
  if (table == null || table.numRows === 0) {
96
104
  return [];
@@ -131,59 +139,12 @@ export const Table = React.memo(function Table({ data, total, filtered, profileT
131
139
  const rows = Array.from({ length: table.numRows }, (_, i) => getRow(i));
132
140
  return rows;
133
141
  }, [table, colorByColors, colorBy]);
134
- const handleTableContextMenu = useCallback((e) => {
135
- e.preventDefault();
136
- // Find the closest table row element
137
- const target = e.target;
138
- const rowElement = target.closest('tr');
139
- if (rowElement !== null) {
140
- // Look for a data attribute that might contain the actual row ID
141
- const rowId = rowElement.getAttribute('data-row-id') ?? rowElement.getAttribute('data-id');
142
- if (rowId != null && rowId.length > 0) {
143
- // Find the row by ID
144
- const actualRowIndex = parseInt(rowId, 10);
145
- if (actualRowIndex >= 0 && actualRowIndex < rows.length) {
146
- const row = rows[actualRowIndex];
147
- contextMenuRef.current?.setRow(row, () => {
148
- show({
149
- event: e,
150
- });
151
- });
152
- return;
153
- }
154
- }
155
- // Fallback: try to find row by matching text content
156
- const nameCell = rowElement.querySelector('td:last-child'); // Name is usually the last column
157
- if (nameCell !== null) {
158
- const cellText = nameCell.textContent?.trim();
159
- if (cellText != null && cellText.length > 0) {
160
- // First try exact match
161
- let matchingRow = rows.find(row => row.name === cellText);
162
- // If no exact match, try partial match (in case of truncation)
163
- if (matchingRow == null) {
164
- matchingRow = rows.find(row => row.name.includes(cellText) || cellText.includes(row.name));
165
- }
166
- // If still no match, try matching the end of the name (for cases like package.function)
167
- if (matchingRow == null) {
168
- matchingRow = rows.find(row => row.name.endsWith(cellText) || cellText.endsWith(row.name.split('.').pop() ?? ''));
169
- }
170
- if (matchingRow != null) {
171
- contextMenuRef.current?.setRow(matchingRow, () => {
172
- show({
173
- event: e,
174
- });
175
- });
176
- }
177
- }
178
- }
179
- }
180
- }, [rows, show]);
181
142
  if (loading) {
182
143
  return (_jsx("div", { className: "overflow-clip h-[700px] min-h-[700px]", children: _jsx(TableSkeleton, { isHalfScreen: isHalfScreen, isDarkMode: isDarkMode }) }));
183
144
  }
184
145
  if (rows.length === 0) {
185
146
  return _jsx("div", { className: "mx-auto text-center", children: "Profile has no samples" });
186
147
  }
187
- return (_jsx(AnimatePresence, { children: _jsx(motion.div, { className: "h-full w-full", initial: { display: 'none', opacity: 0 }, animate: { display: 'block', opacity: 1 }, transition: { duration: 0.5 }, children: _jsxs("div", { className: "relative", children: [_jsx(TableContextMenuWrapper, { ref: contextMenuRef, menuId: MENU_ID }), _jsx("div", { className: "font-robotoMono h-[80vh] w-full", onContextMenu: handleTableContextMenu, children: _jsx(TableComponent, { data: rows, columns: columns, initialSorting: initialSorting, columnVisibility: columnVisibility, onRowClick: onRowClick, usePointerCursor: dashboardItems.length > 1, estimatedRowHeight: ROW_HEIGHT }) })] }) }, "table-loaded") }));
148
+ return (_jsx(AnimatePresence, { children: _jsx(motion.div, { className: "h-full w-full", initial: { display: 'none', opacity: 0 }, animate: { display: 'block', opacity: 1 }, transition: { duration: 0.5 }, children: _jsxs("div", { className: "relative", children: [_jsx(TableContextMenuWrapper, { ref: contextMenuRef, menuId: MENU_ID, unit: unit, total: total, totalUnfiltered: total, columnVisibility: columnVisibility }), _jsx("div", { className: "font-robotoMono h-[80vh] w-full", children: _jsx(TableComponent, { data: rows, columns: columns, initialSorting: initialSorting, columnVisibility: columnVisibility, onRowClick: onRowClick, onRowContextMenu: onRowContextMenu, usePointerCursor: dashboardItems.length > 1, estimatedRowHeight: ROW_HEIGHT }) })] }) }, "table-loaded") }));
188
149
  });
189
150
  export default Table;
package/dist/index.d.ts CHANGED
@@ -11,6 +11,8 @@ export * from './utils';
11
11
  export * from './ProfileTypeSelector';
12
12
  export * from './SourceView';
13
13
  export * from './ProfileMetricsGraph';
14
+ export { default as ProfileFilters } from './ProfileView/components/ProfileFilters';
15
+ export { useProfileFiltersUrlState } from './ProfileView/components/ProfileFilters/useProfileFiltersUrlState';
14
16
  export declare const DEFAULT_PROFILE_EXPLORER_PARAM_VALUES: {
15
17
  dashboard_items: string;
16
18
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAaA,OAAO,EAAC,aAAa,EAAC,MAAM,iBAAiB,CAAC;AAC9C,OAAO,eAAe,EAAE,EAAC,sBAAsB,EAAC,MAAM,mBAAmB,CAAC;AAC1E,OAAO,mBAAmB,MAAM,uBAAuB,CAAC;AACxD,OAAO,YAAY,MAAM,yBAAyB,CAAC;AAEnD,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,OAAO,EAAC,qBAAqB,EAAC,MAAM,2DAA2D,CAAC;AAChG,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,SAAS,CAAC;AACxB,cAAc,uBAAuB,CAAC;AACtC,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AAEtC,eAAO,MAAM,qCAAqC;;CAEjD,CAAC;AAEF,OAAO,EAAC,eAAe,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,YAAY,EAAE,aAAa,EAAC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAaA,OAAO,EAAC,aAAa,EAAC,MAAM,iBAAiB,CAAC;AAC9C,OAAO,eAAe,EAAE,EAAC,sBAAsB,EAAC,MAAM,mBAAmB,CAAC;AAC1E,OAAO,mBAAmB,MAAM,uBAAuB,CAAC;AACxD,OAAO,YAAY,MAAM,yBAAyB,CAAC;AAEnD,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,OAAO,EAAC,qBAAqB,EAAC,MAAM,2DAA2D,CAAC;AAChG,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,SAAS,CAAC;AACxB,cAAc,uBAAuB,CAAC;AACtC,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AAEtC,OAAO,EAAC,OAAO,IAAI,cAAc,EAAC,MAAM,yCAAyC,CAAC;AAClF,OAAO,EAAC,yBAAyB,EAAC,MAAM,mEAAmE,CAAC;AAE5G,eAAO,MAAM,qCAAqC;;CAEjD,CAAC;AAEF,OAAO,EAAC,eAAe,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,YAAY,EAAE,aAAa,EAAC,CAAC"}
package/dist/index.js CHANGED
@@ -23,6 +23,8 @@ export * from './utils';
23
23
  export * from './ProfileTypeSelector';
24
24
  export * from './SourceView';
25
25
  export * from './ProfileMetricsGraph';
26
+ export { default as ProfileFilters } from './ProfileView/components/ProfileFilters';
27
+ export { useProfileFiltersUrlState } from './ProfileView/components/ProfileFilters/useProfileFiltersUrlState';
26
28
  export const DEFAULT_PROFILE_EXPLORER_PARAM_VALUES = {
27
29
  dashboard_items: 'flamegraph',
28
30
  };
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@parca/profile",
3
- "version": "0.19.34",
3
+ "version": "0.19.36",
4
4
  "description": "Profile viewing libraries",
5
5
  "dependencies": {
6
6
  "@floating-ui/react": "^0.27.12",
7
7
  "@headlessui/react": "^1.7.19",
8
8
  "@iconify/react": "^4.0.0",
9
9
  "@parca/client": "0.17.3",
10
- "@parca/components": "0.16.355",
10
+ "@parca/components": "0.16.357",
11
11
  "@parca/dynamicsize": "0.16.65",
12
12
  "@parca/hooks": "0.0.99",
13
13
  "@parca/icons": "0.16.72",
@@ -78,5 +78,5 @@
78
78
  "access": "public",
79
79
  "registry": "https://registry.npmjs.org/"
80
80
  },
81
- "gitHead": "c7c7d4c70deecaf9f4fdf6cd4d40dbd52d88bd9e"
81
+ "gitHead": "27c6a55306acdd0bedc8d2dbd224793d2df46ec8"
82
82
  }
@@ -168,7 +168,11 @@ const numberMatchTypeItems: SelectItem[] = [
168
168
  },
169
169
  ];
170
170
 
171
- const ProfileFilters = (): JSX.Element => {
171
+ export interface ProfileFiltersProps {
172
+ readOnly?: boolean;
173
+ }
174
+
175
+ const ProfileFilters = ({readOnly = false}: ProfileFiltersProps = {}): JSX.Element => {
172
176
  const {profileSource} = useProfileViewContext();
173
177
  const currentProfileType = profileSource?.ProfileType()?.toString();
174
178
  const filterTypeItems = getFilterTypeItems(currentProfileType);
@@ -213,6 +217,7 @@ const ProfileFilters = (): JSX.Element => {
213
217
  items={filterTypeItems}
214
218
  selectedKey={filter.type}
215
219
  placeholder="Select Filter"
220
+ disabled={readOnly}
216
221
  onSelection={key => {
217
222
  // Check if this is a preset selection
218
223
  if (isPresetKey(key)) {
@@ -246,10 +251,13 @@ const ProfileFilters = (): JSX.Element => {
246
251
  }
247
252
  }}
248
253
  className={cx(
249
- 'rounded-l-md pr-1 gap-0 focus:z-50 focus:relative focus:outline-1',
250
- isPresetFilter ? 'rounded-r-none border-r-0' : 'rounded-r-none',
251
- filter.type != null ? 'border-r-0 w-auto' : 'w-32'
254
+ 'gap-0 focus:z-50 focus:relative focus:outline-1',
255
+ readOnly ? '' : 'pr-1',
256
+ readOnly && isPresetFilter ? 'rounded-md' : 'rounded-l-md rounded-r-none',
257
+ !readOnly && (isPresetFilter ? 'rounded-r-none border-r-0' : 'rounded-r-none'),
258
+ readOnly ? 'w-auto' : filter.type != null ? 'border-r-0 w-auto' : 'w-32'
252
259
  )}
260
+ hideCaretDropdown={readOnly}
253
261
  />
254
262
 
255
263
  {filter.type != null && !isPresetFilter && (
@@ -257,6 +265,7 @@ const ProfileFilters = (): JSX.Element => {
257
265
  <Select
258
266
  items={fieldItems}
259
267
  selectedKey={filter.field ?? ''}
268
+ disabled={readOnly}
260
269
  onSelection={key => {
261
270
  const newField = key as ProfileFilter['field'];
262
271
  const isNewFieldNumber = newField === 'address' || newField === 'line_number';
@@ -272,21 +281,31 @@ const ProfileFilters = (): JSX.Element => {
272
281
  updateFilter(filter.id, {field: newField});
273
282
  }
274
283
  }}
275
- className="rounded-none border-r-0 w-32 pr-1 gap-0 focus:z-50 focus:relative focus:outline-1"
284
+ className={cx(
285
+ 'rounded-none border-r-0 w-32 gap-0 focus:z-50 focus:relative focus:outline-1',
286
+ readOnly ? '' : 'pr-1'
287
+ )}
288
+ hideCaretDropdown={readOnly}
276
289
  />
277
290
 
278
291
  <Select
279
292
  items={matchTypeItems}
280
293
  selectedKey={filter.matchType ?? ''}
294
+ disabled={readOnly}
281
295
  onSelection={key =>
282
296
  updateFilter(filter.id, {matchType: key as ProfileFilter['matchType']})
283
297
  }
284
- className="rounded-none border-r-0 pr-1 gap-0 focus:z-50 focus:relative focus:outline-1"
298
+ className={cx(
299
+ 'rounded-none border-r-0 gap-0 focus:z-50 focus:relative focus:outline-1',
300
+ readOnly ? '' : 'pr-1'
301
+ )}
302
+ hideCaretDropdown={readOnly}
285
303
  />
286
304
 
287
305
  <Input
288
306
  placeholder="Value"
289
307
  value={filter.value}
308
+ disabled={readOnly}
290
309
  onChange={e => updateFilter(filter.id, {value: e.target.value})}
291
310
  onKeyDown={handleKeyDown}
292
311
  className="rounded-none w-36 text-sm focus:outline-1"
@@ -294,40 +313,44 @@ const ProfileFilters = (): JSX.Element => {
294
313
  </>
295
314
  )}
296
315
 
297
- <Button
298
- variant="neutral"
299
- onClick={() => {
300
- // If we're displaying local filters and this is the last one, reset everything
301
- if (localFilters.length > 0 && localFilters.length === 1) {
302
- resetFilters();
303
- }
304
- // If we're displaying applied filters and this is the last one, reset everything
305
- else if (localFilters.length === 0 && filtersToRender.length === 1) {
306
- resetFilters();
307
- }
308
- // Otherwise, just remove this specific filter
309
- else {
310
- removeFilter(filter.id);
311
- }
312
- }}
313
- className={cx(
314
- 'h-[38px] p-3',
315
- filter.type != null ? 'rounded-none rounded-r-md' : 'rounded-l-none rounded-r-md'
316
- )}
317
- >
318
- <Icon icon="mdi:close" className="h-4 w-4" />
319
- </Button>
316
+ {!readOnly && (
317
+ <Button
318
+ variant="neutral"
319
+ onClick={() => {
320
+ // If we're displaying local filters and this is the last one, reset everything
321
+ if (localFilters.length > 0 && localFilters.length === 1) {
322
+ resetFilters();
323
+ }
324
+ // If we're displaying applied filters and this is the last one, reset everything
325
+ else if (localFilters.length === 0 && filtersToRender.length === 1) {
326
+ resetFilters();
327
+ }
328
+ // Otherwise, just remove this specific filter
329
+ else {
330
+ removeFilter(filter.id);
331
+ }
332
+ }}
333
+ className={cx(
334
+ 'h-[38px] p-3',
335
+ filter.type != null
336
+ ? 'rounded-none rounded-r-md'
337
+ : 'rounded-l-none rounded-r-md'
338
+ )}
339
+ >
340
+ <Icon icon="mdi:close" className="h-4 w-4" />
341
+ </Button>
342
+ )}
320
343
  </div>
321
344
  );
322
345
  })}
323
346
 
324
- {localFilters.length > 0 && (
347
+ {!readOnly && localFilters.length > 0 && (
325
348
  <Button variant="neutral" onClick={addFilter} className="p-3 h-[38px]">
326
349
  <Icon icon="mdi:filter-plus-outline" className="h-4 w-4" />
327
350
  </Button>
328
351
  )}
329
352
 
330
- {localFilters.length === 0 && (appliedFilters?.length ?? 0) === 0 && (
353
+ {!readOnly && localFilters.length === 0 && (appliedFilters?.length ?? 0) === 0 && (
331
354
  <Button variant="neutral" onClick={addFilter} className="flex items-center gap-2">
332
355
  <Icon icon="mdi:filter-outline" className="h-4 w-4" />
333
356
  <span>Filter</span>
@@ -335,7 +358,7 @@ const ProfileFilters = (): JSX.Element => {
335
358
  )}
336
359
  </div>
337
360
 
338
- {localFilters.length > 0 && (
361
+ {!readOnly && localFilters.length > 0 && (
339
362
  <Button
340
363
  variant="primary"
341
364
  onClick={onApplyFilters}
@@ -13,20 +13,35 @@
13
13
 
14
14
  import {Icon} from '@iconify/react';
15
15
  import cx from 'classnames';
16
- import {Item, Menu} from 'react-contexify';
16
+ import {Item, Menu, Submenu} from 'react-contexify';
17
17
 
18
18
  import 'react-contexify/dist/ReactContexify.css';
19
19
 
20
20
  import {useParcaContext, useURLState} from '@parca/components';
21
+ import {valueFormatter} from '@parca/utilities';
21
22
 
22
23
  import {type Row} from '.';
24
+ import {getTextForCumulative} from '../ProfileFlameGraph/FlameGraphArrow/utils';
25
+ import {truncateString} from '../utils';
26
+ import {type ColumnName} from './utils/functions';
23
27
 
24
28
  interface TableContextMenuProps {
25
29
  menuId: string;
26
30
  row: Row | null;
31
+ unit?: string;
32
+ total?: bigint;
33
+ totalUnfiltered?: bigint;
34
+ columnVisibility?: Record<ColumnName, boolean>;
27
35
  }
28
36
 
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 => {
30
45
  const [_, setSandwichFunctionName] = useURLState<string | undefined>('sandwich_function_name');
31
46
  const [dashboardItems, setDashboardItems] = useURLState<string[]>('dashboard_items', {
32
47
  alwaysReturnArray: true,
@@ -42,6 +57,133 @@ const TableContextMenu = ({menuId, row}: TableContextMenuProps): React.JSX.Eleme
42
57
  }
43
58
  };
44
59
 
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
+
45
187
  const isMenuDisabled = row === null || enableSandwichView !== true;
46
188
 
47
189
  return (
@@ -63,6 +205,31 @@ const TableContextMenu = ({menuId, row}: TableContextMenuProps): React.JSX.Eleme
63
205
  </div>
64
206
  </div>
65
207
  </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>
66
233
  </Menu>
67
234
  );
68
235
  };
@@ -15,9 +15,14 @@ import {forwardRef, useImperativeHandle, useState} from 'react';
15
15
 
16
16
  import {type Row} from '.';
17
17
  import TableContextMenu from './TableContextMenu';
18
+ import {type ColumnName} from './utils/functions';
18
19
 
19
20
  interface TableContextMenuWrapperProps {
20
21
  menuId: string;
22
+ unit?: string;
23
+ total?: bigint;
24
+ totalUnfiltered?: bigint;
25
+ columnVisibility?: Record<ColumnName, boolean>;
21
26
  }
22
27
 
23
28
  export interface TableContextMenuWrapperRef {
@@ -27,7 +32,7 @@ export interface TableContextMenuWrapperRef {
27
32
  const TableContextMenuWrapper = forwardRef<
28
33
  TableContextMenuWrapperRef,
29
34
  TableContextMenuWrapperProps
30
- >(({menuId}, ref) => {
35
+ >(({menuId, unit, total, totalUnfiltered, columnVisibility}, ref) => {
31
36
  const [row, setRow] = useState<Row | null>(null);
32
37
 
33
38
  useImperativeHandle(ref, () => ({
@@ -40,7 +45,16 @@ const TableContextMenuWrapper = forwardRef<
40
45
  },
41
46
  }));
42
47
 
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
+ );
44
58
  });
45
59
 
46
60
  TableContextMenuWrapper.displayName = 'TableContextMenuWrapper';
@@ -149,6 +149,18 @@ export const Table = React.memo(function Table({
149
149
  [selectSpan, dashboardItems.length]
150
150
  );
151
151
 
152
+ const onRowContextMenu = useCallback(
153
+ (e: React.MouseEvent, row: Row) => {
154
+ e.preventDefault();
155
+ contextMenuRef.current?.setRow(row, () => {
156
+ show({
157
+ event: e,
158
+ });
159
+ });
160
+ },
161
+ [show]
162
+ );
163
+
152
164
  const rows: DataRow[] = useMemo(() => {
153
165
  if (table == null || table.numRows === 0) {
154
166
  return [];
@@ -201,72 +213,6 @@ export const Table = React.memo(function Table({
201
213
  return rows;
202
214
  }, [table, colorByColors, colorBy]);
203
215
 
204
- const handleTableContextMenu = useCallback(
205
- (e: React.MouseEvent) => {
206
- e.preventDefault();
207
-
208
- // Find the closest table row element
209
- const target = e.target as Element;
210
- const rowElement = target.closest('tr');
211
-
212
- if (rowElement !== null) {
213
- // Look for a data attribute that might contain the actual row ID
214
- const rowId = rowElement.getAttribute('data-row-id') ?? rowElement.getAttribute('data-id');
215
-
216
- if (rowId != null && rowId.length > 0) {
217
- // Find the row by ID
218
- const actualRowIndex = parseInt(rowId, 10);
219
-
220
- if (actualRowIndex >= 0 && actualRowIndex < rows.length) {
221
- const row = rows[actualRowIndex];
222
-
223
- contextMenuRef.current?.setRow(row, () => {
224
- show({
225
- event: e,
226
- });
227
- });
228
- return;
229
- }
230
- }
231
-
232
- // Fallback: try to find row by matching text content
233
- const nameCell = rowElement.querySelector('td:last-child'); // Name is usually the last column
234
- if (nameCell !== null) {
235
- const cellText = nameCell.textContent?.trim();
236
-
237
- if (cellText != null && cellText.length > 0) {
238
- // First try exact match
239
- let matchingRow = rows.find(row => row.name === cellText);
240
-
241
- // If no exact match, try partial match (in case of truncation)
242
- if (matchingRow == null) {
243
- matchingRow = rows.find(
244
- row => row.name.includes(cellText) || cellText.includes(row.name)
245
- );
246
- }
247
-
248
- // If still no match, try matching the end of the name (for cases like package.function)
249
- if (matchingRow == null) {
250
- matchingRow = rows.find(
251
- row =>
252
- row.name.endsWith(cellText) || cellText.endsWith(row.name.split('.').pop() ?? '')
253
- );
254
- }
255
-
256
- if (matchingRow != null) {
257
- contextMenuRef.current?.setRow(matchingRow, () => {
258
- show({
259
- event: e,
260
- });
261
- });
262
- }
263
- }
264
- }
265
- }
266
- },
267
- [rows, show]
268
- );
269
-
270
216
  if (loading) {
271
217
  return (
272
218
  <div className="overflow-clip h-[700px] min-h-[700px]">
@@ -289,14 +235,22 @@ export const Table = React.memo(function Table({
289
235
  transition={{duration: 0.5}}
290
236
  >
291
237
  <div className="relative">
292
- <TableContextMenuWrapper ref={contextMenuRef} menuId={MENU_ID} />
293
- <div className="font-robotoMono h-[80vh] w-full" onContextMenu={handleTableContextMenu}>
238
+ <TableContextMenuWrapper
239
+ ref={contextMenuRef}
240
+ menuId={MENU_ID}
241
+ unit={unit}
242
+ total={total}
243
+ totalUnfiltered={total}
244
+ columnVisibility={columnVisibility}
245
+ />
246
+ <div className="font-robotoMono h-[80vh] w-full">
294
247
  <TableComponent
295
248
  data={rows}
296
249
  columns={columns}
297
250
  initialSorting={initialSorting}
298
251
  columnVisibility={columnVisibility}
299
252
  onRowClick={onRowClick}
253
+ onRowContextMenu={onRowContextMenu}
300
254
  usePointerCursor={dashboardItems.length > 1}
301
255
  estimatedRowHeight={ROW_HEIGHT}
302
256
  />
package/src/index.tsx CHANGED
@@ -26,6 +26,9 @@ export * from './ProfileTypeSelector';
26
26
  export * from './SourceView';
27
27
  export * from './ProfileMetricsGraph';
28
28
 
29
+ export {default as ProfileFilters} from './ProfileView/components/ProfileFilters';
30
+ export {useProfileFiltersUrlState} from './ProfileView/components/ProfileFilters/useProfileFiltersUrlState';
31
+
29
32
  export const DEFAULT_PROFILE_EXPLORER_PARAM_VALUES = {
30
33
  dashboard_items: 'flamegraph',
31
34
  };