From 80c80b2180b8e69342526c3abed00052ad09d1b9 Mon Sep 17 00:00:00 2001 From: Pranshu Chittora Date: Fri, 8 Jul 2022 16:18:08 +0530 Subject: [PATCH] feat: missing spans handling by returning a forest of trees (#1365) * feat: spanToTree 2.0 * feat: spanToTree EPIFI data * feat: missing spans multiple trees * chore: migrated to popoverss Co-authored-by: Palash --- .../src/container/GantChart/Trace/index.tsx | 9 +- .../src/container/GantChart/Trace/styles.ts | 7 +- frontend/src/container/GantChart/index.tsx | 58 ++-- frontend/src/container/GantChart/styles.ts | 1 + frontend/src/container/GantChart/utils.ts | 42 ++- .../container/TraceDetail/Missingtrace.tsx | 42 +++ .../TraceDetail/SelectedSpanDetails/index.tsx | 1 + frontend/src/container/TraceDetail/index.tsx | 153 ++++++++--- frontend/src/container/TraceDetail/styles.ts | 38 ++- frontend/src/container/TraceDetail/utils.ts | 20 +- .../__tests__/TraceFlameGraph.test.tsx | 1 + .../src/container/TraceFlameGraph/index.tsx | 6 +- frontend/src/types/api/trace/getTraceItem.ts | 12 +- frontend/src/utils/getSpanTreeMetadata.ts | 26 +- frontend/src/utils/spanToTree.ts | 252 +++++++++--------- 15 files changed, 441 insertions(+), 227 deletions(-) create mode 100644 frontend/src/container/TraceDetail/Missingtrace.tsx diff --git a/frontend/src/container/GantChart/Trace/index.tsx b/frontend/src/container/GantChart/Trace/index.tsx index db607092eb..d6982e04e6 100644 --- a/frontend/src/container/GantChart/Trace/index.tsx +++ b/frontend/src/container/GantChart/Trace/index.tsx @@ -39,6 +39,7 @@ function Trace(props: TraceProps): JSX.Element { isExpandAll, intervalUnit, children, + isMissing, } = props; const { isDarkMode } = useThemeMode(); @@ -125,7 +126,7 @@ function Trace(props: TraceProps): JSX.Element { isDarkMode={isDarkMode} /> - + @@ -174,6 +175,7 @@ function Trace(props: TraceProps): JSX.Element { activeSpanPath={activeSpanPath} isExpandAll={isExpandAll} intervalUnit={intervalUnit} + isMissing={child.isMissing} /> ))} @@ -182,6 +184,10 @@ function Trace(props: TraceProps): JSX.Element { ); } +Trace.defaultProps = { + isMissing: false, +}; + interface ITraceGlobal { globalSpread: ITraceMetaData['spread']; globalStart: ITraceMetaData['globalStart']; @@ -196,6 +202,7 @@ interface TraceProps extends ITraceTree, ITraceGlobal { activeSpanPath: string[]; isExpandAll: boolean; intervalUnit: IIntervalUnit; + isMissing?: boolean; } export default Trace; diff --git a/frontend/src/container/GantChart/Trace/styles.ts b/frontend/src/container/GantChart/Trace/styles.ts index ccf139d6c2..7710e77b5b 100644 --- a/frontend/src/container/GantChart/Trace/styles.ts +++ b/frontend/src/container/GantChart/Trace/styles.ts @@ -1,3 +1,4 @@ +import { volcano } from '@ant-design/colors'; import styled, { css, DefaultTheme, @@ -15,7 +16,6 @@ export const Wrapper = styled.ul` padding-top: 0.5rem; position: relative; z-index: 1; - ul { border-left: ${({ isOnlyChild }): StyledCSS => isOnlyChild && 'none'} !important; @@ -36,10 +36,13 @@ export const Wrapper = styled.ul` } `; -export const CardContainer = styled.li` +export const CardContainer = styled.li<{ isMissing?: boolean }>` display: flex; width: 100%; cursor: pointer; + border-radius: 0.25rem; + ${({ isMissing }): string => + isMissing ? `border: 1px dashed ${volcano[6]};` : ''} `; interface Props { diff --git a/frontend/src/container/GantChart/index.tsx b/frontend/src/container/GantChart/index.tsx index a25f4af228..dbe707c2d7 100644 --- a/frontend/src/container/GantChart/index.tsx +++ b/frontend/src/container/GantChart/index.tsx @@ -3,7 +3,7 @@ import { IIntervalUnit } from 'container/TraceDetail/utils'; import React, { useEffect, useState } from 'react'; import { ITraceTree } from 'types/api/trace/getTraceItem'; -import { CardContainer, CardWrapper, CollapseButton, Wrapper } from './styles'; +import { CardContainer, CardWrapper, CollapseButton } from './styles'; import Trace from './Trace'; import { getSpanPath } from './utils'; @@ -36,35 +36,33 @@ function GanttChart(props: GanttChartProps): JSX.Element { setIsExpandAll((prev) => !prev); }; return ( - - - - {isExpandAll ? : } - - - - - - + + + {isExpandAll ? : } + + + + + ); } diff --git a/frontend/src/container/GantChart/styles.ts b/frontend/src/container/GantChart/styles.ts index 4d523c4998..6f05611599 100644 --- a/frontend/src/container/GantChart/styles.ts +++ b/frontend/src/container/GantChart/styles.ts @@ -38,6 +38,7 @@ export const CardWrapper = styled.div` export const CardContainer = styled.li` display: flex; width: 100%; + position: relative; `; export const CollapseButton = styled.div` diff --git a/frontend/src/container/GantChart/utils.ts b/frontend/src/container/GantChart/utils.ts index d229af9839..c91564b3e2 100644 --- a/frontend/src/container/GantChart/utils.ts +++ b/frontend/src/container/GantChart/utils.ts @@ -1,4 +1,5 @@ -import { ITraceTree } from 'types/api/trace/getTraceItem'; +import { set } from 'lodash-es'; +import { ITraceForest, ITraceTree } from 'types/api/trace/getTraceItem'; interface GetTraceMetaData { globalStart: number; @@ -65,25 +66,48 @@ export function getTopLeftFromBody( export const getNodeById = ( searchingId: string, - treeData: ITraceTree, -): ITraceTree | undefined => { - let foundNode: ITraceTree | undefined; - const traverse = (treeNode: ITraceTree, level = 0): void => { + treesData: ITraceForest | undefined, +): ITraceForest => { + const newtreeData: ITraceForest = {} as ITraceForest; + + const traverse = ( + treeNode: ITraceTree, + setCallBack: (arg0: ITraceTree) => void, + level = 0, + ): void => { if (!treeNode) { return; } if (searchingId === treeNode.id) { - foundNode = treeNode; + setCallBack(treeNode); } treeNode.children.forEach((childNode) => { - traverse(childNode, level + 1); + traverse(childNode, setCallBack, level + 1); }); }; - traverse(treeData, 1); - return foundNode; + const spanTreeSetCallback = ( + path: (keyof ITraceForest)[], + value: ITraceTree, + ): ITraceForest => set(newtreeData, path, [value]); + + if (treesData?.spanTree) + treesData.spanTree.forEach((tree) => { + traverse(tree, (value) => spanTreeSetCallback(['spanTree'], value), 1); + }); + + if (treesData?.missingSpanTree) + treesData.missingSpanTree.forEach((tree) => { + traverse( + tree, + (value) => spanTreeSetCallback(['missingSpanTree'], value), + 1, + ); + }); + + return newtreeData; }; const getSpanWithoutChildren = ( diff --git a/frontend/src/container/TraceDetail/Missingtrace.tsx b/frontend/src/container/TraceDetail/Missingtrace.tsx new file mode 100644 index 0000000000..b203f05f68 --- /dev/null +++ b/frontend/src/container/TraceDetail/Missingtrace.tsx @@ -0,0 +1,42 @@ +import { volcano } from '@ant-design/colors'; +import { InfoCircleOutlined } from '@ant-design/icons'; +import { Popover } from 'antd'; +import React from 'react'; + +function PopOverContent(): JSX.Element { + return ( +
+ More details on missing spans{' '} + + here + +
+ ); +} + +function MissingSpansMessage(): JSX.Element { + return ( + +
+ {' '} + This trace has missing spans +
+
+ ); +} + +export default MissingSpansMessage; diff --git a/frontend/src/container/TraceDetail/SelectedSpanDetails/index.tsx b/frontend/src/container/TraceDetail/SelectedSpanDetails/index.tsx index 50f2aa9537..08d6c057a9 100644 --- a/frontend/src/container/TraceDetail/SelectedSpanDetails/index.tsx +++ b/frontend/src/container/TraceDetail/SelectedSpanDetails/index.tsx @@ -18,6 +18,7 @@ const { TabPane } = Tabs; function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element { const { tree } = props; + const { isDarkMode } = useThemeMode(); const OverLayComponentName = useMemo(() => tree?.name, [tree?.name]); diff --git a/frontend/src/container/TraceDetail/index.tsx b/frontend/src/container/TraceDetail/index.tsx index c705123b50..816f59ec2f 100644 --- a/frontend/src/container/TraceDetail/index.tsx +++ b/frontend/src/container/TraceDetail/index.tsx @@ -17,15 +17,23 @@ import dayjs from 'dayjs'; import useUrlQuery from 'hooks/useUrlQuery'; import { spanServiceNameToColorMapping } from 'lib/getRandomColor'; import history from 'lib/history'; +import { map } from 'lodash-es'; import { SPAN_DETAILS_LEFT_COL_WIDTH } from 'pages/TraceDetail/constants'; import React, { useEffect, useMemo, useState } from 'react'; -import { ITraceTree, PayloadProps } from 'types/api/trace/getTraceItem'; +import { ITraceForest, PayloadProps } from 'types/api/trace/getTraceItem'; import { getSpanTreeMetadata } from 'utils/getSpanTreeMetadata'; import { spanToTreeUtil } from 'utils/spanToTree'; +import MissingSpansMessage from './Missingtrace'; import SelectedSpanDetails from './SelectedSpanDetails'; import * as styles from './styles'; -import { getSortedData, IIntervalUnit, INTERVAL_UNITS } from './utils'; +import { FlameGraphMissingSpansContainer, GanttChartWrapper } from './styles'; +import { + getSortedData, + getTreeLevelsCount, + IIntervalUnit, + INTERVAL_UNITS, +} from './utils'; function TraceDetail({ response }: TraceDetailProps): JSX.Element { const spanServiceColors = useMemo( @@ -43,17 +51,23 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element { const [activeHoverId, setActiveHoverId] = useState(''); const [activeSelectedId, setActiveSelectedId] = useState(spanId || ''); - const [treeData, setTreeData] = useState( + const [treesData, setTreesData] = useState( spanToTreeUtil(response[0].events), ); - const { treeData: tree, ...traceMetaData } = useMemo(() => { - const tree = getSortedData(treeData); + const { treesData: tree, ...traceMetaData } = useMemo(() => { + const sortedTreesData: ITraceForest = { + spanTree: map(treesData.spanTree, (tree) => getSortedData(tree)), + missingSpanTree: map( + treesData.missingSpanTree, + (tree) => getSortedData(tree) || [], + ), + }; // Note: Handle undefined /*eslint-disable */ - return getSpanTreeMetadata(tree as ITraceTree, spanServiceColors); + return getSpanTreeMetadata(sortedTreesData, spanServiceColors); /* eslint-enable */ - }, [treeData, spanServiceColors]); + }, [treesData, spanServiceColors]); const [globalTraceMetadata] = useState({ ...traceMetaData, @@ -69,24 +83,34 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element { }, [activeSelectedId]); const getSelectedNode = useMemo(() => { - return getNodeById(activeSelectedId, treeData); - }, [activeSelectedId, treeData]); + return getNodeById(activeSelectedId, treesData); + }, [activeSelectedId, treesData]); // const onSearchHandler = (value: string) => { // setSearchSpanString(value); // setTreeData(spanToTreeUtil(response[0].events)); // }; + const onFocusSelectedSpanHandler = (): void => { const treeNode = getNodeById(activeSelectedId, tree); + if (treeNode) { - setTreeData(treeNode); + setTreesData(treeNode); } }; const onResetHandler = (): void => { - setTreeData(spanToTreeUtil(response[0].events)); + setTreesData(spanToTreeUtil(response[0].events)); }; + const hasMissingSpans = useMemo( + (): boolean => + tree.missingSpanTree && + Array.isArray(tree.missingSpanTree) && + tree.missingSpanTree.length > 0, + [tree], + ); + return ( @@ -101,16 +125,45 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element { {traceMetaData.totalSpans} Span + {hasMissingSpans && } - + {map(tree.spanTree, (tree) => { + return ( + + ); + })} + + {hasMissingSpans && ( + + {map(tree.missingSpanTree, (tree) => { + return ( + + ); + })} + + )} @@ -122,7 +175,9 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element { justifyContent: 'center', }} > - {tree && dayjs(tree.startTime).format('hh:mm:ss a MM/DD')} + {tree && + traceMetaData.globalStart && + dayjs(traceMetaData.globalStart).format('hh:mm:ss a MM/DD')}
- - {/* */} - +