mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-06-04 11:25:52 +08:00
feat: gantt charts for spans (#184)
* feat: create a base component for trace gantt chart * fix: max and min calc * fix: focus on selected paths * fix: build issue * fix: convert duration to ms * fix: gantt chart cells margin left * feat: sorted data by startTime * feat: update layout and add select functionality to table * feat: add UI and functionality * feat: make row clickable in traces, show tags on gant chart click and some fixes * feat: sort flamegraph and show tags on row click on gantt chart * feat: change table type to radio and disable parent selection * fix: left padding of gantt chart lines * fix: line chart duration * fix: sorting flame graph * fix: reset zoom on flame graph * fix: expand children on row click, show tags on page load, default expand on page load * style(gantt-chart): make gantt chart buttons & tags sticky * style(gant-chart): margin bottom in table & padding of gant * feat: update content on trace list
This commit is contained in:
parent
59749d0576
commit
bf2002d6a2
@ -135,6 +135,7 @@
|
||||
"@babel/preset-env": "^7.12.17",
|
||||
"@babel/preset-react": "^7.12.13",
|
||||
"@babel/preset-typescript": "^7.12.17",
|
||||
"@types/lodash-es": "^4.17.4",
|
||||
"autoprefixer": "^9.0.0",
|
||||
"babel-plugin-styled-components": "^1.12.0",
|
||||
"compression-webpack-plugin": "^8.0.0",
|
||||
@ -147,6 +148,7 @@
|
||||
"husky": "4.3.8",
|
||||
"less-plugin-npm-import": "^2.1.0",
|
||||
"lint-staged": "10.5.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
"prettier": "2.2.1",
|
||||
"react-hot-loader": "^4.13.0",
|
||||
"react-is": "^17.0.1",
|
||||
|
@ -1,47 +1,94 @@
|
||||
import React from "react";
|
||||
import { Card, Tabs } from "antd";
|
||||
import { Card, Space, Tabs, Typography } from "antd";
|
||||
import styled from "styled-components";
|
||||
import { pushDStree } from "../../store/actions";
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
interface spanTagItem {
|
||||
key: string;
|
||||
type: string;
|
||||
value: string;
|
||||
}
|
||||
const { Text } = Typography;
|
||||
|
||||
interface SelectedSpanDetailsProps {
|
||||
clickedSpanTags: spanTagItem[];
|
||||
data: pushDStree
|
||||
}
|
||||
|
||||
const Title = styled(Text)`
|
||||
color: "#2D9CDB",
|
||||
fontSize: '12px',
|
||||
`;
|
||||
|
||||
const SelectedSpanDetails = (props: SelectedSpanDetailsProps) => {
|
||||
const callback = (key: any) => {};
|
||||
|
||||
let spanTags = props.data.tags;
|
||||
let service = props.data?.name?.split(":")[0];
|
||||
let operation = props.data?.name?.split(":")[1];
|
||||
|
||||
return (
|
||||
<Card style={{ height: 320 }}>
|
||||
<Tabs defaultActiveKey="1" onChange={callback}>
|
||||
<Card style={{ border: "none", background: "transparent", padding: 0 }} bodyStyle={{ padding: 0 }}>
|
||||
<Space direction="vertical">
|
||||
|
||||
<strong> Details for selected Span </strong>
|
||||
<Space direction="vertical" size={2}>
|
||||
<Text style={{ marginTop: "18px" }}>
|
||||
Service
|
||||
</Text>
|
||||
<Title style={{ color: "#2D9CDB", fontSize: "12px" }}>
|
||||
{service}
|
||||
</Title>
|
||||
</Space>
|
||||
<Space direction="vertical" size={2}>
|
||||
<Text>
|
||||
Operation
|
||||
</Text>
|
||||
<Text style={{ color: "#2D9CDB", fontSize: "12px" }}>
|
||||
{operation}
|
||||
</Text>
|
||||
</Space>
|
||||
</Space>
|
||||
<Tabs defaultActiveKey="1">
|
||||
<TabPane tab="Tags" key="1">
|
||||
<strong> Details for selected Span </strong>
|
||||
{props.clickedSpanTags.map((tags, index) => (
|
||||
<li
|
||||
key={index}
|
||||
style={{ color: "grey", fontSize: "13px", listStyle: "none" }}
|
||||
>
|
||||
<span className="mr-1">{tags.key}</span>:
|
||||
<span className="ml-1">
|
||||
{tags.key === "error" ? "true" : tags.value}
|
||||
</span>
|
||||
</li>
|
||||
))}{" "}
|
||||
{spanTags && spanTags.map((tags, index) => {
|
||||
return (
|
||||
<>
|
||||
{tags.value && (
|
||||
<>
|
||||
<Text style={{ color: "#BDBDBD", fontSize: "12px", marginBottom: "8px" }}>
|
||||
{tags.key}
|
||||
</Text>
|
||||
<div style={{
|
||||
background: "#4F4F4F",
|
||||
color: "#2D9CDB",
|
||||
fontSize: "12px",
|
||||
padding: "6px 8px",
|
||||
wordBreak: "break-all",
|
||||
marginBottom: "16px",
|
||||
}}>
|
||||
{tags.key === "error" ? "true" : tags.value}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</TabPane>
|
||||
<TabPane tab="Errors" key="2">
|
||||
{props.clickedSpanTags
|
||||
{spanTags && spanTags
|
||||
.filter((tags) => tags.key === "error")
|
||||
.map((error) => (
|
||||
<div className="ml-5">
|
||||
<p style={{ color: "grey", fontSize: "10px" }}>
|
||||
<span className="mr-1">{error.key}</span>:
|
||||
<span className="ml-1">true</span>
|
||||
</p>
|
||||
</div>
|
||||
<>
|
||||
<Text style={{ color: "#BDBDBD", fontSize: "12px", marginBottom: "8px" }}>
|
||||
{error.key}
|
||||
</Text>
|
||||
<div style={{
|
||||
background: "#4F4F4F",
|
||||
color: "#2D9CDB",
|
||||
fontSize: "12px",
|
||||
padding: "6px 8px",
|
||||
wordBreak: "break-all",
|
||||
marginBottom: "16px",
|
||||
}}>
|
||||
true
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
|
41
frontend/src/modules/Traces/TraceGantChartHelpers.js
Normal file
41
frontend/src/modules/Traces/TraceGantChartHelpers.js
Normal file
@ -0,0 +1,41 @@
|
||||
// Doing DFS traversal on the tree
|
||||
// resultCount : how many entries you want. where -1 means all possible entries.
|
||||
// func(obj) : takes one element of the data structure and returns true if need to select or not
|
||||
|
||||
// program to implement stack data structure
|
||||
import { isEmpty } from "lodash-es";
|
||||
|
||||
const getTreeData = (tree, callback, resultCount = -1) => {
|
||||
if (resultCount === 0 || isEmpty(tree) || tree.id === "empty") return null;
|
||||
|
||||
let data = tree;
|
||||
let result = [];
|
||||
let stk = [];
|
||||
stk.push(data);
|
||||
|
||||
while (!isEmpty(stk)) {
|
||||
let x = stk[stk.length - 1];
|
||||
|
||||
// marked means seeing the node for the second time.
|
||||
if (x.marked) {
|
||||
delete x.marked;
|
||||
stk.pop();
|
||||
x.map((item) => {
|
||||
if (callback(item) === true) {
|
||||
result.push(item);
|
||||
if (resultCount !== -1 && result.length === resultCount) return result;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
x.marked = true;
|
||||
x.map((item) => {
|
||||
if (item.children.length > 0) {
|
||||
stk.push(item.children);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export default getTreeData;
|
13
frontend/src/modules/Traces/TraceGanttChart.css
Normal file
13
frontend/src/modules/Traces/TraceGanttChart.css
Normal file
@ -0,0 +1,13 @@
|
||||
.row-styles{
|
||||
cursor: pointer
|
||||
}
|
||||
.hide{
|
||||
display: none;
|
||||
}
|
||||
.ant-tabs-nav-list{
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
.ant-table-body table {
|
||||
margin-bottom: 64px;
|
||||
}
|
335
frontend/src/modules/Traces/TraceGanttChart.tsx
Normal file
335
frontend/src/modules/Traces/TraceGanttChart.tsx
Normal file
@ -0,0 +1,335 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Table, Progress, Tabs, Button, Row, Col } from "antd";
|
||||
import "./TraceGanttChart.css";
|
||||
import { max, isEmpty, has } from "lodash-es";
|
||||
import styled from "styled-components";
|
||||
import getTreeData from "Src/modules/Traces/TraceGantChartHelpers";
|
||||
import { pushDStree } from "../../store/actions";
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
color: #f2f2f2;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
`;
|
||||
|
||||
interface TraceGanttChartProps {
|
||||
treeData: pushDStree;
|
||||
clickedSpan: pushDStree;
|
||||
selectedSpan: pushDStree;
|
||||
resetZoom: () => {};
|
||||
setSpanTagsInfo: () => {};
|
||||
}
|
||||
|
||||
const TraceGanttChart = ({
|
||||
treeData,
|
||||
clickedSpan,
|
||||
selectedSpan,
|
||||
resetZoom,
|
||||
setSpanTagsInfo,
|
||||
}: TraceGanttChartProps) => {
|
||||
let checkStrictly = true;
|
||||
const [selectedRows, setSelectedRows] = useState([]);
|
||||
const [clickedSpanData, setClickedSpanData] = useState(clickedSpan);
|
||||
const [defaultExpandedRows, setDefaultExpandedRows] = useState([]);
|
||||
const [sortedTreeData, setSortedTreeData] = useState(treeData);
|
||||
const [isReset, setIsReset] = useState(false);
|
||||
const [rowId, setRowId] = useState(0);
|
||||
const [tabsContainerWidth, setTabsContainerWidth] = useState(0);
|
||||
const tableRef = useRef("");
|
||||
let tabsContainer = document.querySelector(
|
||||
"#collapsable .ant-tabs-nav-list",
|
||||
);
|
||||
|
||||
let tabs = document.querySelectorAll("#collapsable .ant-tabs-tab");
|
||||
|
||||
const { id } = treeData || "id";
|
||||
let maxGlobal = 0;
|
||||
let minGlobal = 0;
|
||||
let medianGlobal = 0;
|
||||
let endTimeArray: [] = [];
|
||||
|
||||
useEffect(() => {
|
||||
if (id !== "empty") {
|
||||
setSortedTreeData(treeData);
|
||||
if (clickedSpan) {
|
||||
setClickedSpanData(clickedSpan);
|
||||
}
|
||||
setTabsContainerWidth(tabsContainer?.offsetWidth)
|
||||
}
|
||||
// handleScroll(selectedSpan?.id);
|
||||
}, [sortedTreeData, treeData, clickedSpan]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isEmpty(clickedSpanData) &&
|
||||
clickedSpan &&
|
||||
!selectedRows.includes(clickedSpan.id)
|
||||
&& !isReset
|
||||
) {
|
||||
setSelectedRows([clickedSpan.id]);
|
||||
getParentKeys(clickedSpan);
|
||||
let keys = [clickedSpan?.id, ...parentKeys];
|
||||
// setDefaultExpandedRows(keys)
|
||||
handleFocusOnSelectedPath("", [clickedSpan.id], clickedSpan);
|
||||
}
|
||||
}, [clickedSpan, selectedRows, isReset, clickedSpanData]);
|
||||
|
||||
let parentKeys = [];
|
||||
let childrenKeys = [];
|
||||
const getParentKeys = (obj) => {
|
||||
if (has(obj, "parent")) {
|
||||
parentKeys.push(obj.parent.id);
|
||||
getParentKeys(obj.parent);
|
||||
}
|
||||
};
|
||||
|
||||
const getChildrenKeys = (obj) =>{
|
||||
if (has(obj, "children")) {
|
||||
childrenKeys.push(obj.id);
|
||||
if(!isEmpty(obj.children)){
|
||||
obj.children.map((item)=>{
|
||||
getChildrenKeys(item);
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEmpty(selectedSpan) && isEmpty(clickedSpan)) {
|
||||
getParentKeys(selectedSpan);
|
||||
let keys = [selectedSpan?.id, ...parentKeys];
|
||||
setDefaultExpandedRows(keys);
|
||||
setSelectedRows([selectedSpan.id, clickedSpan]);
|
||||
// setSpanTagsInfo({data: selectedSpan})
|
||||
} else {
|
||||
setSelectedRows([treeData?.[0]?.id]);
|
||||
setDefaultExpandedRows([treeData?.[0]?.id]);
|
||||
// /.setSpanTagsInfo({data: treeData?.[0]})
|
||||
}
|
||||
|
||||
}, [selectedSpan, treeData]);
|
||||
|
||||
const getMaxEndTime = (treeData) => {
|
||||
if (treeData.length > 0) {
|
||||
if (treeData?.id !== "empty") {
|
||||
return Array.from(treeData).map((item, key) => {
|
||||
if (!isEmpty(item.children)) {
|
||||
endTimeArray.push(item.time / 1000000 + item.startTime);
|
||||
getMaxEndTime(item.children);
|
||||
} else {
|
||||
endTimeArray.push(item.time / 1000000 + item.startTime);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (id !== "empty") {
|
||||
getMaxEndTime(treeData);
|
||||
maxGlobal = max(endTimeArray);
|
||||
minGlobal = treeData?.[0]?.startTime;
|
||||
medianGlobal = (minGlobal + maxGlobal) / 2;
|
||||
}
|
||||
|
||||
/*
|
||||
timeDiff = maxGlobal - startTime
|
||||
totalTime = maxGlobal - minGlobal
|
||||
totalWidth = width of container
|
||||
*/
|
||||
const getPaddingLeft = (timeDiff, totalTime, totalWidth) => {
|
||||
return ((timeDiff / totalTime) * totalWidth ).toFixed(0);
|
||||
};
|
||||
|
||||
let tabMinVal = 0;
|
||||
let tabMedianVal = (medianGlobal - minGlobal).toFixed(0);
|
||||
let tabMaxVal = (maxGlobal - minGlobal).toFixed(0);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "",
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<Tabs>
|
||||
<TabPane tab={tabMinVal + "ms"} key="1" />
|
||||
<TabPane tab={tabMedianVal + "ms"} key="2" />
|
||||
<TabPane tab={tabMaxVal + "ms"} key="3" />
|
||||
</Tabs>
|
||||
),
|
||||
dataIndex: "trace",
|
||||
name: "trace",
|
||||
render: (_, record: pushDStree) => {
|
||||
let widths = [];
|
||||
let length;
|
||||
|
||||
if (widths.length < tabs.length) {
|
||||
Array.from(tabs).map((tab) => {
|
||||
widths.push(tab.offsetWidth);
|
||||
});
|
||||
}
|
||||
|
||||
let paddingLeft = 0;
|
||||
let startTime = parseInt(record.startTime);
|
||||
let duration = parseInt((record.time / 1000000).toFixed(2));
|
||||
paddingLeft = parseInt(getPaddingLeft(startTime - minGlobal, maxGlobal - minGlobal, tabsContainerWidth));
|
||||
let textPadding = paddingLeft;
|
||||
if(paddingLeft === tabsContainerWidth - 20){
|
||||
textPadding = tabsContainerWidth - 40
|
||||
}
|
||||
length = ((duration / (maxGlobal - startTime)) * 100).toFixed(
|
||||
2,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ paddingLeft: textPadding + "px" }}>{duration}ms</div>
|
||||
<Progress
|
||||
percent={length}
|
||||
showInfo={false}
|
||||
style={{ paddingLeft: paddingLeft + "px" }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const handleFocusOnSelectedPath = (event, selectedRowsList = selectedRows) => {
|
||||
if (!isEmpty(selectedRowsList)) {
|
||||
let node: pushDStree = getTreeData(
|
||||
treeData,
|
||||
(item: pushDStree) => item.id === selectedRowsList[0],
|
||||
1,
|
||||
);
|
||||
setSpanTagsInfo({ data: node[0] });
|
||||
|
||||
getParentKeys(node[0]);
|
||||
getChildrenKeys(node[0]);
|
||||
|
||||
let rows = document.querySelectorAll("#collapsable table tbody tr");
|
||||
Array.from(rows).map((row) => {
|
||||
let attribKey = row.getAttribute("data-row-key");
|
||||
if (!selectedRowsList.includes(attribKey)) {
|
||||
row.classList.add("hide");
|
||||
}
|
||||
});
|
||||
setDefaultExpandedRows([...parentKeys, ...childrenKeys]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetFocus = () => {
|
||||
let rows = document.querySelectorAll("#collapsable table tbody tr");
|
||||
Array.from(rows).map((row) => {
|
||||
row.classList.remove("hide");
|
||||
});
|
||||
|
||||
resetZoom(true);
|
||||
};
|
||||
|
||||
const handleScroll = (id) => {
|
||||
let rows = document.querySelectorAll("#collapsable table tbody tr");
|
||||
const table = document.querySelectorAll("#collapsable table");
|
||||
Array.from(rows).map((row) => {
|
||||
let attribKey = row.getAttribute("data-row-key");
|
||||
if (id === attribKey) {
|
||||
let scrollValue = row.offsetTop;
|
||||
table[1].scrollTop = scrollValue;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const rowSelection = {
|
||||
onChange: (selectedRowKeys: []) => {
|
||||
setSelectedRows(selectedRowKeys);
|
||||
setClickedSpanData({});
|
||||
if (isEmpty(selectedRowKeys)) {
|
||||
setIsReset(true);
|
||||
} else {
|
||||
setIsReset(false);
|
||||
}
|
||||
},
|
||||
onSelect:(record)=>{
|
||||
handleRowOnClick(record)
|
||||
},
|
||||
selectedRowKeys: selectedRows,
|
||||
};
|
||||
|
||||
const handleRowOnClick = (record) => {
|
||||
setRowId(record.id);
|
||||
|
||||
let node: pushDStree = getTreeData(
|
||||
treeData,
|
||||
(item: pushDStree) => item.id === record.id,
|
||||
1,
|
||||
);
|
||||
setSpanTagsInfo({ data: node[0] });
|
||||
|
||||
const selectedRowKeys = selectedRows;
|
||||
if (selectedRowKeys.indexOf(record.id) >= 0) {
|
||||
selectedRowKeys.splice(selectedRowKeys.indexOf(record.key), 1);
|
||||
} else {
|
||||
selectedRowKeys.push(record.id);
|
||||
}
|
||||
setSelectedRows([record.id]);
|
||||
};
|
||||
|
||||
const handleOnExpandedRowsChange = (item) => {
|
||||
setDefaultExpandedRows(item);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{id !== "empty" && (
|
||||
<>
|
||||
<Row
|
||||
justify="end"
|
||||
gutter={32}
|
||||
style={{
|
||||
marginBottom: "24px",
|
||||
}}
|
||||
>
|
||||
<Col>
|
||||
<StyledButton onClick={handleFocusOnSelectedPath}>
|
||||
{" "}
|
||||
Focus on selected path{" "}
|
||||
</StyledButton>
|
||||
</Col>
|
||||
<Col>
|
||||
<StyledButton onClick={handleResetFocus}> Reset Focus </StyledButton>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Table
|
||||
refs={tableRef}
|
||||
hideSelectAll={true}
|
||||
columns={columns}
|
||||
rowSelection={{ ...rowSelection, checkStrictly, type:'radio' }}
|
||||
dataSource={sortedTreeData}
|
||||
rowKey="id"
|
||||
sticky={true}
|
||||
onRow={(record, rowIndex) => {
|
||||
return {
|
||||
onClick: () => handleRowOnClick(record, rowIndex), // click row
|
||||
};
|
||||
}}
|
||||
expandedRowKeys={defaultExpandedRows}
|
||||
onExpandedRowsChange={handleOnExpandedRowsChange}
|
||||
pagination={false}
|
||||
scroll={{ y: 540}}
|
||||
rowClassName="row-styles"
|
||||
filterMultiple={false}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TraceGanttChart;
|
@ -1,29 +1,65 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useParams, useLocation } from "react-router-dom";
|
||||
import { flamegraph } from "d3-flame-graph";
|
||||
import { connect } from "react-redux";
|
||||
import { Card, Button, Row, Col, Space } from "antd";
|
||||
import { Card, Row, Col, Space, Affix } from "antd";
|
||||
import * as d3 from "d3";
|
||||
import * as d3Tip from "d3-tip";
|
||||
|
||||
//import * as d3Tip from 'd3-tip';
|
||||
// PNOTE - uninstall @types/d3-tip. issues with importing d3-tip https://github.com/Caged/d3-tip/issues/181
|
||||
|
||||
import "./TraceGraph.css";
|
||||
import { spanToTreeUtil } from "../../utils/spanToTree";
|
||||
import { fetchTraceItem, spansWSameTraceIDResponse } from "../../store/actions";
|
||||
import {
|
||||
fetchTraceItem,
|
||||
pushDStree,
|
||||
spansWSameTraceIDResponse,
|
||||
} from "../../store/actions";
|
||||
import { StoreState } from "../../store/reducers";
|
||||
import SelectedSpanDetails from "./SelectedSpanDetails";
|
||||
import TraceGanttChart from "./TraceGanttChart";
|
||||
import styled from "styled-components";
|
||||
import { isEmpty, sortBy } from "lodash-es";
|
||||
|
||||
interface TraceGraphProps {
|
||||
traceItem: spansWSameTraceIDResponse;
|
||||
fetchTraceItem: Function;
|
||||
}
|
||||
|
||||
const TraceGanttChartContainer = styled(Card)`
|
||||
background: #333333;
|
||||
border-radius: 5px;
|
||||
`;
|
||||
|
||||
const _TraceGraph = (props: TraceGraphProps) => {
|
||||
let location = useLocation();
|
||||
const spanId = location?.state?.spanId;
|
||||
const params = useParams<{ id?: string }>();
|
||||
const [clickedSpanTags, setClickedSpanTags] = useState([]);
|
||||
const [clickedSpanTags, setClickedSpanTags] = useState<pushDStree>([]);
|
||||
const [selectedSpan, setSelectedSpan] = useState({});
|
||||
const [clickedSpan, setClickedSpan] = useState(null);
|
||||
const [resetZoom, setResetZoom] = useState(false);
|
||||
const [sortedTreeData, setSortedTreeData] = useState<pushDStree>([]);
|
||||
|
||||
let sortedData = {};
|
||||
|
||||
const getSortedData = (treeData: [pushDStree], parent = {}) => {
|
||||
if (!isEmpty(treeData)) {
|
||||
if (treeData[0].id !== "empty") {
|
||||
return Array.from(treeData).map((item, key) => {
|
||||
if (!isEmpty(item.children)) {
|
||||
getSortedData(item.children, item);
|
||||
sortedData = sortBy(item.children, (i) => i.startTime);
|
||||
treeData[key].children = sortedData;
|
||||
}
|
||||
if (!isEmpty(parent)) {
|
||||
treeData[key].parent = parent;
|
||||
}
|
||||
return treeData;
|
||||
});
|
||||
}
|
||||
return treeData;
|
||||
}
|
||||
};
|
||||
|
||||
const tree = spanToTreeUtil(props.traceItem[0].events);
|
||||
|
||||
useEffect(() => {
|
||||
//sets span width based on value - which is mapped to duration
|
||||
@ -31,16 +67,31 @@ const _TraceGraph = (props: TraceGraphProps) => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.traceItem || resetZoom) {
|
||||
const tree = spanToTreeUtil(props.traceItem[0].events);
|
||||
if (props.traceItem) {
|
||||
let sortedData = getSortedData([tree]);
|
||||
setSortedTreeData(sortedData?.[0]);
|
||||
getSpanInfo(sortedData?.[0], spanId);
|
||||
// This is causing element to change ref. Can use both useRef or this approach.
|
||||
d3.select("#chart").datum(tree).call(chart);
|
||||
setResetZoom(false);
|
||||
d3.select("#chart").datum(tree).call(chart).sort(item=>item.startTime);
|
||||
}
|
||||
}, [props.traceItem, resetZoom]);
|
||||
}, [props.traceItem]);
|
||||
// if this monitoring of props.traceItem.data is removed then zoom on click doesn't work
|
||||
// Doesn't work if only do initial check, works if monitor an element - as it may get updated in sometime
|
||||
|
||||
useEffect(() => {
|
||||
if(!isEmpty(sortedTreeData) && sortedTreeData?.id !== "empty" && isEmpty(clickedSpanTags)) {
|
||||
setClickedSpanTags(sortedTreeData?.[0]);
|
||||
}
|
||||
}, [sortedTreeData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (resetZoom) {
|
||||
// This is causing element to change ref. Can use both useRef or this approach.
|
||||
d3.select("#chart").datum(tree).call(chart).sort(item=>item.startTime);
|
||||
setResetZoom(false);
|
||||
}
|
||||
}, [resetZoom]);
|
||||
|
||||
const tip = d3Tip
|
||||
.default()
|
||||
.attr("class", "d3-tip")
|
||||
@ -49,30 +100,59 @@ const _TraceGraph = (props: TraceGraphProps) => {
|
||||
});
|
||||
|
||||
const onClick = (z: any) => {
|
||||
setClickedSpanTags(z.data.tags);
|
||||
setClickedSpanTags(z.data);
|
||||
setClickedSpan(z.data);
|
||||
setSelectedSpan([]);
|
||||
console.log(`Clicked on ${z.data.name}, id: "${z.id}"`);
|
||||
};
|
||||
|
||||
const setSpanTagsInfo = (z: any) => {
|
||||
setClickedSpanTags(z.data);
|
||||
};
|
||||
|
||||
const getSpanInfo = (data: [pushDStree], spanId: string): void => {
|
||||
if (resetZoom) {
|
||||
setSelectedSpan({});
|
||||
return;
|
||||
}
|
||||
if (data?.[0]?.id !== "empty") {
|
||||
Array.from(data).map((item) => {
|
||||
if (item.id === spanId) {
|
||||
setSelectedSpan(item);
|
||||
setClickedSpanTags(item);
|
||||
return item;
|
||||
} else if (!isEmpty(item.children)) {
|
||||
getSpanInfo(item.children, spanId);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const chart = flamegraph()
|
||||
.cellHeight(18)
|
||||
.transitionDuration(500)
|
||||
.inverted(true)
|
||||
.tooltip(tip)
|
||||
.minFrameSize(10)
|
||||
.minFrameSize(4)
|
||||
.elided(false)
|
||||
.differential(false)
|
||||
.sort(true)
|
||||
.sort((item) => item.startTime)
|
||||
//Use self value=true when we're using not using aggregated option, Which is not our case.
|
||||
// In that case it's doing step function sort of stuff thru computation.
|
||||
// Source flamegraph.js line 557 and 573.
|
||||
// .selfValue(true)
|
||||
.onClick(onClick);
|
||||
.onClick(onClick)
|
||||
.width(800);
|
||||
|
||||
const handleResetZoom = (value) => {
|
||||
setResetZoom(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 32 }}>
|
||||
<Col md={24} sm={24}>
|
||||
<Col md={18} sm={18}>
|
||||
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
|
||||
<Card bodyStyle={{ padding: 80 }} style={{ height: 320 }}>
|
||||
<Card bodyStyle={{ padding: 24 }} style={{ height: 320 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
@ -84,20 +164,27 @@ const _TraceGraph = (props: TraceGraphProps) => {
|
||||
<div style={{ textAlign: "center" }}>
|
||||
Trace Graph component ID is {params.id}{" "}
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={setResetZoom.bind(this, true)}
|
||||
style={{ width: 160 }}
|
||||
>
|
||||
Reset Zoom
|
||||
</Button>
|
||||
<div id="chart" style={{ fontSize: 12, marginTop: 20 }}></div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<SelectedSpanDetails clickedSpanTags={clickedSpanTags} />
|
||||
<Affix offsetTop={24}>
|
||||
<TraceGanttChartContainer id={"collapsable"}>
|
||||
<TraceGanttChart
|
||||
treeData={sortedTreeData}
|
||||
clickedSpan={clickedSpan}
|
||||
selectedSpan={selectedSpan}
|
||||
resetZoom={handleResetZoom}
|
||||
setSpanTagsInfo={setSpanTagsInfo}
|
||||
/>
|
||||
</TraceGanttChartContainer>
|
||||
</Affix>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col md={6} sm={6}>
|
||||
<Affix offsetTop={24}>
|
||||
<SelectedSpanDetails data={clickedSpanTags} />
|
||||
</Affix>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { useHistory} from "react-router-dom";
|
||||
import { Space, Table } from "antd";
|
||||
import ROUTES from "Src/constants/routes";
|
||||
|
||||
@ -10,9 +10,14 @@ import { isOnboardingSkipped } from "../../utils/app";
|
||||
import moment from "moment";
|
||||
import styled from "styled-components";
|
||||
|
||||
const StyledTable = styled(Table)`
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const TraceHeader = styled.div`
|
||||
margin: 16px 0;
|
||||
`;
|
||||
|
||||
interface TraceListProps {
|
||||
traces: traceResponseNew;
|
||||
fetchTraces: Function;
|
||||
@ -25,26 +30,17 @@ interface TableDataSourceItem {
|
||||
operationName: string;
|
||||
startTime: number;
|
||||
duration: number;
|
||||
service: string;
|
||||
}
|
||||
|
||||
const _TraceList = (props: TraceListProps) => {
|
||||
// PNOTE (TO DO) - Currently this use of useEffect gives warning. May need to memoise fetchtraces - https://stackoverflow.com/questions/55840294/how-to-fix-missing-dependency-warning-when-using-useeffect-react-hook
|
||||
let history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
props.fetchTraces();
|
||||
}, []);
|
||||
|
||||
// PNOTE - code snippet -
|
||||
// renderList(): JSX.Element[] {
|
||||
// return this.props.todos.map((todo: Todo) => {
|
||||
// return (
|
||||
// <div onClick={() => this.onTodoClick(todo.id)} key={todo.id}>
|
||||
// {todo.title}
|
||||
// </div>
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
|
||||
const columns: any = [
|
||||
{
|
||||
title: "Start Time",
|
||||
@ -57,12 +53,9 @@ const _TraceList = (props: TraceListProps) => {
|
||||
// new Date() assumes input in milliseconds. Start Time stamp returned by druid api for span list is in ms
|
||||
},
|
||||
{
|
||||
title: "Duration (in ms)",
|
||||
dataIndex: "duration",
|
||||
key: "duration",
|
||||
sorter: (a: any, b: any) => a.duration - b.duration,
|
||||
sortDirections: ["descend", "ascend"],
|
||||
render: (value: number) => (value / 1000000).toFixed(2),
|
||||
title: "Service",
|
||||
dataIndex: "service",
|
||||
key: "service",
|
||||
},
|
||||
{
|
||||
title: "Operation",
|
||||
@ -70,13 +63,12 @@ const _TraceList = (props: TraceListProps) => {
|
||||
key: "operationName",
|
||||
},
|
||||
{
|
||||
title: "TraceID",
|
||||
dataIndex: "traceid",
|
||||
key: "traceid",
|
||||
render: (text: string) => (
|
||||
<NavLink to={ROUTES.TRACES + "/" + text}>{text.slice(-16)}</NavLink>
|
||||
),
|
||||
//only last 16 chars have traceID, druid makes it 32 by adding zeros
|
||||
title: "Duration (in ms)",
|
||||
dataIndex: "duration",
|
||||
key: "duration",
|
||||
sorter: (a: any, b: any) => a.duration - b.duration,
|
||||
sortDirections: ["descend", "ascend"],
|
||||
render: (value: number) => (value / 1000000).toFixed(2),
|
||||
},
|
||||
];
|
||||
|
||||
@ -87,8 +79,6 @@ const _TraceList = (props: TraceListProps) => {
|
||||
typeof props.traces[0] !== "undefined" &&
|
||||
props.traces[0].events.length > 0
|
||||
) {
|
||||
//PNOTE - Template literal should be wrapped in curly braces for it to be evaluated
|
||||
|
||||
props.traces[0].events.map(
|
||||
(item: (number | string | string[] | pushDStree[])[], index) => {
|
||||
if (
|
||||
@ -96,7 +86,8 @@ const _TraceList = (props: TraceListProps) => {
|
||||
typeof item[4] === "string" &&
|
||||
typeof item[6] === "string" &&
|
||||
typeof item[1] === "string" &&
|
||||
typeof item[2] === "string"
|
||||
typeof item[2] === "string" &&
|
||||
typeof item[3] === "string"
|
||||
)
|
||||
dataSource.push({
|
||||
startTime: item[0],
|
||||
@ -105,13 +96,30 @@ const _TraceList = (props: TraceListProps) => {
|
||||
spanid: item[1],
|
||||
traceid: item[2],
|
||||
key: index.toString(),
|
||||
service: item[3],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
//antd table in typescript - https://codesandbox.io/s/react-typescript-669cv
|
||||
|
||||
return <Table dataSource={dataSource} columns={columns} size="middle" />;
|
||||
return <StyledTable
|
||||
dataSource={dataSource}
|
||||
columns={columns}
|
||||
size="middle"
|
||||
rowClassName=""
|
||||
onRow={(record) => ({
|
||||
onClick: () => {
|
||||
history.push({
|
||||
pathname: ROUTES.TRACES + "/" + record.traceid,
|
||||
state: {
|
||||
spanId: record.spanid,
|
||||
},
|
||||
});
|
||||
}
|
||||
})}
|
||||
/>
|
||||
;
|
||||
} else {
|
||||
if (isOnboardingSkipped()) {
|
||||
return (
|
||||
@ -136,7 +144,7 @@ const _TraceList = (props: TraceListProps) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TraceHeader>List of traces with spanID</TraceHeader>
|
||||
<TraceHeader>List of filtered spans</TraceHeader>
|
||||
<div>{renderTraces()}</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -2136,6 +2136,18 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
|
||||
|
||||
"@types/lodash-es@^4.17.4":
|
||||
version "4.17.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.4.tgz#b2e440d2bf8a93584a9fd798452ec497986c9b97"
|
||||
integrity sha512-BBz79DCJbD2CVYZH67MBeHZRX++HF+5p8Mo5MzjZi64Wac39S3diedJYHZtScbRVf4DjZyN6LzA0SB0zy+HSSQ==
|
||||
dependencies:
|
||||
"@types/lodash" "*"
|
||||
|
||||
"@types/lodash@*":
|
||||
version "4.14.170"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6"
|
||||
integrity sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q==
|
||||
|
||||
"@types/minimatch@*":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
|
||||
@ -8738,6 +8750,11 @@ locate-path@^6.0.0:
|
||||
dependencies:
|
||||
p-locate "^5.0.0"
|
||||
|
||||
lodash-es@^4.17.21:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
|
||||
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
|
||||
|
||||
lodash._reinterpolate@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
|
||||
|
Loading…
x
Reference in New Issue
Block a user