Browse Source

feat: 压测平台接口修改为分页, 开始压测改为添加 case

石玲燕 4 năm trước cách đây
mục cha
commit
7aa0c9ba92

+ 2 - 0
package.json

@@ -10,6 +10,8 @@
     "antd": "^3.26.18",
     "axios": "^0.19.0",
     "dva": "^2.4.1",
+    "echarts": "^5.1.2",
+    "echarts-for-react": "^3.0.1",
     "lodash": "^4.17.15",
     "lodash.clonedeep": "^4.5.0",
     "lodash.throttle": "^4.1.1",

+ 3 - 22
src/pages/halberd/components/addTask/index.js

@@ -34,12 +34,6 @@ class Index extends Component {
   // 设置属性字段
   setDetail (i, v, k) {
     const { params } = this.state
-    if (k === 'rate') {
-      const reg = /^-?[0-9]*(\.[0-9]*)?$/
-      if (!((!isNaN(v) && reg.test(v)) || v === '' || v === '-')) {
-        return
-      }
-    }
     if (params.detail[i]) {
       params.detail[i][k] = v
     }
@@ -58,7 +52,6 @@ class Index extends Component {
         detail: [
           ...params.detail,
           {
-            rate: '',
             url: ''
           }
         ]
@@ -92,6 +85,7 @@ class Index extends Component {
     }
     const { params = {} } = this.state
     const { detail = [] } = params
+
     // 接口内容空值判断
     if (!detail.length) {
       message.warning('压测 接口内容 不能为空!')
@@ -102,11 +96,6 @@ class Index extends Component {
       message.warning('压测 接口 url 不能为空!')
       return
     }
-    // 接口占比 空值判断
-    if (!detail.every((value) => value.rate)) {
-      message.warning('压测 接口占比 不能为空!')
-      return
-    }
     if (typeof this.props.onOk === 'function') { this.props.onOk({ ...params }) }
     if (typeof cb === 'function') {
       // eslint-disable-next-line standard/no-callback-literal
@@ -176,15 +165,7 @@ class Index extends Component {
                     }
                   />
                     
-                  <Input
-                    value={d.rate}
-                    style={{ width: 100 }}
-                    placeholder="接口占比"
-                    onChange={(e) =>
-                      this.setDetail(i, e.target.value, 'rate')
-                    }
-                  />
-                  &nbsp;&nbsp;
+                  {update && <span style={{ width: 130 }}>接口占比:{d.rate}&nbsp;&nbsp;</span>}
                   <Button
                     type="dashed"
                     icon="minus"
@@ -228,7 +209,7 @@ class Index extends Component {
       <Modal
         title={update ? '查看压测任务' : '添加压测任务'}
         visible={showModal}
-        width={900}
+        width={940}
         onOk={this.onOk}
         okText={update ? '更新' : '确认'}
         onCancel={onCancel}

+ 73 - 0
src/pages/halberd/components/psReport/index.js

@@ -0,0 +1,73 @@
+import React from 'react'
+import ReactECharts from 'echarts-for-react'
+import * as echarts from 'echarts'
+const Page = (props) => {
+  const [cross, setCross] = React.useState([])
+  const [vertical, setVertical] = React.useState([])
+
+  React.useEffect(() => {
+    const { pressureData = {} } = props
+    setCross(pressureData.cross)
+    setVertical(pressureData.vertical)
+  }, [])
+
+  const options = {
+    tooltip: {
+      trigger: 'axis',
+      position: function (pt) {
+        return [pt[0], '10%']
+      }
+    },
+    title: {
+      left: 'center',
+      text: '压测报告界面折线图'
+    },
+    toolbox: {
+      feature: {
+        saveAsImage: {}
+      }
+    },
+    xAxis: {
+      type: 'category',
+      boundaryGap: false,
+      data: cross
+    },
+    yAxis: {
+      type: 'value',
+      boundaryGap: [0, '100%']
+    },
+    dataZoom: [{
+      type: 'inside',
+      start: 0,
+      end: 100
+    }, {
+      start: 0,
+      end: 100
+    }],
+    series: [
+      {
+        name: '数据',
+        type: 'line',
+        symbol: 'none',
+        sampling: 'lttb',
+        itemStyle: {
+          color: 'rgb(255, 70, 131)'
+        },
+        areaStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
+            offset: 0,
+            color: 'rgb(255, 158, 68)'
+          }, {
+            offset: 1,
+            color: 'rgb(255, 70, 131)'
+          }])
+        },
+        data: vertical
+      }
+    ]
+  }
+
+  return <ReactECharts option={options} />
+}
+
+export default Page

+ 1 - 1
src/pages/halberd/components/startTask/index.js

@@ -113,7 +113,7 @@ class Index extends Component {
     ]
     return (
       <Modal
-        title={'开始压测'}
+        title={'添加 case'}
         visible={showModal}
         width={900}
         onOk={this.onOk}

+ 261 - 81
src/pages/halberd/index.js

@@ -1,18 +1,13 @@
 import React from 'react'
-import { Button, Input, Table, Badge, Popconfirm } from 'antd'
+import { Button, Table, Badge, Popconfirm, Checkbox, Switch } from 'antd'
 import AddTaskModal from './components/addTask'
 import StartTaskModal from './components/startTask'
 import router from 'umi/router'
-
-import {
-  taskBegin,
-  taskStop,
-  taskUpdate,
-  taskAdd,
-  taskCopy,
-  taskList
-} from './service'
-const { Search } = Input
+import { FilterTable } from 'wptpc-design'
+import { yc } from '@/conf/config'
+import Style from './index.less'
+import { taskBegin, taskStop, taskUpdate, taskAdd, taskCopy, caseAdd, caseDel } from './service'
+const apiUrl = `${yc}/schedule/task/list`
 
 const scheduleState = {
   0: {
@@ -40,9 +35,14 @@ class Halberd extends React.PureComponent {
     addFormVisible: false,
     updateFormVisible: false,
     startTaskVisible: false,
-    params: { detail: [] }
+    params: { detail: [] },
+    copyNeedCase: false,
+    copyNeedNew: false,
+    refreshChecked: false
   };
 
+  refreshTime = 0;
+
   columns = [
     {
       title: '压测任务',
@@ -67,11 +67,45 @@ class Halberd extends React.PureComponent {
       title: '操作',
       key: '_id',
       render: value => {
+        const { copyNeedCase, copyNeedNew } = this.state
+        const titleNode = (
+          <div>
+            确定要复制吗?
+            <div>
+              <Checkbox
+                checked={copyNeedCase}
+                onChange={e => {
+                  this.setState({
+                    copyNeedCase: e.target.checked
+                  })
+                }}
+              >
+                复制 case
+              </Checkbox>
+            </div>
+            <div>
+              <Checkbox
+                checked={copyNeedNew}
+                onChange={e => {
+                  this.setState({
+                    copyNeedNew: e.target.checked
+                  })
+                }}
+              >
+                重新构造数据
+              </Checkbox>
+            </div>
+          </div>
+        )
         return (
           <div>
             <Popconfirm
-              title="确定复制吗?"
-              onConfirm={() => this.copyTask(value._id)}
+              title={titleNode}
+              onConfirm={() => {
+                this.copyTask(value._id, copyNeedCase, copyNeedNew)
+                this.clearCopyState()
+              }}
+              onCancel={this.clearCopyState}
               okText="确定"
               cancelText="取消"
             >
@@ -93,13 +127,12 @@ class Halberd extends React.PureComponent {
             <Button
               type="primary"
               size={'small'}
-              disabled={value.state !== 1 && value.state !== 3}
               style={{ margin: '0 5px' }}
               onClick={() => {
                 this.showStartTaskModal(value._id)
               }}
             >
-              开始压测
+              添加 case
             </Button>
             <Popconfirm
               title="确定停止吗?"
@@ -127,40 +160,20 @@ class Halberd extends React.PureComponent {
     }
   ];
 
-  componentDidMount () {
-    this.getTackList()
-  }
-
-  /**
-   * 获取压测任务列表
-   * @param {*} name
-   */
-  getTackList = (name = '') => {
-    taskList({
-      page: 1, // 页数
-      pageNum: 10000, // 一页展示数量
-      taskName: name // 任务名称
-    }).then(res => {
-      const { code, data } = res
-      if (code === 0) {
-        this.setState({
-          data: data.list || []
-        })
-      }
-    })
-  };
-
   /**
    * 复制任务
    * @param {*} id
    */
-  copyTask = id => {
+  copyTask = (id, needCase, needNew) => {
+    console.log('copy', id, needCase, needNew)
     taskCopy({
-      id // 需要复制的任务id
+      id, // 需要复制的任务id
+      needCase: Number(needCase), // 是否需要复制case,0不需要 1需要
+      needNew: Number(needNew) // 是否更新数据,0不需要 1需要
     }).then(res => {
       const { code } = res
       if (code === 0) {
-        this.getTackList()
+        this.refresh()
       }
     })
   };
@@ -175,35 +188,25 @@ class Halberd extends React.PureComponent {
     }).then(res => {
       const { code } = res
       if (code === 0) {
-        this.getTackList()
+        this.refresh()
       }
     })
-  }
+  };
 
   /**
    * 打开查看详情弹窗
    * @param {*} id
    */
   viewTaskDetail = value => {
-    const { data } = this.state
     // eslint-disable-next-line camelcase
     const { _id: id, name, get_token_url } = value
-    let detail = ''
-
-    data.forEach(item => {
-      if (item._id === id) {
-        console.log('item.detail', value, data)
-
-        detail = item.detail
-      }
-    })
     this.setState({
       updateFormVisible: true,
       params: {
         id,
         name: name,
         getTokenUrl: get_token_url,
-        detail: JSON.parse(detail)
+        detail: JSON.parse(value.detail)
       }
     })
   };
@@ -227,7 +230,7 @@ class Halberd extends React.PureComponent {
    */
   showViewReport = id => {
     router.push(`/halberd/viewReport?id=${id}`)
-  }
+  };
 
   /**
    * 打开添加弹窗
@@ -258,17 +261,19 @@ class Halberd extends React.PureComponent {
     taskAdd({
       name,
       getTokenUrl: getTokenUrl,
-      detail: JSON.stringify(detail.map(item => {
-        return {
-          ...item,
-          rate: Number(item.rate)
-        }
-      }))
+      detail: JSON.stringify(
+        detail.map(item => {
+          return {
+            ...item,
+            rate: Number(item.rate)
+          }
+        })
+      )
     }).then(res => {
       const { code } = res
       if (code === 0) {
         this.hidden()
-        this.getTackList()
+        this.refresh()
       }
     })
   };
@@ -279,7 +284,6 @@ class Halberd extends React.PureComponent {
    */
   updateTask = data => {
     const { name = '', getTokenUrl, detail = [] } = data
-
     taskUpdate({
       id: data.id,
       name,
@@ -295,14 +299,13 @@ class Halberd extends React.PureComponent {
   };
 
   /**
-   * 开始压测任务
+   * 添加 case
    * @param {*} data
    */
-  startTask = data => {
-    console.log(data)
+  taskCaseAdd = data => {
     const { id, connects, threads, timeout, duration } = data
-    taskBegin({
-      id,
+    caseAdd({
+      TaskId: id,
       connects: Number(connects),
       threads: Number(threads),
       timeout: Number(timeout),
@@ -311,28 +314,205 @@ class Halberd extends React.PureComponent {
       const { code } = res
       if (code === 0) {
         this.hidden()
-        this.getTackList()
+        this.refresh()
       }
     })
   };
 
+  /**
+   * 开始压测 case 任务
+   * @param {*} data
+   */
+  startTask = (record, data) => {
+    const { _id: id } = record
+    const { _id } = data
+    taskBegin({
+      id,
+      caseId: _id
+    }).then(() => {
+      this.refresh()
+    })
+  };
+
+  /**
+   * 删除压测 case 任务
+   * @param {*} data
+   */
+  delTask = data => {
+    const { _id } = data
+    caseDel({
+      caseId: _id
+    }).then(() => {
+      this.refresh()
+    })
+  };
+
+  /**
+   * 清空拷贝状态
+   */
+  clearCopyState = () => {
+    this.setState({
+      copyNeedCase: false,
+      copyNeedNew: false
+    })
+  };
+
+  /**
+   * 开始轮训
+   */
+  startRefreshState = () => {
+    this.refresh()
+    this.refreshTime = setInterval(() => {
+      this.refresh()
+    }, 5000)
+  };
+
+  /**
+   * 结束轮训
+   */
+  stopRefreshState = () => {
+    clearInterval(this.refreshTime)
+  };
+
+  /**
+   * 展开行-获取压测case列表
+   */
+  onExpandGetCaseList = record => {
+    if (record.cases && record.cases.length) {
+      const columns = [
+        {
+          title: '线程数',
+          dataIndex: 'threads',
+          key: 'threads',
+          align: 'center',
+          width: 200
+        },
+        {
+          title: '链接数',
+          dataIndex: 'connects',
+          key: 'connects',
+          align: 'center',
+          width: 200
+        },
+        {
+          title: '接口超时时间',
+          dataIndex: 'timeout',
+          key: 'timeout',
+          align: 'center',
+          width: 200
+        },
+        {
+          title: '压测持续时间',
+          dataIndex: 'duration',
+          key: 'duration',
+          align: 'center',
+          width: 200
+        },
+        {
+          title: '操作',
+          align: 'center',
+          key: '_id',
+          render: cases => {
+            return (
+              <div>
+                {record.state === 1 || record.state === 3 ? (
+                  <React.Fragment>
+                    <Popconfirm
+                      title="确定开始压测吗?"
+                      onConfirm={() => this.startTask(record, cases)}
+                      okText="确定"
+                      cancelText="取消"
+                    >
+                      <a>开始</a>&nbsp;&nbsp;&nbsp;
+                    </Popconfirm>
+
+                    <Popconfirm
+                      title="确定删除该 case 吗?"
+                      onConfirm={() => this.delTask(cases)}
+                      okText="确定"
+                      cancelText="取消"
+                    >
+                      <a>删除</a>
+                    </Popconfirm>
+                  </React.Fragment>
+                ) : (
+                  <React.Fragment>
+                    <span style={{ color: 'gray' }}>开始</span>&nbsp;&nbsp;&nbsp;
+                    <span style={{ color: 'gray' }}>删除</span>
+                  </React.Fragment>
+                )}
+              </div>
+            )
+          }
+        }
+      ]
+      return (
+        <div>
+          <Table
+            rowKey="_id"
+            bordered
+            rowClassName={() => Style.rowClass}
+            columns={columns}
+            dataSource={record.cases}
+          />
+        </div>
+      )
+    }
+    return <div>当前任务没有 case</div>
+  };
+
   render () {
-    const { data, addFormVisible, updateFormVisible, startTaskVisible, params } = this.state
+    const { addFormVisible, updateFormVisible, startTaskVisible, params, refreshChecked } = this.state
+    // filtertable的搜索项配置
+    const filterSetting = {
+      isClearSearch: true,
+      // 这个数组里面的每一个元素就是一个搜索项
+      formFields: [
+        {
+          type: 'input',
+          key: 'taskName',
+          placeholder: '请输入压测任务名进行搜索',
+          style: { width: 400 }
+        }
+      ],
+      // 在接口请求前,可以修改给接口的入参
+      beforeSearchFunc: params => {
+        params.pageSize = params.pageNum
+      }
+    }
+
+    // filtertable的列表配置
+    const tableSetting = {
+      rowKey: '_id',
+      pagination: {
+        pageSize: 10
+      },
+      rowClassName: Style.filterRow,
+      expandedRowRender: this.onExpandGetCaseList,
+      columnConfig: this.columns,
+      getRefresh: refresh => {
+        this.refresh = refresh
+      }
+    }
     return (
       <div style={{ margin: '0 auto' }}>
-        <Search
-          placeholder="请输入压测任务名进行搜索"
-          enterButton="搜索"
-          onSearch={value => this.getTackList(value)}
-          style={{ width: 400 }}
-        />
-
         <div style={{ margin: '20px 0' }}>
           <Button type="primary" onClick={this.showAddTask}>
-            添加
+            添加压测任务
           </Button>
+          <span style={{ float: 'right', marginRight: '120px' }}>定时刷新:&nbsp;<Switch checked={refreshChecked} onChange={(checked) => {
+            if (checked) {
+              this.startRefreshState()
+            } else {
+              this.stopRefreshState()
+            }
+            this.setState({
+              refreshChecked: checked
+            })
+          }}/></span>
         </div>
-        <Table dataSource={data} columns={this.columns} rowKey="_id" />
+        <FilterTable filterSetting={filterSetting} tableSetting={tableSetting} apiUrl={apiUrl} />
+
         {addFormVisible && (
           <AddTaskModal showModal={addFormVisible} onOk={this.addTask} onCancel={this.hidden} />
         )}
@@ -348,7 +528,7 @@ class Halberd extends React.PureComponent {
         {startTaskVisible && (
           <StartTaskModal
             showModal={startTaskVisible}
-            onOk={this.startTask}
+            onOk={this.taskCaseAdd}
             onCancel={this.hidden}
             params={params}
           />

+ 13 - 0
src/pages/halberd/index.less

@@ -0,0 +1,13 @@
+
+.filterRow {
+  td {
+    padding: 16px !important;
+  }
+}
+.rowClass {
+  td {
+    padding: 5px !important;
+  }
+}
+
+

+ 18 - 0
src/pages/halberd/service.js

@@ -15,6 +15,24 @@ export async function taskStop (params) {
   return fetchApi(url, params)
 }
 
+// 添加 case
+export async function caseAdd (params) {
+  const url = `${yc}/schedule/task/case/add`
+  return fetchApi(url, params)
+}
+
+// 删除 case
+export async function caseDel (params) {
+  const url = `${yc}/schedule/task/case/del`
+  return fetchApi(url, params)
+}
+
+// 非2xx,3xx 请求数据详情
+export async function eptResponse (params) {
+  const url = `${yc}/schedule/task/ept-response`
+  return fetchApi(url, params)
+}
+
 // 查看压测报告
 export async function viewReport (params) {
   const url = `${yc}/schedule/task/view/report`

+ 350 - 215
src/pages/halberd/viewReport/index.js

@@ -1,17 +1,17 @@
 import React, { Component } from 'react'
-import { Table, PageHeader } from 'antd'
+import { Table, PageHeader, Modal, Pagination, Icon } from 'antd'
 import get from 'lodash/get'
 import router from 'umi/router'
 import moment from 'moment'
-import {
-  viewReport
-} from '../service'
+import { viewReport, eptResponse } from '../service'
 import Styles from './index.less'
+import PsReport from '../components/psReport'
 class Index extends Component {
   state = {
     data: null,
     params: null,
     dataSource: [],
+    showModal: false,
     task: {
       name: ''
     }
@@ -31,13 +31,224 @@ class Index extends Component {
     }
   }
 
+  eptID = '';
+  columns = [
+    {
+      title: '执行人',
+      key: 'perform_user',
+      align: 'center',
+      width: 100,
+      render: data => {
+        // eslint-disable-next-line camelcase
+        const { pressure_data = {} } = data
+        return (<div>
+          {!!(pressure_data.cross && pressure_data.cross.length) && <a onClick={() => {
+            this.setState({
+              pressureData: pressure_data,
+              showPsReportModal: true
+            })
+          }}><Icon type="line-chart" />&nbsp;</a>}
+          {data.perform_user}</div>)
+      }
+    },
+    {
+      title: '线程数',
+      dataIndex: 'threads',
+      key: 'threads',
+      align: 'center'
+    },
+    {
+      title: '链接数',
+      dataIndex: 'connects',
+      key: 'connects',
+      align: 'center'
+    },
+    {
+      title: '接口超过时间',
+      dataIndex: 'timeout',
+      key: 'timeout',
+      align: 'center'
+    },
+    {
+      title: '持续时间',
+      dataIndex: 'duration',
+      key: 'duration',
+      align: 'center'
+    },
+    {
+      title: '完成时间',
+      dataIndex: 'finish_time',
+      key: 'finish_time',
+      width: 150,
+      align: 'center'
+    },
+    {
+      title: '总计',
+      children: [
+        {
+          title: '发送数量',
+          dataIndex: 'summary_bytes',
+          key: 'summary_bytes',
+          align: 'center'
+        },
+        {
+          title: '总请求数',
+          dataIndex: 'summary_requests',
+          key: 'summary_requests',
+          align: 'center'
+        },
+        {
+          title: 'QPS',
+          dataIndex: 'summary_req_sec',
+          key: 'summary_req_sec',
+          align: 'center'
+        }
+      ]
+    },
+    {
+      title: '错误信息',
+      children: [
+        {
+          title: '连接错误数',
+          dataIndex: 'err_connect',
+          key: 'err_connect',
+          align: 'center'
+        },
+        {
+          title: '读错误',
+          dataIndex: 'err_read',
+          key: 'err_read',
+          align: 'center'
+        },
+        {
+          title: '非 2xx/3xx',
+          key: 'err_status',
+          align: 'center',
+          render: error => {
+            return (
+              <a
+                onClick={() => {
+                  this.eptID = error._id
+                  this.getTaskEptResponse()
+                }}
+              >
+                {error.err_status}
+              </a>
+            )
+          }
+        },
+        {
+          title: '超时',
+          dataIndex: 'err_timeout',
+          key: 'err_timeout',
+          align: 'center'
+        },
+        {
+          title: '写错误',
+          dataIndex: 'err_write',
+          key: 'err_write',
+          align: 'center'
+        }
+      ]
+    },
+    {
+      title: '延迟分布',
+      children: [
+        {
+          title: '最大耗时',
+          dataIndex: 'latency_max',
+          key: 'latency_max',
+          align: 'center'
+        },
+        {
+          title: '平均',
+          dataIndex: 'latency_mean',
+          key: 'latency_mean',
+          align: 'center'
+        },
+        {
+          title: '最小耗时',
+          dataIndex: 'latency_min',
+          key: 'latency_min',
+          align: 'center'
+        },
+        {
+          title: '50%',
+          dataIndex: 'latency_p50',
+          key: 'latency_p50',
+          align: 'center'
+        },
+        {
+          title: '90%',
+          dataIndex: 'latency_p90',
+          key: 'latency_p90',
+          align: 'center'
+        },
+        {
+          title: '99%',
+          dataIndex: 'latency_p99',
+          key: 'latency_p99',
+          align: 'center'
+        },
+        {
+          title: '99.999%',
+          dataIndex: 'latency_p99.999',
+          key: 'latency_p99.999',
+          align: 'center'
+        },
+        {
+          title: '标准差',
+          dataIndex: 'latency_stdev',
+          key: 'latency_stdev',
+          align: 'center'
+        }
+      ]
+    },
+
+    {
+      title: '线程统计',
+      children: [
+        {
+          title: '最大值',
+          dataIndex: 'ts_max',
+          key: 'ts_max',
+          align: 'center'
+        },
+        {
+          title: '平均数',
+          dataIndex: 'ts_mean',
+          key: 'ts_mean',
+          align: 'center'
+        },
+        {
+          title: '最小值',
+          dataIndex: 'ts_min',
+          key: 'ts_min',
+          align: 'center'
+        },
+        {
+          title: '标准差',
+          dataIndex: 'ts_stdev',
+          key: 'ts_stdev',
+          align: 'center'
+        }
+      ]
+    },
+    {
+      title: '原因',
+      dataIndex: 'reason',
+      key: 'reason',
+      align: 'center'
+    }
+  ];
+
   componentDidMount () {
     const _id = get(this.props.location, ['query', 'id'])
     viewReport({
       id: _id, // 压测详情 id
       page: 1, // 页数
       pageNum: 10000 // 一页展示数量
-    }).then((res) => {
+    }).then(res => {
       const { code, data } = res
       if (code === 0) {
         this.setState({
@@ -45,7 +256,8 @@ class Index extends Component {
             return {
               ...item.report,
               ...item,
-              finish_time: item.finish_time && moment.unix(item.finish_time).format('YYYY-MM-DD HH:mm:ss')
+              finish_time:
+                item.finish_time && moment.unix(item.finish_time).format('YYYY-MM-DD HH:mm:ss')
             }
           }),
           task: data.task
@@ -54,218 +266,141 @@ class Index extends Component {
     })
   }
 
- columns = [
-   {
-     title: '执行人',
-     dataIndex: 'perform_user',
-     key: 'perform_user',
-     align: 'center',
-     className: Styles.head
-   },
-   {
-     title: '线程数',
-     dataIndex: 'threads',
-     key: 'threads',
-     align: 'center'
-   },
-   {
-     title: '链接数',
-     dataIndex: 'connects',
-     key: 'connects',
-     align: 'center'
-   },
-   {
-     title: '接口超过时间',
-     dataIndex: 'timeout',
-     key: 'timeout',
-     align: 'center'
-   },
-   {
-     title: '持续时间',
-     dataIndex: 'duration',
-     key: 'duration',
-     align: 'center'
-   },
-   {
-     title: '完成时间',
-     dataIndex: 'finish_time',
-     key: 'finish_time',
-     width: 150,
-     align: 'center'
-   },
-   {
-     title: '总计',
-     children: [
-       {
-         title: '发送数量',
-         dataIndex: 'summary_bytes',
-         key: 'summary_bytes',
-         align: 'center'
-       },
-       //  {
-       //    title: '持续时间',
-       //    dataIndex: 'summary_duration',
-       //    key: 'summary_duration',
-       //    align: 'center'
-       //  },
-       {
-         title: '总请求数',
-         dataIndex: 'summary_requests',
-         key: 'summary_requests',
-         align: 'center'
-       },
-       {
-         title: 'QPS',
-         dataIndex: 'summary_req_sec',
-         key: 'summary_req_sec',
-         align: 'center'
-       }
-     ]
-   },
-   {
-     title: '错误信息',
-     children: [
-       {
-         title: '连接错误数',
-         dataIndex: 'err_connect',
-         key: 'err_connect',
-         align: 'center'
-       },
-       {
-         title: '读错误',
-         dataIndex: 'err_read',
-         key: 'err_read',
-         align: 'center'
-       },
-       {
-         title: '非 2xx/3xx',
-         dataIndex: 'err_status',
-         key: 'err_status',
-         align: 'center'
-       },
-       {
-         title: '超时',
-         dataIndex: 'err_timeout',
-         key: 'err_timeout',
-         align: 'center'
-       },
-       {
-         title: '写错误',
-         dataIndex: 'err_write',
-         key: 'err_write',
-         align: 'center'
-       }
+  // 获取非 2xx/3xx 数据
+  getTaskEptResponse = (page = 1) => {
+    eptResponse({
+      page,
+      pageNum: 10,
+      eptID: this.eptID
+    }).then(res => {
+      const { data = {} } = res
+      this.setState({
+        modalDataSource: data,
+        showModal: true
+      })
+    })
+  };
 
-     ]
-   },
-   {
-     title: '延迟分布',
-     children: [
-       {
-         title: '最大耗时',
-         dataIndex: 'latency_max',
-         key: 'latency_max',
-         align: 'center'
-       },
-       {
-         title: '平均',
-         dataIndex: 'latency_mean',
-         key: 'latency_mean',
-         align: 'center'
-       },
-       {
-         title: '最小耗时',
-         dataIndex: 'latency_min',
-         key: 'latency_min',
-         align: 'center'
-       },
-       {
-         title: '50%',
-         dataIndex: 'latency_p50',
-         key: 'latency_p50',
-         align: 'center'
-       },
-       {
-         title: '90%',
-         dataIndex: 'latency_p90',
-         key: 'latency_p90',
-         align: 'center'
-       },
-       {
-         title: '99%',
-         dataIndex: 'latency_p99',
-         key: 'latency_p99',
-         align: 'center'
-       },
-       {
-         title: '99.999%',
-         dataIndex: 'latency_p99.999',
-         key: 'latency_p99.999',
-         align: 'center'
-       },
-       {
-         title: '标准差',
-         dataIndex: 'latency_stdev',
-         key: 'latency_stdev',
-         align: 'center'
-       }
-     ]
-   },
+  /**
+   * 隐藏 modal
+   */
+  hidden = () => {
+    this.setState({
+      showModal: false
+    })
+  };
+
+  /**
+   * 隐藏 折线图modal
+   */
+  hiddenPsReport = () => {
+    this.setState({
+      showPsReportModal: false
+    })
+  }
 
-   {
-     title: '线程统计',
-     children: [
-       {
-         title: '最大值',
-         dataIndex: 'ts_max',
-         key: 'ts_max',
-         align: 'center'
-       },
-       {
-         title: '平均数',
-         dataIndex: 'ts_mean',
-         key: 'ts_mean',
-         align: 'center'
-       },
-       {
-         title: '最小值',
-         dataIndex: 'ts_min',
-         key: 'ts_min',
-         align: 'center'
-       },
-       {
-         title: '标准差',
-         dataIndex: 'ts_stdev',
-         key: 'ts_stdev',
-         align: 'center'
-       }
-     ]
-   }
+  render () {
+    const { dataSource, modalDataSource = {}, task = {}, showModal, showPsReportModal, pressureData } = this.state
+    const modalColumns = [
+      {
+        title: '时间',
+        dataIndex: 'time_stamp',
+        key: 'time_stamp',
+        ellipsis: true
+      },
+      {
+        title: '压测 URI',
+        dataIndex: 'url',
+        key: 'url',
+        ellipsis: true
+      },
+      {
+        title: 'UrlQuery',
+        dataIndex: 'url_query',
+        key: 'url_query',
+        ellipsis: true
+      },
+      {
+        title: 'Header',
+        dataIndex: 'header',
+        key: 'header',
+        ellipsis: true
+      },
+      {
+        title: 'RequestBody',
+        dataIndex: 'request_body',
+        key: 'request_body',
+        width: 400,
+        render: (data) => {
+          return <pre style={{ width: 400, maxHeight: 150 }}>{data}</pre>
+        }
+      },
+      {
+        title: 'Response',
+        dataIndex: 'response',
+        key: 'response'
+      }
+    ]
+    return (
+      <div>
+        {dataSource && <React.Fragment>
+          <PageHeader
+            style={{
+              border: '1px solid rgb(235, 237, 240)'
+            }}
+            onBack={() => {
+              router.push('/halberd')
+            }}
+            title={`${task.name || ''}-压测报告`}
+          />
+          <Table
+            columns={this.columns}
+            className={Styles.head}
+            bordered
+            rowKey="_id"
+            dataSource={dataSource}
+            scroll={{ x: '170%' }}
+          />
 
- ];
+        </React.Fragment>}
 
- render () {
-   const { dataSource, task = {} } = this.state
-   return (
-     <div>
-       <PageHeader
-         style={{
-           border: '1px solid rgb(235, 237, 240)'
-         }}
-         onBack={() => {
-           router.push('/halberd')
-         }}
-         title={`${task.name || ''}-压测报告`}
-       />
-       <Table
-         columns={this.columns}
-         className={Styles.head}
-         bordered
-         rowKey="_id"
-         dataSource={dataSource}
-         scroll={{ x: '170%' }}/>
-     </div>
-   )
- }
+        <Modal
+          visible={showPsReportModal}
+          width={900}
+          destroyOnClose
+          closable={false}
+          onCancel={() => {
+            this.hiddenPsReport()
+          }}
+          onOk={() => {
+            this.hiddenPsReport()
+          }}
+        >
+          <PsReport pressureData={pressureData}/>
+        </Modal>
+        <Modal
+          visible={showModal}
+          width={1200}
+          destroyOnClose
+          onCancel={() => {
+            this.hidden()
+          }}
+          onOk={() => {
+            this.hidden()
+          }}
+        >
+          <Table columns={modalColumns} dataSource={modalDataSource.list} pagination={false} />
+          <div style={{ textAlign: 'center', marginTop: '24px' }}>
+            <Pagination
+              onChange={page => {
+                this.getTaskEptResponse(page)
+              }}
+              total={modalDataSource.total || 0}
+            />
+          </div>
+        </Modal>
+      </div>
+    )
+  }
 }
 export default Index

+ 38 - 0
yarn.lock

@@ -5962,6 +5962,22 @@ ecc-jsbn@~0.1.1:
     jsbn "~0.1.0"
     safer-buffer "^2.1.0"
 
+echarts-for-react@^3.0.1:
+  version "3.0.1"
+  resolved "http://npm.wpt.la/echarts-for-react/-/echarts-for-react-3.0.1.tgz#cf9de60e468b342c244314be2325e00d765a5137"
+  integrity sha1-z53mDkaLNCwkQxS+IyXgDXZaUTc=
+  dependencies:
+    fast-deep-equal "^3.1.3"
+    size-sensor "^1.0.1"
+
+echarts@^5.1.2:
+  version "5.1.2"
+  resolved "http://npm.wpt.la/echarts/-/echarts-5.1.2.tgz#aa1ab0cef5b74fa2f7c620261a5f286893d30fd1"
+  integrity sha1-qhqwzvW3T6L3xiAmGl8oaJPTD9E=
+  dependencies:
+    tslib "2.0.3"
+    zrender "5.1.1"
+
 editions@^2.1.3:
   version "2.2.0"
   resolved "http://npm.wpt.la/editions/-/editions-2.2.0.tgz#dacd0c2a9441ebef592bba316a6264febb337f35"
@@ -7053,6 +7069,11 @@ fast-deep-equal@^2.0.1:
   resolved "http://npm.wpt.la/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
   integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
 
+fast-deep-equal@^3.1.3:
+  version "3.1.3"
+  resolved "http://npm.wpt.la/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+  integrity sha1-On1WtVnWy8PrUSMlJE5hmmXGxSU=
+
 fast-glob@^2.0.2, fast-glob@^2.2.6:
   version "2.2.7"
   resolved "http://npm.wpt.la/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d"
@@ -15755,6 +15776,11 @@ sisteransi@^1.0.3:
   resolved "http://npm.wpt.la/sisteransi/-/sisteransi-1.0.3.tgz#98168d62b79e3a5e758e27ae63c4a053d748f4eb"
   integrity sha1-mBaNYreeOl51jieuY8SgU9dI9Os=
 
+size-sensor@^1.0.1:
+  version "1.0.1"
+  resolved "http://npm.wpt.la/size-sensor/-/size-sensor-1.0.1.tgz#f84e46206d3e259faff1d548e4b3beca93219dbb"
+  integrity sha1-+E5GIG0+JZ+v8dVI5LO+ypMhnbs=
+
 slash2@2.0.0:
   version "2.0.0"
   resolved "http://npm.wpt.la/slash2/-/slash2-2.0.0.tgz#f4e0a11708b8545b912695981cf7096f52c63487"
@@ -16971,6 +16997,11 @@ tslib@1.11.1:
   resolved "http://npm.wpt.la/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
   integrity sha1-6xXRKIJ/vuKEFUnhcfRe0zisfjU=
 
+tslib@2.0.3:
+  version "2.0.3"
+  resolved "http://npm.wpt.la/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c"
+  integrity sha1-jgdBrEX8DCJuWKF7/D5kubxsphw=
+
 tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0:
   version "1.10.0"
   resolved "http://npm.wpt.la/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
@@ -18935,6 +18966,13 @@ yeoman-generator@4.0.1:
     through2 "^3.0.1"
     yeoman-environment "^2.3.4"
 
+zrender@5.1.1:
+  version "5.1.1"
+  resolved "http://npm.wpt.la/zrender/-/zrender-5.1.1.tgz#0515f4f8cc0f4742f02a6b8819550a6d13d64c5c"
+  integrity sha1-BRX0+MwPR0LwKmuIGVUKbRPWTFw=
+  dependencies:
+    tslib "2.0.3"
+
 zscroller@~0.4.0:
   version "0.4.8"
   resolved "http://npm.wpt.la/zscroller/-/zscroller-0.4.8.tgz#69eed68690808eedf81f9714014356b36cdd20f4"