@kylincloud/flamegraph 0.35.21 → 0.35.22
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 +7 -0
- package/dist/FlameGraph/FlameGraphRenderer.d.ts.map +1 -1
- package/dist/ProfilerTable.d.ts +4 -0
- package/dist/ProfilerTable.d.ts.map +1 -1
- package/dist/i18n.d.ts +3 -0
- package/dist/i18n.d.ts.map +1 -1
- package/dist/index.cjs.js +2 -2
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +4 -4
- package/dist/index.esm.js.map +1 -1
- package/dist/index.node.cjs.js +2 -2
- package/dist/index.node.cjs.js.map +1 -1
- package/dist/index.node.esm.js +4 -4
- package/dist/index.node.esm.js.map +1 -1
- package/dist/shims/Table.d.ts +4 -2
- package/dist/shims/Table.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/FlameGraph/FlameGraphRenderer.tsx +5 -1
- package/src/ProfilerTable.module.scss +43 -0
- package/src/ProfilerTable.tsx +184 -11
- package/src/i18n.tsx +15 -0
- package/src/shims/Table.tsx +24 -9
package/dist/shims/Table.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ReactNode, CSSProperties, RefObject } from 'react';
|
|
1
|
+
import React, { ReactNode, CSSProperties, RefObject } from 'react';
|
|
2
2
|
interface CustomProp {
|
|
3
|
-
[k: string]: string | CSSProperties | ReactNode | number | undefined;
|
|
3
|
+
[k: string]: string | CSSProperties | ReactNode | number | undefined | ((e: React.MouseEvent) => void) | (() => void);
|
|
4
4
|
}
|
|
5
5
|
export interface Cell extends CustomProp {
|
|
6
6
|
value: ReactNode | string;
|
|
@@ -18,6 +18,8 @@ export interface BodyRow {
|
|
|
18
18
|
isRowDisabled?: boolean;
|
|
19
19
|
cells: Cell[];
|
|
20
20
|
onClick?: () => void;
|
|
21
|
+
onDoubleClick?: () => void;
|
|
22
|
+
onContextMenu?: (e: React.MouseEvent) => void;
|
|
21
23
|
className?: string;
|
|
22
24
|
}
|
|
23
25
|
export type TableBodyType = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Table.d.ts","sourceRoot":"","sources":["../../src/shims/Table.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Table.d.ts","sourceRoot":"","sources":["../../src/shims/Table.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAAY,SAAS,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAU7E,UAAU,UAAU;IAClB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC;CACvH;AAED,MAAM,WAAW,IAAK,SAAQ,UAAU;IACtC,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC;IAC1B,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAED,UAAU,QAAS,SAAQ,UAAU;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,OAAO;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,aAAa,GACrB;IACE,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GACD;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB,CAAC;AAEN,KAAK,KAAK,GAAG,aAAa,GAAG;IAC3B,OAAO,EAAE,QAAQ,EAAE,CAAC;CACrB,CAAC;AAEF,UAAU,cAAc;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,eAAe,EAAE,MAAM,GAAG,KAAK,CAAC;CACjC;AAED,eAAO,MAAM,YAAY,YAAa,QAAQ,EAAE,KAAG,cAsBlD,CAAC;AAEF,UAAU,UAAU;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/C,KAAK,EAAE,KAAK,CAAC;IACb,YAAY,CAAC,EAAE,SAAS,CAAC,uBAAuB,CAAC,CAAC;IAClD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,iBAAS,KAAK,CAAC,EACb,eAAe,EACf,MAAM,EACN,gBAAgB,EAChB,KAAK,EACL,YAAY,EACZ,SAAS,EACT,SAAS,EACT,YAAY,GACb,EAAE,UAAU,2CAsGZ;AA2DD,eAAe,KAAK,CAAC"}
|
package/package.json
CHANGED
|
@@ -337,7 +337,7 @@ class FlameGraphRenderer extends Component<
|
|
|
337
337
|
return (
|
|
338
338
|
this.state.selectedItem.isJust ||
|
|
339
339
|
JSON.stringify(this.initialFlamegraphState) !==
|
|
340
|
-
|
|
340
|
+
JSON.stringify(this.state.flamegraphConfigs)
|
|
341
341
|
);
|
|
342
342
|
};
|
|
343
343
|
|
|
@@ -371,6 +371,10 @@ class FlameGraphRenderer extends Component<
|
|
|
371
371
|
selectedItem={this.state.selectedItem}
|
|
372
372
|
handleTableItemClick={this.setActiveItem}
|
|
373
373
|
palette={this.state.palette}
|
|
374
|
+
// 新增:传递聚焦和视图切换能力给表格
|
|
375
|
+
onFocusOnNode={this.onFocusOnNode}
|
|
376
|
+
updateView={this.props.onlyDisplay ? undefined : this.updateView}
|
|
377
|
+
enableSandwichView={this.props.enableSandwichView}
|
|
374
378
|
/>
|
|
375
379
|
</div>
|
|
376
380
|
);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/ProfilerTable.module.scss
|
|
2
|
+
|
|
3
|
+
.tableContextMenu {
|
|
4
|
+
|
|
5
|
+
// 继承 react-menu 的基础样式
|
|
6
|
+
:global(.szh-menu) {
|
|
7
|
+
background-color: var(--ps-ui-background, #fff);
|
|
8
|
+
border: 1px solid var(--ps-ui-border, #ccc);
|
|
9
|
+
border-radius: 4px;
|
|
10
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
11
|
+
padding: 4px 0;
|
|
12
|
+
min-width: 180px;
|
|
13
|
+
z-index: 1000;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
:global(.szh-menu__item) {
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
gap: 8px;
|
|
20
|
+
padding: 8px 12px;
|
|
21
|
+
cursor: pointer;
|
|
22
|
+
color: var(--ps-ui-foreground-text, #333);
|
|
23
|
+
font-size: 13px;
|
|
24
|
+
transition: background-color 0.15s;
|
|
25
|
+
|
|
26
|
+
&:hover {
|
|
27
|
+
background-color: var(--ps-ui-element-bg-highlight, #f0f0f0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
svg {
|
|
31
|
+
width: 14px;
|
|
32
|
+
height: 14px;
|
|
33
|
+
flex-shrink: 0;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.sandwichItem {
|
|
39
|
+
svg {
|
|
40
|
+
width: 16px;
|
|
41
|
+
height: 16px;
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/ProfilerTable.tsx
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// src/ProfilerTable.tsx
|
|
2
|
-
import React, { useRef, RefObject } from 'react';
|
|
2
|
+
import React, { useRef, RefObject, useCallback, useState } from 'react';
|
|
3
3
|
import type Color from 'color';
|
|
4
4
|
import cl from 'classnames';
|
|
5
5
|
import type { Maybe } from 'true-myth';
|
|
6
|
-
import {
|
|
6
|
+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
7
|
+
import { faCrosshairs } from '@fortawesome/free-solid-svg-icons/faCrosshairs';
|
|
8
|
+
import { faCopy } from '@fortawesome/free-solid-svg-icons/faCopy';
|
|
9
|
+
import { faHighlighter } from '@fortawesome/free-solid-svg-icons/faHighlighter';
|
|
10
|
+
import { doubleFF, singleFF, Flamebearer, createFF } from './models';
|
|
7
11
|
// until ui is moved to its own package this should do it
|
|
8
12
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
9
13
|
import TableUI, {
|
|
@@ -11,6 +15,7 @@ import TableUI, {
|
|
|
11
15
|
BodyRow,
|
|
12
16
|
TableBodyType,
|
|
13
17
|
} from './shims/Table';
|
|
18
|
+
import { ControlledMenu, MenuItem, useMenuState } from './shims/Menu';
|
|
14
19
|
import TableTooltip from './Tooltip/TableTooltip';
|
|
15
20
|
import { getFormatter, ratioToPercent, diffPercent } from './format/format';
|
|
16
21
|
import {
|
|
@@ -20,10 +25,14 @@ import {
|
|
|
20
25
|
import { fitIntoTableCell, FitModes } from './fitMode/fitMode';
|
|
21
26
|
import { isMatch } from './search';
|
|
22
27
|
import type { FlamegraphPalette } from './FlameGraph/FlameGraphComponent/colorPalette';
|
|
28
|
+
import { SandwichIcon } from './Icons';
|
|
23
29
|
import {
|
|
24
30
|
useFlamegraphI18n,
|
|
25
31
|
type FlamegraphMessages,
|
|
26
32
|
} from './i18n';
|
|
33
|
+
import type { ViewTypes } from './FlameGraph/FlameGraphComponent/viewTypes';
|
|
34
|
+
|
|
35
|
+
import styles from './ProfilerTable.module.scss';
|
|
27
36
|
|
|
28
37
|
const zero = (v?: number) => v || 0;
|
|
29
38
|
|
|
@@ -138,6 +147,42 @@ function generateTable(
|
|
|
138
147
|
return Array.from(hash.values());
|
|
139
148
|
}
|
|
140
149
|
|
|
150
|
+
/**
|
|
151
|
+
* 根据函数名在 flamebearer 中查找 total 值最大的节点位置
|
|
152
|
+
* 返回 { i, j } 坐标,用于聚焦
|
|
153
|
+
*/
|
|
154
|
+
function findNodeByName(
|
|
155
|
+
flamebearer: Flamebearer,
|
|
156
|
+
targetName: string
|
|
157
|
+
): { i: number; j: number } | null {
|
|
158
|
+
if (!flamebearer || !flamebearer.levels) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const { names, levels, format } = flamebearer;
|
|
163
|
+
const ff = createFF(format);
|
|
164
|
+
|
|
165
|
+
let bestMatch: { i: number; j: number; total: number } | null = null;
|
|
166
|
+
|
|
167
|
+
for (let i = 0; i < levels.length; i++) {
|
|
168
|
+
const level = levels[i];
|
|
169
|
+
for (let j = 0; j < level.length; j += ff.jStep) {
|
|
170
|
+
const nameIndex = ff.getBarName(level, j);
|
|
171
|
+
const name = names[nameIndex];
|
|
172
|
+
|
|
173
|
+
if (name === targetName) {
|
|
174
|
+
const total = ff.getBarTotal(level, j);
|
|
175
|
+
// 找到 total 值最大的节点
|
|
176
|
+
if (!bestMatch || total > bestMatch.total) {
|
|
177
|
+
bestMatch = { i, j, total };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return bestMatch ? { i: bestMatch.i, j: bestMatch.j } : null;
|
|
184
|
+
}
|
|
185
|
+
|
|
141
186
|
// the value must be negative or zero
|
|
142
187
|
function neg(v: number) {
|
|
143
188
|
return Math.min(0, v);
|
|
@@ -200,6 +245,8 @@ interface GetTableBodyRowsProps
|
|
|
200
245
|
sortBy: string;
|
|
201
246
|
sortByDirection: string;
|
|
202
247
|
messages: FlamegraphMessages;
|
|
248
|
+
onRowContextMenu: (e: React.MouseEvent, name: string) => void;
|
|
249
|
+
onRowDoubleClick: (name: string) => void;
|
|
203
250
|
}
|
|
204
251
|
|
|
205
252
|
const getTableBody = ({
|
|
@@ -212,6 +259,8 @@ const getTableBody = ({
|
|
|
212
259
|
palette,
|
|
213
260
|
selectedItem,
|
|
214
261
|
messages,
|
|
262
|
+
onRowContextMenu,
|
|
263
|
+
onRowDoubleClick,
|
|
215
264
|
}: GetTableBodyRowsProps): TableBodyType => {
|
|
216
265
|
const { numTicks, maxSelf, sampleRate, spyName, units } = flamebearer;
|
|
217
266
|
|
|
@@ -292,6 +341,8 @@ const getTableBody = ({
|
|
|
292
341
|
'data-row': `${x.type};${x.name};${x.self};${x.total}`,
|
|
293
342
|
isRowSelected: isRowSelected(x.name),
|
|
294
343
|
onClick: () => handleTableItemClick(x),
|
|
344
|
+
onContextMenu: (e: React.MouseEvent) => onRowContextMenu(e, x.name),
|
|
345
|
+
onDoubleClick: () => onRowDoubleClick(x.name),
|
|
295
346
|
cells: [
|
|
296
347
|
{ value: nameCell(x) },
|
|
297
348
|
{
|
|
@@ -337,6 +388,8 @@ const getTableBody = ({
|
|
|
337
388
|
'data-row': `${x.type};${x.name};${x.totalLeft};${x.leftTicks};${x.totalRght};${x.rightTicks}`,
|
|
338
389
|
isRowSelected: isRowSelected(x.name),
|
|
339
390
|
onClick: () => handleTableItemClick(x),
|
|
391
|
+
onContextMenu: (e: React.MouseEvent) => onRowContextMenu(e, x.name),
|
|
392
|
+
onDoubleClick: () => onRowDoubleClick(x.name),
|
|
340
393
|
cells: [
|
|
341
394
|
{ value: nameCell(x) },
|
|
342
395
|
{ value: `${leftPercent} %` },
|
|
@@ -389,6 +442,13 @@ export interface ProfilerTableProps {
|
|
|
389
442
|
selectedItem: Maybe<string>;
|
|
390
443
|
|
|
391
444
|
tableBodyRef: RefObject<HTMLTableSectionElement>;
|
|
445
|
+
|
|
446
|
+
// 新增:聚焦到火焰图节点
|
|
447
|
+
onFocusOnNode?: (i: number, j: number) => void;
|
|
448
|
+
// 新增:切换视图(用于 sandwich view)
|
|
449
|
+
updateView?: (v: ViewTypes) => void;
|
|
450
|
+
// 新增:是否启用 sandwich 视图菜单项
|
|
451
|
+
enableSandwichView?: boolean;
|
|
392
452
|
}
|
|
393
453
|
|
|
394
454
|
function Table({
|
|
@@ -400,8 +460,76 @@ function Table({
|
|
|
400
460
|
highlightQuery,
|
|
401
461
|
selectedItem,
|
|
402
462
|
palette,
|
|
463
|
+
onFocusOnNode,
|
|
464
|
+
updateView,
|
|
465
|
+
enableSandwichView = true,
|
|
403
466
|
}: ProfilerTableProps & { isDoubles: boolean }) {
|
|
404
467
|
const i18n = useFlamegraphI18n();
|
|
468
|
+
const [menuProps, toggleMenu] = useMenuState({ transition: true });
|
|
469
|
+
const [anchorPoint, setAnchorPoint] = useState({ x: 0, y: 0 });
|
|
470
|
+
const [contextMenuTarget, setContextMenuTarget] = useState<string | null>(null);
|
|
471
|
+
|
|
472
|
+
// 双击处理:聚焦到火焰图
|
|
473
|
+
const handleDoubleClick = useCallback(
|
|
474
|
+
(name: string) => {
|
|
475
|
+
if (!onFocusOnNode) return;
|
|
476
|
+
|
|
477
|
+
const node = findNodeByName(flamebearer, name);
|
|
478
|
+
if (node) {
|
|
479
|
+
onFocusOnNode(node.i, node.j);
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
[flamebearer, onFocusOnNode]
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
// 右键菜单处理
|
|
486
|
+
const handleContextMenu = useCallback(
|
|
487
|
+
(e: React.MouseEvent, name: string) => {
|
|
488
|
+
e.preventDefault();
|
|
489
|
+
setAnchorPoint({ x: e.clientX, y: e.clientY });
|
|
490
|
+
setContextMenuTarget(name);
|
|
491
|
+
toggleMenu(true);
|
|
492
|
+
},
|
|
493
|
+
[toggleMenu]
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
const handleMenuClose = useCallback(() => {
|
|
497
|
+
toggleMenu(false);
|
|
498
|
+
setContextMenuTarget(null);
|
|
499
|
+
}, [toggleMenu]);
|
|
500
|
+
|
|
501
|
+
// 菜单项:聚焦到此函数
|
|
502
|
+
const handleFocusClick = useCallback(() => {
|
|
503
|
+
if (!contextMenuTarget || !onFocusOnNode) return;
|
|
504
|
+
|
|
505
|
+
const node = findNodeByName(flamebearer, contextMenuTarget);
|
|
506
|
+
if (node) {
|
|
507
|
+
onFocusOnNode(node.i, node.j);
|
|
508
|
+
}
|
|
509
|
+
handleMenuClose();
|
|
510
|
+
}, [contextMenuTarget, flamebearer, onFocusOnNode, handleMenuClose]);
|
|
511
|
+
|
|
512
|
+
// 菜单项:复制函数名
|
|
513
|
+
const handleCopyClick = useCallback(() => {
|
|
514
|
+
if (!contextMenuTarget || !navigator.clipboard) return;
|
|
515
|
+
navigator.clipboard.writeText(contextMenuTarget);
|
|
516
|
+
handleMenuClose();
|
|
517
|
+
}, [contextMenuTarget, handleMenuClose]);
|
|
518
|
+
|
|
519
|
+
// 菜单项:高亮相似节点
|
|
520
|
+
const handleHighlightClick = useCallback(() => {
|
|
521
|
+
if (!contextMenuTarget) return;
|
|
522
|
+
handleTableItemClick({ name: contextMenuTarget });
|
|
523
|
+
handleMenuClose();
|
|
524
|
+
}, [contextMenuTarget, handleTableItemClick, handleMenuClose]);
|
|
525
|
+
|
|
526
|
+
// 菜单项:在 sandwich 视图中打开
|
|
527
|
+
const handleSandwichClick = useCallback(() => {
|
|
528
|
+
if (!contextMenuTarget || !updateView) return;
|
|
529
|
+
updateView('sandwich');
|
|
530
|
+
handleTableItemClick({ name: contextMenuTarget });
|
|
531
|
+
handleMenuClose();
|
|
532
|
+
}, [contextMenuTarget, updateView, handleTableItemClick, handleMenuClose]);
|
|
405
533
|
|
|
406
534
|
const tableFormat = React.useMemo(
|
|
407
535
|
() =>
|
|
@@ -463,19 +591,58 @@ function Table({
|
|
|
463
591
|
palette,
|
|
464
592
|
selectedItem,
|
|
465
593
|
messages: i18n,
|
|
594
|
+
onRowContextMenu: handleContextMenu,
|
|
595
|
+
onRowDoubleClick: handleDoubleClick,
|
|
596
|
+
onFocusOnNode,
|
|
597
|
+
updateView,
|
|
598
|
+
enableSandwichView,
|
|
466
599
|
}),
|
|
467
600
|
};
|
|
468
601
|
|
|
602
|
+
const isHighlighted =
|
|
603
|
+
selectedItem.isJust && selectedItem.value === contextMenuTarget;
|
|
604
|
+
|
|
469
605
|
return (
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
'flamegraph-table
|
|
477
|
-
|
|
478
|
-
|
|
606
|
+
<>
|
|
607
|
+
<TableUI
|
|
608
|
+
/* eslint-disable-next-line react/jsx-props-no-spreading */
|
|
609
|
+
{...tableSortProps}
|
|
610
|
+
tableBodyRef={tableBodyRef}
|
|
611
|
+
table={table}
|
|
612
|
+
className={cl('flamegraph-table', {
|
|
613
|
+
'flamegraph-table-doubles': isDoubles,
|
|
614
|
+
})}
|
|
615
|
+
/>
|
|
616
|
+
|
|
617
|
+
{/* 右键菜单 */}
|
|
618
|
+
<ControlledMenu
|
|
619
|
+
{...menuProps}
|
|
620
|
+
anchorPoint={anchorPoint}
|
|
621
|
+
onClose={handleMenuClose}
|
|
622
|
+
className={styles.tableContextMenu}
|
|
623
|
+
>
|
|
624
|
+
{onFocusOnNode && (
|
|
625
|
+
<MenuItem onClick={handleFocusClick}>
|
|
626
|
+
<FontAwesomeIcon icon={faCrosshairs} />
|
|
627
|
+
{i18n.focusOnThisFunction}
|
|
628
|
+
</MenuItem>
|
|
629
|
+
)}
|
|
630
|
+
<MenuItem onClick={handleCopyClick}>
|
|
631
|
+
<FontAwesomeIcon icon={faCopy} />
|
|
632
|
+
{i18n.copyFunctionName}
|
|
633
|
+
</MenuItem>
|
|
634
|
+
<MenuItem onClick={handleHighlightClick}>
|
|
635
|
+
<FontAwesomeIcon icon={faHighlighter} />
|
|
636
|
+
{isHighlighted ? i18n.clearHighlight : i18n.highlightSimilarNodes}
|
|
637
|
+
</MenuItem>
|
|
638
|
+
{updateView && enableSandwichView && (
|
|
639
|
+
<MenuItem onClick={handleSandwichClick} className={styles.sandwichItem}>
|
|
640
|
+
<SandwichIcon fill="currentColor" />
|
|
641
|
+
{i18n.openInSandwichView}
|
|
642
|
+
</MenuItem>
|
|
643
|
+
)}
|
|
644
|
+
</ControlledMenu>
|
|
645
|
+
</>
|
|
479
646
|
);
|
|
480
647
|
}
|
|
481
648
|
|
|
@@ -486,6 +653,9 @@ const ProfilerTable = React.memo(function ProfilerTable({
|
|
|
486
653
|
highlightQuery,
|
|
487
654
|
palette,
|
|
488
655
|
selectedItem,
|
|
656
|
+
onFocusOnNode,
|
|
657
|
+
updateView,
|
|
658
|
+
enableSandwichView,
|
|
489
659
|
}: Omit<ProfilerTableProps, 'tableBodyRef'>) {
|
|
490
660
|
const tableBodyRef = useRef<HTMLTableSectionElement>(null);
|
|
491
661
|
const isDoubles = flamebearer.format === 'double';
|
|
@@ -510,6 +680,9 @@ const ProfilerTable = React.memo(function ProfilerTable({
|
|
|
510
680
|
handleTableItemClick={handleTableItemClick}
|
|
511
681
|
palette={palette}
|
|
512
682
|
selectedItem={selectedItem}
|
|
683
|
+
onFocusOnNode={onFocusOnNode}
|
|
684
|
+
updateView={updateView}
|
|
685
|
+
enableSandwichView={enableSandwichView}
|
|
513
686
|
/>
|
|
514
687
|
</div>
|
|
515
688
|
|
package/src/i18n.tsx
CHANGED
|
@@ -68,6 +68,11 @@ export type FlamegraphMessages = {
|
|
|
68
68
|
searchPlaceholder: string;
|
|
69
69
|
syncSearchBars: string;
|
|
70
70
|
unsyncSearchBars: string;
|
|
71
|
+
|
|
72
|
+
// 表格右键菜单(新增)
|
|
73
|
+
focusOnThisFunction: string;
|
|
74
|
+
tableDoubleClickToFocus: string;
|
|
75
|
+
tableRightClickForOptions: string;
|
|
71
76
|
};
|
|
72
77
|
|
|
73
78
|
const defaultTooltipUnitTitles: Record<Units, TooltipUnitMessages> = {
|
|
@@ -217,6 +222,11 @@ export const defaultMessages: FlamegraphMessages = {
|
|
|
217
222
|
searchPlaceholder: 'Search...',
|
|
218
223
|
syncSearchBars: 'Sync Search Bars',
|
|
219
224
|
unsyncSearchBars: 'Unsync Search Bars',
|
|
225
|
+
|
|
226
|
+
// 表格右键菜单(新增)
|
|
227
|
+
focusOnThisFunction: 'Focus on this function',
|
|
228
|
+
tableDoubleClickToFocus: 'Double-click to focus in flamegraph',
|
|
229
|
+
tableRightClickForOptions: 'Right-click for more options',
|
|
220
230
|
};
|
|
221
231
|
|
|
222
232
|
export const zhCNMessages: FlamegraphMessages = {
|
|
@@ -271,6 +281,11 @@ export const zhCNMessages: FlamegraphMessages = {
|
|
|
271
281
|
searchPlaceholder: '搜索...',
|
|
272
282
|
syncSearchBars: '同步搜索栏',
|
|
273
283
|
unsyncSearchBars: '取消同步搜索栏',
|
|
284
|
+
|
|
285
|
+
// 表格右键菜单(新增)
|
|
286
|
+
focusOnThisFunction: '聚焦到此函数',
|
|
287
|
+
tableDoubleClickToFocus: '双击可聚焦到火焰图',
|
|
288
|
+
tableRightClickForOptions: '右键查看更多选项',
|
|
274
289
|
};
|
|
275
290
|
|
|
276
291
|
const I18nContext = createContext<FlamegraphMessages>(defaultMessages);
|
package/src/shims/Table.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// src/shims/Table.tsx
|
|
1
2
|
/* eslint-disable react/jsx-props-no-spreading */
|
|
2
3
|
import React, { useState, ReactNode, CSSProperties, RefObject } from 'react';
|
|
3
4
|
import { faChevronLeft } from '@fortawesome/free-solid-svg-icons/faChevronLeft';
|
|
@@ -10,7 +11,7 @@ import styles from './Table.module.scss';
|
|
|
10
11
|
import Button from './Button';
|
|
11
12
|
|
|
12
13
|
interface CustomProp {
|
|
13
|
-
[k: string]: string | CSSProperties | ReactNode | number | undefined;
|
|
14
|
+
[k: string]: string | CSSProperties | ReactNode | number | undefined | ((e: React.MouseEvent) => void) | (() => void);
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export interface Cell extends CustomProp {
|
|
@@ -31,19 +32,21 @@ export interface BodyRow {
|
|
|
31
32
|
isRowDisabled?: boolean;
|
|
32
33
|
cells: Cell[];
|
|
33
34
|
onClick?: () => void;
|
|
35
|
+
onDoubleClick?: () => void;
|
|
36
|
+
onContextMenu?: (e: React.MouseEvent) => void;
|
|
34
37
|
className?: string;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
export type TableBodyType =
|
|
38
41
|
| {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
type: 'not-filled';
|
|
43
|
+
value: string | ReactNode;
|
|
44
|
+
bodyClassName?: string;
|
|
45
|
+
}
|
|
43
46
|
| {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
type: 'filled';
|
|
48
|
+
bodyRows: BodyRow[];
|
|
49
|
+
};
|
|
47
50
|
|
|
48
51
|
type Table = TableBodyType & {
|
|
49
52
|
headRow: HeadCell[];
|
|
@@ -151,7 +154,16 @@ function Table({
|
|
|
151
154
|
</tr>
|
|
152
155
|
) : (
|
|
153
156
|
paginate(table.bodyRows, currPage, itemsPerPage).map(
|
|
154
|
-
({
|
|
157
|
+
({
|
|
158
|
+
cells,
|
|
159
|
+
isRowSelected,
|
|
160
|
+
isRowDisabled,
|
|
161
|
+
className,
|
|
162
|
+
onClick,
|
|
163
|
+
onDoubleClick,
|
|
164
|
+
onContextMenu,
|
|
165
|
+
...rest
|
|
166
|
+
}) => {
|
|
155
167
|
// The problem is that when you switch apps or time-range and the function
|
|
156
168
|
// names stay the same it leads to an issue where rows don't get re-rendered
|
|
157
169
|
// So we force a rerender each time.
|
|
@@ -161,6 +173,9 @@ function Table({
|
|
|
161
173
|
<tr
|
|
162
174
|
key={renderID}
|
|
163
175
|
{...rest}
|
|
176
|
+
onClick={onClick}
|
|
177
|
+
onDoubleClick={onDoubleClick}
|
|
178
|
+
onContextMenu={onContextMenu}
|
|
164
179
|
className={clsx(className, {
|
|
165
180
|
[styles.isRowSelected]: isRowSelected,
|
|
166
181
|
[styles.isRowDisabled]: isRowDisabled,
|