diff --git a/web/src/components/AnalyzerResult/Rule.tsx b/web/src/components/AnalyzerResult/Rule.tsx
index 2b93effdfd..731c09c4e2 100644
--- a/web/src/components/AnalyzerResult/Rule.tsx
+++ b/web/src/components/AnalyzerResult/Rule.tsx
@@ -3,7 +3,6 @@ import {CaretUpFilled} from '@ant-design/icons';
import {Space, Tooltip, Typography} from 'antd';
import {LinterResultPluginRule} from 'models/LinterResult.model';
import Trace from 'models/Trace.model';
-import Span from 'models/Span.model';
import {LinterRuleErrorLevel} from 'models/Linter.model';
import {useAppDispatch} from 'redux/hooks';
import {selectSpan} from 'redux/slices/Trace.slice';
@@ -17,12 +16,6 @@ interface IProps {
trace: Trace;
}
-function getSpanName(spans: Span[], spanId: string) {
- // TODO: find an easier way to get the span name
- const span = spans.find(s => s.id === spanId);
- return span?.name ?? '';
-}
-
const Rule = ({
rule: {id, tips, passed, description, name, errorDescription, results = [], level, weight = 0},
trace,
@@ -69,7 +62,7 @@ const Rule = ({
type="link"
$error={!result.passed}
>
- {getSpanName(trace.spans, result.spanId)}
+ {trace.flat[result.spanId].name ?? ''}
{!result.passed && result.errors.length > 1 && (
diff --git a/web/src/components/LoadingSpinner/LoadingSpinner.styled.ts b/web/src/components/LoadingSpinner/LoadingSpinner.styled.ts
new file mode 100644
index 0000000000..04c78890e0
--- /dev/null
+++ b/web/src/components/LoadingSpinner/LoadingSpinner.styled.ts
@@ -0,0 +1,8 @@
+import styled from 'styled-components';
+
+export const SpinnerContainer = styled.div`
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+`;
diff --git a/web/src/components/LoadingSpinner/index.ts b/web/src/components/LoadingSpinner/index.ts
index 6e7f1055ec..b2e5482e70 100644
--- a/web/src/components/LoadingSpinner/index.ts
+++ b/web/src/components/LoadingSpinner/index.ts
@@ -1,2 +1,6 @@
+import {SpinnerContainer} from './LoadingSpinner.styled';
+
+export {SpinnerContainer};
+
// eslint-disable-next-line no-restricted-exports
export {default} from './LoadingSpinner';
diff --git a/web/src/components/RunDetailTest/TestDAG.tsx b/web/src/components/RunDetailTest/TestDAG.tsx
new file mode 100644
index 0000000000..3597fe4893
--- /dev/null
+++ b/web/src/components/RunDetailTest/TestDAG.tsx
@@ -0,0 +1,62 @@
+import {useCallback, useEffect} from 'react';
+import {Node, NodeChange} from 'react-flow-renderer';
+
+import DAG from 'components/Visualization/components/DAG';
+import {useSpan} from 'providers/Span/Span.provider';
+import {useAppDispatch, useAppSelector} from 'redux/hooks';
+import {initNodes, onNodesChange as onNodesChangeAction} from 'redux/slices/DAG.slice';
+import DAGSelectors from 'selectors/DAG.selectors';
+import TraceDiagramAnalyticsService from 'services/Analytics/TraceDiagramAnalytics.service';
+import Trace from 'models/Trace.model';
+import {useTestSpecForm} from '../TestSpecForm/TestSpecForm.provider';
+import LoadingSpinner, {SpinnerContainer} from '../LoadingSpinner';
+
+export interface IProps {
+ trace: Trace;
+ onNavigateToSpan(spanId: string): void;
+}
+
+const TestDAG = ({trace: {spans}, onNavigateToSpan}: IProps) => {
+ const dispatch = useAppDispatch();
+ const edges = useAppSelector(DAGSelectors.selectEdges);
+ const nodes = useAppSelector(DAGSelectors.selectNodes);
+ const {onSelectSpan, matchedSpans, focusedSpan} = useSpan();
+ const {isOpen} = useTestSpecForm();
+
+ useEffect(() => {
+ dispatch(initNodes({spans}));
+ }, [dispatch, spans]);
+
+ const onNodesChange = useCallback((changes: NodeChange[]) => dispatch(onNodesChangeAction({changes})), [dispatch]);
+
+ const onNodeClick = useCallback(
+ (event, {id}: Node) => {
+ TraceDiagramAnalyticsService.onClickSpan(id);
+ onSelectSpan(id);
+ },
+ [onSelectSpan]
+ );
+
+ if (spans.length && !nodes.length) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+ 0 || isOpen}
+ matchedSpans={matchedSpans}
+ nodes={nodes}
+ onNavigateToSpan={onNavigateToSpan}
+ onNodesChange={onNodesChange}
+ onNodeClick={onNodeClick}
+ selectedSpan={focusedSpan}
+ />
+ );
+};
+
+export default TestDAG;
diff --git a/web/src/components/RunDetailTest/TestPanel.tsx b/web/src/components/RunDetailTest/TestPanel.tsx
index 4dac39af6d..b1bf882227 100644
--- a/web/src/components/RunDetailTest/TestPanel.tsx
+++ b/web/src/components/RunDetailTest/TestPanel.tsx
@@ -131,7 +131,7 @@ const TestPanel = ({run, testId, runEvents}: IProps) => {
isDAGDisabled={isDAGDisabled}
runEvents={runEvents}
runState={run.state}
- spans={run?.trace?.spans ?? []}
+ trace={run.trace}
type={visualizationType}
/>
diff --git a/web/src/components/RunDetailTest/Visualization.tsx b/web/src/components/RunDetailTest/Visualization.tsx
index 21b6ba2f6e..f08bdc6f23 100644
--- a/web/src/components/RunDetailTest/Visualization.tsx
+++ b/web/src/components/RunDetailTest/Visualization.tsx
@@ -1,61 +1,38 @@
import {useCallback, useEffect} from 'react';
-import {Node, NodeChange} from 'react-flow-renderer';
import {VisualizationType} from 'components/RunDetailTrace/RunDetailTrace';
import RunEvents from 'components/RunEvents';
import {useTestSpecForm} from 'components/TestSpecForm/TestSpecForm.provider';
-import DAG from 'components/Visualization/components/DAG';
import Timeline from 'components/Visualization/components/Timeline';
import {TestRunStage} from 'constants/TestRunEvents.constants';
import {NodeTypesEnum} from 'constants/Visualization.constants';
-import Span from 'models/Span.model';
import TestRunEvent from 'models/TestRunEvent.model';
import {useSpan} from 'providers/Span/Span.provider';
-import {useAppDispatch, useAppSelector} from 'redux/hooks';
-import {initNodes, onNodesChange as onNodesChangeAction} from 'redux/slices/DAG.slice';
-import DAGSelectors from 'selectors/DAG.selectors';
import TraceAnalyticsService from 'services/Analytics/TestRunAnalytics.service';
-import TraceDiagramAnalyticsService from 'services/Analytics/TraceDiagramAnalytics.service';
+import Trace from 'models/Trace.model';
import TestRunService from 'services/TestRun.service';
import {TTestRunState} from 'types/TestRun.types';
+import TestDAG from './TestDAG';
export interface IProps {
isDAGDisabled: boolean;
runEvents: TestRunEvent[];
runState: TTestRunState;
- spans: Span[];
type: VisualizationType;
+ trace: Trace;
}
-const Visualization = ({isDAGDisabled, runEvents, runState, spans, type}: IProps) => {
- const dispatch = useAppDispatch();
- const edges = useAppSelector(DAGSelectors.selectEdges);
- const nodes = useAppSelector(DAGSelectors.selectNodes);
- const {onSelectSpan, matchedSpans, onSetFocusedSpan, focusedSpan, selectedSpan} = useSpan();
+const Visualization = ({isDAGDisabled, runEvents, runState, trace, trace: {spans}, type}: IProps) => {
+ const {onSelectSpan, matchedSpans, onSetFocusedSpan, selectedSpan} = useSpan();
const {isOpen} = useTestSpecForm();
- useEffect(() => {
- if (isDAGDisabled) return;
- dispatch(initNodes({spans}));
- }, [dispatch, isDAGDisabled, spans]);
-
useEffect(() => {
if (selectedSpan) return;
const firstSpan = spans.find(span => !span.parentId);
onSelectSpan(firstSpan?.id ?? '');
}, [onSelectSpan, selectedSpan, spans]);
- const onNodesChange = useCallback((changes: NodeChange[]) => dispatch(onNodesChangeAction({changes})), [dispatch]);
-
- const onNodeClick = useCallback(
- (event, {id}: Node) => {
- TraceDiagramAnalyticsService.onClickSpan(id);
- onSelectSpan(id);
- },
- [onSelectSpan]
- );
-
const onNodeClickTimeline = useCallback(
(spanId: string) => {
TraceAnalyticsService.onTimelineSpanClick(spanId);
@@ -76,17 +53,8 @@ const Visualization = ({isDAGDisabled, runEvents, runState, spans, type}: IProps
return ;
}
- return type === VisualizationType.Dag ? (
- 0 || isOpen}
- matchedSpans={matchedSpans}
- nodes={nodes}
- onNavigateToSpan={onNavigateToSpan}
- onNodesChange={onNodesChange}
- onNodeClick={onNodeClick}
- selectedSpan={focusedSpan}
- />
+ return type === VisualizationType.Dag && !isDAGDisabled ? (
+
) : (
0 || isOpen}
diff --git a/web/src/components/RunDetailTrace/TraceDAG.tsx b/web/src/components/RunDetailTrace/TraceDAG.tsx
new file mode 100644
index 0000000000..cbfedce81b
--- /dev/null
+++ b/web/src/components/RunDetailTrace/TraceDAG.tsx
@@ -0,0 +1,62 @@
+import {useAppDispatch, useAppSelector} from 'redux/hooks';
+import TraceSelectors from 'selectors/Trace.selectors';
+import {Node, NodeChange} from 'react-flow-renderer';
+import {changeNodes, initNodes, selectSpan} from 'redux/slices/Trace.slice';
+import TraceDiagramAnalyticsService from 'services/Analytics/TraceDiagramAnalytics.service';
+import {useCallback, useEffect} from 'react';
+import Trace from 'models/Trace.model';
+import DAG from '../Visualization/components/DAG';
+import LoadingSpinner, {SpinnerContainer} from '../LoadingSpinner';
+
+interface IProps {
+ trace: Trace;
+ onNavigateToSpan(spanId: string): void;
+}
+
+const TraceDAG = ({trace: {spans}, onNavigateToSpan}: IProps) => {
+ const matchedSpans = useAppSelector(TraceSelectors.selectMatchedSpans);
+ const selectedSpan = useAppSelector(TraceSelectors.selectSelectedSpan);
+ const nodes = useAppSelector(TraceSelectors.selectNodes);
+ const edges = useAppSelector(TraceSelectors.selectEdges);
+ const isMatchedMode = Boolean(matchedSpans.length);
+ const dispatch = useAppDispatch();
+
+ // TODO: Trace will never change, we can calculate this once and then keep using it
+ useEffect(() => {
+ dispatch(initNodes({spans}));
+ }, [dispatch, spans]);
+
+ const onNodesChange = useCallback((changes: NodeChange[]) => dispatch(changeNodes({changes})), [dispatch]);
+
+ const onNodeClick = useCallback(
+ (event: React.MouseEvent, {id}: Node) => {
+ event.stopPropagation();
+ TraceDiagramAnalyticsService.onClickSpan(id);
+ dispatch(selectSpan({spanId: id}));
+ },
+ [dispatch]
+ );
+
+ if (spans.length && !nodes.length) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+export default TraceDAG;
diff --git a/web/src/components/RunDetailTrace/TracePanel.tsx b/web/src/components/RunDetailTrace/TracePanel.tsx
index a12b1708d7..ed958e3f09 100644
--- a/web/src/components/RunDetailTrace/TracePanel.tsx
+++ b/web/src/components/RunDetailTrace/TracePanel.tsx
@@ -51,7 +51,7 @@ const TracePanel = ({run, testId, runEvents, skipTraceCollection}: TProps) => {
isDAGDisabled={isDAGDisabled}
runEvents={runEvents}
runState={run.state}
- spans={run?.trace?.spans ?? []}
+ trace={run.trace}
type={visualizationType}
/>
diff --git a/web/src/components/RunDetailTrace/Visualization.tsx b/web/src/components/RunDetailTrace/Visualization.tsx
index 864ad3381e..7c5ca645f5 100644
--- a/web/src/components/RunDetailTrace/Visualization.tsx
+++ b/web/src/components/RunDetailTrace/Visualization.tsx
@@ -3,59 +3,36 @@ import {TestRunStage} from 'constants/TestRunEvents.constants';
import {NodeTypesEnum} from 'constants/Visualization.constants';
import TestRunEvent from 'models/TestRunEvent.model';
import {useCallback, useEffect} from 'react';
-import {Node, NodeChange} from 'react-flow-renderer';
import {useAppDispatch, useAppSelector} from 'redux/hooks';
-import {changeNodes, initNodes, selectSpan} from 'redux/slices/Trace.slice';
+import {selectSpan} from 'redux/slices/Trace.slice';
import TraceSelectors from 'selectors/Trace.selectors';
import TraceAnalyticsService from 'services/Analytics/TestRunAnalytics.service';
-import TraceDiagramAnalyticsService from 'services/Analytics/TraceDiagramAnalytics.service';
import TestRunService from 'services/TestRun.service';
+import Trace from 'models/Trace.model';
import {TTestRunState} from 'types/TestRun.types';
-import Span from 'models/Span.model';
-import DAG from '../Visualization/components/DAG';
import Timeline from '../Visualization/components/Timeline';
import {VisualizationType} from './RunDetailTrace';
+import TraceDAG from './TraceDAG';
interface IProps {
isDAGDisabled: boolean;
runEvents: TestRunEvent[];
runState: TTestRunState;
- spans: Span[];
+ trace: Trace;
type: VisualizationType;
}
-const Visualization = ({isDAGDisabled, runEvents, runState, spans, type}: IProps) => {
+const Visualization = ({isDAGDisabled, runEvents, runState, trace, trace: {spans, rootSpan}, type}: IProps) => {
const dispatch = useAppDispatch();
- const edges = useAppSelector(TraceSelectors.selectEdges);
const matchedSpans = useAppSelector(TraceSelectors.selectMatchedSpans);
- const nodes = useAppSelector(TraceSelectors.selectNodes);
const selectedSpan = useAppSelector(TraceSelectors.selectSelectedSpan);
const isMatchedMode = Boolean(matchedSpans.length);
- // TODO: Trace will never change, we can calculate this once and then keep using it
- useEffect(() => {
- if (isDAGDisabled) return;
- dispatch(initNodes({spans}));
- }, [dispatch, isDAGDisabled, spans]);
-
useEffect(() => {
if (selectedSpan) return;
- // TODO: Find an easier way to get the first span
- const firstSpan = spans.find(span => !span.parentId);
- dispatch(selectSpan({spanId: firstSpan?.id ?? ''}));
- }, [dispatch, selectedSpan, spans]);
-
- const onNodesChange = useCallback((changes: NodeChange[]) => dispatch(changeNodes({changes})), [dispatch]);
-
- const onNodeClick = useCallback(
- (event: React.MouseEvent, {id}: Node) => {
- event.stopPropagation();
- TraceDiagramAnalyticsService.onClickSpan(id);
- dispatch(selectSpan({spanId: id}));
- },
- [dispatch]
- );
+ dispatch(selectSpan({spanId: rootSpan.id ?? ''}));
+ }, [dispatch, rootSpan.id, selectedSpan, spans]);
const onNodeClickTimeline = useCallback(
(spanId: string) => {
@@ -76,17 +53,8 @@ const Visualization = ({isDAGDisabled, runEvents, runState, spans, type}: IProps
return ;
}
- return type === VisualizationType.Dag ? (
-
+ return type === VisualizationType.Dag && !isDAGDisabled ? (
+
) : (
nodes.length >= MAX_DAG_NODES && onNavigateToSpan(nodes[0]?.id)}
onNodeClick={onNodeClick}
onNodeDragStop={onNodeClick}
onNodesChange={onNodesChange}
+ onlyRenderVisibleElements
selectionKeyCode={null}
+ fitView={nodes.length <= MAX_DAG_NODES}
>
{isMiniMapActive && }
diff --git a/web/src/components/Visualization/components/DAG/TestSpanNode/TestSpanNode.tsx b/web/src/components/Visualization/components/DAG/TestSpanNode/TestSpanNode.tsx
index cf57b0ada0..8dccceee61 100644
--- a/web/src/components/Visualization/components/DAG/TestSpanNode/TestSpanNode.tsx
+++ b/web/src/components/Visualization/components/DAG/TestSpanNode/TestSpanNode.tsx
@@ -9,7 +9,7 @@ import useSelectAsCurrent from '../../../hooks/useSelectAsCurrent';
interface IProps extends NodeProps {}
-const TestSpanNode = ({data, id, selected}: IProps) => {
+const TestSpanNode = ({data, id, selected, ...props}: IProps) => {
const {span, testSpecs, testOutputs} = useSpanData(id);
const {isLoading, onSelectAsCurrent, showSelectAsCurrent} = useSelectAsCurrent({
selected,
diff --git a/web/src/models/DAG.model.ts b/web/src/models/DAG.model.ts
index afbb69eedf..80744a0d6f 100644
--- a/web/src/models/DAG.model.ts
+++ b/web/src/models/DAG.model.ts
@@ -20,7 +20,10 @@ function DAG(spans: Span[], type: NodeTypesEnum) {
if (b.id > a.id) return 1;
return 0;
});
+
return DAGService.getEdgesAndNodes(nodesDatum);
}
+export const getShouldShowDAG = (spanCount: number): boolean => spanCount <= 200;
+
export default DAG;
diff --git a/web/src/models/Span.model.ts b/web/src/models/Span.model.ts
index 37f9c4cbae..580f156b82 100644
--- a/web/src/models/Span.model.ts
+++ b/web/src/models/Span.model.ts
@@ -48,7 +48,16 @@ const getSpanSignature = (
}, []);
};
-const Span = ({id = '', attributes = {}, startTime = 0, endTime = 0, parentId = '', name = ''}: TRawSpan): Span => {
+const defaultSpan: TRawSpan = {
+ id: '',
+ parentId: '',
+ name: '',
+ attributes: {},
+ startTime: 0,
+ endTime: 0,
+};
+
+const Span = ({id = '', attributes = {}, startTime = 0, endTime = 0, parentId = '', name = ''} = defaultSpan): Span => {
const mappedAttributeList: TSpanFlatAttribute[] = [{key: 'name', value: name}];
const attributeList = Object.entries(attributes)
.map(([key, value]) => ({
diff --git a/web/src/models/TestRun.model.ts b/web/src/models/TestRun.model.ts
index 4e39fedef8..e4dbcc3678 100644
--- a/web/src/models/TestRun.model.ts
+++ b/web/src/models/TestRun.model.ts
@@ -20,7 +20,7 @@ type TestRun = Model<
TRawTestRun,
{
result: AssertionResults;
- trace?: Trace;
+ trace: Trace;
totalAssertionCount: number;
failedAssertionCount: number;
passedAssertionCount: number;
@@ -138,7 +138,7 @@ const TestRun = ({
spanId,
state,
testVersion,
- trace: trace ? Trace(trace) : undefined,
+ trace: trace ? Trace(trace) : Trace(),
totalAssertionCount: getTestResultCount(result),
failedAssertionCount: getTestResultCount(result, 'failed'),
passedAssertionCount: getTestResultCount(result, 'passed'),
diff --git a/web/src/models/Trace.model.ts b/web/src/models/Trace.model.ts
index 45bcc80f28..d89eb3800e 100644
--- a/web/src/models/Trace.model.ts
+++ b/web/src/models/Trace.model.ts
@@ -1,18 +1,32 @@
-import { TTraceSchemas } from 'types/Common.types';
+import {TTraceSchemas} from 'types/Common.types';
import Span from './Span.model';
export type TRawTrace = TTraceSchemas['Trace'];
+export type TSpanMap = Record;
type Trace = {
+ flat: TSpanMap;
spans: Span[];
traceId: string;
+ rootSpan: Span;
};
-// TODO: keep the flat map of spans for easy access
-const Trace = ({traceId = '', flat = {}}: TRawTrace): Trace => {
- return {
- traceId,
- spans: Object.values(flat).map(rawSpan => Span(rawSpan)),
- };
+const defaultTrace: TRawTrace = {
+ traceId: '',
+ flat: {},
+ tree: {},
};
+const Trace = ({traceId = '', flat = {}, tree = {}} = defaultTrace): Trace => ({
+ traceId,
+ rootSpan: Span(tree),
+ flat: Object.values(flat).reduce(
+ (acc, span) => ({
+ ...acc,
+ [span.id || '']: Span(span),
+ }),
+ {}
+ ),
+ spans: Object.values(flat).map(rawSpan => Span(rawSpan)),
+});
+
export default Trace;
diff --git a/web/src/models/__tests__/TestRun.model.test.ts b/web/src/models/__tests__/TestRun.model.test.ts
index 91c806fb08..2aa1012335 100644
--- a/web/src/models/__tests__/TestRun.model.test.ts
+++ b/web/src/models/__tests__/TestRun.model.test.ts
@@ -7,7 +7,6 @@ describe('Test Run', () => {
const testRunResult = TestRun(rawTestRunResult);
expect(testRunResult.id).toEqual(rawTestRunResult.id);
- expect(testRunResult.trace).not.toEqual(undefined);
expect(testRunResult.totalAssertionCount).toEqual(0);
expect(testRunResult.passedAssertionCount).toEqual(0);
expect(testRunResult.failedAssertionCount).toEqual(0);
@@ -21,7 +20,6 @@ describe('Test Run', () => {
const testRunResult = TestRun(rawTestRunResult);
- expect(testRunResult.trace).toEqual(undefined);
expect(testRunResult.executionTime).toEqual(0);
});
});
diff --git a/web/src/providers/TestRun/TestRun.provider.tsx b/web/src/providers/TestRun/TestRun.provider.tsx
index 57a378658d..169d8d5801 100644
--- a/web/src/providers/TestRun/TestRun.provider.tsx
+++ b/web/src/providers/TestRun/TestRun.provider.tsx
@@ -5,6 +5,7 @@ import TestRun, {isRunStateFinished} from 'models/TestRun.model';
import TestRunEvent from 'models/TestRunEvent.model';
import TracetestAPI from 'redux/apis/Tracetest';
import TestProvider from '../Test';
+import LoadingSpinner, { SpinnerContainer } from '../../components/LoadingSpinner';
const {useGetRunByIdQuery, useGetRunEventsQuery, useStopRunMutation, useSkipPollingMutation} = TracetestAPI.instance;
@@ -76,7 +77,9 @@ const TestRunProvider = ({children, testId, runId = 0}: IProps) => {
) : (
-
+
+
+
);
};
diff --git a/web/src/providers/TestSpecs/TestSpecs.provider.tsx b/web/src/providers/TestSpecs/TestSpecs.provider.tsx
index 42e8fe4420..ac4179c0e2 100644
--- a/web/src/providers/TestSpecs/TestSpecs.provider.tsx
+++ b/web/src/providers/TestSpecs/TestSpecs.provider.tsx
@@ -60,11 +60,11 @@ const TestSpecsProvider = ({children, testId, runId}: IProps) => {
const {test} = useTest();
const {run} = useTestRun();
- const assertionResults = useAppSelector(state => TestSpecsSelectors.selectAssertionResults(state));
- const specs = useAppSelector(state => TestSpecsSelectors.selectSpecs(state));
- const isDraftMode = useAppSelector(state => TestSpecsSelectors.selectIsDraftMode(state));
- const isLoading = useAppSelector(state => TestSpecsSelectors.selectIsLoading(state));
- const isInitialized = useAppSelector(state => TestSpecsSelectors.selectIsInitialized(state));
+ const assertionResults = useAppSelector(TestSpecsSelectors.selectAssertionResults);
+ const specs = useAppSelector(TestSpecsSelectors.selectSpecs);
+ const isDraftMode = useAppSelector(TestSpecsSelectors.selectIsDraftMode);
+ const isLoading = useAppSelector(TestSpecsSelectors.selectIsLoading);
+ const isInitialized = useAppSelector(TestSpecsSelectors.selectIsInitialized);
const selectedSpec = useAppSelector(TestSpecsSelectors.selectSelectedSpec);
const selectedTestSpec = useAppSelector(state => TestSpecsSelectors.selectAssertionBySelector(state, selectedSpec!));
diff --git a/web/src/redux/slices/DAG.slice.ts b/web/src/redux/slices/DAG.slice.ts
index db55e0aa55..cea4c45145 100644
--- a/web/src/redux/slices/DAG.slice.ts
+++ b/web/src/redux/slices/DAG.slice.ts
@@ -1,4 +1,4 @@
-import {createSlice, PayloadAction} from '@reduxjs/toolkit';
+import {createAsyncThunk, createSlice, PayloadAction} from '@reduxjs/toolkit';
import {applyNodeChanges, Edge, MarkerType, Node, NodeChange} from 'react-flow-renderer';
import {theme} from 'constants/Theme.constants';
@@ -23,8 +23,7 @@ const dagSlice = createSlice({
name: 'dag',
initialState,
reducers: {
- initNodes(state, {payload}: PayloadAction<{spans: Span[]}>) {
- const {edges, nodes} = DAGModel(payload.spans, NodeTypesEnum.TestSpan);
+ initNodes(state, {payload: {edges, nodes}}: PayloadAction<{edges: Edge[]; nodes: Node[]}>) {
state.edges = edges;
state.nodes = nodes;
},
@@ -78,5 +77,13 @@ const dagSlice = createSlice({
},
});
-export const {initNodes, onNodesChange} = dagSlice.actions;
+export const initNodes = createAsyncThunk(
+ 'dag/generateDagLayout',
+ async ({spans}, {dispatch}) => {
+ const {edges, nodes} = await DAGModel(spans, NodeTypesEnum.TestSpan);
+ dispatch(dagSlice.actions.initNodes({edges, nodes}));
+ }
+);
+
+export const {onNodesChange} = dagSlice.actions;
export default dagSlice.reducer;
diff --git a/web/src/redux/slices/Trace.slice.ts b/web/src/redux/slices/Trace.slice.ts
index d258bd9032..542c3f7d10 100644
--- a/web/src/redux/slices/Trace.slice.ts
+++ b/web/src/redux/slices/Trace.slice.ts
@@ -1,4 +1,4 @@
-import {createSlice, PayloadAction} from '@reduxjs/toolkit';
+import {createAsyncThunk, createSlice, PayloadAction} from '@reduxjs/toolkit';
import {applyNodeChanges, Edge, MarkerType, Node, NodeChange} from 'react-flow-renderer';
import {theme} from 'constants/Theme.constants';
@@ -26,10 +26,10 @@ const traceSlice = createSlice({
name: 'trace',
initialState,
reducers: {
- initNodes(state, {payload}: PayloadAction<{spans: Span[]}>) {
- const {edges, nodes} = DAGModel(payload.spans, NodeTypesEnum.TraceSpan);
+ initNodes(state, {payload: {edges, nodes}}: PayloadAction<{edges: Edge[]; nodes: Node[]}>) {
state.edges = edges;
state.nodes = nodes;
+
// Clear state
state.matchedSpans = [];
state.searchText = '';
@@ -70,5 +70,13 @@ const traceSlice = createSlice({
},
});
-export const {initNodes, changeNodes, selectSpan, matchSpans, setSearchText} = traceSlice.actions;
+export const initNodes = createAsyncThunk(
+ 'trace/generateDagLayout',
+ async ({spans}, {dispatch}) => {
+ const {edges, nodes} = await DAGModel(spans, NodeTypesEnum.TraceSpan);
+ dispatch(traceSlice.actions.initNodes({edges, nodes}));
+ }
+);
+
+export const {changeNodes, selectSpan, matchSpans, setSearchText} = traceSlice.actions;
export default traceSlice.reducer;
diff --git a/web/src/selectors/TestRun.selectors.ts b/web/src/selectors/TestRun.selectors.ts
index 31f8ff57f8..307fd8dd68 100644
--- a/web/src/selectors/TestRun.selectors.ts
+++ b/web/src/selectors/TestRun.selectors.ts
@@ -16,7 +16,7 @@ const selectTestRun = (state: RootState, params: {testId: string; runId: number;
// TODO: look for a simpler way of getting the span by id
export const selectSpanById = createSelector([selectTestRun, selectParams], (testRun, params) => {
const {trace} = testRun;
- return trace?.spans?.find(span => span.id === params.spanId) ?? Span({id: params.spanId});
+ return trace.flat[params.spanId] || Span({id: params.spanId});
});
const selectAnalyzerErrors = createSelector([selectTestRun], testRun => {
diff --git a/web/src/services/DAG.service.ts b/web/src/services/DAG.service.ts
index c8ec9358e6..e1fb1a7050 100644
--- a/web/src/services/DAG.service.ts
+++ b/web/src/services/DAG.service.ts
@@ -1,10 +1,11 @@
import {coordCenter, Dag, dagStratify, layeringSimplex, sugiyama} from 'd3-dag';
-import {MarkerType} from 'react-flow-renderer';
+import {Edge, MarkerType, Node} from 'react-flow-renderer';
import {theme} from 'constants/Theme.constants';
import {INodeDatum} from 'types/DAG.types';
+import {withLowPriority} from '../utils/Common';
-function getDagLayout(nodesDatum: INodeDatum[]) {
+function getDagLayout(nodesDatum: INodeDatum[]): Dag, undefined> {
const stratify = dagStratify();
const dag = stratify(nodesDatum);
@@ -18,7 +19,7 @@ function getDagLayout(nodesDatum: INodeDatum[]) {
return dag;
}
-function getNodes(dagLayout: Dag, undefined>) {
+function getNodes(dagLayout: Dag, undefined>): Node[] {
return dagLayout.descendants().map(({data: {id, data, type}, x, y}) => ({
data,
id,
@@ -27,7 +28,7 @@ function getNodes(dagLayout: Dag, undefined>) {
}));
}
-function getEdges(dagLayout: Dag, undefined>) {
+function getEdges(dagLayout: Dag, undefined>): Edge[] {
return dagLayout.links().map(({source, target}) => ({
animated: false,
id: `${source.data.id}-${target.data.id}`,
@@ -39,12 +40,12 @@ function getEdges(dagLayout: Dag, undefined>) {
}
const DAGService = () => ({
- getEdgesAndNodes(nodesDatum: INodeDatum[]) {
+ async getEdgesAndNodes(nodesDatum: INodeDatum[]): Promise<{edges: Edge[]; nodes: Node[]}> {
if (!nodesDatum.length) return {edges: [], nodes: []};
- const dagLayout = getDagLayout(nodesDatum);
- const edges = getEdges(dagLayout);
- const nodes = getNodes(dagLayout);
+ const dagLayout = await withLowPriority(getDagLayout)(nodesDatum);
+ const edges = await withLowPriority(getEdges)(dagLayout);
+ const nodes = await withLowPriority(getNodes)(dagLayout);
return {edges, nodes};
},
diff --git a/web/src/utils/Common.ts b/web/src/utils/Common.ts
index 6cdc3dab52..ef2c86c1e9 100644
--- a/web/src/utils/Common.ts
+++ b/web/src/utils/Common.ts
@@ -87,3 +87,12 @@ export const getParsedURL = (rawUrl: string): URL => {
return new URL(rawUrl);
};
+
+export const withLowPriority =
+ any>(fn: T): ((...args: Parameters) => Promise>) =>
+ (...args: Parameters): Promise> =>
+ new Promise(resolve => {
+ setTimeout(() => {
+ resolve(fn(...args));
+ }, 0);
+ });