From 7c1a4fa81727d59f3bb2c8ad033c71630848e036 Mon Sep 17 00:00:00 2001 From: Nathan Mo <54135657+QizhengMo@users.noreply.github.com> Date: Thu, 26 Sep 2024 17:21:30 +0800 Subject: [PATCH] Refactor/batch send (#714) * refactor: extracted send request from batch run result block * chore: ts * refactor: batch run * refactor: extracted send request from batch run result block --- Dockerfile | 2 +- packages/arex-core/src/utils/json.ts | 2 +- .../src/helpers/utils/sendRequest.ts | 3 +- packages/arex/src/i18n/locales/cn/page.json | 11 ++- packages/arex/src/i18n/locales/en/page.json | 11 ++- packages/arex/src/panes/BatchRun/BatchRun.tsx | 73 ++++++++------ .../BatchRunResultGroup/ByInterface.tsx | 53 +++++++++++ .../BatchRun/BatchRunResultGroup/ByStatus.tsx | 76 +++++++++++++++ .../BatchRun/BatchRunResultGroup/Flat.tsx | 14 +++ .../BatchRun/BatchRunResultGroup/common.ts | 8 ++ .../src/panes/BatchRun/BatchRunResultItem.tsx | 22 ++--- .../panes/BatchRun/RequestTestStatusBlock.tsx | 57 ++++------- .../panes/BatchRun/RequestTestStatusMap.tsx | 94 +++++++++++++------ 13 files changed, 312 insertions(+), 114 deletions(-) create mode 100644 packages/arex/src/panes/BatchRun/BatchRunResultGroup/ByInterface.tsx create mode 100644 packages/arex/src/panes/BatchRun/BatchRunResultGroup/ByStatus.tsx create mode 100644 packages/arex/src/panes/BatchRun/BatchRunResultGroup/Flat.tsx create mode 100644 packages/arex/src/panes/BatchRun/BatchRunResultGroup/common.ts diff --git a/Dockerfile b/Dockerfile index 35448b03..147a2e8d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" RUN npm i -g pnpm@latest-9 -# RUN pnpm config set electron_mirror "https://npm.taobao.org/mirrors/electron/" +# RUN pnpm config set electron_mirror "https://npmmirror.com/mirrors/electron/" FROM pnpm-base AS base COPY . /app diff --git a/packages/arex-core/src/utils/json.ts b/packages/arex-core/src/utils/json.ts index f53dedcd..8337515c 100644 --- a/packages/arex-core/src/utils/json.ts +++ b/packages/arex-core/src/utils/json.ts @@ -10,7 +10,7 @@ export function tryParseJsonString<T>( try { return parser.parse(jsonString || '{}') as T; } catch (e) { - console.error(e); + // console.error(e); errorTip && window.message.warning(errorTip); return jsonString as T; } diff --git a/packages/arex-request/src/helpers/utils/sendRequest.ts b/packages/arex-request/src/helpers/utils/sendRequest.ts index e7739996..61ebe025 100644 --- a/packages/arex-request/src/helpers/utils/sendRequest.ts +++ b/packages/arex-request/src/helpers/utils/sendRequest.ts @@ -125,7 +125,6 @@ export async function sendRequest( // } }, item: function (err: any, cursor: any, item: any, visualizer: any) { - console.log('item'); resolve({ response: res, testResult: assertionsBox, @@ -149,7 +148,7 @@ export async function sendRequest( if (err) { reject(err); } - console.log('response', cursor, response, request, item, cookies, history); + // console.log('response', cursor, response, request, item, cookies, history); res = { type: 'success', // TODO check response status headers: response?.headers.members, diff --git a/packages/arex/src/i18n/locales/cn/page.json b/packages/arex/src/i18n/locales/cn/page.json index 4485b1fc..2c5a8353 100644 --- a/packages/arex/src/i18n/locales/cn/page.json +++ b/packages/arex/src/i18n/locales/cn/page.json @@ -19,7 +19,16 @@ "requestSuccess": "请求成功", "selectCaseTip": "请选择测试用例", "statusBlockStructure": "状态块组成", - "testStatus": "测试状态" + "testStatus": "测试状态", + + "flatten": "平铺", + "groupByInterface": "接口分组", + "groupByStatus": "状态分组", + + "sendNormalTestAbnormal": "测试失败", + "sendAbnormalTestNormal": "HTTP异常", + "sendNormalTestNormal": "正常", + "sendAbnormalTestAbnormal": "HTTP异常/测试失败" }, "folderPage": { "tests": "测试" diff --git a/packages/arex/src/i18n/locales/en/page.json b/packages/arex/src/i18n/locales/en/page.json index 7d9c1286..af497a46 100644 --- a/packages/arex/src/i18n/locales/en/page.json +++ b/packages/arex/src/i18n/locales/en/page.json @@ -18,7 +18,16 @@ "requestSuccess": "Request Success", "selectCaseTip": "Select cases for batch execution", "statusBlockStructure": "Status Block Structure", - "testStatus": "Test Status" + "testStatus": "Test Status", + + "flatten": "Flatten", + "groupByInterface": "Group by Interface", + "groupByStatus": "Group by Status", + + "sendNormalTestAbnormal": "Test Failed", + "sendAbnormalTestNormal": "HTTP Failed", + "sendNormalTestNormal": "Passed", + "sendAbnormalTestAbnormal": "HTTP/Test Failed" }, "folderPage": { "tests": "Tests" diff --git a/packages/arex/src/panes/BatchRun/BatchRun.tsx b/packages/arex/src/panes/BatchRun/BatchRun.tsx index 435bf650..c851271f 100644 --- a/packages/arex/src/panes/BatchRun/BatchRun.tsx +++ b/packages/arex/src/panes/BatchRun/BatchRun.tsx @@ -8,7 +8,12 @@ import { SpaceBetweenWrapper, useTranslation, } from '@arextest/arex-core'; -import { ArexEnvironment, ArexResponse, EnvironmentSelect } from '@arextest/arex-request'; +import { + ArexEnvironment, + ArexResponse, + EnvironmentSelect, + sendRequest, +} from '@arextest/arex-request'; import { ArexRESTRequest } from '@arextest/arex-request/src'; import { useLocalStorageState, useRequest } from 'ahooks'; import { App, Button, Divider, Flex, Slider, TreeSelect, Typography } from 'antd'; @@ -23,6 +28,11 @@ import { EnvironmentService, FileSystemService } from '@/services'; import { useCollections } from '@/store'; import { decodePaneKey } from '@/store/useMenusPanes'; +export type RunResult = { + request: ArexRESTRequest; + response?: ArexResponse; +}; +type FsNode = { infoId: string; nodeType: CollectionNodeType }; const BatchRun: ArexPaneFC = (props) => { const { paneKey } = props; const { t } = useTranslation('page'); @@ -31,15 +41,15 @@ const BatchRun: ArexPaneFC = (props) => { const { collectionsTreeData } = useCollections(); const [activeEnvironment, setActiveEnvironment] = useState<ArexEnvironment>(); - const [selectNodes, setSelectNodes] = useState< - { infoId: string; nodeType: CollectionNodeType }[] - >(id ? [{ infoId: id, nodeType: CollectionNodeType.folder }] : []); + const [selectNodes, setSelectNodes] = useState<FsNode[]>( + id ? [{ infoId: id, nodeType: CollectionNodeType.folder }] : [], + ); const selectNodesInfoId = useMemo(() => selectNodes.map((node) => node.infoId), [selectNodes]); const [processing, setProcessing] = useState(false); - const [casesResults, setCasesResults] = useImmer<ArexRESTRequest[]>([]); - const [runResult, setRunResult] = useState<{ + const [casesResults, setCasesResults] = useImmer<RunResult[]>([]); + const [currentResult, setCurrentResult] = useState<{ request: ArexRESTRequest; response?: ArexResponse; }>(); @@ -69,33 +79,40 @@ const BatchRun: ArexPaneFC = (props) => { }); const batchGetInterfaceCaseCallback = useCallback( - async (res: ArexRESTRequest[], _timestamp?: number) => { - async function processPromiseArray(promiseArray: typeof res, qps: number) { - for (let i = 0; i < promiseArray.length; i++) { + async (requests: ArexRESTRequest[], _timestamp?: number) => { + setProcessing(false); + async function processPromiseArray(qps: number) { + for (let i = 0; i < requests.length; i++) { if (timestampRef.current !== _timestamp) { - console.log('timestamp changed, stop batch run'); - setProcessing(false); + // console.log('timestamp changed, stop batch run'); break; } - const batch = promiseArray.slice(i, i + 1); - - setCasesResults((result) => { - result.push(...batch); + const request = requests[i]; + setCasesResults((results) => { + const idx = results.length; + const result: RunResult = { request }; + results.push(result); + if (!request.endpoint || !request.endpoint.trim()) { + return; + } + sendRequest(request, activeEnvironment).then((response) => { + setCasesResults((results) => { + results[idx] = { ...result, response }; + }); + }); }); - if (i + 1 < promiseArray.length) { + if (i + 1 < requests.length) { await new Promise((resolve) => setTimeout(resolve, 1000 / qps)); } } } - setProcessing(true); - processPromiseArray(res, qps || 10).then(() => { - setProcessing(false); - console.log('batch run finished'); - }); - setRunResult(undefined); + processPromiseArray(qps || 10); + + // reset result to empty + setCurrentResult(undefined); }, [timestamp], ); @@ -107,6 +124,7 @@ const BatchRun: ArexPaneFC = (props) => { } = useRequest(FileSystemService.batchGetInterfaceCase, { manual: true, onBefore: () => { + setProcessing(true); setCasesResults([]); }, onSuccess: (res, [params, _timestamp]) => { @@ -151,7 +169,6 @@ const BatchRun: ArexPaneFC = (props) => { showCheckedStrategy={TreeSelect.SHOW_PARENT} onChange={(id, labelList, extra) => { casesResults.length && setCasesResults([]); // reset cases results - runResult && setRunResult(undefined); try { setSelectNodes( extra.allCheckedNodes.map((item) => ({ @@ -187,9 +204,9 @@ const BatchRun: ArexPaneFC = (props) => { <RequestTestStatusMap key={selectNodes.length} // Add key to force re-render data={casesResults} - selectedKey={runResult?.request.id} + selectedKey={currentResult?.request.id} environment={activeEnvironment} - onClick={setRunResult} + onClick={setCurrentResult} /> <EmptyWrapper @@ -199,11 +216,11 @@ const BatchRun: ArexPaneFC = (props) => { overflow: auto; `} > - {runResult && ( + {currentResult && ( <BatchRunResultItem environment={activeEnvironment} - request={runResult.request} - response={runResult.response} + request={currentResult.request} + response={currentResult.response} /> )} </EmptyWrapper> diff --git a/packages/arex/src/panes/BatchRun/BatchRunResultGroup/ByInterface.tsx b/packages/arex/src/panes/BatchRun/BatchRunResultGroup/ByInterface.tsx new file mode 100644 index 00000000..40f13579 --- /dev/null +++ b/packages/arex/src/panes/BatchRun/BatchRunResultGroup/ByInterface.tsx @@ -0,0 +1,53 @@ +import { Card } from 'antd'; +import * as React from 'react'; +import { ReactElement } from 'react'; + +import { CollectionNodeType } from '@/constant'; +import { RunResult } from '@/panes/BatchRun/BatchRun'; +import { GroupProps } from '@/panes/BatchRun/BatchRunResultGroup/common'; + +function groupByInterface(blockMap: Map<RunResult, ReactElement>) { + const interfaceMap = new Map<string, RunResult[]>(); + for (const runResult of blockMap.keys()) { + const request = runResult.request; + const parent = request.parentPath[request.parentPath.length - 1]; + if (parent.nodeType !== CollectionNodeType.interface) { + interfaceMap.set(request.id, [runResult]); + } + } + for (const runResult of blockMap.keys()) { + const parent = runResult.request.parentPath[runResult.request.parentPath.length - 1]; + if (parent.nodeType === CollectionNodeType.interface) { + interfaceMap.get(parent.id)?.push(runResult); + } + } + return interfaceMap; +} + +export function ByInterface(props: GroupProps) { + const { blockMap } = props; + // console.log(blockMap); + const interfaceMap = groupByInterface(blockMap); + // console.log(interfaceMap.values()); + return ( + <div style={{ maxHeight: 350, overflowY: 'scroll' }}> + {Array.from(interfaceMap.values()).map((casesOfInterface) => { + const interfaceItem = casesOfInterface[0]; + return ( + <Card + style={{ marginBottom: 8 }} + size='small' + key={interfaceItem.request.id} + title={interfaceItem.request.name} + > + <div style={{ display: 'flex', flexFlow: 'row wrap' }}> + {casesOfInterface.map((runResult) => { + return blockMap.get(runResult); + })} + </div> + </Card> + ); + })} + </div> + ); +} diff --git a/packages/arex/src/panes/BatchRun/BatchRunResultGroup/ByStatus.tsx b/packages/arex/src/panes/BatchRun/BatchRunResultGroup/ByStatus.tsx new file mode 100644 index 00000000..8f844a73 --- /dev/null +++ b/packages/arex/src/panes/BatchRun/BatchRunResultGroup/ByStatus.tsx @@ -0,0 +1,76 @@ +import { useTranslation } from '@arextest/arex-core'; +import { Card } from 'antd'; +import * as React from 'react'; +import { ReactElement, ReactNode } from 'react'; + +import { RunResult } from '@/panes/BatchRun/BatchRun'; +import { GroupProps } from '@/panes/BatchRun/BatchRunResultGroup/common'; + +const SendAbnormal = 0b1; +const SendNormal = 0b1 << 1; +const TestNormal = 0b1 << 2; +const TestAbnormal = 0b1 << 3; + +const SendAbnormalTestAbnormal = SendAbnormal | TestAbnormal; +const SendAbnormalTestNormal = SendAbnormal | TestNormal; +const SendNormalTestAbnormal = SendNormal | TestAbnormal; +const SendNormalTestNormal = SendNormal | TestNormal; + +function buildStatusMap(blockMap: Map<RunResult, React.ReactElement>) { + const statusMap = new Map<number, React.ReactElement[]>(); + for (const key of blockMap.keys()) { + const { response } = key; + const statusCode = response?.response?.statusCode ?? 0; + const send = statusCode >= 200 && statusCode < 300 ? SendNormal : SendAbnormal; + const test = + // no case or all passed + !response?.testResult?.length || response?.testResult?.every((t) => t.passed) + ? TestNormal + : TestAbnormal; + + const status = send | test; + if (status === SendAbnormalTestAbnormal) { + console.log('SendAbnormalTestAbnormal', key); + } + + if (!statusMap.has(status)) { + statusMap.set(status, []); + } + statusMap.get(status)!.push(blockMap.get(key)!); + } + return statusMap; +} + +const GroupCard = (props: { title: string; children?: ReactElement[] }) => { + return ( + <Card + size='small' + title={props.title} + style={{ display: props.children?.length ? '' : 'none', marginBottom: 8 }} + > + <div style={{ display: 'flex', flexFlow: 'row wrap' }}>{props.children}</div> + </Card> + ); +}; + +export function ByStatus(props: GroupProps) { + const { t } = useTranslation('page'); + const { blockMap } = props; + const statusMap = buildStatusMap(blockMap); + return ( + <> + <GroupCard title={t('batchRunPage.sendNormalTestAbnormal')}> + {statusMap.get(SendNormalTestAbnormal)} + </GroupCard> + <GroupCard title={t('batchRunPage.sendAbnormalTestNormal')}> + {statusMap.get(SendAbnormalTestNormal)} + </GroupCard> + <GroupCard title={t('batchRunPage.sendAbnormalTestAbnormal')}> + {statusMap.get(SendAbnormalTestAbnormal)} + </GroupCard> + <GroupCard title={t('batchRunPage.sendNormalTestNormal')}> + {statusMap.get(SendNormalTestNormal)} + </GroupCard> + </> + ); +} diff --git a/packages/arex/src/panes/BatchRun/BatchRunResultGroup/Flat.tsx b/packages/arex/src/panes/BatchRun/BatchRunResultGroup/Flat.tsx new file mode 100644 index 00000000..5c56d5a8 --- /dev/null +++ b/packages/arex/src/panes/BatchRun/BatchRunResultGroup/Flat.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; + +import { GroupProps } from '@/panes/BatchRun/BatchRunResultGroup/common'; + +export function Flat(props: GroupProps) { + const { blockMap } = props; + return ( + <> + <div style={{ display: 'flex', flexFlow: 'row wrap' }}> + {...Array.from(blockMap.values())} + </div> + </> + ); +} diff --git a/packages/arex/src/panes/BatchRun/BatchRunResultGroup/common.ts b/packages/arex/src/panes/BatchRun/BatchRunResultGroup/common.ts new file mode 100644 index 00000000..f89db5d3 --- /dev/null +++ b/packages/arex/src/panes/BatchRun/BatchRunResultGroup/common.ts @@ -0,0 +1,8 @@ +import { ReactElement } from 'react'; + +import { RunResult } from '@/panes/BatchRun/BatchRun'; + +export type GroupProps = { + blockMap: Map<RunResult, ReactElement>; + selectedKey?: string; +}; diff --git a/packages/arex/src/panes/BatchRun/BatchRunResultItem.tsx b/packages/arex/src/panes/BatchRun/BatchRunResultItem.tsx index a8e91e35..f43501eb 100644 --- a/packages/arex/src/panes/BatchRun/BatchRunResultItem.tsx +++ b/packages/arex/src/panes/BatchRun/BatchRunResultItem.tsx @@ -97,7 +97,7 @@ const BatchRunResultItem: FC<BatchRunResultItemProps> = (props) => { <Card size='small'> <SpaceBetweenWrapper> <Space> - {nodeType === CollectionNodeType.case && <RequestMethodIcon.case />} + {/*{nodeType === CollectionNodeType.case && <RequestMethodIcon.case />}*/} {React.createElement(RequestMethodIcon[method], { // @ts-ignore style: { display: 'flex', width: 'max-content' }, @@ -145,10 +145,11 @@ const BatchRunResultItem: FC<BatchRunResultItemProps> = (props) => { contextmenu: false, }} value={ - props.request.body.body && - tryStringifyJson(tryParseJsonString(props.request.body.body), { - prettier: true, - }) + props.request.body.body + ? tryStringifyJson(tryParseJsonString(props.request.body.body), { + prettier: true, + }) + : '' } /> ) : ( @@ -188,12 +189,11 @@ const BatchRunResultItem: FC<BatchRunResultItemProps> = (props) => { contextmenu: false, }} value={ - // @ts-ignore - props.response?.response?.body && - // @ts-ignore - tryStringifyJson(tryParseJsonString(props.response?.response?.body), { - prettier: true, - }) + props.response?.response?.body + ? tryStringifyJson(tryParseJsonString(props.response.response.body), { + prettier: true, + }) + : '' } /> ) : ( diff --git a/packages/arex/src/panes/BatchRun/RequestTestStatusBlock.tsx b/packages/arex/src/panes/BatchRun/RequestTestStatusBlock.tsx index c7c97f7e..afb85c81 100644 --- a/packages/arex/src/panes/BatchRun/RequestTestStatusBlock.tsx +++ b/packages/arex/src/panes/BatchRun/RequestTestStatusBlock.tsx @@ -1,53 +1,34 @@ import { css } from '@arextest/arex-core'; -import { ArexEnvironment, ArexResponse, sendRequest } from '@arextest/arex-request'; +import { ArexEnvironment, ArexResponse } from '@arextest/arex-request'; import { ArexRESTRequest } from '@arextest/arex-request/src'; -import { useRequest } from 'ahooks'; import { theme } from 'antd'; -import React, { useEffect, useState } from 'react'; +import React from 'react'; + +import { RunResult } from '@/panes/BatchRun/BatchRun'; export type RequestTestStatusBlockProps = { - environment?: ArexEnvironment; - data: ArexRESTRequest; + data: RunResult; selected?: boolean; onClick?: (data: { request: ArexRESTRequest; response?: ArexResponse }) => void; }; const RequestTestStatusBlock = (props: RequestTestStatusBlockProps) => { + const { data } = props; const { token } = theme.useToken(); - const [init, setInit] = useState(true); - - const { data, loading, run, cancel } = useRequest<ArexResponse, any>( - () => sendRequest(props.data, props.environment), - { - manual: true, - onBefore: () => { - setInit(false); - }, - }, - ); + const { request, response } = data; - useEffect(() => { - run(); - return () => { - cancel(); - }; - }, []); + const testAllSuccess = response?.testResult?.every((test) => test.passed) ?? true; + const testAllFail = response?.testResult?.every((test) => !test.passed) ?? false; - const testAllSuccess = data?.testResult?.every((test) => test.passed) ?? true; - const testAllFail = data?.testResult?.every((test) => !test.passed) ?? false; - - const requestStatusColor = - init || loading - ? token.colorFillSecondary - : // @ts-ignore - data?.response?.statusCode < 300 - ? token.colorSuccess - : // @ts-ignore - data?.response?.statusCode < 400 - ? token.colorWarning - : token.colorError; + const requestStatusColor = !response + ? token.colorFillSecondary + : (response.response?.statusCode ?? 0) < 300 + ? token.colorSuccess + : (response.response?.statusCode ?? 0) < 400 + ? token.colorWarning + : token.colorError; - const testResultStatusColor = data?.testResult?.length + const testResultStatusColor = response?.testResult?.length ? testAllSuccess ? token.colorSuccess : testAllFail @@ -59,8 +40,8 @@ const RequestTestStatusBlock = (props: RequestTestStatusBlockProps) => { <div onClick={() => { props.onClick?.({ - request: props.data, - response: data, + request, + response, }); }} style={{ diff --git a/packages/arex/src/panes/BatchRun/RequestTestStatusMap.tsx b/packages/arex/src/panes/BatchRun/RequestTestStatusMap.tsx index b6aa2648..f44accea 100644 --- a/packages/arex/src/panes/BatchRun/RequestTestStatusMap.tsx +++ b/packages/arex/src/panes/BatchRun/RequestTestStatusMap.tsx @@ -1,11 +1,13 @@ import { QuestionCircleOutlined } from '@ant-design/icons'; import { useTranslation } from '@arextest/arex-core'; import { ArexEnvironment } from '@arextest/arex-request'; -import { ArexRESTRequest } from '@arextest/arex-request/src'; -import { useAutoAnimate } from '@formkit/auto-animate/react'; -import { Button, Flex, Popover, Typography } from 'antd'; -import React, { FC } from 'react'; +import { Card, Flex, Popover, Radio, Typography } from 'antd'; +import React, { FC, memo, ReactElement, useMemo, useState } from 'react'; +import { RunResult } from '@/panes/BatchRun/BatchRun'; +import { ByInterface } from '@/panes/BatchRun/BatchRunResultGroup/ByInterface'; +import { ByStatus } from '@/panes/BatchRun/BatchRunResultGroup/ByStatus'; +import { Flat } from '@/panes/BatchRun/BatchRunResultGroup/Flat'; import RequestTestStatusBlock, { RequestTestStatusBlockProps, } from '@/panes/BatchRun/RequestTestStatusBlock'; @@ -13,40 +15,70 @@ import StatusBlockTooltip from '@/panes/BatchRun/StatusBlockTooltip'; export type RequestTestStatusMapProps = { environment?: ArexEnvironment; - data: ArexRESTRequest[]; + data: RunResult[]; selectedKey?: React.Key; onClick?: RequestTestStatusBlockProps['onClick']; }; -const RequestTestStatusMap: FC<RequestTestStatusMapProps> = (props) => { - const { t } = useTranslation('page'); - if (!props.data || !Object.values(props.data).length) return null; +const UseGuide = memo(() => { + const { t } = useTranslation('page'); return ( - <div style={{ padding: '0 16px 4px', marginBottom: '4px' }}> - <div style={{ display: 'flex', flexFlow: 'row wrap' }}> - {props.data.map((data) => ( - <RequestTestStatusBlock - key={data.id} - data={data} - selected={props.selectedKey === data.id} - environment={props.environment} - onClick={props.onClick} + <Flex justify='space-between'> + <Flex> + <Typography.Text>{t('batchRunPage.checkRequestDetail')}</Typography.Text> + <Popover title={<StatusBlockTooltip />} overlayStyle={{ maxWidth: '500px' }}> + <QuestionCircleOutlined + style={{ + margin: '0 4px', + }} /> - ))} - </div> - <Flex justify='space-between'> - <Flex> - <Typography.Text type='secondary'>{t('batchRunPage.checkRequestDetail')}</Typography.Text> - <Popover title={<StatusBlockTooltip />} overlayStyle={{ maxWidth: '500px' }}> - <QuestionCircleOutlined - style={{ - margin: '0 4px', - display: Object.values(props.data).length ? 'inherit' : 'none', - }} - /> - </Popover> - </Flex> + </Popover> </Flex> + </Flex> + ); +}); + +type GroupBy = 'flat' | 'interface' | 'status' | 'testResult'; + +const RequestTestStatusMap: FC<RequestTestStatusMapProps> = (props) => { + const { t } = useTranslation('page'); + const { data } = props; + const [groupBy, setGroupBy] = useState<GroupBy>('flat'); + + const blockMap = useMemo(() => { + const result = new Map<RunResult, ReactElement>(); + data.forEach((item) => { + result.set( + item, + <RequestTestStatusBlock + key={item.request.id} + data={item} + selected={item.request.id === props.selectedKey} + onClick={props.onClick} + />, + ); + }); + return result; + }, [data, props.selectedKey]); + + if (!data || !Object.values(data).length) return null; + + return ( + <div style={{ padding: '16px 16px 0 16px' }}> + <Card + title={<UseGuide />} + extra={ + <Radio.Group value={groupBy} onChange={(e) => setGroupBy(e.target.value)}> + <Radio.Button value='flat'>{t('batchRunPage.flatten')}</Radio.Button> + <Radio.Button value='interface'>{t('batchRunPage.groupByInterface')}</Radio.Button> + <Radio.Button value='status'>{t('batchRunPage.groupByStatus')}</Radio.Button> + </Radio.Group> + } + > + {groupBy === 'flat' && <Flat blockMap={blockMap} />} + {groupBy === 'interface' && <ByInterface blockMap={blockMap} />} + {groupBy === 'status' && <ByStatus blockMap={blockMap} />} + </Card> </div> ); };