Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature(frontend): adding test spec snippets #2366

Merged
merged 3 commits into from
Apr 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions web/src/components/TestResults/AddTestSpecButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React, {useCallback, useEffect, useMemo, useRef} from 'react';
import {CaretDownOutlined} from '@ant-design/icons';
import {Dropdown, Menu} from 'antd';
import SpanService from 'services/Span.service';
import Span from 'models/Span.model';
import {TEST_SPEC_SNIPPETS, TSnippet} from 'constants/TestSpecs.constants';
import * as S from './TestResults.styled';
import {useTestSpecForm} from '../TestSpecForm/TestSpecForm.provider';

interface IProps {
selectedSpan: Span;
visibleByDefault?: boolean;
}

const AddTestSpecButton = ({selectedSpan, visibleByDefault = false}: IProps) => {
const {open} = useTestSpecForm();
const caretRef = useRef<HTMLElement>(null);
const handleEmptyTestSpec = useCallback(() => {
const selector = SpanService.getSelectorInformation(selectedSpan);

open({
isEditing: false,
selector,
defaultValues: {
selector,
},
});
}, [open, selectedSpan]);

const onSnippetClick = useCallback(
(snippet: TSnippet) => {
open({
isEditing: false,
selector: snippet.selector,
defaultValues: snippet,
});
},
[open]
);

useEffect(() => {
if (visibleByDefault && caretRef.current) {
caretRef.current?.click();
}
}, [visibleByDefault]);

const menu = useMemo(
() => (
<Menu
items={[
{
label: 'Try these snippets for quick testing:',
key: 'test',
type: 'group',
children: TEST_SPEC_SNIPPETS.map(snippet => ({
label: snippet.name,
key: snippet.name,
onClick: () => onSnippetClick(snippet),
})),
Comment on lines +55 to +59
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking if we should include an option to create an empty test spec. I know you can click the left part of the button but maybe users won't get it.

Screenshot 2023-04-11 at 14 08 33

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it behave differently than clicking the left button? I think we can gather some feedback on the current UX and then we can think of updating it.

We also need @kdhamric feedback on what other snippets he'd like to add

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should have the same behavior as clicking the left button. It could be the last item in the list (with a visual separator).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating a list now.

I am afraid many people will not find this list of available test specs. A cheesy, but possibly workable solution, would be to have the dropdown list 'open' by default Proposal: if you go to the Test screen and there are no Test Specs defined, the dropdown list is Open. @olha23 thoughts?

Copy link
Collaborator

@kdhamric kdhamric Apr 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are 3 more 'prompted test specs'

export const TRIGGER_SPAN_RESPONSE_BODY_CONTAINS: TSnippet = {
    name: 'Trigger Span: Response body contains "this string"',
    selector: 'span[tracetest.span.type="general" name="Tracetest trigger"]',
    assertions: [
      {
        left: 'attr:tracetest.response.body',
        comparator: 'contains',
        right: '"this string"',
      },
    ],
  };


  export const GRPC_SPANS_STATUS_CODE: TSnippet = {
    name: 'All gRPC Spans: Status is Ok',
    selector: 'span[tracetest.span.type="rpc" rpc.system="grpc"]',
    assertions: [
      {
        left: 'attr:grpc.status_code',
        comparator: '=',
        right: '0',
      },
    ],
  };


  export const DB_SPANS_QUALITY_DB_STATEMENT_PRESENT: TSnippet = {
    name: 'All Database Spans: db.statement should always be defined (QUALITY)',
    selector: 'span[tracetest.span.type="database"]',
    assertions: [
      {
        left: 'attr:db.system',
        comparator: '!=',
        right: '""',
      },
    ],
  };

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all done

},
{type: 'divider'},
{
label: 'Empty Test Spec',
key: 'empty-test-spec',
onClick: handleEmptyTestSpec,
},
]}
/>
),
[handleEmptyTestSpec, onSnippetClick]
);

return (
<Dropdown.Button
overlay={menu}
trigger={['click']}
placement="bottomRight"
onClick={handleEmptyTestSpec}
type="primary"
buttonsRender={([leftButton]) => [
React.cloneElement(leftButton as React.ReactElement<any, string>, {'data-cy': 'add-test-spec-button'}),
<S.CaretDropdownButton ref={caretRef} type="primary" data-cy="create-button">
<CaretDownOutlined />
</S.CaretDropdownButton>,
]}
>
Add Test Spec
</Dropdown.Button>
);
};

export default AddTestSpecButton;
21 changes: 3 additions & 18 deletions web/src/components/TestResults/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import {StepsID} from 'components/GuidedTour/testRunSteps';
import {useTestSpecForm} from 'components/TestSpecForm/TestSpecForm.provider';
import SpanService from 'services/Span.service';
import {singularOrPlural} from 'utils/Common';
import Span from 'models/Span.model';
import * as S from './TestResults.styled';
import AddTestSpecButton from './AddTestSpecButton';

