Skip to content

Commit

Permalink
feat(frontend): Adding Span Result Detail Drawer (#3680)
Browse files Browse the repository at this point in the history
* feat(frontend):

* feat(frontend): Adding Span Result Detail Drawer
  • Loading branch information
xoscar committed Feb 23, 2024
1 parent 3db3b9f commit 24d538e
Show file tree
Hide file tree
Showing 19 changed files with 431 additions and 132 deletions.
20 changes: 18 additions & 2 deletions web/src/components/RunDetailTest/TestPanel.tsx
Expand Up @@ -24,6 +24,7 @@ import * as S from './RunDetailTest.styled';
import Visualization from './Visualization';
import {FillPanel} from '../ResizablePanels';
import SkipTraceCollectionInfo from '../SkipTraceCollectionInfo';
import SpanResultDetail from '../SpanResultDetail';

const TABS = {
SPECS: 'specs',
Expand All @@ -39,8 +40,17 @@ interface IProps {
const TestPanel = ({run, testId, runEvents}: IProps) => {
const [query, updateQuery] = useSearchParams();
const {selectedSpan, onSetFocusedSpan} = useSpan();
const {remove, revert, selectedTestSpec, setSelectedSpec, setSelectorSuggestions, setPrevSelector, specs} =
useTestSpecs();
const {
remove,
revert,
selectedTestSpec,
selectedSpanResult,
setSelectedSpanResult,
setSelectedSpec,
setSelectorSuggestions,
setPrevSelector,
specs,
} = useTestSpecs();
const {isOpen: isTestSpecFormOpen, formProps, onSubmit, open, close, isValid, onIsValid} = useTestSpecForm();
const {
isEditing,
Expand Down Expand Up @@ -219,6 +229,12 @@ const TestPanel = ({run, testId, runEvents}: IProps) => {
selectedSpan={selectedSpan?.id}
testSpec={selectedTestSpec}
/>

<SpanResultDetail
isOpen={!!selectedSpanResult}
spanResult={selectedSpanResult}
onClose={() => setSelectedSpanResult()}
/>
</S.SectionRight>
</S.Container>
</FillPanel>
Expand Down
Expand Up @@ -5,7 +5,7 @@ import {ICheckResult} from 'types/Assertion.types';
import {TCompareOperatorSymbol} from 'types/Operator.types';
import {SupportedEditors} from 'constants/Editor.constants';
import {Editor} from 'components/Inputs';
import * as S from './TestSpecDetail.styled';
import * as S from './SpanResultDetail.styled';

interface IProps {
check: ICheckResult;
Expand All @@ -15,14 +15,14 @@ interface IProps {
}

const Assertion = ({check, testId, runId, selector}: IProps) => (
<S.CheckItemContainer>
<S.CheckItemContainer $isSuccessful={check.result.passed}>
<S.GridContainer>
{check.result.error && AssertionService.isValidError(check.result.error) ? (
<>
<S.Row $justify="center">
<S.IconWarning />
<S.IconError />
</S.Row>
<AttributeValue strong type="warning" value={check.result.error} />
<AttributeValue strong type="danger" value={check.result.error} />
</>
) : (
<>
Expand Down
52 changes: 52 additions & 0 deletions web/src/components/SpanResultDetail/Content.tsx
@@ -0,0 +1,52 @@
import {useMemo} from 'react';
import {useTest} from 'providers/Test/Test.provider';
import {ICheckResult} from 'types/Assertion.types';
import AssertionService from 'services/Assertion.service';
import Span from 'models/Span.model';
import {useTestRun} from 'providers/TestRun/TestRun.provider';
import * as S from './SpanResultDetail.styled';
import Assertion from './Assertion';
import Header from './Header';
import {useTestSpecs} from '../../providers/TestSpecs/TestSpecs.provider';

interface IProps {
span: Span;
checkResults: ICheckResult[];
onClose(): void;
}

const Content = ({checkResults, span, onClose}: IProps) => {
const {
run: {id: runId},
} = useTestRun();
const {
test: {id: testId},
} = useTest();

const totalPassedChecks = useMemo(() => AssertionService.getTotalPassedSpanChecks(checkResults), [checkResults]);
const {selectedTestSpec} = useTestSpecs();

return (
<>
<Header
span={span}
onClose={onClose}
assertionsFailed={checkResults.length - totalPassedChecks}
assertionsPassed={totalPassedChecks}
/>
<S.AssertionsContainer>
{checkResults.map(checkResult => (
<Assertion
testId={testId}
runId={runId}
selector={selectedTestSpec?.selector || ''}
check={checkResult}
key={`${checkResult.result.spanId}-${checkResult.assertion}`}
/>
))}
</S.AssertionsContainer>
</>
);
};

export default Content;
66 changes: 66 additions & 0 deletions web/src/components/SpanResultDetail/Detail.tsx
@@ -0,0 +1,66 @@
import Span from 'models/Span.model';
import {ClockCircleOutlined, SettingOutlined, ToolOutlined} from '@ant-design/icons';
import {Tooltip} from 'antd';
import * as STestSpec from 'components/TestSpec/TestSpec.styled';
import {SemanticGroupNamesToText} from 'constants/SemanticGroupNames.constants';
import * as SSpanNode from 'components/Visualization/components/DAG/BaseSpanNode/BaseSpanNode.styled';
import * as S from './SpanResultDetail.styled';

interface IProps {
assertionsFailed: number;
assertionsPassed: number;
span: Span;
}

const Detail = ({
assertionsFailed,
assertionsPassed,
span: {duration, name, id, service, kind, system, type},

Check warning on line 18 in web/src/components/SpanResultDetail/Detail.tsx

View workflow job for this annotation

GitHub Actions / WebUI unit tests

'id' is defined but never used
}: IProps) => {
return (
<>
<S.DetailsWrapper>
<S.SpanHeaderContainer>
<SSpanNode.BadgeType count={SemanticGroupNamesToText[type]} $type={type} />
<Tooltip title={name}>
<S.HeaderTitle level={3}>{name}</S.HeaderTitle>
</Tooltip>
</S.SpanHeaderContainer>
<div>
{Boolean(assertionsPassed) && (
<STestSpec.HeaderDetail>
<STestSpec.HeaderDot $passed />
{assertionsPassed}
</STestSpec.HeaderDetail>
)}
{Boolean(assertionsFailed) && (
<STestSpec.HeaderDetail>
<STestSpec.HeaderDot $passed={false} />
{assertionsFailed}
</STestSpec.HeaderDetail>
)}
</div>
</S.DetailsWrapper>

<S.SpanHeaderContainer>
<S.HeaderItem>
<SettingOutlined />
<S.HeaderItemText>{`${service} ${kind}`}</S.HeaderItemText>
</S.HeaderItem>
{Boolean(system) && (
<S.HeaderItem>
<ToolOutlined />
<S.HeaderItemText>{system}</S.HeaderItemText>
</S.HeaderItem>
)}

<S.HeaderItem>
<ClockCircleOutlined />
<S.HeaderItemText>{duration}</S.HeaderItemText>
</S.HeaderItem>
</S.SpanHeaderContainer>
</>
);
};

export default Detail;
28 changes: 28 additions & 0 deletions web/src/components/SpanResultDetail/Header.tsx
@@ -0,0 +1,28 @@
import Span from 'models/Span.model';
import {ArrowLeftOutlined} from '@ant-design/icons';
import {Button, Divider} from 'antd';
import * as S from './SpanResultDetail.styled';
import Detail from './Detail';

interface IProps {
assertionsFailed: number;
assertionsPassed: number;
onClose(): void;
span: Span;
}

const Header = ({span, assertionsFailed, assertionsPassed, onClose}: IProps) => (
<>
<S.HeaderContainer>
<S.Row $hasGap>
<Button icon={<ArrowLeftOutlined />} onClick={onClose} type="link" />
<S.HeaderTitle level={2}>Span Result Detail</S.HeaderTitle>
</S.Row>
</S.HeaderContainer>
<Divider />
<Detail span={span} assertionsFailed={assertionsFailed} assertionsPassed={assertionsPassed} />
<Divider />
</>
);

export default Header;
121 changes: 121 additions & 0 deletions web/src/components/SpanResultDetail/SpanResultDetail.styled.ts
@@ -0,0 +1,121 @@
import {CheckCircleFilled, CloseCircleFilled, MinusCircleFilled} from '@ant-design/icons';
import {Drawer, Typography} from 'antd';
import styled from 'styled-components';
import {SemanticGroupNames, SemanticGroupNamesToColor} from 'constants/SemanticGroupNames.constants';

export const AssertionsContainer = styled.div`
cursor: pointer;
display: flex;
flex-direction: column;
gap: 16px;
border-top: 1px solid ${({theme}) => theme.color.borderLight};
`;

export const AssertionContainer = styled.div`
span {
overflow-wrap: anywhere;
}
`;

export const DrawerContainer = styled(Drawer)<{$type: SemanticGroupNames}>`
position: absolute;
overflow: hidden;
.ant-drawer-body {
display: flex;
flex-direction: column;
border-top: ${({$type}) => `4px solid ${SemanticGroupNamesToColor[$type]}`};
}
`;

export const DrawerRow = styled.div`
flex: 1;
`;

export const GridContainer = styled.div`
display: grid;
grid-template-columns: 4.5% 1fr;
align-items: center;
`;

export const CheckItemContainer = styled.div<{$isSuccessful: boolean}>`
padding: 10px 12px;
border: 1px solid ${({theme}) => theme.color.borderLight};
border-top: ${({$isSuccessful, theme}) => `4px solid ${$isSuccessful ? theme.color.success : theme.color.error}`};
`;

export const HeaderContainer = styled.div`
align-items: center;
display: flex;
justify-content: space-between;
`;

export const HeaderItem = styled.div`
align-items: center;
color: ${({theme}) => theme.color.text};
display: flex;
font-size: ${({theme}) => theme.size.sm};
`;

export const HeaderItemText = styled(Typography.Text)`
color: inherit;
margin-left: 5px;
`;

export const HeaderTitle = styled(Typography.Title)`
&& {
text-overflow: ellipsis;
max-width: 250px;
text-wrap: nowrap;
overflow: hidden;
margin-bottom: 0;
}
`;

export const IconError = styled(MinusCircleFilled)`
color: ${({theme}) => theme.color.error};
`;

export const IconSuccess = styled(CheckCircleFilled)`
color: ${({theme}) => theme.color.success};
`;

export const Row = styled.div<{$align?: string; $hasGap?: boolean; $justify?: string}>`
align-items: ${({$align}) => $align || 'center'};
display: flex;
justify-content: ${({$justify}) => $justify || 'flex-start'};
gap: ${({$hasGap}) => $hasGap && '8px'};
`;

export const SecondaryText = styled(Typography.Text)`
color: ${({theme}) => theme.color.textSecondary};
font-size: ${({theme}) => theme.size.sm};
`;

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

export const DetailsWrapper = styled.div`
align-items: center;
cursor: pointer;
justify-content: space-between;
display: flex;
`;

export const ClearSearchIcon = styled(CloseCircleFilled)`
position: absolute;
right: 8px;
top: 8px;
color: ${({theme}) => theme.color.textLight};
cursor: pointer;
`;

export const SearchContainer = styled(Row)`
margin-bottom: 16px;
`;
38 changes: 38 additions & 0 deletions web/src/components/SpanResultDetail/SpanResultDetail.tsx
@@ -0,0 +1,38 @@
import {useTestRun} from 'providers/TestRun/TestRun.provider';
import {ISpanResult} from 'types/TestSpecs.types';
import * as S from './SpanResultDetail.styled';
import Content from './Content';

interface IProps {
isOpen: boolean;
onClose(): void;
spanResult?: ISpanResult;
}

const SpanResultDetail = ({isOpen, onClose, spanResult}: IProps) => {
const {
run: {trace},
} = useTestRun();

if (!spanResult) return null;

const span = trace?.flat[spanResult.spanId] || {};

return (
<S.DrawerContainer
closable={false}
getContainer={false}
mask={false}
onClose={onClose}
placement="right"
visible={isOpen}
width="100%"
height="100%"
$type={span.type}
>
{!!spanResult && <Content onClose={onClose} span={span} checkResults={spanResult.checkResults} />}
</S.DrawerContainer>
);
};

export default SpanResultDetail;
2 changes: 2 additions & 0 deletions web/src/components/SpanResultDetail/index.ts
@@ -0,0 +1,2 @@
// eslint-disable-next-line no-restricted-exports
export {default} from './SpanResultDetail';

0 comments on commit 24d538e

Please sign in to comment.