CN-982: 实体详情--流量折线图开发
This commit is contained in:
@@ -79,3 +79,4 @@
|
||||
@import 'views/administration/Appearance.scss';
|
||||
|
||||
@import 'views/setting/knowledgeBase';
|
||||
@import "views/charts2/EntityDetailLine";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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' // 样式入口
|
||||
|
||||
81
src/mock/entity.js
Normal file
81
src/mock/entity.js
Normal file
@@ -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
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import './npm'
|
||||
import './linkMonitor'
|
||||
import './dns'
|
||||
import './entity'
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,669 @@
|
||||
<template>
|
||||
<div style="border: 1px solid #ff5500; height: 100%;">{{entity}}</div>
|
||||
<div class="entity-detail-line">
|
||||
<div class="line network">
|
||||
<chart-error v-if="showError" :content="errorMsg"/>
|
||||
<div class="line-header" v-if="!showError">
|
||||
<div class="line-header-left">
|
||||
<div class="line-value-active" v-if="lineTab"></div>
|
||||
<div class="line-value">
|
||||
<template v-for="(item, index) in tabs">
|
||||
<div class="line-value-tabs"
|
||||
:class=" {'is-active': lineTab === item.class, 'mousemove-cursor': mousemoveCursor === item.class}"
|
||||
v-if="item.show"
|
||||
:key="index"
|
||||
@mouseenter="mouseenter(item)"
|
||||
@mouseleave="mouseleave(item)"
|
||||
@click="activeChange(item, index,true)"
|
||||
:test-id="`tab${index}`"
|
||||
>
|
||||
<div class="line-value-tabs-name">
|
||||
<div :class="item.class"></div>
|
||||
<div class="tabs-name" :test-id="`tabTitle${index}`">{{ $t(item.name) }}</div>
|
||||
</div>
|
||||
<div class="line-value-unit" :test-id="`tabContent${index}`">
|
||||
<span class="line-value-unit-number">
|
||||
{{ getTabUnit(item.analysis.avg) }}
|
||||
</span>
|
||||
<span class="line-value-unit-number2">
|
||||
<span>{{ unitConvert(item.analysis.avg, unitTypes.number)[1] }}</span>
|
||||
<span v-if="item.unitType">{{ item.unitType }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="line-select line-header-right">
|
||||
<div class="panel__tools">
|
||||
<div class="panel__time">
|
||||
<date-time-range
|
||||
class="date-time-range"
|
||||
:start-time="timeFilter.startTime"
|
||||
:end-time="timeFilter.endTime"
|
||||
:date-range="timeFilter.dateRangeValue"
|
||||
ref="dateTimeRange"
|
||||
@change="reload"
|
||||
/>
|
||||
</div>
|
||||
<el-select
|
||||
size="mini"
|
||||
v-model="metric"
|
||||
placeholder=""
|
||||
popper-class="common-select"
|
||||
:popper-append-to-body="false"
|
||||
@change="metricChange"
|
||||
>
|
||||
<template #prefix>
|
||||
<span class="select-prefix">Metric:</span>
|
||||
</template>
|
||||
<el-option v-for="item in metricOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="line-select-reference-line">
|
||||
<span>{{ $t('network.referenceLine') }}:</span>
|
||||
<div class="line-select__operation">
|
||||
<el-select
|
||||
size="mini"
|
||||
v-model="lineRefer"
|
||||
:disabled="!lineTab"
|
||||
popper-class="common-select"
|
||||
:popper-append-to-body="false"
|
||||
@change="referenceSelectChange"
|
||||
>
|
||||
<el-option v-for="item in options2" :key="item.value" :label="item.label" :value="item.value"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="height: calc(100% - 74px); position: relative">
|
||||
<chart-no-data v-if="isNoData && !showError"></chart-no-data>
|
||||
<div class="chart-drawing" v-show="showMarkLine && !isNoData && !showError" ref="overviewLineChart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import chartMixin from '@/views/charts2/chart-mixin'
|
||||
import * as echarts from 'echarts'
|
||||
import { stackedLineChartOption } from '@/views/charts2/charts/options/echartOption'
|
||||
import unitConvert from '@/utils/unit-convert'
|
||||
import { unitTypes, chartColor3, chartColor4 } from '@/utils/constants.js'
|
||||
import { ref, shallowRef } from 'vue'
|
||||
import { stackedLineTooltipFormatter } from '@/views/charts/charts/tools'
|
||||
import _ from 'lodash'
|
||||
import axios from 'axios'
|
||||
import { api } from '@/utils/api'
|
||||
import { getNowTime, getSecond } from '@/utils/date-util'
|
||||
import ChartNoData from '@/views/charts/charts/ChartNoData'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { getLineType, getMarkLineByLineRefer, overwriteUrl, urlParamsHandler } from '@/utils/tools'
|
||||
import ChartError from '@/components/common/Error'
|
||||
import { dataForNetworkOverviewLine } from '@/utils/static-data'
|
||||
import { metricOptions } from '@/utils/constants'
|
||||
|
||||
export default {
|
||||
name: 'EntityDetailLine',
|
||||
mixins: [chartMixin],
|
||||
mounted () {
|
||||
components: {
|
||||
ChartError,
|
||||
ChartNoData
|
||||
},
|
||||
setup () {
|
||||
const { query } = useRoute()
|
||||
const lineRefer = ref(query.lineRefer || 'Average')
|
||||
const lineTab = ref(query.lineTab || '')
|
||||
const queryCondition = ref(query.queryCondition || '')
|
||||
const tabOperationType = ref(query.tabOperationType)
|
||||
const networkOverviewBeforeTab = ref(query.networkOverviewBeforeTab)
|
||||
const metric = ref(query.metric || 'Bits/s')
|
||||
|
||||
// 获取url携带的range、startTime、endTime
|
||||
const rangeParam = query.range
|
||||
const startTimeParam = query.startTime
|
||||
const endTimeParam = query.endTime
|
||||
// 若url携带了,使用携带的值,否则使用默认值。
|
||||
|
||||
const dateRangeValue = rangeParam ? parseInt(query.range) : 60
|
||||
const timeFilter = ref({ dateRangeValue })
|
||||
if (!startTimeParam || !endTimeParam) {
|
||||
const { startTime, endTime } = getNowTime(60)
|
||||
timeFilter.value.startTime = startTime
|
||||
timeFilter.value.endTime = endTime
|
||||
} else {
|
||||
timeFilter.value.startTime = parseInt(startTimeParam)
|
||||
timeFilter.value.endTime = parseInt(endTimeParam)
|
||||
}
|
||||
|
||||
return {
|
||||
lineRefer,
|
||||
lineTab,
|
||||
queryCondition,
|
||||
tabOperationType,
|
||||
networkOverviewBeforeTab,
|
||||
myChart: shallowRef(null),
|
||||
metric,
|
||||
timeFilter
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
options2: dataForNetworkOverviewLine.options2,
|
||||
tabsTemplate: dataForNetworkOverviewLine.tabsTemplate,
|
||||
tabs: [],
|
||||
unitConvert,
|
||||
unitTypes,
|
||||
chartDateObject: [],
|
||||
timer: null,
|
||||
mousemoveCursor: '',
|
||||
leftOffset: 0,
|
||||
sizes: [3, 4, 6, 8, 9, 10],
|
||||
dynamicVariable: '',
|
||||
showMarkLine: true,
|
||||
mouseDownFlag: false,
|
||||
brushHistory: [],
|
||||
showError: false,
|
||||
errorMsg: '',
|
||||
metricOptions
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
lineTab (n) {
|
||||
this.$nextTick(() => {
|
||||
this.handleActiveBar(n)
|
||||
this.reloadUrl({ lineTab: n })
|
||||
})
|
||||
},
|
||||
lineRefer (n) {
|
||||
this.reloadUrl({ lineRefer: n })
|
||||
},
|
||||
timeFilter: {
|
||||
handler () {
|
||||
if (this.lineTab) {
|
||||
this.init(this.metric, this.showMarkLine, 'active')
|
||||
} else {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
},
|
||||
metric (n) {
|
||||
this.handleActiveBar()
|
||||
this.showMarkLine = !this.showMarkLine
|
||||
this.tabs.forEach((e) => {
|
||||
if (!e.invertTab) {
|
||||
e.invertTab = true
|
||||
}
|
||||
})
|
||||
this.init(n, this.showMarkLine, '', n)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
reloadUrl (newParam) {
|
||||
const { query } = this.$route
|
||||
const newUrl = urlParamsHandler(window.location.href, query, newParam)
|
||||
overwriteUrl(newUrl)
|
||||
},
|
||||
init (val, show, active, n) {
|
||||
const newVal = val ? _.clone(val) : this.metric
|
||||
|
||||
const params = {
|
||||
startTime: getSecond(this.timeFilter.startTime),
|
||||
endTime: getSecond(this.timeFilter.endTime)
|
||||
}
|
||||
|
||||
if (this.queryCondition) {
|
||||
params.q = this.queryCondition
|
||||
}
|
||||
|
||||
this.toggleLoading(true)
|
||||
// axios.get(api.netWorkOverview.totalTrafficAnalysis, { params: params }).then(response => {
|
||||
axios.get(api.entity.totalTrafficAnalysis, { params: params }).then(response => {
|
||||
const res = response.data
|
||||
|
||||
if (res.code === 200) {
|
||||
this.isNoData = res.data.result.length === 0
|
||||
this.showError = false
|
||||
if (this.isNoData) {
|
||||
this.lineTab = ''
|
||||
this.tabs = _.cloneDeep(this.tabsTemplate)
|
||||
} else {
|
||||
this.initData(res.data.result, newVal, active, show, n)
|
||||
}
|
||||
} else {
|
||||
this.httpError(res)
|
||||
}
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
this.httpError(e)
|
||||
}).finally(() => {
|
||||
this.toggleLoading(false)
|
||||
})
|
||||
},
|
||||
echartsInit (echartsData, show) {
|
||||
// echarts内容在单元测试时不执行
|
||||
if (!this.isUnitTesting) {
|
||||
if (this.lineTab) {
|
||||
this.handleActiveBar()
|
||||
echartsData = echartsData.filter(t => t.show === true && t.class === this.lineTab) // t.invertTab === false
|
||||
} else {
|
||||
echartsData = echartsData.filter(t => t.show === true)
|
||||
}
|
||||
const _this = this
|
||||
this.chartOption = stackedLineChartOption
|
||||
const chartOption = this.chartOption.series[0]
|
||||
this.chartOption.series = echartsData.map((t, i) => {
|
||||
return {
|
||||
...chartOption,
|
||||
name: t.name,
|
||||
type: 'line',
|
||||
showSymbol: false,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
lineStyle: {
|
||||
color: chartColor3[t.positioning],
|
||||
width: 1
|
||||
},
|
||||
stack: t.name !== 'network.total' ? 'network.total' : '',
|
||||
symbolSize: function (value) {
|
||||
return _this.symbolSizeSortChange(i, value[0])
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
borderColor: chartColor4[t.positioning],
|
||||
borderWidth: 2,
|
||||
shadowColor: chartColor4[t.positioning],
|
||||
shadowBlur: this.sizes[t.positioning] + 2
|
||||
}
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: chartColor3[t.positioning]
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: chartColor3[t.positioning]
|
||||
}
|
||||
])
|
||||
},
|
||||
data: t.data.map(v => [Number(v[0]) * 1000, Number(v[1]), 'number']),
|
||||
markLine: {
|
||||
silent: true,
|
||||
lineStyle: {
|
||||
color: '#B4B1A8'
|
||||
},
|
||||
symbol: 'none',
|
||||
label: {
|
||||
formatter (params) {
|
||||
const arr = unitConvert(params.value, unitTypes.number).join('')
|
||||
return _this.lineRefer + '(' + arr + echartsData[0].unitType + ')'
|
||||
},
|
||||
position: 'insideStartTop',
|
||||
color: '#717171',
|
||||
fontFamily: 'NotoSansSChineseRegular'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
if (!show && !this.lineTab) {
|
||||
this.chartOption.series.forEach((t) => {
|
||||
t.markLine.label.show = false
|
||||
t.markLine = []
|
||||
})
|
||||
}
|
||||
|
||||
if (show) {
|
||||
this.chartOption.series.forEach((t, i) => {
|
||||
t.markLine.label.show = true
|
||||
t.markLine.data = [
|
||||
{
|
||||
yAxis: echartsData[i].analysis[getMarkLineByLineRefer(this.lineRefer)]
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
this.chartOption.tooltip.formatter = (params) => {
|
||||
params.forEach(t => {
|
||||
t.seriesName = this.$t(t.seriesName)
|
||||
this.tabs.forEach(e => {
|
||||
if (this.$t(e.name) === t.seriesName) {
|
||||
t.borderColor = chartColor3[e.positioning]
|
||||
}
|
||||
})
|
||||
})
|
||||
return stackedLineTooltipFormatter(params)
|
||||
}
|
||||
this.showMarkLine = true
|
||||
this.$nextTick(() => {
|
||||
if (this.myChart) {
|
||||
this.myChart.dispose()
|
||||
}
|
||||
this.myChart = echarts.init(this.$refs.overviewLineChart)
|
||||
this.myChart.setOption(this.chartOption)
|
||||
|
||||
this.myChart.dispatchAction({
|
||||
type: 'takeGlobalCursor',
|
||||
key: 'brush',
|
||||
brushOption: {
|
||||
brushType: 'lineX',
|
||||
xAxisIndex: 'all',
|
||||
brushMode: 'single',
|
||||
throttleType: 'debounce'
|
||||
}
|
||||
})
|
||||
// 选中tab并刷新界面时,自动触发,避免markLine不显示的情况
|
||||
if (this.lineTab) {
|
||||
this.referenceSelectChange(this.lineRefer)
|
||||
}
|
||||
|
||||
this.myChart.on('brushEnd', this.brushEcharts)
|
||||
})
|
||||
}
|
||||
},
|
||||
activeChange (item, index, isClick) { // isClick:代表是通过点击操作来的
|
||||
if (this.isNoData) return
|
||||
if (isClick && this.lineTab === item.class) { // 点击高亮 tab 后取消高亮,恢复到全不高亮的状态
|
||||
this.legendSelectChange(item, index, 'active', true)
|
||||
this.lineTab = ''
|
||||
this.showMarkLine = false
|
||||
} else {
|
||||
this.lineTab = item.class
|
||||
this.legendSelectChange(item, index, 'active')
|
||||
this.showMarkLine = !item.invertTab
|
||||
}
|
||||
this.init(this.metric, this.showMarkLine, 'active')
|
||||
},
|
||||
mouseenter (item) {
|
||||
if (this.isNoData) return
|
||||
this.mousemoveCursor = item.class
|
||||
this.handleActiveBar(item.class)
|
||||
},
|
||||
mouseleave () {
|
||||
this.mousemoveCursor = ''
|
||||
},
|
||||
dispatchSelectAction (type, name) {
|
||||
this.myChart && this.myChart.dispatchAction({
|
||||
type: type,
|
||||
name: name
|
||||
})
|
||||
},
|
||||
legendSelectChange (item, index, val, isActiveAll) {
|
||||
if (index === 'index') {
|
||||
this.dispatchSelectAction('legendSelect', item.name)
|
||||
} else if (this.tabs[index] && this.tabs[index].name === item.name) {
|
||||
if (isActiveAll) {
|
||||
this.tabs.forEach((t) => {
|
||||
this.dispatchSelectAction('legendSelect', t.name)
|
||||
})
|
||||
} else {
|
||||
this.dispatchSelectAction('legendSelect', item.name)
|
||||
this.tabs.forEach((t) => {
|
||||
if (t.name !== item.name) {
|
||||
this.dispatchSelectAction('legendUnSelect', t.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
if (val === 'active') {
|
||||
this.tabs.forEach(t => {
|
||||
t.invertTab = item.name === t.name ? !t.invertTab : true
|
||||
if (t.invertTab && item.name === t.name) {
|
||||
this.lineTab = this.lineTab ? '' : t.class
|
||||
this.tabs.forEach((e) => {
|
||||
this.dispatchSelectAction('legendSelect', e.name)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
handleActiveBar () {
|
||||
if (document.querySelector('.network .line-value-tabs.is-active')) {
|
||||
const {
|
||||
offsetLeft,
|
||||
clientWidth,
|
||||
clientLeft
|
||||
} = document.querySelector('.network .line-value-tabs.is-active')
|
||||
const activeBar = document.querySelector('.network .line-value-active')
|
||||
activeBar.style.cssText += `width: ${clientWidth}px; left: ${offsetLeft + this.leftOffset + clientLeft}px;`
|
||||
}
|
||||
},
|
||||
resize () {
|
||||
if (this.myChart) {
|
||||
this.myChart.resize()
|
||||
}
|
||||
},
|
||||
referenceSelectChange (val) {
|
||||
this.lineRefer = val
|
||||
let echartsData
|
||||
if (this.lineTab) {
|
||||
echartsData = this.tabs.filter(t => t.show === true && t.class === this.lineTab) // t.invertTab === false
|
||||
} else {
|
||||
echartsData = this.tabs.filter(t => t.show === true)
|
||||
}
|
||||
if (!this.isUnitTesting) {
|
||||
const chartOption = this.myChart.getOption()
|
||||
|
||||
if (this.showMarkLine) {
|
||||
chartOption.series.forEach(t => {
|
||||
if (t.name === echartsData[0].name) {
|
||||
t.markLine.data = [{ yAxis: echartsData[0].analysis[getMarkLineByLineRefer(this.lineRefer)] }]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.myChart.setOption(chartOption)
|
||||
}
|
||||
},
|
||||
symbolSizeSortChange (index, time) {
|
||||
const dataIntegrationArray = []
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (stackedLineChartOption.series[i]) {
|
||||
const item = stackedLineChartOption.series[i].data.find(t => t[0] === time)
|
||||
if (item) {
|
||||
dataIntegrationArray.push(item)
|
||||
item[2] = i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dataIntegrationArray.sort((a, b) => {
|
||||
return a[1] - b[1]
|
||||
})
|
||||
const sortIndex = dataIntegrationArray.findIndex(a => a[2] === index)
|
||||
return this.sizes[sortIndex]
|
||||
},
|
||||
initData (data, val, active, show, n) {
|
||||
let lineData = []
|
||||
if (data !== undefined && data.length > 0) {
|
||||
data.forEach((item) => {
|
||||
item.type = getLineType(item.type)
|
||||
if (item.type === val) {
|
||||
lineData = Object.keys(item).map(t => {
|
||||
return {
|
||||
...item[t]
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
lineData.splice(0, 1)
|
||||
if (val === 'Sessions/s') {
|
||||
const tabs = _.cloneDeep(this.tabsTemplate)
|
||||
lineData.forEach((d, i) => {
|
||||
tabs[i].data = d.values
|
||||
tabs[i].analysis = d.analysis
|
||||
})
|
||||
tabs.forEach((e, i) => {
|
||||
if (i !== 0) {
|
||||
e.show = false
|
||||
}
|
||||
e.unitType = 'sessions/s'
|
||||
e.invertTab = false
|
||||
this.lineTab = 'total'
|
||||
this.legendSelectChange(e, 0)
|
||||
})
|
||||
this.tabs = tabs
|
||||
this.$nextTick(() => {
|
||||
this.echartsInit(this.tabs, true)
|
||||
})
|
||||
} else {
|
||||
const unit = val === 'Bits/s' ? 'bps' : 'packets/s'
|
||||
this.legendInit(lineData, active, show, unit, n)
|
||||
}
|
||||
},
|
||||
legendInit (data, active, show, type, n) {
|
||||
const tabs = _.cloneDeep(this.tabsTemplate)
|
||||
data.forEach((d, i) => {
|
||||
tabs[i].data = d.values
|
||||
tabs[i].analysis = d.analysis
|
||||
})
|
||||
let num = 0
|
||||
const self = this
|
||||
tabs.forEach(e => {
|
||||
e.unitType = type
|
||||
if (e.name !== 'network.total' && parseFloat(e.analysis.avg) === 0) {
|
||||
e.show = false
|
||||
num += 1
|
||||
} else {
|
||||
e.show = true
|
||||
if (!active && show !== self.lineRefer) {
|
||||
self.legendSelectChange(e, 'index')
|
||||
}
|
||||
}
|
||||
if (self.lineTab === e.class) {
|
||||
if (parseFloat(e.analysis.avg) <= 0) {
|
||||
self.lineTab = ''
|
||||
self.lineRefer = ''
|
||||
self.init()
|
||||
}
|
||||
}
|
||||
})
|
||||
this.tabs = tabs
|
||||
if (num === 5) {
|
||||
tabs[0].invertTab = false
|
||||
this.lineTab = 'total'
|
||||
this.legendSelectChange(tabs[0], 0)
|
||||
this.$nextTick(() => {
|
||||
this.echartsInit(this.tabs, true)
|
||||
})
|
||||
} else {
|
||||
if (n) this.lineTab = ''
|
||||
this.$nextTick(() => {
|
||||
this.echartsInit(this.tabs, show)
|
||||
if (!this.lineRefer) this.lineRefer = 'Average'
|
||||
})
|
||||
}
|
||||
},
|
||||
/**
|
||||
* echarts框选
|
||||
* @param params
|
||||
*/
|
||||
brushEcharts (params) {
|
||||
this.myChart.dispatchAction({
|
||||
type: 'brush',
|
||||
areas: [] // 删除选框
|
||||
})
|
||||
if (!this.mouseDownFlag) {
|
||||
// 避免点击空白区域报错
|
||||
if (params.areas && params.areas.length > 0) {
|
||||
this.brushHistory.unshift({
|
||||
startTime: _.cloneDeep(this.timeFilter.startTime) * 1000,
|
||||
endTime: _.cloneDeep(this.timeFilter.endTime) * 1000
|
||||
})
|
||||
|
||||
const rangeObj = {
|
||||
startTime: Math.ceil(params.areas[0].coordRange[0]),
|
||||
endTime: Math.ceil(params.areas[0].coordRange[1])
|
||||
}
|
||||
|
||||
// 暂定框选最小范围为5分钟,后续可能会变动
|
||||
if (rangeObj.endTime - rangeObj.startTime < 5 * 60 * 1000) {
|
||||
rangeObj.startTime = rangeObj.endTime - 5 * 60 * 1000
|
||||
}
|
||||
this.$store.commit('setRangeEchartsData', rangeObj)
|
||||
}
|
||||
}
|
||||
},
|
||||
httpError (e) {
|
||||
this.isNoData = false
|
||||
this.showError = true
|
||||
this.errorMsg = this.errorMsgHandler(e)
|
||||
},
|
||||
metricChange (value) {
|
||||
const { query } = this.$route
|
||||
const newUrl = urlParamsHandler(window.location.href, query, {
|
||||
metric: value
|
||||
})
|
||||
overwriteUrl(newUrl)
|
||||
},
|
||||
reload (startTime, endTime, dateRangeValue) {
|
||||
this.timeFilter = { startTime: getSecond(startTime), endTime: getSecond(endTime) }
|
||||
const { query } = this.$route
|
||||
// 因为详情页是打开的新标签页,设置时间并不会影响到其他界面,所以store的名称不必改变
|
||||
this.$store.commit('setTimeRangeArray', [this.timeFilter.startTime, this.timeFilter.endTime])
|
||||
this.$store.commit('setTimeRangeFlag', dateRangeValue.value)
|
||||
|
||||
const newUrl = urlParamsHandler(window.location.href, query, {
|
||||
startTime: this.timeFilter.startTime,
|
||||
endTime: this.timeFilter.endTime,
|
||||
range: dateRangeValue.value
|
||||
})
|
||||
overwriteUrl(newUrl)
|
||||
},
|
||||
timeRefreshChange () {
|
||||
// 不是自选时间
|
||||
if (this.$refs.dateTimeRange) {
|
||||
if (!this.$refs.dateTimeRange.isCustom) {
|
||||
const value = this.timeFilter.dateRangeValue
|
||||
this.$refs.dateTimeRange.quickChange(value)
|
||||
} else {
|
||||
this.timeFilter = JSON.parse(JSON.stringify(this.timeFilter))
|
||||
}
|
||||
} else {
|
||||
this.timeFilter = JSON.parse(JSON.stringify(this.timeFilter))
|
||||
}
|
||||
},
|
||||
// 对lineTab进行转换
|
||||
getTabUnit (avg) {
|
||||
let number = unitConvert(avg, unitTypes.number)[0]
|
||||
if (number !== '-' && JSON.parse(number) > 0 && JSON.parse(number) < 1) {
|
||||
number = '<1'
|
||||
}
|
||||
return number
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.myChart = null
|
||||
this.chartOption = null
|
||||
const self = this
|
||||
self.timer = setTimeout(() => {
|
||||
if (self.lineTab && self.metric !== 'Sessions/s') {
|
||||
const data = self.tabsTemplate.find(t => t.class === self.lineTab)
|
||||
self.activeChange(data, data.positioning)
|
||||
} else {
|
||||
self.init()
|
||||
}
|
||||
}, 200)
|
||||
window.addEventListener('resize', this.resize)
|
||||
},
|
||||
beforeUnmount () {
|
||||
clearTimeout(this.timer)
|
||||
window.removeEventListener('resize', this.resize)
|
||||
|
||||
let myChart = echarts.getInstanceByDom(this.$refs.overviewLineChart)
|
||||
if (myChart) {
|
||||
echarts.dispose(myChart)
|
||||
}
|
||||
if (this.myChart) {
|
||||
echarts.dispose(this.myChart)
|
||||
}
|
||||
// 检测时发现该方法占用较大内存,且未被释放
|
||||
this.unitConvert = null
|
||||
myChart = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
163
test/views/charts2/charts/entityDetail/EntityDetailLine.test.js
Normal file
163
test/views/charts2/charts/entityDetail/EntityDetailLine.test.js
Normal file
@@ -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))
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user