Skip to content

Commit

Permalink
feature(frontend): switching current span during spec creation (#2205)
Browse files Browse the repository at this point in the history
* feature(frontend): switching current span during spec creation

* enabling span switching

* showing the unsaved changes prompt when the forms are valid

* fixing unit tests

* resetting output form
  • Loading branch information
xoscar committed Mar 23, 2023
1 parent e32538f commit d923c65
Show file tree
Hide file tree
Showing 14 changed files with 310 additions and 92 deletions.
@@ -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;
`;
59 changes: 59 additions & 0 deletions 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 (
<S.Container className="matched">
<S.FloatingText onClick={() => !isTriggerSelectorLoading && handleOnClick()}>
{isTriggerSelectorLoading ? (
<>
Updating selected span <LoadingOutlined />
</>
) : (
<>Select as current span</>
)}
</S.FloatingText>
</S.Container>
);
};

export default CurrentSpanSelector;
2 changes: 2 additions & 0 deletions web/src/components/CurrentSpanSelector/index.ts
@@ -0,0 +1,2 @@
// eslint-disable-next-line no-restricted-exports
export {default} from './CurrentSpanSelector';
8 changes: 7 additions & 1 deletion web/src/components/RunDetailTest/RunDetailTest.tsx
Expand Up @@ -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,
Expand All @@ -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();
Expand Down Expand Up @@ -134,6 +136,8 @@ const RunDetailTest = ({run, testId}: IProps) => {
setPrevSelector('');
onSubmit(values);
}}
isValid={isValid}
onIsValid={onIsValid}
runId={run.id}
testId={testId}
{...formProps}
Expand All @@ -160,6 +164,8 @@ const RunDetailTest = ({run, testId}: IProps) => {
output={output}
runId={run.id}
testId={testId}
isValid={isOutputFormValid}
onValidate={onValidate}
/>
)}

Expand Down
30 changes: 26 additions & 4 deletions web/src/components/TestOutputForm/TestOutputForm.tsx
Expand Up @@ -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';

Expand All @@ -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<TestOutput>();
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')) {
Expand All @@ -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 (
<S.Container>
<S.Title>{isEditing ? 'Edit Test Output' : 'Add Test Output'}</S.Title>
Expand Down
36 changes: 30 additions & 6 deletions web/src/components/TestSpecForm/TestSpecForm.provider.tsx
Expand Up @@ -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;
Expand All @@ -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 = {
Expand All @@ -35,10 +38,12 @@ const initialFormProps = {

export const Context = createContext<IContext>({
isOpen: false,
isValid: false,
formProps: initialFormProps,
open: noop,
close: noop,
formProps: initialFormProps,
onSubmit: noop,
onIsValid: noop,
});

export const useTestSpecForm = () => useContext<IContext>(Context);
Expand All @@ -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<IFormProps>(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);
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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 (
Expand Down
58 changes: 40 additions & 18 deletions 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';
Expand All @@ -30,45 +31,60 @@ 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,
onClickPrevSelector,
onSubmit,
runId,
testId,
isValid,
onIsValid,
}: IProps) => {
const [form] = Form.useForm<IValues>();
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);

Expand Down Expand Up @@ -98,7 +114,13 @@ const TestSpecForm = ({
<S.FormSectionHeaderSelector>
<S.FormSectionRow1>
<S.FormSectionTitle $noMargin>1. Select Spans</S.FormSectionTitle>
<Tag color="blue">{`${spanIdList.length} ${singularOrPlural('span', spanIdList.length)} selected`}</Tag>
<Tag color="blue">
{isTriggerSelectorLoading ? (
<LoadingOutlined />
) : (
`${spanIdList.length} ${singularOrPlural('span', spanIdList.length)} selected`
)}
</Tag>
</S.FormSectionRow1>
<a href={SELECTOR_LANGUAGE_CHEAT_SHEET_URL} target="_blank">
<S.ReadIcon /> SL cheat sheet
Expand All @@ -107,7 +129,7 @@ const TestSpecForm = ({
<S.FormSectionRow>
<S.FormSectionText>Select the spans to which a set of assertions will be applied</S.FormSectionText>
</S.FormSectionRow>
<SelectorInput form={form} testId={testId} runId={runId} onValidSelector={setIsValid} />
<SelectorInput form={form} testId={testId} runId={runId} onValidSelector={onIsValid} />

<S.SuggestionsContainer>
<SelectorSuggestions
Expand Down
Expand Up @@ -9,6 +9,8 @@ const defaultProps = {
onClickPrevSelector: jest.fn(),
testId: 'testId',
runId: 'runId',
isValid: false,
onIsValid: jest.fn(),
};

describe('TestSpecForm', () => {
Expand Down

0 comments on commit d923c65

Please sign in to comment.