interface IProps {
selectedSpan: Span;
Expand All @@ -12,18 +10,7 @@ interface IProps {
}

const Header = ({selectedSpan, totalFailedSpecs, totalPassedSpecs}: IProps) => {
const {open} = useTestSpecForm();

const handleAddTestSpecOnClick = () => {
const selector = SpanService.getSelectorInformation(selectedSpan!);
open({
isEditing: false,
selector,
defaultValues: {
selector,
},
});
};
const hasSpecs = !!(totalFailedSpecs || totalPassedSpecs);

return (
<S.HeaderContainer>
Expand All @@ -45,9 +32,7 @@ const Header = ({selectedSpan, totalFailedSpecs, totalPassedSpecs}: IProps) => {
</div>
</S.Row>

<S.PrimaryButton data-tour={StepsID.TestSpecs} data-cy="add-test-spec-button" onClick={handleAddTestSpecOnClick}>
Add Test Spec
</S.PrimaryButton>
<AddTestSpecButton selectedSpan={selectedSpan} visibleByDefault={!hasSpecs} />
</S.HeaderContainer>
);
};
Expand Down
11 changes: 7 additions & 4 deletions web/src/components/TestResults/TestResults.styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,15 @@ export const LoadingContainer = styled.div`
text-align: center;
`;

export const PrimaryButton = styled(Button).attrs({
type: 'primary',
})``;

export const Row = styled.div`
align-items: center;
display: flex;
gap: 8px;
`;

export const CaretDropdownButton = styled(Button)`
font-weight: 600;
opacity: 0.7;
width: 32px;
padding: 0px;
`;
23 changes: 14 additions & 9 deletions web/src/components/TestResults/TestResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,27 @@ const TestResults = ({onDelete, onEdit, onRevert}: IProps) => {

return (
<S.Container>
<Header selectedSpan={selectedSpan!} totalFailedSpecs={totalFailedSpecs} totalPassedSpecs={totalPassedSpecs} />

{isLoading && (
<S.LoadingContainer>
<LoadingSpinner />
</S.LoadingContainer>
)}

{!isLoading && (
<TestSpecs
assertionResults={assertionResults}
onDelete={onDelete}
onEdit={onEdit}
onOpen={handleOpen}
onRevert={onRevert}
/>
<>
<Header
selectedSpan={selectedSpan!}
totalFailedSpecs={totalFailedSpecs}
totalPassedSpecs={totalPassedSpecs}
/>
<TestSpecs
assertionResults={assertionResults}
onDelete={onDelete}
onEdit={onEdit}
onOpen={handleOpen}
onRevert={onRevert}
/>
</>
)}
</S.Container>
);
Expand Down
84 changes: 84 additions & 0 deletions web/src/constants/TestSpecs.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {IValues} from 'components/TestSpecForm/TestSpecForm';

export type TSnippet = Required<IValues>;

export const HTTP_SPANS_STATUS_CODE: TSnippet = {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's where we can continue adding snippets, you can add it manually using the UI and then copy the details in this file @kdhamric

name: 'All HTTP Spans: Status code is 200',
selector: 'span[tracetest.span.type="http"]',
assertions: [
{
left: 'attr:http.status_code',
comparator: '=',
right: '200',
},
],
};

export const TRIGGER_SPAN_RESPONSE_TIME: TSnippet = {
name: 'Trigger Span: Response time is less than 200ms',
selector: 'span[tracetest.span.type="general" name="Tracetest trigger"]',
assertions: [
{
left: 'attr:tracetest.span.duration',
comparator: '<',
right: '200ms',
},
],
};

export const DB_SPANS_RESPONSE_TIME: TSnippet = {
name: 'All Database Spans: Processing time is less than 100ms',
selector: 'span[tracetest.span.type="database"]',
assertions: [
{
left: 'attr:tracetest.span.duration',
comparator: '<',
right: '100ms',
},
],
};

export const TRIGGER_SPAN_RESPONSE_BODY_CONTAINS: TSnippet = {
name: 'Trigger Span: Response body contains "this string"',
selector: 'span[tracetest.span.type="general" name="Tracetest trigger"]',
assertions: [
{
left: 'attr:tracetest.response.body',
comparator: 'contains',
right: '"this string"',
},
],
};

export const GRPC_SPANS_STATUS_CODE: TSnippet = {
name: 'All gRPC Spans: Status is Ok',
selector: 'span[tracetest.span.type="rpc" rpc.system="grpc"]',
assertions: [
{
left: 'attr:grpc.status_code',
comparator: '=',
right: '0',
},
],
};

export const DB_SPANS_QUALITY_DB_STATEMENT_PRESENT: TSnippet = {
name: 'All Database Spans: db.statement should always be defined (QUALITY)',
selector: 'span[tracetest.span.type="database"]',
assertions: [
{
left: 'attr:db.system',
comparator: '!=',
right: '""',
},
],
};

export const TEST_SPEC_SNIPPETS: TSnippet[] = [
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dont forget to add it to this array

HTTP_SPANS_STATUS_CODE,
GRPC_SPANS_STATUS_CODE,
TRIGGER_SPAN_RESPONSE_TIME,
TRIGGER_SPAN_RESPONSE_BODY_CONTAINS,
DB_SPANS_RESPONSE_TIME,
DB_SPANS_QUALITY_DB_STATEMENT_PRESENT,
];