diff --git a/src/assets/css/components/index.scss b/src/assets/css/components/index.scss
index b739d2c4..85e89510 100644
--- a/src/assets/css/components/index.scss
+++ b/src/assets/css/components/index.scss
@@ -79,3 +79,4 @@
@import 'views/administration/Appearance.scss';
@import 'views/setting/knowledgeBase';
+@import "views/charts2/EntityDetailLine";
diff --git a/src/assets/css/components/views/charts2/EntityDetailLine.scss b/src/assets/css/components/views/charts2/EntityDetailLine.scss
new file mode 100644
index 00000000..e847b0c5
--- /dev/null
+++ b/src/assets/css/components/views/charts2/EntityDetailLine.scss
@@ -0,0 +1,40 @@
+.entity-detail-line {
+ height: 100%;
+ border-radius: 4px;
+
+ .line-header-right {
+ .panel__tools {
+ display: flex;
+ align-items: center;
+
+ & > .el-select {
+ width: 162px;
+ margin-right: 10px;
+
+ .select-prefix {
+ font-size: 14px;
+ color: #999;
+ padding: 0 6px 0 3px;
+ }
+
+ .el-input__inner {
+ font-size: 14px;
+ color: #353636;
+ }
+
+ .common-select {
+ top: 32px !important;
+ }
+ }
+
+ .panel__time {
+ display: flex;
+ }
+ }
+
+ .line-select-reference-line {
+ margin-left: 0 !important;
+ line-height: 1;
+ }
+ }
+}
diff --git a/src/main.js b/src/main.js
index 2a3aaabc..0ad8cf2a 100644
--- a/src/main.js
+++ b/src/main.js
@@ -9,7 +9,7 @@ import commonMixin from '@/mixins/common'
import { cancelWithChange, noData } from '@/utils/tools'
import { ClickOutside } from 'element-plus/lib/directives'
import i18n from '@/i18n'
-// import '@/mock/index.js'
+import '@/mock/index.js'
import hljsVuePlugin from '@highlightjs/vue-plugin'
import 'highlight.js/styles/color-brewer.css'
import '@/assets/css/main.scss' // 样式入口
diff --git a/src/mock/entity.js b/src/mock/entity.js
new file mode 100644
index 00000000..ea79d623
--- /dev/null
+++ b/src/mock/entity.js
@@ -0,0 +1,81 @@
+import Mock from 'mockjs'
+
+const openMock = true
+if (openMock) {
+ Mock.mock(new RegExp(BASE_CONFIG.baseUrl + 'interface/entityDetail/totalTrafficAnalysis.*'), 'get', function (requestObj) {
+ const titleList = ['totalBitsRate', 'inboundBitsRate', 'outboundBitsRate', 'internalBitsRate', 'throughBitsRate', 'other']
+ const arr = [{ type: 'Bits/s' }, { type: 'Packets/s' }, { type: 'Sessions/s' }]
+
+ for (let i = 0; i < arr.length; i++) {
+ for (const j in titleList) {
+ // 目前模拟数据仅支持1小时的时间选择范围
+ let startTime = JSON.parse(getQuery(requestObj.url).startTime)
+ const values = []
+ let max = 2975
+ let min = 0
+ if (titleList[j] === 'totalBitsRate') {
+ max = 4462975
+ min = 1162975
+ }
+ for (let i = 0; i < 101; i++) {
+ const random = Math.floor(Math.random() * (max - min) + min)
+ values.push([startTime, random])
+ startTime += 36
+ }
+
+ const newValues = JSON.parse(JSON.stringify(values))
+ const sortArr = newValues.sort((a, b) => a[1] - b[1])
+ const maxAnalysis = Math.floor(sortArr[sortArr.length - 1][1])
+ let avg = 0
+ let sum = 0
+ newValues.forEach((item) => {
+ sum += item[1]
+ })
+ avg = JSON.parse(sum / newValues.length)
+
+ const analysis = {
+ avg: avg,
+ max: maxAnalysis,
+ min: Math.floor(sortArr[0][1]),
+ p95: maxAnalysis * 0.95 // 模拟值,p95并未最大值的95%
+ }
+
+ // Metric为Packets/s时,没有other的tab选项
+ if (arr[i].type === 'Packets/s' && titleList[j] === 'other') {
+ analysis.avg = 0
+ }
+
+ if (arr[i].type === 'Sessions/s') {
+ // Metric为Sessions/s时,只有total选项,故total填充数据完毕终止循环,节省性能
+ arr[i].totalBitsRate = { values: values, analysis: analysis }
+ break
+ } else {
+ arr[i][titleList[j]] = { values: values, analysis: analysis }
+ }
+ }
+ }
+
+ return {
+ msg: 'success',
+ code: 200,
+ data: {
+ result: arr
+ }
+ }
+ })
+}
+
+const getQuery = (url) => {
+ // str为?之后的参数部分字符串
+ const str = url.substr(url.indexOf('?') + 1)
+ // arr每个元素都是完整的参数键值
+ const arr = str.split('&')
+ // result为存储参数键值的集合
+ const result = {}
+ for (let i = 0; i < arr.length; i++) {
+ // item的两个元素分别为参数名和参数值
+ const item = arr[i].split('=')
+ result[item[0]] = item[1]
+ }
+ return result
+}
diff --git a/src/mock/index.js b/src/mock/index.js
index 6d2019b6..9720bd9a 100644
--- a/src/mock/index.js
+++ b/src/mock/index.js
@@ -1,3 +1,4 @@
import './npm'
import './linkMonitor'
import './dns'
+import './entity'
diff --git a/src/store/index.js b/src/store/index.js
index 6aac6e55..32d52966 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -1,7 +1,6 @@
import { createStore } from 'vuex'
import user from './modules/user'
import panel from './modules/panel'
-import { storageKey } from '@/utils/constants'
const store = createStore({
modules: {
diff --git a/src/utils/api.js b/src/utils/api.js
index 840928e2..2fdb695e 100644
--- a/src/utils/api.js
+++ b/src/utils/api.js
@@ -232,6 +232,9 @@ export const api = {
totalTrafficAnalysis: '/interface/dns/overview/totalTrafficAnalysis',
eventChart: '/interface/dnsInsight/eventChart',
drilldownTrafficAnalysis: '/interface/dns/overview/drilldown/trafficAnalysis'
+ },
+ entity: {
+ totalTrafficAnalysis: 'interface/entityDetail/totalTrafficAnalysis'
}
}
diff --git a/src/views/charts2/charts/entityDetail/EntityDetailLine.vue b/src/views/charts2/charts/entityDetail/EntityDetailLine.vue
index 5dfaf19c..b1ffdc44 100644
--- a/src/views/charts2/charts/entityDetail/EntityDetailLine.vue
+++ b/src/views/charts2/charts/entityDetail/EntityDetailLine.vue
@@ -1,16 +1,669 @@
- {{entity}}
+
diff --git a/test/views/charts2/charts/entityDetail/EntityDetailLine.test.js b/test/views/charts2/charts/entityDetail/EntityDetailLine.test.js
new file mode 100644
index 00000000..2eecfb35
--- /dev/null
+++ b/test/views/charts2/charts/entityDetail/EntityDetailLine.test.js
@@ -0,0 +1,163 @@
+import EntityDetailLine from '@/views/charts2/charts/entityDetail/EntityDetailLine'
+import { mount } from '@vue/test-utils'
+import axios from 'axios'
+import mockData from './mockData/EntityDetailLine'
+
+const timeFilter = {
+ dateRangeValue: -1,
+ startTime: 1673244000000,
+ endTime: 1673247600000
+}
+const chart = {
+ id: 2108,
+ name: 'APP流量图',
+ i18n: '',
+ panelId: 20,
+ pid: 0,
+ type: 107,
+ x: 0,
+ y: 6,
+ w: 30,
+ h: 6,
+ children: [],
+ panel: {
+ id: 20,
+ name: 'APP entity detail'
+ },
+ i: 2108,
+ category: 'echarts',
+ firstShow: false,
+ moved: false
+}
+
+function init (query) {
+ require('vue-router').useRoute.mockReturnValue({ query: query || {} })
+}
+
+describe('views/charts2/charts/entityDetail/EntityDetailLine.vue测试', () => {
+ test('Metric=Bits/s,无数据(空数组)', async () => {
+ init()
+ axios.get.mockResolvedValue(mockData.empty)
+ const wrapper = mount(EntityDetailLine, {
+ propsData: {
+ timeFilter,
+ chart
+ }
+ })
+ // 延迟等待渲染。使用wrapper.vm.$nextTick有时不管用(例如组件中使用了setTimeout的时候)
+ await new Promise(resolve => setTimeout(async () => {
+ const textNode0 = await wrapper.get('[test-id="tabContent0"]')
+ const textNode1 = await wrapper.get('[test-id="tabContent1"]')
+ const textNode2 = await wrapper.get('[test-id="tabContent2"]')
+ const textNode3 = await wrapper.get('[test-id="tabContent3"]')
+ const textNode4 = await wrapper.get('[test-id="tabContent4"]')
+ const textNode5 = await wrapper.get('[test-id="tabContent5"]')
+ expect(textNode0.text()).toEqual('-')
+ expect(textNode1.text()).toEqual('-')
+ expect(textNode2.text()).toEqual('-')
+ expect(textNode3.text()).toEqual('-')
+ expect(textNode4.text()).toEqual('-')
+ expect(textNode5.text()).toEqual('-')
+ resolve()
+ }, 200))
+ })
+ test('Metric=Bits/s,0和大数值', async () => {
+ init()
+ axios.get.mockResolvedValue(mockData.bytes.boundary)
+ const wrapper = mount(EntityDetailLine, {
+ propsData: {
+ timeFilter,
+ chart
+ }
+ })
+ // 延迟等待渲染。使用wrapper.vm.$nextTick有时不管用(例如组件中使用了setTimeout的时候)
+ await new Promise(resolve => setTimeout(async () => {
+ // total页签固定显示,数据是0也一样
+ const titleNode0 = await wrapper.get('[test-id="tabTitle0"]')
+ const titleNode1 = await wrapper.get('[test-id="tabTitle2"]')
+ const titleNode2 = await wrapper.get('[test-id="tabTitle5"]')
+ const textNode0 = await wrapper.get('[test-id="tabContent0"]')
+ const textNode1 = await wrapper.get('[test-id="tabContent2"]')
+ const textNode2 = await wrapper.get('[test-id="tabContent5"]')
+ expect(titleNode0.text()).toEqual('network.total')
+ expect(titleNode1.text()).toEqual('network.outbound')
+ expect(titleNode2.text()).toEqual('network.other')
+ expect(textNode0.text()).toEqual('0bps')
+ expect(textNode1.text()).toEqual('95.23Ebps')
+ expect(textNode2.text()).toEqual('<1bps')
+ resolve()
+ }, 200))
+ })
+ test('Metric=Bits/s,点击第三个tab', async () => {
+ init()
+ // 模拟axios返回数据
+ axios.get.mockResolvedValue(mockData.common)
+ // 加载vue组件,获得实例
+ const wrapper = mount(EntityDetailLine, {
+ propsData: {
+ timeFilter,
+ chart
+ }
+ })
+
+ // 延迟等待渲染。使用wrapper.vm.$nextTick有时不管用(例如组件中使用了setTimeout的时候)
+ await new Promise(resolve => setTimeout(async () => {
+ const textNode0 = await wrapper.get('[test-id="tabContent0"]')
+ const textNode1 = await wrapper.get('[test-id="tabContent1"]')
+ const textNode2 = await wrapper.get('[test-id="tabContent2"]')
+ expect(textNode0.text()).toEqual('112.04Mbps')
+ expect(textNode1.text()).toEqual('18.59Mbps')
+ expect(textNode2.text()).toEqual('87.56Mbps')
+ resolve()
+ }, 200))
+
+ // 点击tab后,是否切换高亮状态
+ const textNode3 = await wrapper.get('[test-id="tab2"]')
+ await textNode3.trigger('click')
+ expect(textNode3.classes()).toContain('is-active')
+ })
+ test('Metric=Packets/s', async () => {
+ const query = { entityType: 'app', name: 'uplive', startTime: 1675388605, endTime: 1675410205, range: 60, metric: 'Packets/s' }
+ init(query)
+ // 模拟axios返回数据
+ axios.get.mockResolvedValue(mockData.common)
+ // 加载vue组件,获得实例
+ const wrapper = mount(EntityDetailLine, {
+ propsData: {
+ timeFilter,
+ chart,
+ metric: 'Packets/s'
+ }
+ })
+
+ await new Promise(resolve => setTimeout(async () => {
+ const textNode0 = await wrapper.get('[test-id="tabContent0"]')
+ const textNode1 = await wrapper.get('[test-id="tabContent1"]')
+ const textNode2 = await wrapper.get('[test-id="tabContent2"]')
+ expect(textNode0.text()).toEqual('14.06Kpackets/s')
+ expect(textNode1.text()).toEqual('4.24Kpackets/s')
+ expect(textNode2.text()).toEqual('9.17Kpackets/s')
+ resolve()
+ }, 200))
+ })
+ test('Metric=Sessions/s', async () => {
+ const query = { entityType: 'app', name: 'uplive', startTime: 1675388605, endTime: 1675410205, range: 60, metric: 'Sessions/s' }
+ init(query)
+ // 模拟axios返回数据
+ axios.get.mockResolvedValue(mockData.common)
+ // 加载vue组件,获得实例
+ const wrapper = mount(EntityDetailLine, {
+ propsData: {
+ timeFilter,
+ chart,
+ metric: 'Sessions/s'
+ }
+ })
+
+ await new Promise(resolve => setTimeout(async () => {
+ const textNode0 = await wrapper.get('[test-id="tabContent0"]')
+ expect(textNode0.text()).toEqual('29.89sessions/s')
+ resolve()
+ }, 200))
+ })
+})
diff --git a/test/views/charts2/charts/entityDetail/mockData/EntityDetailLine.js b/test/views/charts2/charts/entityDetail/mockData/EntityDetailLine.js
new file mode 100644
index 00000000..c811e2b1
--- /dev/null
+++ b/test/views/charts2/charts/entityDetail/mockData/EntityDetailLine.js
@@ -0,0 +1,207 @@
+const mockData = {
+ // 空
+ empty: {
+ data: {
+ status: 200,
+ code: 200,
+ data: {
+ resultType: 'object',
+ result: []
+ }
+ }
+ },
+ bytes: {
+ // 边界
+ boundary: {
+ data: {
+ status: 200,
+ code: 200,
+ data: {
+ resultType: 'object',
+ result: [
+ {
+ type: 'bytes',
+ totalBitsRate: {
+ analysis: {
+ avg: '0'
+ }
+ },
+ inboundBitsRate: {
+ analysis: {
+ avg: '0'
+ }
+ },
+ outboundBitsRate: {
+ analysis: {
+ avg: '95229000000000000000'
+ }
+ },
+ internalBitsRate: {
+ analysis: {
+ avg: '0'
+ }
+ },
+ throughBitsRate: {
+ analysis: {
+ avg: '0'
+ }
+ },
+ other: {
+ analysis: {
+ avg: '0.01'
+ }
+ }
+ },
+ {
+ type: 'packets'
+ },
+ {
+ type: 'sessions'
+ }
+ ]
+ },
+ msg: 'OK'
+ }
+ }
+ },
+ // 正常数据
+ common: {
+ data: {
+ status: 200,
+ code: 200,
+ data: {
+ resultType: 'object',
+ result: [
+ {
+ type: 'bytes',
+ totalBitsRate: {
+ values: [[1673247564, '96801019.52']],
+ analysis: {
+ avg: '112042808.24',
+ max: '301842105.76',
+ min: '52096324',
+ p95: '168089003.35199997'
+ }
+ },
+ inboundBitsRate: {
+ values: [[1673247564, '11814508.48']],
+ analysis: {
+ avg: '18587597.36',
+ max: '137528138.88',
+ min: '3181142.88',
+ p95: '49561521.447999954'
+ }
+ },
+ outboundBitsRate: {
+ values: [[1673247564, '84282965.52']],
+ analysis: {
+ avg: '87557861.44',
+ max: '290402258',
+ min: '45337684.48',
+ p95: '121041718.81199999'
+ }
+ },
+ internalBitsRate: {
+ values: [[1673247564, '9125.12']],
+ analysis: {
+ avg: '278114.32',
+ max: '2215460.48',
+ min: '0',
+ p95: '923494.5719999998'
+ }
+ },
+ throughBitsRate: {
+ values: [[1673247564, '694420.48']],
+ analysis: {
+ avg: '5619235.12',
+ max: '42455480.24',
+ min: '262607.76',
+ p95: '13559588.195999999'
+ }
+ },
+ other: {
+ values: [[1673247564, '0.00']],
+ analysis: {
+ avg: '0.01',
+ max: '0.08',
+ min: '0.00',
+ p95: '0.08'
+ }
+ }
+ },
+ {
+ type: 'packets',
+ totalPacketsRate: {
+ values: [[1673247564, '12077.53']],
+ analysis: {
+ avg: '14062.37',
+ max: '32840.42',
+ min: '6564.17',
+ p95: '20923.167999999987'
+ }
+ },
+ inboundPacketsRate: {
+ values: [[1673247564, '3865.58']],
+ analysis: {
+ avg: '4241.61',
+ max: '15460.03',
+ min: '1918.22',
+ p95: '8549.799999999997'
+ }
+ },
+ outboundPacketsRate: {
+ values: [[1673247564, '8118.89']],
+ analysis: {
+ avg: '9170.98',
+ max: '27134.58',
+ min: '4510.25',
+ p95: '13690.540999999996'
+ }
+ },
+ internalPacketsRate: {
+ values: [[1673247564, '15.89']],
+ analysis: {
+ avg: '35.95',
+ max: '276.47',
+ min: '0.00',
+ p95: '122.49749999999999'
+ }
+ },
+ throughPacketsRate: {
+ values: [[1673247564, '77.17']],
+ analysis: {
+ avg: '613.82',
+ max: '3768.56',
+ min: '42.92',
+ p95: '1279.757499999999'
+ }
+ },
+ other: {
+ values: [[1673247564, '0.00']],
+ analysis: {
+ avg: '0',
+ max: '0.01',
+ min: '0.00',
+ p95: '0.01'
+ }
+ }
+ },
+ {
+ type: 'sessions',
+ totalSessionsRate: {
+ values: [[1673247564, '29.92']],
+ analysis: {
+ avg: '29.89',
+ max: '29.92',
+ min: '29.67',
+ p95: '29.92'
+ }
+ }
+ }]
+ },
+ msg: 'OK'
+ }
+ }
+}
+
+export default mockData