Skip to content

Commit 3dcf26c

Browse files
committed
feat: add search and link func into timeline UI
1 parent a314e90 commit 3dcf26c

File tree

9 files changed

+332
-89
lines changed

9 files changed

+332
-89
lines changed

web-frontend/src/main/v3/packages/atoms/src/transaction.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ import { HeatmapDrag, TransactionInfo } from '@pinpoint-fe/constants';
33

44
export const transactionListDatasAtom = atom<HeatmapDrag.Response | undefined>(undefined);
55
export const transactionInfoDatasAtom = atom<TransactionInfo.Response | undefined>(undefined);
6+
export const transactionInfoCurrentTabId = atom<string>('');
7+
export const transactionInfoCallTreeFocusId = atom<string>('');

web-frontend/src/main/v3/packages/ui/src/components/DataTable/VirtualizedDataTable.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export function VirtualizedDataTable<TData, TValue>({
146146
onChangeRowSelection?.([...selectedRowData]);
147147
}, [rowSelection]);
148148

149-
useUpdateEffect(() => {
149+
React.useEffect(() => {
150150
if (focusRowIndex) {
151151
rowVirtualizer.scrollToIndex(focusRowIndex, { align: 'center' });
152152
}

web-frontend/src/main/v3/packages/ui/src/components/FlameGraph/FlameGraph.tsx

Lines changed: 53 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
11
import React from 'react';
22
import { throttle } from 'lodash';
33
import cloneDeep from 'lodash.clonedeep';
4-
import { FlameNode, FlameNodeClickHandler, FlameNodeType } from './FlameNode';
4+
import { FlameNode, FlameNodeType, FlameNodeProps } from './FlameNode';
55
import { FlameAxis } from './FlameAxis';
66
import { FlameGraphConfigContext, flameGraphDefaultConfig } from './FlameGraphConfigContext';
77
import { FlameTimeline } from './FlameTimeline';
88

9-
export interface FlameGraphProps<T> {
10-
data: FlameNodeType<T>[];
9+
export interface FlameGraphProps<T>
10+
extends Pick<FlameNodeProps<T>, 'customNodeStyle' | 'customTextStyle' | 'onClickNode'> {
11+
data: FlameNodeType<T>[][];
1112
start?: number;
1213
end?: number;
13-
onClickNode?: FlameNodeClickHandler<T>;
1414
}
1515

16-
export const FlameGraph = <T,>({ data, start = 0, end = 0, onClickNode }: FlameGraphProps<T>) => {
16+
export const FlameGraph = <T,>({
17+
data,
18+
start = 0,
19+
end = 0,
20+
onClickNode,
21+
customNodeStyle,
22+
customTextStyle,
23+
}: FlameGraphProps<T>) => {
1724
const widthOffset = end - start || 1;
1825
const [config] = React.useState(flameGraphDefaultConfig);
1926

@@ -60,8 +67,12 @@ export const FlameGraph = <T,>({ data, start = 0, end = 0, onClickNode }: FlameG
6067

6168
function getContainerHeight() {
6269
const { height, padding } = config;
63-
return data.reduce((acc, curr) => {
64-
return acc + (getMaxDepth(curr) + 1) * height.node + padding.group;
70+
71+
return data.reduce((acc, group) => {
72+
const groupHeights = group.map(
73+
(node) => (getMaxDepth(node) + 1) * height.node + padding.group,
74+
);
75+
return acc + Math.max(...groupHeights);
6576
}, 2 * padding.bottom);
6677
}
6778

@@ -88,29 +99,42 @@ export const FlameGraph = <T,>({ data, start = 0, end = 0, onClickNode }: FlameG
8899
<FlameAxis width={containerWidth} />
89100
{containerWidth &&
90101
// group별 렌더링
91-
data.map((node, i) => {
102+
data.map((group, i) => {
92103
if (i === 0) prevDepth.current = 0;
93-
94-
const { height, padding, color } = config;
95-
const newNode = cloneDeep(node);
96-
const currentNodeDepth = getMaxDepth(newNode);
97-
const yOffset = prevDepth.current * height.node + padding.group * i;
98-
99-
styleNode(newNode, 0, 0, yOffset);
100-
prevDepth.current = currentNodeDepth + prevDepth.current + 1;
101-
102-
return (
103-
<React.Fragment key={node.id}>
104-
<FlameNode node={newNode} svgRef={svgRef} onClickNode={onClickNode} />
105-
<line
106-
x1={0}
107-
y1={yOffset - padding.group / 2 + padding.top}
108-
x2={containerWidth}
109-
y2={yOffset - padding.group / 2 + padding.top}
110-
stroke={color.axis}
111-
/>
112-
</React.Fragment>
113-
);
104+
else {
105+
const prevGroupMaxDepth = Math.max(
106+
...data[i - 1].map((node) => getMaxDepth(node)),
107+
);
108+
prevDepth.current = prevGroupMaxDepth + prevDepth.current + 1;
109+
}
110+
111+
// node별 렌더링
112+
return group.map((node) => {
113+
const { height, padding, color } = config;
114+
const newNode = cloneDeep(node);
115+
const yOffset = prevDepth.current * height.node + padding.group * i;
116+
117+
styleNode(newNode, 0, 0, yOffset);
118+
119+
return (
120+
<React.Fragment key={node.id}>
121+
<FlameNode
122+
node={newNode}
123+
svgRef={svgRef}
124+
customNodeStyle={customNodeStyle}
125+
customTextStyle={customTextStyle}
126+
onClickNode={onClickNode}
127+
/>
128+
<line
129+
x1={0}
130+
y1={yOffset - padding.group / 2 + padding.top}
131+
x2={containerWidth}
132+
y2={yOffset - padding.group / 2 + padding.top}
133+
stroke={color.axis}
134+
/>
135+
</React.Fragment>
136+
);
137+
});
114138
})}
115139
</svg>
116140
</div>

web-frontend/src/main/v3/packages/ui/src/components/FlameGraph/FlameNode.tsx

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,52 @@ export interface FlameNodeType<T> {
1616
children: FlameNodeType<T>[];
1717
}
1818

19-
export type FlameNodeClickHandler<T> = (node: FlameNodeType<T | unknown>) => void;
19+
export type FlameNodeColorType = {
20+
color: string;
21+
hoverColor: string;
22+
};
23+
24+
export type FlameNodeClickHandler<T> = (node: FlameNodeType<T>) => void;
2025

2126
export interface FlameNodeProps<T> {
2227
node: FlameNodeType<T>;
2328
svgRef?: React.RefObject<SVGSVGElement>;
24-
onClickNode?: FlameNodeClickHandler<T>;
29+
onClickNode: FlameNodeClickHandler<T>;
30+
customNodeStyle?: (node: FlameNodeType<T>, color: FlameNodeColorType) => React.CSSProperties;
31+
customTextStyle?: (node: FlameNodeType<T>, color: string) => React.CSSProperties;
32+
// renderText?: (
33+
// text: string,
34+
// elementAttributes: React.SVGTextElementAttributes<SVGTextElement>,
35+
// ) => ReactElement;
2536
}
2637

27-
export const FlameNode = React.memo(<T,>({ node, svgRef, onClickNode }: FlameNodeProps<T>) => {
28-
const { x = 0, y = 0, width = 1, height = 1, name, nodeStyle, textStyle } = node;
29-
const colorMap = React.useRef<{ [key: string]: { color: string; hoverColor: string } }>({});
38+
const FlameNodeComponent = <T,>({
39+
node,
40+
svgRef,
41+
onClickNode,
42+
customNodeStyle,
43+
customTextStyle,
44+
}: FlameNodeProps<T>) => {
45+
const { x = 0, y = 0, width = 1, height = 1, name } = node;
46+
const colorMap = React.useRef<{ [key: string]: FlameNodeColorType }>({});
3047
const color = colorMap.current[name]?.color || getRandomColor();
3148
const hoverColor = colorMap.current[name]?.hoverColor || getDarkenHexColor(color);
3249
const ellipsizedText = React.useMemo(
3350
() => getEllipsizedText(name, width, svgRef),
3451
[name, width, svgRef],
3552
);
36-
const [isHover, setHover] = React.useState(false);
3753

54+
const [isHover, setHover] = React.useState(false);
3855
if (!colorMap.current[name]) colorMap.current[name] = { color, hoverColor };
56+
const contrastringTextColor = getContrastingTextColor(color);
57+
const nodeStyle = {
58+
...node.nodeStyle,
59+
...customNodeStyle?.(node, colorMap.current[name]),
60+
};
61+
const textStyle = {
62+
...node.textStyle,
63+
...customTextStyle?.(node, contrastringTextColor),
64+
};
3965

4066
return (
4167
<>
@@ -52,8 +78,10 @@ export const FlameNode = React.memo(<T,>({ node, svgRef, onClickNode }: FlameNod
5278
y={y}
5379
width={width}
5480
height={height}
55-
fill={isHover ? hoverColor : color}
56-
style={nodeStyle}
81+
style={{
82+
...nodeStyle,
83+
fill: isHover ? hoverColor : color,
84+
}}
5785
/>
5886
<text
5987
x={x + width / 2}
@@ -62,24 +90,32 @@ export const FlameNode = React.memo(<T,>({ node, svgRef, onClickNode }: FlameNod
6290
fontSize="0.75rem"
6391
letterSpacing={-0.5}
6492
textAnchor="middle"
65-
fill={getContrastingTextColor(color)}
93+
fill={contrastringTextColor}
6694
style={textStyle}
6795
>
6896
{ellipsizedText}
6997
</text>
7098
</g>
7199
{node.children &&
72-
node.children.map((childNode, i) => (
73-
<FlameNode
74-
key={i}
75-
node={childNode as FlameNodeType<T>}
76-
svgRef={svgRef}
77-
onClickNode={onClickNode}
78-
/>
79-
))}
100+
node.children.map((childNode, i) => {
101+
return (
102+
<FlameNode<T>
103+
key={i}
104+
node={childNode}
105+
svgRef={svgRef}
106+
onClickNode={onClickNode}
107+
customNodeStyle={customNodeStyle}
108+
customTextStyle={customTextStyle}
109+
/>
110+
);
111+
})}
80112
</>
81113
);
82-
});
114+
};
115+
116+
export const FlameNode = React.memo(FlameNodeComponent) as <T>(
117+
props: FlameNodeProps<T>,
118+
) => JSX.Element;
83119

84120
const getEllipsizedText = (text: string, maxWidth = 1, svgRef?: React.RefObject<SVGSVGElement>) => {
85121
if (!svgRef?.current) return text;

web-frontend/src/main/v3/packages/ui/src/components/Transaction/call-tree/CallTree.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import {
2323
import { TransactionInfo } from '@pinpoint-fe/constants';
2424
import { RxMagnifyingGlass } from 'react-icons/rx';
2525
import { HighLightCode } from '../../HighLightCode';
26+
import { useAtomValue } from 'jotai';
27+
import { transactionInfoCallTreeFocusId } from '@pinpoint-fe/atoms';
2628

2729
export interface CallTreeProps {
2830
data: TransactionInfo.CallStackKeyValueMap[];
@@ -59,6 +61,15 @@ export const CallTree = ({ data, mapData, metaData }: CallTreeProps) => {
5961
});
6062
},
6163
});
64+
const focusIdFromTimeline = useAtomValue(transactionInfoCallTreeFocusId);
65+
66+
React.useEffect(() => {
67+
setFocusRowId(undefined);
68+
}, [data]);
69+
70+
React.useEffect(() => {
71+
setFocusRowId(focusIdFromTimeline);
72+
}, [focusIdFromTimeline]);
6273

6374
useUpdateEffect(() => {
6475
if (filter === 'hasException') {
@@ -116,7 +127,7 @@ export const CallTree = ({ data, mapData, metaData }: CallTreeProps) => {
116127

117128
return (
118129
<div className="relative h-full">
119-
<div className="absolute flex gap-1 rounded -top-11 right-4 h-7">
130+
<div className="absolute flex gap-1 rounded -top-10 right-4 h-7">
120131
<Select value={filter} onValueChange={(value) => setFilter(value)}>
121132
<SelectTrigger className="w-24 h-full text-xs">
122133
<SelectValue placeholder="Theme" />
@@ -131,7 +142,7 @@ export const CallTree = ({ data, mapData, metaData }: CallTreeProps) => {
131142
</Select>
132143
<div className="border flex rounded pr-0.5 w-64">
133144
<Input
134-
className="h-full text-xs border-none shadow-none focus-visible:ring-0"
145+
className="h-full text-xs border-none shadow-none focus-visible:ring-0 placeholder:text-xs"
135146
placeholder="Filter call tree..."
136147
value={input}
137148
onChange={(e) => setInput(e.currentTarget.value)}
@@ -190,6 +201,7 @@ export const CallTree = ({ data, mapData, metaData }: CallTreeProps) => {
190201
<CallTreeTable
191202
data={data}
192203
metaData={metaData}
204+
// scrollToIndex={(row) => row.findIndex((r) => r.original.id === callTreeFocusId)}
193205
focusRowIndex={Number(focusRowId) - 1}
194206
filteredRowIds={filteredListIds}
195207
onDoubleClickCell={(cell) => {

web-frontend/src/main/v3/packages/ui/src/components/Transaction/timeline/TimelineDetail.tsx

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,50 @@ import { IoMdClose } from 'react-icons/io';
33
import { TraceViewerData } from '@pinpoint-fe/constants';
44
import { Separator } from '@radix-ui/react-dropdown-menu';
55
import { Button } from '../../../components/ui';
6-
import { FlameNodeType } from '../../FlameGraph/FlameNode';
6+
import { useSetAtom } from 'jotai';
7+
import { transactionInfoCallTreeFocusId, transactionInfoCurrentTabId } from '@pinpoint-fe/atoms';
78

89
export interface TimelineDetailProps {
910
start: number;
10-
node: FlameNodeType<TraceViewerData.TraceEvent>;
11+
data: TraceViewerData.TraceEvent;
1112
onClose?: () => void;
1213
}
1314

14-
export const TimelineDetail = ({ start, node, onClose }: TimelineDetailProps) => {
15+
export const TimelineDetail = ({ start, data, onClose }: TimelineDetailProps) => {
16+
const setCurrentTab = useSetAtom(transactionInfoCurrentTabId);
17+
const setCallTreeFocusId = useSetAtom(transactionInfoCallTreeFocusId);
18+
1519
return (
1620
<div className="w-2/5 border-l min-w-96">
1721
<div className="flex items-center h-12 p-2 text-sm font-semibold border-b relativ bg-secondary/50">
1822
Timeline detail
19-
<Button className="ml-auto" variant={'ghost'} size={'icon'} onClick={() => onClose?.()}>
20-
<IoMdClose className="w-5 h-5" />
21-
</Button>
23+
<div className="flex items-center ml-auto">
24+
<Button
25+
className="text-xs"
26+
variant="link"
27+
onClick={() => {
28+
setCurrentTab('callTree');
29+
setCallTreeFocusId(data.args.id);
30+
}}
31+
>
32+
View in Call Tree
33+
</Button>
34+
<Button variant={'ghost'} size={'icon'} onClick={() => onClose?.()}>
35+
<IoMdClose className="w-5 h-5" />
36+
</Button>
37+
</div>
2238
</div>
2339
<Separator />
2440
<div className="overflow-auto h-[calc(100%-3.2rem)]">
2541
<div className="p-2 pl-3 pb-4 text-xs [&>*:nth-child(2n-1)]:font-semibold grid grid-cols-[10rem_auto] [&>*:nth-child(2n)]:break-all gap-1">
26-
<div>Name </div>
27-
<div>{node.name}</div>
2842
<div>Category </div>
29-
<div>{node.detail.cat}</div>
43+
<div>{data.cat}</div>
3044
<div>Start time </div>
31-
<div>{(node.detail.ts - start * 1000) / 1000}ms</div>
45+
<div>{(data.ts - start * 1000) / 1000}ms</div>
3246
<div>Duration </div>
33-
<div>{node.duration}ms</div>
34-
{node.detail?.args &&
35-
Object.entries(node.detail.args).map(([key, value]) => {
47+
<div>{data.dur}ms</div>
48+
{data?.args &&
49+
Object.entries(data.args).map(([key, value]) => {
3650
return (
3751
<React.Fragment key={key}>
3852
<div>{key}</div>
@@ -41,7 +55,7 @@ export const TimelineDetail = ({ start, node, onClose }: TimelineDetailProps) =>
4155
);
4256
})}
4357
<div>track_id</div>
44-
<div>{node.detail.tid}</div>
58+
<div>{data.tid}</div>
4559
</div>
4660
</div>
4761
</div>

0 commit comments

Comments
 (0)