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 @@ 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