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>
   );
 };