Skip to content

Commit

Permalink
feat: filter spans
Browse files Browse the repository at this point in the history
  • Loading branch information
Jianhao Chen committed Jan 9, 2025
1 parent 56b17bc commit 4fd73e8
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 20 deletions.
71 changes: 51 additions & 20 deletions frontend/src/container/TraceDetail/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import './TraceDetails.styles.scss';

import { FilterOutlined } from '@ant-design/icons';
import { Button, Col, Layout, Typography } from 'antd';
import { Button, Col, Empty, Input, Layout, Typography } from 'antd';
import cx from 'classnames';
import {
StyledCol,
Expand All @@ -12,12 +12,14 @@ import {
StyledTypography,
} from 'components/Styled';
import { Flex, Spacing } from 'components/Styled/styles';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import GanttChart, { ITraceMetaData } from 'container/GantChart';
import { getNodeById } from 'container/GantChart/utils';
import Timeline from 'container/Timeline';
import TraceFlameGraph from 'container/TraceFlameGraph';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import useUrlQuery from 'hooks/useUrlQuery';
import { spanServiceNameToColorMapping } from 'lib/getRandomColor';
import history from 'lib/history';
Expand All @@ -26,7 +28,11 @@ import { PanelRight } from 'lucide-react';
import { SPAN_DETAILS_LEFT_COL_WIDTH } from 'pages/TraceDetail/constants';
import { useTimezone } from 'providers/Timezone';
import { useEffect, useMemo, useState } from 'react';
import { ITraceForest, PayloadProps } from 'types/api/trace/getTraceItem';
import {
ITraceForest,
ITraceTree,
PayloadProps,
} from 'types/api/trace/getTraceItem';
import { getSpanTreeMetadata } from 'utils/getSpanTreeMetadata';
import { spanToTreeUtil } from 'utils/spanToTree';

Expand All @@ -36,7 +42,9 @@ import * as styles from './styles';
import { FlameGraphMissingSpansContainer, GanttChartWrapper } from './styles';
import SubTreeMessage from './SubTree';
import {
DEFAULT_FILTER_KEYS,
formUrlParams,
getFilteredData,
getSortedData,
getTreeLevelsCount,
IIntervalUnit,
Expand All @@ -59,6 +67,7 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {

const urlQuery = useUrlQuery();
const [spanId] = useState<string | null>(urlQuery.get('spanId'));
const [searchText, setSearchText] = useState<string>('');

const [intervalUnit, setIntervalUnit] = useState<IIntervalUnit>(
INTERVAL_UNITS[0],
Expand All @@ -78,18 +87,27 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
);

const { treesData: tree, ...traceMetaData } = useMemo(() => {
const filteredTreesData: ITraceForest = {
spanTree: map(treesData.spanTree, (tree) =>
getFilteredData(tree, searchText, DEFAULT_FILTER_KEYS),
).filter(Boolean) as ITraceTree[],
missingSpanTree: map(treesData.missingSpanTree, (tree) =>
getFilteredData(tree, searchText, DEFAULT_FILTER_KEYS),
).filter(Boolean) as ITraceTree[],
};

const sortedTreesData: ITraceForest = {
spanTree: map(treesData.spanTree, (tree) => getSortedData(tree)),
spanTree: map(filteredTreesData.spanTree, (tree) => getSortedData(tree)),
missingSpanTree: map(
treesData.missingSpanTree,
filteredTreesData.missingSpanTree,
(tree) => getSortedData(tree) || [],
),
};
// Note: Handle undefined
/*eslint-disable */
return getSpanTreeMetadata(sortedTreesData, spanServiceColors);
/* eslint-enable */
}, [treesData, spanServiceColors]);
}, [treesData, spanServiceColors, searchText]);

const firstSpanStartTime = tree.spanTree[0]?.startTime;

Expand Down Expand Up @@ -127,6 +145,10 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
setTreesData(spanToTreeUtil(response[0].events));
};

const setSearchTextDebounced = useDebouncedFn((...args) => {
setSearchText(args[0] as string);
}, DEBOUNCE_DELAY);

const hasMissingSpans = useMemo(
(): boolean =>
tree.missingSpanTree &&
Expand Down Expand Up @@ -225,6 +247,11 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
<Col flex={`${SPAN_DETAILS_LEFT_COL_WIDTH}px`} />
<Col flex="auto">
<StyledSpace styledclass={[styles.floatRight]}>
<Input.Search
placeholder="Filter spans"
allowClear
onChange={(e): void => setSearchTextDebounced(e.target.value)}
/>
<Button
onClick={onFocusSelectedSpanHandler}
icon={<FilterOutlined />}
Expand All @@ -243,21 +270,25 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
</Col>
</StyledRow>
<StyledDiv styledclass={[styles.ganttChartContainer]}>
<GanttChartWrapper>
{map([...tree.spanTree, ...tree.missingSpanTree], (tree) => (
<GanttChart
key={tree as never}
traceMetaData={traceMetaData}
data={tree}
activeSelectedId={activeSelectedId}
activeHoverId={activeHoverId}
setActiveHoverId={setActiveHoverId}
setActiveSelectedId={setActiveSelectedId}
spanId={spanId || ''}
intervalUnit={intervalUnit}
/>
))}
</GanttChartWrapper>
{[...tree.spanTree, ...tree.missingSpanTree].length === 0 ? (
<Empty />
) : (
<GanttChartWrapper>
{map([...tree.spanTree, ...tree.missingSpanTree], (tree) => (
<GanttChart
key={tree as never}
traceMetaData={traceMetaData}
data={tree}
activeSelectedId={activeSelectedId}
activeHoverId={activeHoverId}
setActiveHoverId={setActiveHoverId}
setActiveSelectedId={setActiveSelectedId}
spanId={spanId || ''}
intervalUnit={intervalUnit}
/>
))}
</GanttChartWrapper>
)}
</StyledDiv>
</StyledCol>

Expand Down
40 changes: 40 additions & 0 deletions frontend/src/container/TraceDetail/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export const filterSpansByString = (
});

type TTimeUnitName = 'ms' | 's' | 'm' | 'hr' | 'day' | 'week';
type FilterKeys = (keyof ITraceTree)[];

export const DEFAULT_FILTER_KEYS: FilterKeys = ['name', 'serviceName'];

export interface IIntervalUnit {
name: TTimeUnitName;
Expand Down Expand Up @@ -73,6 +76,43 @@ export const convertTimeToRelevantUnit = (
return relevantTime;
};

export const getFilteredData = (
treeData: ITraceTree,
searchValue: string,
filterKeys: FilterKeys,
): ITraceTree | null => {
if (!searchValue) {
return treeData;
}
function traverse(node: ITraceTree): ITraceTree | null {
const newNode: ITraceTree = { ...node, children: [] };

node.children.forEach((child) => {
const newChild = traverse(child);
if (newChild) {
newNode.children.push(newChild);
}
});

if (
filterKeys.some((key) => {
const val = node[key];
if (!val) return false;
return JSON.stringify(val)
.toLowerCase()
.includes(searchValue.toLowerCase());
}) ||
newNode.children.length > 0
) {
return newNode;
}

return null;
}

return traverse(treeData);
};

export const getSortedData = (treeData: ITraceTree): ITraceTree => {
const traverse = (treeNode: ITraceTree, level = 0): void => {
if (!treeNode) {
Expand Down

0 comments on commit 4fd73e8

Please sign in to comment.