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); + });