diff --git a/web/src/components/CurrentSpanSelector/CurrentSpanSelector.styled.ts b/web/src/components/CurrentSpanSelector/CurrentSpanSelector.styled.ts
new file mode 100644
index 0000000000..5d2b72ac86
--- /dev/null
+++ b/web/src/components/CurrentSpanSelector/CurrentSpanSelector.styled.ts
@@ -0,0 +1,16 @@
+import styled from 'styled-components';
+
+export const Container = styled.div`
+ display: flex;
+ justify-content: center;
+ margin-top: 2px;
+`;
+
+export const FloatingText = styled.div`
+ background-color: ${({theme}) => theme.color.interactive};
+ color: ${({theme}) => theme.color.white};
+ border-radius: 12px;
+ padding: 2px 6px;
+ font-size: ${({theme}) => theme.size.xs};
+ cursor: pointer;
+`;
diff --git a/web/src/components/CurrentSpanSelector/CurrentSpanSelector.tsx b/web/src/components/CurrentSpanSelector/CurrentSpanSelector.tsx
new file mode 100644
index 0000000000..468a0d417f
--- /dev/null
+++ b/web/src/components/CurrentSpanSelector/CurrentSpanSelector.tsx
@@ -0,0 +1,59 @@
+import {useCallback} from 'react';
+import {LoadingOutlined} from '@ant-design/icons';
+import SpanService from 'services/Span.service';
+import {useTest} from 'providers/Test/Test.provider';
+import {useTestRun} from 'providers/TestRun/TestRun.provider';
+import {useAppSelector} from 'redux/hooks';
+import SpanSelectors from 'selectors/Span.selectors';
+import {useSpan} from 'providers/Span/Span.provider';
+import TestOutput from 'models/TestOutput.model';
+import {useTestOutput} from 'providers/TestOutput/TestOutput.provider';
+import {useTestSpecForm} from '../TestSpecForm/TestSpecForm.provider';
+import * as S from './CurrentSpanSelector.styled';
+
+interface IProps {
+ spanId: string;
+}
+
+const CurrentSpanSelector = ({spanId}: IProps) => {
+ const {open, isOpen: isTestSpecFormOpen} = useTestSpecForm();
+ const {onOpen} = useTestOutput();
+ const {
+ run: {id: runId},
+ } = useTestRun();
+ const {
+ test: {id: testId},
+ } = useTest();
+ const {isTriggerSelectorLoading} = useSpan();
+ const span = useAppSelector(state => SpanSelectors.selectSpanById(state, spanId, testId, runId));
+
+ const handleOnClick = useCallback(() => {
+ const selector = SpanService.getSelectorInformation(span!);
+
+ if (isTestSpecFormOpen)
+ open({
+ isEditing: false,
+ selector,
+ defaultValues: {
+ selector,
+ },
+ });
+ else onOpen(TestOutput({selector: {query: selector}}));
+ }, [isTestSpecFormOpen, onOpen, open, span]);
+
+ return (
+
+ !isTriggerSelectorLoading && handleOnClick()}>
+ {isTriggerSelectorLoading ? (
+ <>
+ Updating selected span
+ >
+ ) : (
+ <>Select as current span>
+ )}
+
+
+ );
+};
+
+export default CurrentSpanSelector;
diff --git a/web/src/components/CurrentSpanSelector/index.ts b/web/src/components/CurrentSpanSelector/index.ts
new file mode 100644
index 0000000000..eb19aff230
--- /dev/null
+++ b/web/src/components/CurrentSpanSelector/index.ts
@@ -0,0 +1,2 @@
+// eslint-disable-next-line no-restricted-exports
+export {default} from './CurrentSpanSelector';
diff --git a/web/src/components/RunDetailTest/RunDetailTest.tsx b/web/src/components/RunDetailTest/RunDetailTest.tsx
index b2c467822a..9e85d8b240 100644
--- a/web/src/components/RunDetailTest/RunDetailTest.tsx
+++ b/web/src/components/RunDetailTest/RunDetailTest.tsx
@@ -40,7 +40,7 @@ const RunDetailTest = ({run, testId}: IProps) => {
const {selectedSpan, onSetFocusedSpan, onSelectSpan} = useSpan();
const {remove, revert, selectedTestSpec, setSelectedSpec, setSelectorSuggestions, setPrevSelector, specs} =
useTestSpecs();
- const {isOpen: isTestSpecFormOpen, formProps, onSubmit, open, close} = useTestSpecForm();
+ const {isOpen: isTestSpecFormOpen, formProps, onSubmit, open, close, isValid, onIsValid} = useTestSpecForm();
const {
isEditing,
isLoading,
@@ -49,6 +49,8 @@ const RunDetailTest = ({run, testId}: IProps) => {
onSubmit: onSubmitTestOutput,
output,
outputs,
+ isValid: isOutputFormValid,
+ onValidate,
} = useTestOutput();
const [visualizationType, setVisualizationType] = useState(VisualizationType.Dag);
const {isGuidedTourRunning, setGuidedTourStep} = useGuidedTour();
@@ -134,6 +136,8 @@ const RunDetailTest = ({run, testId}: IProps) => {
setPrevSelector('');
onSubmit(values);
}}
+ isValid={isValid}
+ onIsValid={onIsValid}
runId={run.id}
testId={testId}
{...formProps}
@@ -160,6 +164,8 @@ const RunDetailTest = ({run, testId}: IProps) => {
output={output}
runId={run.id}
testId={testId}
+ isValid={isOutputFormValid}
+ onValidate={onValidate}
/>
)}
diff --git a/web/src/components/TestOutputForm/TestOutputForm.tsx b/web/src/components/TestOutputForm/TestOutputForm.tsx
index b3602e0747..fb35fb2ff3 100644
--- a/web/src/components/TestOutputForm/TestOutputForm.tsx
+++ b/web/src/components/TestOutputForm/TestOutputForm.tsx
@@ -8,7 +8,6 @@ import {useAppSelector} from 'redux/hooks';
import SpanSelectors from 'selectors/Span.selectors';
import {singularOrPlural} from 'utils/Common';
import TestOutput from 'models/TestOutput.model';
-import useValidateOutput from './hooks/useValidateOutput';
import SelectorInput from './SelectorInput';
import * as S from './TestOutputForm.styled';
@@ -17,16 +16,27 @@ interface IProps {
isLoading?: boolean;
onCancel(): void;
onSubmit(values: TestOutput, spanId?: string): void;
+ onValidate(_: any, output: TestOutput): void;
+ isValid: boolean;
output?: TestOutput;
runId: string;
testId: string;
}
-const TestOutputForm = ({isEditing = false, isLoading = false, onCancel, onSubmit, output, runId, testId}: IProps) => {
+const TestOutputForm = ({
+ isEditing = false,
+ isLoading = false,
+ onCancel,
+ onSubmit,
+ output,
+ runId,
+ testId,
+ onValidate,
+ isValid,
+}: IProps) => {
const [form] = Form.useForm();
- const spanIdList = useAppSelector(SpanSelectors.selectMatchedSpans);
- const {isValid, onValidate} = useValidateOutput({spanIdList});
const selector = Form.useWatch('selector', form) || '';
+ const spanIdList = useAppSelector(SpanSelectors.selectMatchedSpans);
useEffect(() => {
if (form.getFieldValue('selector') && form.getFieldValue('value') && form.getFieldValue('name')) {
@@ -35,6 +45,18 @@ const TestOutputForm = ({isEditing = false, isLoading = false, onCancel, onSubmi
}
}, [form, onValidate]);
+ useEffect(() => {
+ if (!isEditing) {
+ form.setFieldsValue({selector: output?.selector, value: output?.value, name: output?.name});
+ }
+ }, [form, isEditing, output?.name, output?.selector, output?.value]);
+
+ useEffect(() => {
+ return () => {
+ onCancel();
+ };
+ }, []);
+
return (
{isEditing ? 'Edit Test Output' : 'Add Test Output'}
diff --git a/web/src/components/TestSpecForm/TestSpecForm.provider.tsx b/web/src/components/TestSpecForm/TestSpecForm.provider.tsx
index bfa576c6b9..c788dda3ae 100644
--- a/web/src/components/TestSpecForm/TestSpecForm.provider.tsx
+++ b/web/src/components/TestSpecForm/TestSpecForm.provider.tsx
@@ -12,8 +12,9 @@ import RouterActions from 'redux/actions/Router.actions';
import TestSpecsSelectors from 'selectors/TestSpecs.selectors';
import CreateAssertionModalAnalyticsService from 'services/Analytics/CreateAssertionModalAnalytics.service';
import AssertionService from 'services/Assertion.service';
-import { TTestSpecEntry } from 'models/TestSpecs.model';
+import {TTestSpecEntry} from 'models/TestSpecs.model';
import {IValues} from './TestSpecForm';
+import {useConfirmationModal} from '../../providers/ConfirmationModal/ConfirmationModal.provider';
interface IFormProps {
defaultValues?: IValues;
@@ -23,10 +24,12 @@ interface IFormProps {
interface IContext {
isOpen: boolean;
+ isValid: boolean;
+ formProps: IFormProps;
open(props?: IFormProps): void;
close(): void;
onSubmit(values: IValues, spanId?: string): void;
- formProps: IFormProps;
+ onIsValid(isValid: boolean): void;
}
const initialFormProps = {
@@ -35,10 +38,12 @@ const initialFormProps = {
export const Context = createContext({
isOpen: false,
+ isValid: false,
+ formProps: initialFormProps,
open: noop,
close: noop,
- formProps: initialFormProps,
onSubmit: noop,
+ onIsValid: noop,
});
export const useTestSpecForm = () => useContext(Context);
@@ -48,13 +53,15 @@ const TestSpecFormProvider: React.FC<{testId: string}> = ({children}) => {
const [isOpen, setIsOpen] = useState(false);
const [isConfirmationModalOpen, setIsConfirmationModalOpen] = useState(false);
const [formProps, setFormProps] = useState(initialFormProps);
+ const [isValid, onIsValid] = useState(false);
const {test} = useTest();
const {update, add, isDraftMode} = useTestSpecs();
const {run} = useTestRun();
const {onClearMatchedSpans} = useSpan();
+ const {onOpen} = useConfirmationModal();
const specs = useAppSelector(state => TestSpecsSelectors.selectSpecs(state));
- const open = useCallback(
+ const handleOpen = useCallback(
(props: IFormProps = {}) => {
const {isEditing, defaultValues: {assertions = [], selector: defaultSelector, name = ''} = {}} = props;
const spec = specs.find(({selector}) => defaultSelector === selector);
@@ -90,11 +97,28 @@ const TestSpecFormProvider: React.FC<{testId: string}> = ({children}) => {
[dispatch, specs, isDraftMode, run.testVersion, test?.version]
);
+ const open = useCallback(
+ (props: IFormProps) => {
+ if (isValid) {
+ onOpen({
+ title: 'Unsaved changes',
+ heading: 'Discard unsaved changes?',
+ okText: 'Discard',
+ onConfirm: () => {
+ handleOpen(props);
+ },
+ });
+ } else handleOpen(props);
+ },
+ [handleOpen, isValid, onOpen]
+ );
+
const close = useCallback(() => {
setFormProps(initialFormProps);
onClearMatchedSpans();
setIsOpen(false);
+ onIsValid(false);
}, [onClearMatchedSpans]);
const onConfirm = useCallback(() => {
@@ -130,8 +154,8 @@ const TestSpecFormProvider: React.FC<{testId: string}> = ({children}) => {
);
const contextValue = useMemo(
- () => ({isOpen, open, close, formProps, onSubmit}),
- [isOpen, open, close, formProps, onSubmit]
+ () => ({isOpen, open, close, formProps, onSubmit, isValid, onIsValid}),
+ [isOpen, open, close, formProps, onSubmit, isValid, onIsValid]
);
return (
diff --git a/web/src/components/TestSpecForm/TestSpecForm.tsx b/web/src/components/TestSpecForm/TestSpecForm.tsx
index 0834bd9d1e..95c23577b9 100644
--- a/web/src/components/TestSpecForm/TestSpecForm.tsx
+++ b/web/src/components/TestSpecForm/TestSpecForm.tsx
@@ -1,15 +1,16 @@
import {Button, Form, Input, Tag} from 'antd';
-import {useEffect, useState} from 'react';
+import {LoadingOutlined} from '@ant-design/icons';
+import {useEffect} from 'react';
import {SELECTOR_LANGUAGE_CHEAT_SHEET_URL} from 'constants/Common.constants';
import {CompareOperator} from 'constants/Operator.constants';
import {useAppSelector} from 'redux/hooks';
import AssertionSelectors from 'selectors/Assertion.selectors';
-import SpanSelectors from 'selectors/Span.selectors';
import TestSpecsSelectors from 'selectors/TestSpecs.selectors';
import OperatorService from 'services/Operator.service';
import {TStructuredAssertion} from 'types/Assertion.types';
import {singularOrPlural} from 'utils/Common';
+import {useSpan} from 'providers/Span/Span.provider';
import AssertionCheckList from './AssertionCheckList';
import useOnFieldsChange from './hooks/useOnFieldsChange';
import useOnValuesChange from './hooks/useOnValuesChange';
@@ -30,22 +31,22 @@ interface IProps {
onClearSelectorSuggestions(): void;
onClickPrevSelector(prevSelector: string): void;
onSubmit(values: IValues): void;
+ isValid: boolean;
+ onIsValid(isValid: boolean): void;
runId: string;
testId: string;
}
+const initialAssertions = [
+ {
+ left: '',
+ comparator: OperatorService.getOperatorSymbol(CompareOperator.EQUALS),
+ right: '',
+ },
+];
+
const TestSpecForm = ({
- defaultValues: {
- assertions = [
- {
- left: '',
- comparator: OperatorService.getOperatorSymbol(CompareOperator.EQUALS),
- right: '',
- },
- ],
- selector = '',
- name = '',
- } = {},
+ defaultValues: {assertions = initialAssertions, selector = '', name = ''} = {},
isEditing = false,
onCancel,
onClearSelectorSuggestions,
@@ -53,22 +54,37 @@ const TestSpecForm = ({
onSubmit,
runId,
testId,
+ isValid,
+ onIsValid,
}: IProps) => {
const [form] = Form.useForm();
- const [isValid, setIsValid] = useState(false);
- const spanIdList = useAppSelector(SpanSelectors.selectMatchedSpans);
+ const {matchedSpans: spanIdList, isTriggerSelectorLoading} = useSpan();
const attributeList = useAppSelector(state =>
AssertionSelectors.selectAttributeList(state, testId, runId, spanIdList)
);
- const onValuesChange = useOnValuesChange({setIsValid});
+ const onValuesChange = useOnValuesChange({setIsValid: onIsValid});
const onFieldsChange = useOnFieldsChange();
useEffect(() => {
onValuesChange(null, {assertions, selector, name});
+
+ return () => {
+ onCancel();
+ };
}, []);
+ useEffect(() => {
+ if (!isEditing) {
+ form.setFieldsValue({
+ selector,
+ name,
+ assertions,
+ });
+ }
+ }, [form, isEditing, name, selector]);
+
const selectorSuggestions = useAppSelector(TestSpecsSelectors.selectSelectorSuggestions);
const prevSelector = useAppSelector(TestSpecsSelectors.selectPrevSelector);
@@ -98,7 +114,13 @@ const TestSpecForm = ({
1. Select Spans
- {`${spanIdList.length} ${singularOrPlural('span', spanIdList.length)} selected`}
+
+ {isTriggerSelectorLoading ? (
+
+ ) : (
+ `${spanIdList.length} ${singularOrPlural('span', spanIdList.length)} selected`
+ )}
+
SL cheat sheet
@@ -107,7 +129,7 @@ const TestSpecForm = ({
Select the spans to which a set of assertions will be applied
-
+
{
diff --git a/web/src/components/TestSpecForm/hooks/useQuerySelector.ts b/web/src/components/TestSpecForm/hooks/useQuerySelector.ts
index 21e95cefd3..eda03c7dd1 100644
--- a/web/src/components/TestSpecForm/hooks/useQuerySelector.ts
+++ b/web/src/components/TestSpecForm/hooks/useQuerySelector.ts
@@ -5,7 +5,6 @@ import {useEffect, useMemo, useState} from 'react';
import {SupportedEditors} from 'constants/Editor.constants';
import {useSpan} from 'providers/Span/Span.provider';
import {useTestSpecs} from 'providers/TestSpecs/TestSpecs.provider';
-import {useLazyGetSelectedSpansQuery} from 'redux/apis/TraceTest.api';
import {useAppSelector} from 'redux/hooks';
import SpanSelectors from 'selectors/Span.selectors';
import EditorService from 'services/Editor.service';
@@ -28,12 +27,12 @@ interface IProps {
}
const useQuerySelector = ({form, runId, testId, onValidSelector}: IProps) => {
- const {onSetMatchedSpans, onClearMatchedSpans, selectedSpan} = useSpan();
+ const {onClearMatchedSpans, selectedSpan, onTriggerSelector, triggerSelectorResult, isTriggerSelectorError} =
+ useSpan();
const {setSelectorSuggestions} = useTestSpecs();
const [isLoading, setIsLoading] = useState(true);
const {currentSelector} = useAssertionFormValues(form);
- const [onTriggerSelectedSpans, {data: selectedSpansData, isError}] = useLazyGetSelectedSpansQuery();
- const [isValid, setIsValid] = useState(!isError);
+ const [isValid, setIsValid] = useState(!isTriggerSelectorError);
const selectedParentSpan = useAppSelector(state =>
SpanSelectors.selectSpanById(state, selectedSpan?.parentId ?? '', testId, runId)
);
@@ -45,18 +44,11 @@ const useQuerySelector = ({form, runId, testId, onValidSelector}: IProps) => {
setIsValid(isValidSelector);
- if (isValidSelector) {
- const data = await onTriggerSelectedSpans({
- query: q,
- testId: tId,
- runId: rId,
- }).unwrap();
- onSetMatchedSpans(data.spanIds);
- }
+ if (isValidSelector) await onTriggerSelector(q, tId, rId);
setIsLoading(false);
}, 500),
- [onSetMatchedSpans, onTriggerSelectedSpans]
+ [onTriggerSelector]
);
useEffect(() => {
@@ -65,21 +57,21 @@ const useQuerySelector = ({form, runId, testId, onValidSelector}: IProps) => {
}, [handleSelector, currentSelector, runId, testId]);
useEffect(() => {
- if (!selectedSpansData) return;
+ if (!triggerSelectorResult) return;
const selectedSpanId = selectedSpan?.id ?? '';
const selectedSpanSelector = SpanService.getSelectorInformation(selectedSpan!);
const selectedParentSpanSelector = selectedParentSpan ? SpanService.getSelectorInformation(selectedParentSpan) : '';
const selectorSuggestions = SelectorSuggestionsService.getSuggestions(
- selectedSpansData.selector,
- selectedSpansData.spanIds,
+ triggerSelectorResult.selector,
+ triggerSelectorResult.spanIds,
selectedSpanId,
selectedSpanSelector,
selectedParentSpanSelector
);
setSelectorSuggestions(selectorSuggestions);
- }, [selectedParentSpan, selectedSpan, selectedSpansData, setSelectorSuggestions]);
+ }, [selectedParentSpan, selectedSpan, setSelectorSuggestions, triggerSelectorResult]);
useEffect(() => {
return () => {
@@ -88,8 +80,8 @@ const useQuerySelector = ({form, runId, testId, onValidSelector}: IProps) => {
}, [onClearMatchedSpans]);
useEffect(() => {
- setIsValid(!isError);
- }, [isError]);
+ setIsValid(!isTriggerSelectorError);
+ }, [isTriggerSelectorError]);
useEffect(() => {
form.setFields([
@@ -102,7 +94,7 @@ const useQuerySelector = ({form, runId, testId, onValidSelector}: IProps) => {
}, [form, isValid, onValidSelector]);
return {
- spanIdList: selectedSpansData?.spanIds ?? [],
+ spanIdList: triggerSelectorResult?.spanIds ?? [],
isValid,
isLoading,
};
diff --git a/web/src/components/Visualization/components/DAG/DAG.styled.tsx b/web/src/components/Visualization/components/DAG/DAG.styled.tsx
index f55d00b227..54ddf00e68 100644
--- a/web/src/components/Visualization/components/DAG/DAG.styled.tsx
+++ b/web/src/components/Visualization/components/DAG/DAG.styled.tsx
@@ -46,7 +46,7 @@ export const Container = styled.div<{$showMatched: boolean}>`
${({$showMatched}) =>
$showMatched &&
css`
- .react-flow__node-span > div:not(.matched) {
+ .react-flow__node-span > div:not(.matched):not(.selectedAsCurrent) {
opacity: 0.5;
}
`}
diff --git a/web/src/components/Visualization/components/DAG/SpanNode.tsx b/web/src/components/Visualization/components/DAG/SpanNode.tsx
index 62fe4f8d7e..91d63caabf 100644
--- a/web/src/components/Visualization/components/DAG/SpanNode.tsx
+++ b/web/src/components/Visualization/components/DAG/SpanNode.tsx
@@ -11,6 +11,10 @@ import {INodeDataSpan} from 'types/DAG.types';
import AssertionResultChecks from 'components/AssertionResultChecks/AssertionResultChecks';
import {selectOutputsBySpanId} from 'redux/testOutputs/selectors';
import TestOutputMark from 'components/TestOutputMark';
+import {useTestSpecForm} from 'components/TestSpecForm/TestSpecForm.provider';
+import CurrentSpanSelector from 'components/CurrentSpanSelector';
+import {useSpan} from 'providers/Span/Span.provider';
+import {useTestOutput} from 'providers/TestOutput/TestOutput.provider';
import * as S from './SpanNode.styled';
interface IProps extends NodeProps {}
@@ -19,53 +23,60 @@ const SpanNode = ({data, id, selected}: IProps) => {
const assertions = useAppSelector(state => TestSpecsSelectors.selectAssertionResultsBySpan(state, data?.id || ''));
const outputs = useAppSelector(state => selectOutputsBySpanId(state, data?.id || ''));
const {failed, passed} = useMemo(() => SpanService.getAssertionResultSummary(assertions), [assertions]);
-
- const className = data.isMatched ? 'matched' : '';
+ const {isOpen: isTestSpecFormOpen} = useTestSpecForm();
+ const {isOpen: isTestOutputFormOpen} = useTestOutput();
+ const {matchedSpans} = useSpan();
+ const showSelectAsCurrent =
+ !data.isMatched && !!matchedSpans.length && (isTestSpecFormOpen || isTestOutputFormOpen) && selected;
+ const className = `${data.isMatched ? 'matched' : ''} ${showSelectAsCurrent ? 'selectedAsCurrent' : ''}`;
return (
-
-
+ <>
+
+
-
+
-
-
-
-
- {data.name}
-
+
+
+
+
+ {data.name}
+
-
-
-
-
- {data.service} {SpanKindToText[data.kind]}
-
-
- {Boolean(data.system) && (
+
+
+
+
+ {data.service} {SpanKindToText[data.kind]}
+
+
+ {Boolean(data.system) && (
+
+
+ {data.system}
+
+ )}
-
- {data.system}
+
+ {data.duration}
- )}
-
-
- {data.duration}
-
-
+
-
- {!!outputs.length && }
-
-
+
+ {!!outputs.length && }
+
+
-
-
+
+
+ {showSelectAsCurrent && }
+ >
);
};
diff --git a/web/src/providers/Span/Span.provider.tsx b/web/src/providers/Span/Span.provider.tsx
index e8e57de7ad..45a16cca2f 100644
--- a/web/src/providers/Span/Span.provider.tsx
+++ b/web/src/providers/Span/Span.provider.tsx
@@ -7,14 +7,20 @@ import SpanSelectors from 'selectors/Span.selectors';
import {RouterSearchFields} from 'constants/Common.constants';
import Span from 'models/Span.model';
import RouterActions from 'redux/actions/Router.actions';
+import {useLazyGetSelectedSpansQuery} from 'redux/apis/TraceTest.api';
+import SelectedSpans from 'models/SelectedSpans.model';
interface IContext {
selectedSpan?: Span;
+ triggerSelectorResult?: SelectedSpans;
matchedSpans: string[];
focusedSpan: string;
+ isTriggerSelectorError: boolean;
+ isTriggerSelectorLoading: boolean;
onSelectSpan(spanId: string): void;
onSetMatchedSpans(spanIdList: string[]): void;
onSetFocusedSpan(spanId: string): void;
+ onTriggerSelector(query: string, testId: string, runId: string): void;
onClearMatchedSpans(): void;
onClearSelectedSpan(): void;
}
@@ -27,6 +33,9 @@ export const Context = createContext({
onClearMatchedSpans: noop,
onSetMatchedSpans: noop,
onClearSelectedSpan: noop,
+ onTriggerSelector: noop,
+ isTriggerSelectorError: false,
+ isTriggerSelectorLoading: false,
});
interface IProps {
@@ -40,6 +49,10 @@ const SpanProvider = ({children}: IProps) => {
const selectedSpan = useSelector(SpanSelectors.selectSelectedSpan);
const matchedSpans = useSelector(SpanSelectors.selectMatchedSpans);
const focusedSpan = useSelector(SpanSelectors.selectFocusedSpan);
+ const [
+ triggerSelector,
+ {data: triggerSelectorResult, isError: isTriggerSelectorError, isFetching: isTriggerSelectorLoading},
+ ] = useLazyGetSelectedSpansQuery();
useEffect(() => {
return () => {
@@ -66,6 +79,18 @@ const SpanProvider = ({children}: IProps) => {
[dispatch]
);
+ const onTriggerSelector = useCallback(
+ async (query: string, testId: string, runId: string) => {
+ const {spanIds = []} = await triggerSelector({
+ query,
+ testId,
+ runId,
+ }).unwrap();
+ onSetMatchedSpans(spanIds);
+ },
+ [onSetMatchedSpans, triggerSelector]
+ );
+
const onSetFocusedSpan = useCallback(
(spanId: string) => {
dispatch(setFocusedSpan({spanId}));
@@ -87,16 +112,24 @@ const SpanProvider = ({children}: IProps) => {
onSetFocusedSpan,
onClearMatchedSpans,
onClearSelectedSpan,
+ onTriggerSelector,
+ triggerSelectorResult,
+ isTriggerSelectorError,
+ isTriggerSelectorLoading,
}),
[
+ selectedSpan,
matchedSpans,
focusedSpan,
- onClearMatchedSpans,
onSelectSpan,
onSetMatchedSpans,
onSetFocusedSpan,
- selectedSpan,
+ onClearMatchedSpans,
onClearSelectedSpan,
+ onTriggerSelector,
+ triggerSelectorResult,
+ isTriggerSelectorError,
+ isTriggerSelectorLoading,
]
);
diff --git a/web/src/providers/TestOutput/TestOutput.provider.tsx b/web/src/providers/TestOutput/TestOutput.provider.tsx
index a3edf4cf4b..9d7420a1c1 100644
--- a/web/src/providers/TestOutput/TestOutput.provider.tsx
+++ b/web/src/providers/TestOutput/TestOutput.provider.tsx
@@ -15,17 +15,20 @@ import {
outputsTestRunOutputsMerged,
outputUpdated,
} from 'redux/testOutputs/slice';
-import TestOutput from '../../models/TestOutput.model';
+import TestOutput from 'models/TestOutput.model';
+import SpanSelectors from 'selectors/Span.selectors';
import {useConfirmationModal} from '../ConfirmationModal/ConfirmationModal.provider';
import {useEnvironment} from '../Environment/Environment.provider';
import {useTest} from '../Test/Test.provider';
import {useTestRun} from '../TestRun/TestRun.provider';
+import useValidateOutput from './hooks/useValidateOutput';
interface IContext {
isDraftMode: boolean;
isEditing: boolean;
isLoading: boolean;
isOpen: boolean;
+ isValid: boolean;
onCancel(): void;
onClose(): void;
onDelete(id: number): void;
@@ -33,6 +36,7 @@ interface IContext {
onOpen(draft?: TestOutput): void;
onSubmit(values: TestOutput): void;
onSelectedOutputs(outputs: TestOutput[]): void;
+ onValidate(_: any, output: TestOutput): void;
output?: TestOutput;
outputs: TestOutput[];
selectedOutputs: TestOutput[];
@@ -43,6 +47,7 @@ export const Context = createContext({
isEditing: false,
isLoading: false,
isOpen: false,
+ isValid: false,
onCancel: noop,
onClose: noop,
onDelete: noop,
@@ -50,6 +55,7 @@ export const Context = createContext({
onOpen: noop,
onSubmit: noop,
onSelectedOutputs: noop,
+ onValidate: noop,
output: undefined,
outputs: [],
selectedOutputs: [],
@@ -81,6 +87,8 @@ const TestOutputProvider = ({children, runId, testId}: IProps) => {
const outputs = useAppSelector(state => selectTestOutputs(state));
const selectedOutputs = useAppSelector(selectSelectedOutputs);
const isDraftMode = useAppSelector(selectIsPending);
+ const spanIdList = useAppSelector(SpanSelectors.selectMatchedSpans);
+ const {isValid, onValidate} = useValidateOutput({spanIdList});
useEffect(() => {
dispatch(outputsInitiated(testOutputs));
@@ -94,16 +102,33 @@ const TestOutputProvider = ({children, runId, testId}: IProps) => {
dispatch(outputsTestRunOutputsMerged(runOutputs));
}, [dispatch, runOutputs]);
- const onOpen = useCallback((values?: TestOutput) => {
+ const handleOpen = useCallback((values?: TestOutput) => {
setDraft(values);
setIsOpen(true);
const id = values?.id ?? -1;
setIsEditing(id !== -1);
}, []);
+ const onOpen = useCallback(
+ (values?: TestOutput) => {
+ if (isValid) {
+ onOpenConfirmationModal({
+ title: 'Unsaved changes',
+ heading: 'Discard unsaved changes?',
+ okText: 'Discard',
+ onConfirm: () => {
+ handleOpen(values);
+ },
+ });
+ } else handleOpen(values);
+ },
+ [handleOpen, isValid, onOpenConfirmationModal]
+ );
+
const onClose = useCallback(() => {
setDraft(undefined);
setIsOpen(false);
+ onValidate(undefined, TestOutput({}));
}, []);
const onCancel = useCallback(() => {
@@ -171,6 +196,8 @@ const TestOutputProvider = ({children, runId, testId}: IProps) => {
isEditing,
isLoading,
isOpen,
+ isValid,
+ onValidate,
onCancel,
onClose,
onDelete,
@@ -188,6 +215,7 @@ const TestOutputProvider = ({children, runId, testId}: IProps) => {
isEditing,
isLoading,
isOpen,
+ isValid,
onCancel,
onClose,
onDelete,
@@ -195,6 +223,7 @@ const TestOutputProvider = ({children, runId, testId}: IProps) => {
onOpen,
onSelectedOutputs,
onSubmit,
+ onValidate,
outputs,
selectedOutputs,
]
diff --git a/web/src/components/TestOutputForm/hooks/useValidateOutput.ts b/web/src/providers/TestOutput/hooks/useValidateOutput.ts
similarity index 100%
rename from web/src/components/TestOutputForm/hooks/useValidateOutput.ts
rename to web/src/providers/TestOutput/hooks/useValidateOutput.ts