@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 +8 -0
- package/dist/ProfileView/components/ProfileFilters/index.d.ts +4 -1
- package/dist/ProfileView/components/ProfileFilters/index.d.ts.map +1 -1
- package/dist/ProfileView/components/ProfileFilters/index.js +8 -6
- package/dist/Table/TableContextMenu.d.ts +6 -1
- package/dist/Table/TableContextMenu.d.ts.map +1 -1
- package/dist/Table/TableContextMenu.js +124 -5
- package/dist/Table/TableContextMenuWrapper.d.ts +5 -0
- package/dist/Table/TableContextMenuWrapper.d.ts.map +1 -1
- package/dist/Table/TableContextMenuWrapper.js +2 -2
- package/dist/Table/index.d.ts.map +1 -1
- package/dist/Table/index.js +9 -48
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/package.json +3 -3
- package/src/ProfileView/components/ProfileFilters/index.tsx +55 -32
- package/src/Table/TableContextMenu.tsx +169 -2
- package/src/Table/TableContextMenuWrapper.tsx +16 -2
- package/src/Table/index.tsx +22 -68
- package/src/index.tsx +3 -0
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
|
-
|
|
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,
|
|
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('
|
|
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:
|
|
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
|
|
220
|
-
|
|
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;
|
|
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
|
-
|
|
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 (
|
|
35
|
-
|
|
36
|
-
|
|
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;
|
|
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,
|
|
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"}
|
package/dist/Table/index.js
CHANGED
|
@@ -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",
|
|
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
|
};
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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.
|
|
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.
|
|
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": "
|
|
81
|
+
"gitHead": "27c6a55306acdd0bedc8d2dbd224793d2df46ec8"
|
|
82
82
|
}
|
|
@@ -168,7 +168,11 @@ const numberMatchTypeItems: SelectItem[] = [
|
|
|
168
168
|
},
|
|
169
169
|
];
|
|
170
170
|
|
|
171
|
-
|
|
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
|
-
'
|
|
250
|
-
|
|
251
|
-
|
|
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=
|
|
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=
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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 = ({
|
|
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
|
|
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';
|
package/src/Table/index.tsx
CHANGED
|
@@ -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
|
|
293
|
-
|
|
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
|
};
|