diff --git a/web/src/components/Inputs/Editor/Expression/hooks/useAutoComplete.ts b/web/src/components/Inputs/Editor/Expression/hooks/useAutoComplete.ts
index 8336836d75..ea25aaebb0 100644
--- a/web/src/components/Inputs/Editor/Expression/hooks/useAutoComplete.ts
+++ b/web/src/components/Inputs/Editor/Expression/hooks/useAutoComplete.ts
@@ -1,10 +1,9 @@
import {useCallback} from 'react';
-import {noop, uniqBy} from 'lodash';
+import {noop} from 'lodash';
import {Completion, CompletionContext} from '@codemirror/autocomplete';
import {useAppStore} from 'redux/hooks';
-import AssertionSelectors from 'selectors/Assertion.selectors';
import VariableSetSelectors from 'selectors/VariableSet.selectors';
-import SpanSelectors from 'selectors/Span.selectors';
+import {selectExpressionAttributeList} from 'selectors/Editor.selectors';
import EditorService from 'services/Editor.service';
import {SupportedEditors} from 'constants/Editor.constants';
@@ -18,14 +17,10 @@ interface IProps {
const useAutoComplete = ({testId, runId, onSelect = noop, autocompleteCustomValues}: IProps) => {
const {getState} = useAppStore();
- const getAttributeList = useCallback(() => {
- const state = getState();
- const spanIdList = SpanSelectors.selectMatchedSpans(state);
- // TODO: this list is calculated multiple times while typing, we should memoize it
- const attributeList = AssertionSelectors.selectAttributeList(state, testId, runId, spanIdList);
-
- return uniqBy(attributeList, 'key');
- }, [getState, runId, testId]);
+ const getAttributeList = useCallback(
+ () => selectExpressionAttributeList(getState(), testId, runId),
+ [getState, runId, testId]
+ );
const getSelectedVariableSetEntryList = useCallback(() => {
const state = getState();
diff --git a/web/src/components/Inputs/Editor/Selector/hooks/useAutoComplete.ts b/web/src/components/Inputs/Editor/Selector/hooks/useAutoComplete.ts
index c4350a1fea..c01a679f9d 100644
--- a/web/src/components/Inputs/Editor/Selector/hooks/useAutoComplete.ts
+++ b/web/src/components/Inputs/Editor/Selector/hooks/useAutoComplete.ts
@@ -11,8 +11,8 @@ import {
Tokens,
} from 'constants/Editor.constants';
import {useAppStore} from 'redux/hooks';
-import AssertionSelectors from 'selectors/Assertion.selectors';
import {escapeString} from 'utils/Common';
+import {selectSelectorAttributeList} from 'selectors/Editor.selectors';
interface IProps {
testId: string;
@@ -22,13 +22,10 @@ interface IProps {
const useAutoComplete = ({testId, runId}: IProps) => {
const {getState} = useAppStore();
- const getAttributeList = useCallback(() => {
- const state = getState();
- // TODO: this list is calculated multiple times while typing, we should memoize it
- const defaultList = AssertionSelectors.selectAllAttributeList(state, testId, runId);
-
- return defaultList;
- }, [getState, runId, testId]);
+ const getAttributeList = useCallback(
+ () => selectSelectorAttributeList(getState(), testId, runId),
+ [getState, runId, testId]
+ );
return useCallback(
async (context: CompletionContext) => {
diff --git a/web/src/components/RunDetailTest/Visualization.tsx b/web/src/components/RunDetailTest/Visualization.tsx
index f08bdc6f23..c3ac6b3567 100644
--- a/web/src/components/RunDetailTest/Visualization.tsx
+++ b/web/src/components/RunDetailTest/Visualization.tsx
@@ -22,16 +22,15 @@ export interface IProps {
trace: Trace;
}
-const Visualization = ({isDAGDisabled, runEvents, runState, trace, trace: {spans}, type}: IProps) => {
+const Visualization = ({isDAGDisabled, runEvents, runState, trace, trace: {spans, rootSpan}, type}: IProps) => {
const {onSelectSpan, matchedSpans, onSetFocusedSpan, selectedSpan} = useSpan();
const {isOpen} = useTestSpecForm();
useEffect(() => {
if (selectedSpan) return;
- const firstSpan = spans.find(span => !span.parentId);
- onSelectSpan(firstSpan?.id ?? '');
- }, [onSelectSpan, selectedSpan, spans]);
+ onSelectSpan(rootSpan.id);
+ }, [onSelectSpan, rootSpan, selectedSpan, spans]);
const onNodeClickTimeline = useCallback(
(spanId: string) => {
diff --git a/web/src/components/RunDetailTrace/Search.tsx b/web/src/components/RunDetailTrace/Search.tsx
index 0f33e2cb83..5e04dfd2a6 100644
--- a/web/src/components/RunDetailTrace/Search.tsx
+++ b/web/src/components/RunDetailTrace/Search.tsx
@@ -5,16 +5,13 @@ import {useCallback, useMemo, useState} from 'react';
import {Editor} from 'components/Inputs';
import {SupportedEditors} from 'constants/Editor.constants';
-import {useTestRun} from 'providers/TestRun/TestRun.provider';
import TracetestAPI from 'redux/apis/Tracetest';
import {useAppDispatch, useAppSelector} from 'redux/hooks';
import {matchSpans, selectSpan, setSearchText} from 'redux/slices/Trace.slice';
import TraceSelectors from 'selectors/Trace.selectors';
-import SpanService from 'services/Span.service';
-import EditorService from 'services/Editor.service';
import * as S from './RunDetailTrace.styled';
-const {useLazyGetSelectedSpansQuery} = TracetestAPI.instance;
+const {useGetSearchedSpansMutation} = TracetestAPI.instance;
interface IProps {
runId: number;
@@ -25,35 +22,25 @@ const Search = ({runId, testId}: IProps) => {
const [search, setSearch] = useState('');
const dispatch = useAppDispatch();
const matchedSpans = useAppSelector(TraceSelectors.selectMatchedSpans);
- const {
- run: {trace: {spans = []} = {}},
- } = useTestRun();
- const [getSelectedSpans] = useLazyGetSelectedSpansQuery();
+ const [getSearchedSpans] = useGetSearchedSpansMutation();
const handleSearch = useCallback(
async (query: string) => {
- const isValidSelector = EditorService.getIsQueryValid(SupportedEditors.Selector, query || '');
if (!query) {
dispatch(matchSpans({spanIds: []}));
dispatch(selectSpan({spanId: ''}));
return;
}
- let spanIds = [];
- if (isValidSelector) {
- const selectedSpansData = await getSelectedSpans({query, runId, testId}).unwrap();
- spanIds = selectedSpansData.spanIds;
- } else {
- dispatch(setSearchText({searchText: query}));
- spanIds = SpanService.searchSpanList(spans, query);
- }
-
+ const {spanIds} = await getSearchedSpans({query, runId, testId}).unwrap();
+ dispatch(setSearchText({searchText: query}));
dispatch(matchSpans({spanIds}));
+
if (spanIds.length) {
dispatch(selectSpan({spanId: spanIds[0]}));
}
},
- [dispatch, getSelectedSpans, runId, spans, testId]
+ [dispatch, getSearchedSpans, runId, testId]
);
const onSearch = useMemo(() => debounce(handleSearch, 500), [handleSearch]);
@@ -67,7 +54,7 @@ const Search = ({runId, testId}: IProps) => {
{
onSearch(query);
setSearch(query);
diff --git a/web/src/components/RunDetailTrace/TraceDAG.tsx b/web/src/components/RunDetailTrace/TraceDAG.tsx
index cbfedce81b..be414bc6e9 100644
--- a/web/src/components/RunDetailTrace/TraceDAG.tsx
+++ b/web/src/components/RunDetailTrace/TraceDAG.tsx
@@ -21,7 +21,6 @@ const TraceDAG = ({trace: {spans}, onNavigateToSpan}: IProps) => {
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]);
diff --git a/web/src/models/DAG.model.ts b/web/src/models/DAG.model.ts
index 80744a0d6f..d1d4ea3598 100644
--- a/web/src/models/DAG.model.ts
+++ b/web/src/models/DAG.model.ts
@@ -13,7 +13,6 @@ function getNodesDatumFromSpans(spans: Span[], type: NodeTypesEnum): INodeDatum<
}
function DAG(spans: Span[], type: NodeTypesEnum) {
- // TODO: this runs twice for the list of spans
const nodesDatum = getNodesDatumFromSpans(spans, type).sort((a, b) => {
if (b.data.startTime !== a.data.startTime) return b.data.startTime - a.data.startTime;
if (b.id < a.id) return -1;
diff --git a/web/src/models/SearchSpansResult.model.ts b/web/src/models/SearchSpansResult.model.ts
new file mode 100644
index 0000000000..affc074b76
--- /dev/null
+++ b/web/src/models/SearchSpansResult.model.ts
@@ -0,0 +1,22 @@
+import {Model, TTestSchemas} from '../types/Common.types';
+
+export type TRawSearchSpansResult = TTestSchemas['SearchSpansResult'];
+type SearchSpansResult = Model<
+ TRawSearchSpansResult,
+ {
+ spanIds: string[];
+ spansIds?: undefined;
+ }
+>;
+
+const defaultSearchSpansResult: TRawSearchSpansResult = {
+ spansIds: [],
+};
+
+function SearchSpansResult({spansIds = []} = defaultSearchSpansResult): SearchSpansResult {
+ return {
+ spanIds: spansIds,
+ };
+}
+
+export default SearchSpansResult;
diff --git a/web/src/redux/apis/Tracetest/endpoints/TestRun.endpoint.ts b/web/src/redux/apis/Tracetest/endpoints/TestRun.endpoint.ts
index e19a9ff977..61621b1f0b 100644
--- a/web/src/redux/apis/Tracetest/endpoints/TestRun.endpoint.ts
+++ b/web/src/redux/apis/Tracetest/endpoints/TestRun.endpoint.ts
@@ -8,6 +8,7 @@ import SelectedSpans, {TRawSelectedSpans} from 'models/SelectedSpans.model';
import Test from 'models/Test.model';
import TestRun, {TRawTestRun} from 'models/TestRun.model';
import TestRunEvent, {TRawTestRunEvent} from 'models/TestRunEvent.model';
+import SearchSpansResult, {TRawSearchSpansResult} from 'models/SearchSpansResult.model';
import {KnownSources} from 'models/RunMetadata.model';
import {TRawTestSpecs} from 'models/TestSpecs.model';
import {TTestApiEndpointBuilder} from '../Tracetest.api';
@@ -113,6 +114,14 @@ export const testRunEndpoints = (builder: TTestApiEndpointBuilder) => ({
providesTags: (result, error, {query}) => (result ? [{type: TracetestApiTags.SPAN, id: `${query}-LIST`}] : []),
transformResponse: (rawSpanList: TRawSelectedSpans) => SelectedSpans(rawSpanList),
}),
+ getSearchedSpans: builder.mutation({
+ query: ({query, testId, runId}) => ({
+ url: `/tests/${testId}/run/${runId}/search-spans`,
+ method: HTTP_METHOD.POST,
+ body: JSON.stringify({query}),
+ }),
+ transformResponse: (raw: TRawSearchSpansResult) => SearchSpansResult(raw),
+ }),
getRunEvents: builder.query({
query: ({runId, testId}) => `/tests/${testId}/run/${runId}/events`,
diff --git a/web/src/redux/apis/Tracetest/index.ts b/web/src/redux/apis/Tracetest/index.ts
index f81596602d..c0c7935bab 100644
--- a/web/src/redux/apis/Tracetest/index.ts
+++ b/web/src/redux/apis/Tracetest/index.ts
@@ -68,6 +68,7 @@ const {
useLazyTestOtlpConnectionQuery,
useTestOtlpConnectionQuery,
useResetTestOtlpConnectionMutation,
+ useGetSearchedSpansMutation,
endpoints,
} = TracetestAPI.instance;
@@ -129,5 +130,6 @@ export {
useLazyTestOtlpConnectionQuery,
useTestOtlpConnectionQuery,
useResetTestOtlpConnectionMutation,
+ useGetSearchedSpansMutation,
endpoints,
};
diff --git a/web/src/selectors/Assertion.selectors.ts b/web/src/selectors/Assertion.selectors.ts
index 7351d3a2c8..8635b74055 100644
--- a/web/src/selectors/Assertion.selectors.ts
+++ b/web/src/selectors/Assertion.selectors.ts
@@ -29,7 +29,7 @@ const selectMatchedSpanList = createSelector(stateSelector, paramsSelector, (sta
const {data: {trace} = {}} = TracetestAPI.instance.endpoints.getRunById.select({testId, runId})(state);
if (!spanIdList.length) return trace?.spans || [];
- return trace?.spans.filter(({id}) => spanIdList.includes(id)) || [];
+ return spanIdList.map((spanId) => trace!.flat[spanId]);
});
const AssertionSelectors = () => {
diff --git a/web/src/selectors/Editor.selectors.ts b/web/src/selectors/Editor.selectors.ts
new file mode 100644
index 0000000000..9f511e96c2
--- /dev/null
+++ b/web/src/selectors/Editor.selectors.ts
@@ -0,0 +1,26 @@
+import {uniqBy} from 'lodash';
+import {createSelector} from '@reduxjs/toolkit';
+import {RootState} from 'redux/store';
+import AssertionSelectors from './Assertion.selectors';
+import SpanSelectors from './Span.selectors';
+
+const stateSelector = (state: RootState) => state;
+const paramsSelector = (state: RootState, testId: string, runId: number) => ({
+ testId,
+ runId,
+});
+
+export const selectSelectorAttributeList = createSelector(stateSelector, paramsSelector, (state, {testId, runId}) =>
+ AssertionSelectors.selectAllAttributeList(state, testId, runId)
+);
+
+export const selectExpressionAttributeList = createSelector(
+ stateSelector,
+ paramsSelector,
+ SpanSelectors.selectMatchedSpans,
+ (state, {testId, runId}, spanIds) => {
+ const attributeList = AssertionSelectors.selectAttributeList(state, testId, runId, spanIds);
+
+ return uniqBy(attributeList, 'key');
+ }
+);
diff --git a/web/src/selectors/Span.selectors.ts b/web/src/selectors/Span.selectors.ts
index 4709fda53f..9e6ecc8cc0 100644
--- a/web/src/selectors/Span.selectors.ts
+++ b/web/src/selectors/Span.selectors.ts
@@ -22,11 +22,8 @@ const SpanSelectors = () => ({
selectMatchedSpans,
selectSpanById: createSelector(stateSelector, paramsSelector, (state, {spanId, testId, runId}) => {
const {data: {trace} = {}} = TracetestAPI.instance.endpoints.getRunById.select({testId, runId})(state);
-
- // TODO: look for a simpler way of getting the span by id
- const spanList = trace?.spans || [];
- return spanList.find(span => span.id === spanId);
+ return trace?.flat[spanId];
}),
selectSelectedSpan: createSelector(spansStateSelector, ({selectedSpan}) => selectedSpan),
selectFocusedSpan: createSelector(spansStateSelector, ({focusedSpan}) => focusedSpan),
diff --git a/web/src/selectors/TestRun.selectors.ts b/web/src/selectors/TestRun.selectors.ts
index 307fd8dd68..9638ed067f 100644
--- a/web/src/selectors/TestRun.selectors.ts
+++ b/web/src/selectors/TestRun.selectors.ts
@@ -13,7 +13,6 @@ const selectTestRun = (state: RootState, params: {testId: string; runId: number;
return data ?? TestRun({});
};
-// 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.flat[params.spanId] || Span({id: params.spanId});