@kylincloud/flamegraph 0.35.21 → 0.35.23
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 +16 -0
- package/dist/FlameGraph/FlameGraphComponent/Flamegraph.d.ts +6 -1
- package/dist/FlameGraph/FlameGraphComponent/Flamegraph.d.ts.map +1 -1
- package/dist/FlameGraph/FlameGraphComponent/Flamegraph_render.d.ts +7 -0
- package/dist/FlameGraph/FlameGraphComponent/Flamegraph_render.d.ts.map +1 -1
- package/dist/FlameGraph/FlameGraphComponent/index.d.ts.map +1 -1
- package/dist/FlameGraph/FlameGraphRenderer.d.ts +4 -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 +5 -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 +3 -3
- package/dist/index.esm.js.map +1 -1
- package/dist/index.node.cjs.js +4 -4
- 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/FlameGraphComponent/Flamegraph.ts +7 -2
- package/src/FlameGraph/FlameGraphComponent/Flamegraph_render.ts +31 -2
- package/src/FlameGraph/FlameGraphComponent/index.tsx +14 -3
- package/src/FlameGraph/FlameGraphRenderer.tsx +74 -6
- package/src/ProfilerTable.module.scss +42 -0
- package/src/ProfilerTable.tsx +234 -55
- package/src/i18n.tsx +27 -0
- package/src/shims/Table.module.scss +9 -2
- package/src/shims/Table.tsx +55 -36
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} %` },
|
|
@@ -373,11 +426,11 @@ const getTableBody = ({
|
|
|
373
426
|
return rows.length > 0
|
|
374
427
|
? { bodyRows: rows, type: 'filled' as const }
|
|
375
428
|
: {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
429
|
+
value: (
|
|
430
|
+
<div className="unsupported-format">{messages.noItemsFound}</div>
|
|
431
|
+
),
|
|
432
|
+
type: 'not-filled' as const,
|
|
433
|
+
};
|
|
381
434
|
};
|
|
382
435
|
|
|
383
436
|
export interface ProfilerTableProps {
|
|
@@ -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,53 +460,127 @@ 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
|
+
// 先确保该项被高亮(如果未高亮则高亮,如果已高亮则保持)
|
|
476
|
+
if (!selectedItem.isJust || selectedItem.value !== name) {
|
|
477
|
+
handleTableItemClick({ name });
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// 然后聚焦到火焰图
|
|
481
|
+
if (!onFocusOnNode) return;
|
|
482
|
+
|
|
483
|
+
const node = findNodeByName(flamebearer, name);
|
|
484
|
+
if (node) {
|
|
485
|
+
onFocusOnNode(node.i, node.j);
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
[flamebearer, onFocusOnNode, selectedItem, handleTableItemClick]
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
// 右键菜单处理
|
|
492
|
+
const handleContextMenu = useCallback(
|
|
493
|
+
(e: React.MouseEvent, name: string) => {
|
|
494
|
+
e.preventDefault();
|
|
495
|
+
setAnchorPoint({ x: e.clientX, y: e.clientY });
|
|
496
|
+
setContextMenuTarget(name);
|
|
497
|
+
toggleMenu(true);
|
|
498
|
+
},
|
|
499
|
+
[toggleMenu]
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
const handleMenuClose = useCallback(() => {
|
|
503
|
+
toggleMenu(false);
|
|
504
|
+
setContextMenuTarget(null);
|
|
505
|
+
}, [toggleMenu]);
|
|
506
|
+
|
|
507
|
+
// 菜单项:聚焦到此函数
|
|
508
|
+
const handleFocusClick = useCallback(() => {
|
|
509
|
+
if (!contextMenuTarget || !onFocusOnNode) return;
|
|
510
|
+
|
|
511
|
+
const node = findNodeByName(flamebearer, contextMenuTarget);
|
|
512
|
+
if (node) {
|
|
513
|
+
onFocusOnNode(node.i, node.j);
|
|
514
|
+
}
|
|
515
|
+
handleMenuClose();
|
|
516
|
+
}, [contextMenuTarget, flamebearer, onFocusOnNode, handleMenuClose]);
|
|
517
|
+
|
|
518
|
+
// 菜单项:复制函数名
|
|
519
|
+
const handleCopyClick = useCallback(() => {
|
|
520
|
+
if (!contextMenuTarget || !navigator.clipboard) return;
|
|
521
|
+
navigator.clipboard.writeText(contextMenuTarget);
|
|
522
|
+
handleMenuClose();
|
|
523
|
+
}, [contextMenuTarget, handleMenuClose]);
|
|
524
|
+
|
|
525
|
+
// 菜单项:高亮相似节点
|
|
526
|
+
const handleHighlightClick = useCallback(() => {
|
|
527
|
+
if (!contextMenuTarget) return;
|
|
528
|
+
handleTableItemClick({ name: contextMenuTarget });
|
|
529
|
+
handleMenuClose();
|
|
530
|
+
}, [contextMenuTarget, handleTableItemClick, handleMenuClose]);
|
|
531
|
+
|
|
532
|
+
// 菜单项:在 sandwich 视图中打开
|
|
533
|
+
const handleSandwichClick = useCallback(() => {
|
|
534
|
+
if (!contextMenuTarget || !updateView) return;
|
|
535
|
+
updateView('sandwich');
|
|
536
|
+
handleTableItemClick({ name: contextMenuTarget });
|
|
537
|
+
handleMenuClose();
|
|
538
|
+
}, [contextMenuTarget, updateView, handleTableItemClick, handleMenuClose]);
|
|
405
539
|
|
|
406
540
|
const tableFormat = React.useMemo(
|
|
407
541
|
() =>
|
|
408
542
|
isDoubles
|
|
409
543
|
? [
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
544
|
+
{
|
|
545
|
+
sortable: 1,
|
|
546
|
+
name: 'name' as const,
|
|
547
|
+
label: i18n.location,
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
sortable: 1,
|
|
551
|
+
name: 'baseline' as const,
|
|
552
|
+
label: i18n.baseline,
|
|
553
|
+
default: true,
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
sortable: 1,
|
|
557
|
+
name: 'comparison' as const,
|
|
558
|
+
label: i18n.comparison,
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
sortable: 1,
|
|
562
|
+
name: 'diff' as const,
|
|
563
|
+
label: i18n.diff,
|
|
564
|
+
},
|
|
565
|
+
]
|
|
432
566
|
: [
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
567
|
+
{
|
|
568
|
+
sortable: 1,
|
|
569
|
+
name: 'name' as const,
|
|
570
|
+
label: i18n.location,
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
sortable: 1,
|
|
574
|
+
name: 'self' as const,
|
|
575
|
+
label: i18n.self,
|
|
576
|
+
default: true,
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
sortable: 1,
|
|
580
|
+
name: 'total' as const,
|
|
581
|
+
label: i18n.total,
|
|
582
|
+
},
|
|
583
|
+
],
|
|
450
584
|
[i18n, isDoubles]
|
|
451
585
|
);
|
|
452
586
|
|
|
@@ -463,19 +597,58 @@ function Table({
|
|
|
463
597
|
palette,
|
|
464
598
|
selectedItem,
|
|
465
599
|
messages: i18n,
|
|
600
|
+
onRowContextMenu: handleContextMenu,
|
|
601
|
+
onRowDoubleClick: handleDoubleClick,
|
|
602
|
+
onFocusOnNode,
|
|
603
|
+
updateView,
|
|
604
|
+
enableSandwichView,
|
|
466
605
|
}),
|
|
467
606
|
};
|
|
468
607
|
|
|
608
|
+
const isHighlighted =
|
|
609
|
+
selectedItem.isJust && selectedItem.value === contextMenuTarget;
|
|
610
|
+
|
|
469
611
|
return (
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
'flamegraph-table
|
|
477
|
-
|
|
478
|
-
|
|
612
|
+
<>
|
|
613
|
+
<TableUI
|
|
614
|
+
/* eslint-disable-next-line react/jsx-props-no-spreading */
|
|
615
|
+
{...tableSortProps}
|
|
616
|
+
tableBodyRef={tableBodyRef}
|
|
617
|
+
table={table}
|
|
618
|
+
className={cl('flamegraph-table', {
|
|
619
|
+
'flamegraph-table-doubles': isDoubles,
|
|
620
|
+
})}
|
|
621
|
+
/>
|
|
622
|
+
|
|
623
|
+
{/* 右键菜单 */}
|
|
624
|
+
<ControlledMenu
|
|
625
|
+
{...menuProps}
|
|
626
|
+
anchorPoint={anchorPoint}
|
|
627
|
+
onClose={handleMenuClose}
|
|
628
|
+
className={styles.tableContextMenu}
|
|
629
|
+
>
|
|
630
|
+
{onFocusOnNode && (
|
|
631
|
+
<MenuItem onClick={handleFocusClick}>
|
|
632
|
+
<FontAwesomeIcon icon={faCrosshairs} />
|
|
633
|
+
{i18n.focusOnThisFunction}
|
|
634
|
+
</MenuItem>
|
|
635
|
+
)}
|
|
636
|
+
<MenuItem onClick={handleCopyClick}>
|
|
637
|
+
<FontAwesomeIcon icon={faCopy} />
|
|
638
|
+
{i18n.copyFunctionName}
|
|
639
|
+
</MenuItem>
|
|
640
|
+
<MenuItem onClick={handleHighlightClick}>
|
|
641
|
+
<FontAwesomeIcon icon={faHighlighter} />
|
|
642
|
+
{isHighlighted ? i18n.clearHighlight : i18n.highlightSimilarNodes}
|
|
643
|
+
</MenuItem>
|
|
644
|
+
{updateView && enableSandwichView && (
|
|
645
|
+
<MenuItem onClick={handleSandwichClick} className={styles.sandwichItem}>
|
|
646
|
+
<SandwichIcon fill="currentColor" />
|
|
647
|
+
{i18n.openInSandwichView}
|
|
648
|
+
</MenuItem>
|
|
649
|
+
)}
|
|
650
|
+
</ControlledMenu>
|
|
651
|
+
</>
|
|
479
652
|
);
|
|
480
653
|
}
|
|
481
654
|
|
|
@@ -486,6 +659,9 @@ const ProfilerTable = React.memo(function ProfilerTable({
|
|
|
486
659
|
highlightQuery,
|
|
487
660
|
palette,
|
|
488
661
|
selectedItem,
|
|
662
|
+
onFocusOnNode,
|
|
663
|
+
updateView,
|
|
664
|
+
enableSandwichView,
|
|
489
665
|
}: Omit<ProfilerTableProps, 'tableBodyRef'>) {
|
|
490
666
|
const tableBodyRef = useRef<HTMLTableSectionElement>(null);
|
|
491
667
|
const isDoubles = flamebearer.format === 'double';
|
|
@@ -510,6 +686,9 @@ const ProfilerTable = React.memo(function ProfilerTable({
|
|
|
510
686
|
handleTableItemClick={handleTableItemClick}
|
|
511
687
|
palette={palette}
|
|
512
688
|
selectedItem={selectedItem}
|
|
689
|
+
onFocusOnNode={onFocusOnNode}
|
|
690
|
+
updateView={updateView}
|
|
691
|
+
enableSandwichView={enableSandwichView}
|
|
513
692
|
/>
|
|
514
693
|
</div>
|
|
515
694
|
|
package/src/i18n.tsx
CHANGED
|
@@ -68,6 +68,15 @@ 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;
|
|
76
|
+
|
|
77
|
+
// 火焰图聚焦时的 collapsed 提示
|
|
78
|
+
collapsedLevelsSingular: string; // "total (1 level collapsed)"
|
|
79
|
+
collapsedLevelsPlural: string; // "total (n levels collapsed)"
|
|
71
80
|
};
|
|
72
81
|
|
|
73
82
|
const defaultTooltipUnitTitles: Record<Units, TooltipUnitMessages> = {
|
|
@@ -217,6 +226,15 @@ export const defaultMessages: FlamegraphMessages = {
|
|
|
217
226
|
searchPlaceholder: 'Search...',
|
|
218
227
|
syncSearchBars: 'Sync Search Bars',
|
|
219
228
|
unsyncSearchBars: 'Unsync Search Bars',
|
|
229
|
+
|
|
230
|
+
// 表格右键菜单(新增)
|
|
231
|
+
focusOnThisFunction: 'Focus on this function',
|
|
232
|
+
tableDoubleClickToFocus: 'Double-click to focus in flamegraph',
|
|
233
|
+
tableRightClickForOptions: 'Right-click for more options',
|
|
234
|
+
|
|
235
|
+
// 火焰图聚焦时的 collapsed 提示
|
|
236
|
+
collapsedLevelsSingular: 'total (1 level collapsed)',
|
|
237
|
+
collapsedLevelsPlural: 'total ({n} levels collapsed)',
|
|
220
238
|
};
|
|
221
239
|
|
|
222
240
|
export const zhCNMessages: FlamegraphMessages = {
|
|
@@ -271,6 +289,15 @@ export const zhCNMessages: FlamegraphMessages = {
|
|
|
271
289
|
searchPlaceholder: '搜索...',
|
|
272
290
|
syncSearchBars: '同步搜索栏',
|
|
273
291
|
unsyncSearchBars: '取消同步搜索栏',
|
|
292
|
+
|
|
293
|
+
// 表格右键菜单(新增)
|
|
294
|
+
focusOnThisFunction: '聚焦到此函数',
|
|
295
|
+
tableDoubleClickToFocus: '双击可聚焦到火焰图',
|
|
296
|
+
tableRightClickForOptions: '右键查看更多选项',
|
|
297
|
+
|
|
298
|
+
// 火焰图聚焦时的 collapsed 提示
|
|
299
|
+
collapsedLevelsSingular: '总计(已折叠 1 层)',
|
|
300
|
+
collapsedLevelsPlural: '总计(已折叠 {n} 层)',
|
|
274
301
|
};
|
|
275
302
|
|
|
276
303
|
const I18nContext = createContext<FlamegraphMessages>(defaultMessages);
|
|
@@ -49,10 +49,17 @@
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
tr {
|
|
52
|
+
/* 选中行样式 - 使用更明显的 antd 风格蓝色 */
|
|
52
53
|
&.isRowSelected {
|
|
53
54
|
cursor: pointer;
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
/* antd 选中蓝色,更明显但不会与红绿/红蓝火焰图冲突 */
|
|
56
|
+
background: var(--ps-table-highlight-row-bg, #bae0ff) !important;
|
|
57
|
+
color: var(--ps-table-highlight-row-text, #0958d9);
|
|
58
|
+
|
|
59
|
+
/* 左侧添加强调边框 */
|
|
60
|
+
td:first-child {
|
|
61
|
+
box-shadow: inset 3px 0 0 0 var(--ps-table-highlight-border, #1677ff);
|
|
62
|
+
}
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
&.isRowDisabled td {
|
package/src/shims/Table.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
// src/shims/Table.tsx
|
|
1
2
|
/* eslint-disable react/jsx-props-no-spreading */
|
|
2
|
-
import React, { useState, ReactNode, CSSProperties, RefObject } from 'react';
|
|
3
|
+
import React, { useState, useRef, ReactNode, CSSProperties, RefObject } from 'react';
|
|
3
4
|
import { faChevronLeft } from '@fortawesome/free-solid-svg-icons/faChevronLeft';
|
|
4
5
|
import { faChevronRight } from '@fortawesome/free-solid-svg-icons/faChevronRight';
|
|
5
6
|
import clsx from 'clsx';
|
|
@@ -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[];
|
|
@@ -91,6 +94,46 @@ interface TableProps {
|
|
|
91
94
|
itemsPerPage?: number;
|
|
92
95
|
}
|
|
93
96
|
|
|
97
|
+
/**
|
|
98
|
+
* 表格行组件
|
|
99
|
+
* 单击立即高亮,双击同时触发高亮和聚焦
|
|
100
|
+
*/
|
|
101
|
+
function TableRow({ row }: { row: BodyRow }) {
|
|
102
|
+
const {
|
|
103
|
+
cells,
|
|
104
|
+
isRowSelected,
|
|
105
|
+
isRowDisabled,
|
|
106
|
+
className,
|
|
107
|
+
onClick,
|
|
108
|
+
onDoubleClick,
|
|
109
|
+
onContextMenu,
|
|
110
|
+
...rest
|
|
111
|
+
} = row;
|
|
112
|
+
|
|
113
|
+
const renderID = useRef(Math.random()).current;
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<tr
|
|
117
|
+
{...rest}
|
|
118
|
+
onClick={onClick}
|
|
119
|
+
onDoubleClick={onDoubleClick}
|
|
120
|
+
onContextMenu={onContextMenu}
|
|
121
|
+
className={clsx(className, {
|
|
122
|
+
[styles.isRowSelected]: isRowSelected,
|
|
123
|
+
[styles.isRowDisabled]: isRowDisabled,
|
|
124
|
+
})}
|
|
125
|
+
>
|
|
126
|
+
{cells &&
|
|
127
|
+
cells.map(({ style, value, ...cellRest }: Cell, index: number) => (
|
|
128
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
129
|
+
<td key={renderID + index} style={style} {...cellRest}>
|
|
130
|
+
{value}
|
|
131
|
+
</td>
|
|
132
|
+
))}
|
|
133
|
+
</tr>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
94
137
|
function Table({
|
|
95
138
|
sortByDirection,
|
|
96
139
|
sortBy,
|
|
@@ -151,33 +194,9 @@ function Table({
|
|
|
151
194
|
</tr>
|
|
152
195
|
) : (
|
|
153
196
|
paginate(table.bodyRows, currPage, itemsPerPage).map(
|
|
154
|
-
(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
// So we force a rerender each time.
|
|
158
|
-
const renderID = Math.random();
|
|
159
|
-
|
|
160
|
-
return (
|
|
161
|
-
<tr
|
|
162
|
-
key={renderID}
|
|
163
|
-
{...rest}
|
|
164
|
-
className={clsx(className, {
|
|
165
|
-
[styles.isRowSelected]: isRowSelected,
|
|
166
|
-
[styles.isRowDisabled]: isRowDisabled,
|
|
167
|
-
})}
|
|
168
|
-
>
|
|
169
|
-
{cells &&
|
|
170
|
-
cells.map(
|
|
171
|
-
({ style, value, ...rest }: Cell, index: number) => (
|
|
172
|
-
// eslint-disable-next-line react/no-array-index-key
|
|
173
|
-
<td key={renderID + index} style={style} {...rest}>
|
|
174
|
-
{value}
|
|
175
|
-
</td>
|
|
176
|
-
)
|
|
177
|
-
)}
|
|
178
|
-
</tr>
|
|
179
|
-
);
|
|
180
|
-
}
|
|
197
|
+
(row, idx) => (
|
|
198
|
+
<TableRow key={row['data-row'] ?? `row-${idx}`} row={row} />
|
|
199
|
+
)
|
|
181
200
|
)
|
|
182
201
|
)}
|
|
183
202
|
</tbody>
|