CN-1508 feat: subscriber流量曲线图开发

This commit is contained in:
chenjinsong
2023-12-06 16:16:12 +08:00
parent 2c12284415
commit 74d95f110e
10 changed files with 1612 additions and 17 deletions

View File

@@ -1,25 +1,25 @@
const BASE_CONFIG = {
baseUrl: 'http://192.168.44.54:8090/',
version: '23.10',
version: '23.12',
apiVersion: 'v1'
}
// 默认时间过滤条件,单位分钟. 0表示请求接口时不传时间参数
const DEFAULT_TIME_FILTER_RANGE = {
dashboard: 60,
dashboard: 60, // 所有dashboard
entity: {
list: 60,
trafficLine: 60,
subscriberKpi: 60,
subscriberTopApp: 60,
subscriberMap: 60,
informationAggregation: 0,
relatedEntity: 60 * 24 * 7,
openPort: 60 * 24 * 7,
deviceInformation: 60 * 24 * 7,
accountInformation: 60 * 24 * 7,
securityEvent: 60 * 24 * 7,
performanceEvent: 60 * 24 * 7,
behaviorPattern: 60 * 24 * 7
list: 60, // 实体列表
trafficLine: 60, // 实体详情--通用--流量曲线
subscriberKpi: 60, // 实体详情--subscriber--kpi
subscriberTopApp: 60, // 实体详情--subscriber--topApp
subscriberMap: 60, // 实体详情--subscriber--地图
informationAggregation: 0, // 实体详情--通用--信息聚合
relatedEntity: 60 * 24 * 7, // 实体详情--通用--相关实体
openPort: 60 * 24 * 7, // 实体详情--通用--开放端口
deviceInformation: 60 * 24 * 7, // 实体详情--subscriber--设备信息
accountInformation: 60 * 24 * 7, // 实体详情--subscriber--账户信息
securityEvent: 60 * 24 * 7, // 实体详情--通用--安全事件
performanceEvent: 60 * 24 * 7, // 实体详情--通用--性能事件
behaviorPattern: 60 * 24 * 7 // 实体详情--IP--行为模式
},
detection: 60
detection: 60 // 事件日志列表
}

View File

@@ -86,6 +86,7 @@
@import 'views/charts2/EntityDetailSubscriberKpi.scss';
@import 'views/charts2/EntityDetailSubscriberTopApp.scss';
@import 'views/charts2/entityDetailSubscriberMap.scss';
@import 'views/charts2/entityDetailSubscriberLine.scss';
@import 'views/charts2/entityDetailTabs';
@import 'views/charts2/digitalCertificate';

View File

@@ -0,0 +1,51 @@
.entity-detail-line--subscriber {
$blue: #046ECA;
$grey: #353636;
height:100%;
.el-tabs__content {
overflow: visible;
}
.cn-chart__tabs {
height:100%;
.tab-pane {
height:100%;
}
.el-tabs__header {
margin-bottom: 10px;
width: calc(100% - 272px);
}
.el-tabs__nav-wrap::after {
height: 1px;
background-color: transparent ;
}
.el-tabs__nav.is-top {
height: 33px;
.el-tabs__active-bar {
background-color: $blue;
}
.el-tabs__item {
padding: 0 10px;
height: 33px;
color: $grey;
font-size: 14px;
&.el-tabs__item.is-top.is-active {
color:$blue;
}
&:nth-child(2) {
padding-left: 0;
}
}
}
.el-tabs__content {
height: calc(100% - 40px);
border:none;
.el-table__body-wrapper {
height: calc(100% - 45px) !important;
}
}
}
}

View File

@@ -496,6 +496,40 @@ export function stackedLineTooltipFormatter (params) {
str += '</div>'
return str
}
// subscriber详情页的app页签
export function stackedLineWithLegendTooltipFormatter (params) {
// 堆叠图tooltip倒叙操作
params = params.reverse()
let str = '<div>'
params.forEach((item, i) => {
const tData = item.data[0]
if (i === 0) {
str += '<div style="margin-bottom: 5px">'
str += dateFormatByAppearance(tData)
str += '</div>'
}
})
str += '<div class="cn-chart-body">'
str += '<div class="cn-chart-tooltip">'
params.forEach((item, i) => {
str += '<span class="cn-chart-tooltip-box">'
str += `<span style="display:inline-block;margin-right:4px;border-radius:2px;width:10px;height:10px;background-color:${item.borderColor};"></span>`
str += `<span class="cn-chart-tooltip-content">${item.seriesName.split('(')[0]}</span>`
str += '</span>'
})
str += '</div>'
str += '<div class="cn-chart-tooltip">'
params.forEach((item, i) => {
str += `<span class="cn-chart-tooltip-value cn-chart-tooltip__color">
${valueToRangeValue(item.data[1], item.value[2]).join(' ')}
</span>`
})
str += '</div>'
str += '</div>'
str += '</div>'
return str
}
export function appStackedLineTooltipFormatter (params) {
let str = '<div>'
params.forEach((item, i) => {

View File

@@ -184,6 +184,12 @@
:entity="entity"
@toggleLoading="toggleLoading"
></entity-detail-line>
<entity-detail-subscriber-line
v-else-if="chart.type === typeMapping.entityDetail.subscriberLine"
:chart="chart"
:entity="entity"
@toggleLoading="toggleLoading"
></entity-detail-subscriber-line>
<entity-detail-tabs-chart
v-else-if="chart.type === typeMapping.entityDetail.tabsChart"
:chart="chart"
@@ -228,6 +234,7 @@ import DnsRecentEvents from '@/views/charts2/charts/dnsInsight/DnsRecentEvents'
import DnsTrafficLine from '@/views/charts2/charts/dnsInsight/DnsTrafficLine'
import EntityDetailBasicInfo from '@/views/charts2/charts/entityDetail/EntityDetailBasicInfo'
import EntityDetailLine from '@/views/charts2/charts/entityDetail/EntityDetailLine'
import EntityDetailSubscriberLine from '@/views/charts2/charts/entityDetail/EntityDetailSubscriberLine'
import EntityDetailSubscriberKpi from '@/views/charts2/charts/entityDetail/EntityDetailSubscriberKpi'
import EntityDetailSubscriberTopApp from '@/views/charts2/charts/entityDetail/EntityDetailSubscriberTopApp'
import EntityDetailTabsChart from '@/views/charts2/charts/entityDetail/EntityDetailTabs'
@@ -269,6 +276,7 @@ export default {
EntityDetailSubscriberKpi,
EntityDetailSubscriberTopApp,
EntityDetailLine,
EntityDetailSubscriberLine,
EntityDetailTabsChart,
EntityDetailMap
},

View File

@@ -37,6 +37,7 @@ export const typeMapping = {
subscriberKpi: 714,
subscriberTopApp: 715,
line: 107,
subscriberLine: 108,
tabsChart: 713,
map: 3
}

View File

@@ -0,0 +1,61 @@
<template>
<div class="entity-detail-line entity-detail-line--subscriber">
<el-tabs v-model="lineOuterTab" class="cn-chart__tabs" @tab-click="switchOuterTab">
<el-tab-pane name="serviceName" class="tab-pane" :label="$t('subscriber.serviceName')">
<service-name
v-if="lineOuterTab === 'serviceName'"
:chart="chart"
:entity="entity"
@toggleLoading="toggleLoading"
></service-name>
</el-tab-pane>
<el-tab-pane name="app" class="tab-pane" label="APP">
<app
v-if="lineOuterTab === 'app'"
:chart="chart"
:entity="entity"
@toggleLoading="toggleLoading"
></app>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import ServiceName from '@/views/charts2/charts/entityDetail/lines/ServiceName'
import App from '@/views/charts2/charts/entityDetail/lines/App'
import {useRoute} from 'vue-router'
import {ref} from 'vue'
import {overwriteUrl, urlParamsHandler} from '@/utils/tools'
export default {
name: 'EntityDetailLine',
components: {
ServiceName,
App
},
props: {
chart: Object,
entity: Object
},
methods: {
switchOuterTab (tab) {
const { query } = this.$route
const newUrl = urlParamsHandler(window.location.href, query, {
lineOuterTab: tab.paneName
})
overwriteUrl(newUrl)
},
toggleLoading (loading) {
this.$emit('toggleLoading', loading)
}
},
setup () {
const { query } = useRoute()
const lineOuterTab = ref(query.lineOuterTab || 'serviceName')
return {
lineOuterTab
}
}
}
</script>

View File

@@ -0,0 +1,653 @@
<template>
<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="tabs-name" :test-id="`tabTitle${index}`" style="padding-left: 0;">{{ $t(item.name) }}</div>
</div>
<div class="line-value-unit" :test-id="`tabContent${index}`">
<span class="line-value-unit-number">
{{ valueToRangeValue(item.analysis.avg, unitTypes.number)[0] }}
</span>
<span class="line-value-unit-number2">
<span>{{ valueToRangeValue(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 line-date-right">
<date-time-range
class="entity-detail-date-time-range"
:start-time="timeFilter.startTime"
:end-time="timeFilter.endTime"
:date-range="timeFilter.dateRangeValue"
ref="dateTimeRange"
@change="reload"
/>
</div>
<div class="line-select-metric">
<span class="select-prefix line-margin-right">{{$t('network.metric')}}:</span>
<el-select
size="mini"
v-model="metric"
placeholder=" "
popper-class="common-select"
:popper-append-to-body="false"
@change="metricChange"
>
<el-option v-for="item in metricOptions" :key="item.value" :label="$t(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="!isNoData && !showError" ref="overviewLineChart"></div>
</div>
</div>
</template>
<script>
import chartMixin from '@/views/charts2/chart-mixin'
import * as echarts from 'echarts'
import { stackedLineWithLegendChartOption } from '@/views/charts2/charts/options/echartOption'
import unitConvert, { valueToRangeValue } from '@/utils/unit-convert'
import { unitTypes, chartColor3, chartColor4, metricType, metricOptions } from '@/utils/constants.js'
import { ref, shallowRef } from 'vue'
import { stackedLineWithLegendTooltipFormatter } 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, overwriteUrl, urlParamsHandler } from '@/utils/tools'
import ChartError from '@/components/common/Error'
import { dataForNetworkOverviewLine } from '@/utils/static-data'
export default {
name: 'EntityDetailLine',
mixins: [chartMixin],
components: {
ChartError,
ChartNoData
},
setup () {
const { query } = useRoute()
const lineTab = ref(query.lineTab || 'total')
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 > config.js > 默认值。
const dateRangeValue = rangeParam ? parseInt(rangeParam) : (DEFAULT_TIME_FILTER_RANGE.entity.trafficLine || 60)
const timeFilter = ref({ dateRangeValue })
if (!startTimeParam || !endTimeParam) {
const { startTime, endTime } = getNowTime(dateRangeValue)
timeFilter.value.startTime = startTime
timeFilter.value.endTime = endTime
} else {
timeFilter.value.startTime = parseInt(startTimeParam)
timeFilter.value.endTime = parseInt(endTimeParam)
}
return {
lineTab,
queryCondition,
tabOperationType,
networkOverviewBeforeTab,
myChart: shallowRef(null),
metric,
timeFilter
}
},
data () {
return {
tabsTemplate: dataForNetworkOverviewLine.tabsTemplate,
tabs: [],
unitConvert,
valueToRangeValue,
unitTypes,
chartDateObject: [],
timer: null,
mousemoveCursor: '',
leftOffset: 0,
sizes: [3, 4, 6, 8, 9, 10],
dynamicVariable: '',
mouseDownFlag: false,
brushHistory: [],
showError: false,
errorMsg: '',
metricOptions
}
},
watch: {
lineTab (n) {
this.$nextTick(() => {
this.handleActiveBar(n)
this.reloadUrl({ lineTab: n })
})
},
timeFilter: {
handler () {
if (this.lineTab) {
this.init(this.metric, 'active')
} else {
this.init()
}
}
},
metric (n) {
this.handleActiveBar()
if (n === metricType.Sessions) {
this.lineTab = 'total'
}
this.init(n, 'active')
}
},
methods: {
reloadUrl (newParam) {
const { query } = this.$route
const newUrl = urlParamsHandler(window.location.href, query, newParam)
overwriteUrl(newUrl)
},
init (val, active) {
const newVal = val ? _.clone(val) : this.metric
const params = {
resource: this.entity.entityName,
startTime: getSecond(this.timeFilter.startTime),
endTime: getSecond(this.timeFilter.endTime),
metric: newVal
}
if (this.queryCondition) {
params.q = this.queryCondition
}
this.toggleLoading(true)
axios.get(`${api.entity.throughput}/${this.entity.entityType}/relate/app`, { params: params }).then(response => {
const res = response.data
if (response.status === 200) {
this.isNoData = res.data.result.length === 0
this.showError = false
if (!active) {
this.tabs = _.cloneDeep(this.tabsTemplate)
}
if (this.isNoData) {
this.lineTab = ''
this.tabs = _.cloneDeep(this.tabsTemplate)
} else {
this.initData(res.data.result, newVal, active)
}
} else {
this.httpError(res)
}
}).catch(e => {
console.error(e)
this.httpError(e)
}).finally(() => {
this.toggleLoading(false)
})
},
echartsInit (echartsData) {
// echarts内容在单元测试时不执行
if (!this.isUnitTesting) {
if (this.lineTab) {
this.handleActiveBar()
}
const _this = this
this.chartOption = stackedLineWithLegendChartOption
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[i],
width: 1
},
stack: 'stack',
symbolSize: function (value) {
return _this.symbolSizeSortChange(i, value[0])
},
emphasis: {
itemStyle: {
borderColor: chartColor4[i],
borderWidth: 2,
shadowColor: chartColor4[i]
}
},
areaStyle: {
opacity: 0.1,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: chartColor3[i]
},
{
offset: 1,
color: chartColor3[i]
}
])
},
data: t.values.map(v => [Number(v[0]) * 1000, Number(v[1]), 'number'])
}
})
this.chartOption.tooltip.formatter = (params) => {
params.forEach(t => {
echartsData.forEach((e, i) => {
if (e.name === t.seriesName) {
t.borderColor = chartColor3[i]
}
})
})
return stackedLineWithLegendTooltipFormatter(params)
}
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'
}
})
this.myChart.on('brushEnd', this.brushEcharts)
})
}
},
activeChange (item, index, isClick) { // isClick:代表是通过点击操作来的
if (this.isNoData) return
if (this.lineTab === item.class) return
this.lineTab = item.class
this.legendSelectChange(item, index, 'active')
this.init(this.metric, '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()
}
},
symbolSizeSortChange (index, time) {
const dataIntegrationArray = []
for (let i = 0; i < 5; i++) {
if (stackedLineWithLegendChartOption.series[i]) {
const item = stackedLineWithLegendChartOption.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) {
const lineData = []
const tabData = []
if (data && data.length > 0) {
data.forEach(app => {
Object.keys(app).forEach(appName => {
if (app[appName] && app[appName].length > 0) {
app[appName].forEach(item => {
// 整理tab数据
const templateList = ['total', 'inbound', 'outbound', 'internal', 'through', 'other']
let typeData = tabData.find(t => t.type === item.type)
if (!typeData) {
tabData.push({
type: item.type
})
typeData = tabData.find(t => t.type === item.type)
}
for (const key in templateList) {
const code = Object.keys(item).find(d => d.indexOf(templateList[key]) > -1)
if (code) {
if (typeData[code]) {
typeData[code].analysis.avg += Number(item[code].analysis.avg)
typeData[code].analysis.min += Number(item[code].analysis.min)
typeData[code].analysis.max += Number(item[code].analysis.max)
typeData[code].analysis.p95 += Number(item[code].analysis.p95)
} else {
typeData[code] = {
analysis: {
avg: Number(item[code].analysis.avg),
min: Number(item[code].analysis.min),
max: Number(item[code].analysis.max),
p95: Number(item[code].analysis.p95)
}
}
}
// 整理line数据
if (templateList[key] === this.lineTab && this.metric === getLineType(item.type)) {
lineData.push({ name: appName, ...item[code] })
}
}
}
})
}
})
})
}
tabData.forEach(t => {
t.type = getLineType(t.type)
})
// TODO 下面的逻辑是判断曲线的尾部数据每条曲线从尾往前数0值的个数若每条曲线的值全部为0个数大于0所有曲线都从尾部去掉相同数量的点最多4个
const zeroPointCount = []
lineData.forEach(l => {
if (_.get(l, 'values', []).length > 4) {
let count = 0
for (let i = l.values.length - 1; i >= l.values.length - 4; i--) {
if (l.values[i].length > 1 && l.values[i][1] === 0) {
count++
} else {
break
}
}
zeroPointCount.push(count)
}
})
if (zeroPointCount.length > 0) {
const minCount = _.min(zeroPointCount)
if (minCount > 0) {
lineData.forEach(l => {
l.values.splice(l.values.length - minCount, minCount)
})
}
}
if (val === metricType.Sessions) {
const tabs = _.cloneDeep(this.tabsTemplate)
tabData.forEach((t, i) => {
if (this.metric === t.type) {
tabs[i].analysis = t.analysis
}
})
tabs.forEach((tab, i) => {
if (i !== 0) {
tab.show = false
}
tab.invertTab = false
tabData.forEach((t, i) => {
if (this.metric === t.type) {
Object.keys(t).forEach(k => {
if (k !== 'type' && k.indexOf(tab.class) > -1) {
tab.analysis = t[k].analysis
}
})
}
})
const metric = metricOptions.find(item => item.value === metricType.Sessions)
tab.unitType = metric ? this.$t(metric.label) : ''
this.lineTab = 'total'
this.legendSelectChange(tab, 0)
})
/*tabs.forEach((e, i) => {
if (i !== 0) {
e.show = false
}
const metric = metricOptions.find(item => item.value === metricType.Sessions)
e.unitType = metric ? this.$t(metric.label) : ''
e.invertTab = false
this.lineTab = 'total'
this.legendSelectChange(e, 0)
})*/
this.tabs = tabs
this.$nextTick(() => {
this.echartsInit(lineData)
})
} else {
let metric = metricOptions.find(item => item.value === val)
if (!metric) {
metric = metricOptions.find(item => item.value === metricType.Packets)
}
const unit = metric ? this.$t(metric.label) : ''
this.legendInit(tabData, lineData, active, unit, val)
}
},
legendInit (tabData, lineData, active, type, val) {
const tabs = _.cloneDeep(this.tabsTemplate)
tabs.forEach(tab => {
tabData.forEach((t, i) => {
if (this.metric === t.type) {
Object.keys(t).forEach(k => {
if (k !== 'type' && k.indexOf(tab.class) > -1) {
tab.analysis = t[k].analysis
}
})
}
})
})
let num = 0
const self = this
tabs.forEach(e => {
e.unitType = type
if (e.name !== 'network.total' && parseFloat(e.analysis.max) === 0) {
e.show = false
num += 1
} else {
e.show = true
if (!active) {
self.legendSelectChange(e, 'index')
}
}
if (self.lineTab === e.class) {
if (parseFloat(e.analysis.max) <= 0) {
self.lineTab = ''
// self.init() // 后续多测试关注
}
}
})
const emptyData = tabs.filter(d => parseFloat(d.analysis.max) === 0)
this.isNoData = emptyData.length === tabs.length
if (this.isNoData) {
return true
}
this.tabs = tabs
if (num === 5) {
tabs[0].invertTab = false
this.lineTab = 'total'
this.legendSelectChange(tabs[0], 0)
this.$nextTick(() => {
this.echartsInit(lineData)
})
} else {
this.$nextTick(() => {
this.echartsInit(lineData)
})
}
},
/**
* 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])
}
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))
}
}
},
mounted () {
this.myChart = null
this.chartOption = null
this.timer = setTimeout(() => {
this.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
this.valueToRangeValue = null
myChart = null
}
}
</script>

View File

@@ -0,0 +1,720 @@
<template>
<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">
{{ valueToRangeValue(item.analysis.avg, unitTypes.number)[0] }}
</span>
<span class="line-value-unit-number2">
<span>{{ valueToRangeValue(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 line-date-right">
<date-time-range
class="entity-detail-date-time-range"
:start-time="timeFilter.startTime"
:end-time="timeFilter.endTime"
:date-range="timeFilter.dateRangeValue"
ref="dateTimeRange"
@change="reload"
/>
</div>
<div class="line-select-metric">
<span class="select-prefix line-margin-right">{{$t('network.metric')}}:</span>
<el-select
size="mini"
v-model="metric"
placeholder=" "
popper-class="common-select"
:popper-append-to-body="false"
@change="metricChange"
>
<el-option v-for="item in metricOptions" :key="item.value" :label="$t(item.label)" :value="item.value"></el-option>
</el-select>
</div>
</div>
<div class="line-select-reference-line">
<span class="line-margin-right">{{ $t('network.referenceLine') }}:</span>
<div class="line-select__operation">
<el-select
size="mini"
v-model="lineRefer"
:disabled="!lineTab"
placeholder=" "
popper-class="common-select"
:popper-append-to-body="false"
@change="referenceSelectChange"
>
<el-option :key="options2[0].value" :label="$t(options2[0].label)" :value="options2[0].value"></el-option>
<el-option :key="options2[1].value" :label="$t(options2[1].label[0], options2[1].label[1])" :value="options2[1].value"></el-option>
<el-option :key="options2[2].value" :label="$t(options2[2].label)" :value="options2[2].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>
</template>
<script>
import chartMixin from '@/views/charts2/chart-mixin'
import * as echarts from 'echarts'
import { stackedLineChartOption } from '@/views/charts2/charts/options/echartOption'
import unitConvert, { valueToRangeValue } from '@/utils/unit-convert'
import { unitTypes, chartColor3, chartColor4, metricType, metricOptions } 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'
export default {
name: 'EntityDetailLine',
mixins: [chartMixin],
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 > config.js > 默认值。
const dateRangeValue = rangeParam ? parseInt(rangeParam) : (DEFAULT_TIME_FILTER_RANGE.entity.trafficLine || 60)
const timeFilter = ref({ dateRangeValue })
if (!startTimeParam || !endTimeParam) {
const { startTime, endTime } = getNowTime(dateRangeValue)
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,
valueToRangeValue,
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 = {
resource: this.entity.entityName,
startTime: getSecond(this.timeFilter.startTime),
endTime: getSecond(this.timeFilter.endTime),
metric: newVal
}
if (this.queryCondition) {
params.q = this.queryCondition
}
this.toggleLoading(true)
axios.get(`${api.entity.throughput}/${this.entity.entityType}`, { params: params }).then(response => {
const res = response.data
if (response.status === 200) {
this.isNoData = res.data.result.length === 0
this.showError = false
if (!active) {
this.tabs = _.cloneDeep(this.tabsTemplate)
}
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]
echartsData = echartsData.reverse()
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('')
const referIndex = _this.options2.findIndex(o => o.value === _this.lineRefer)
if (referIndex > -1) {
if (referIndex === 1) {
return _this.$t(_this.options2[1].label[0], _this.options2[1].label[1]) + '(' + arr + echartsData[0].unitType + ')'
} else {
return _this.$t(_this.options2[referIndex].label) + '(' + arr + echartsData[0].unitType + ')'
}
}
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 = []
const newData = [] // 接口数据的新构造排序数据
if (data && data.length > 0) {
data.forEach(item => {
const obj = {}
// 模板数据,按此进行排序
const templateList = ['type', 'total', 'inbound', 'outbound', 'internal', 'through', 'other']
for (const key in templateList) {
const code = Object.keys(item).find(d => d.indexOf(templateList[key]) > -1)
if (code) {
obj[code] = _.clone(item[code])
}
}
newData.push(obj)
})
}
if (data && data.length > 0) {
newData.forEach((item) => {
item.type = getLineType(item.type)
if (item.type === val) {
lineData = Object.keys(item).map(t => {
return {
...item[t]
}
})
}
})
}
lineData.splice(0, 1)
// TODO 下面的逻辑是判断total曲线的尾部数据从尾往前数0值的个数若个数大于0所有曲线都从尾部去掉相同数量的点最多4个
const totalData = lineData[0]
if (_.get(totalData, 'values', []).length > 4) {
let count = 0
for (let i = totalData.values.length - 1; i >= totalData.values.length - 4; i--) {
if (totalData.values[i].length > 1 && totalData.values[i][1] === 0) {
count++
} else {
break
}
}
if (count > 0) {
lineData.forEach(l => {
l.values.splice(l.values.length - count, count)
})
}
}
if (val === metricType.Sessions) {
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
}
const metric = metricOptions.find(item => item.value === metricType.Sessions)
e.unitType = metric ? this.$t(metric.label) : ''
e.invertTab = false
this.lineTab = 'total'
this.legendSelectChange(e, 0)
})
this.tabs = tabs
this.$nextTick(() => {
this.lineRefer = 'Average'
this.echartsInit(this.tabs, true)
})
} else {
let metric = metricOptions.find(item => item.value === val)
if (!metric) {
metric = metricOptions.find(item => item.value === metricType.Packets)
}
const unit = metric ? this.$t(metric.label) : ''
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.max) === 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.max) <= 0) {
self.lineTab = ''
self.lineRefer = ''
// self.init() // 后续多测试关注
}
}
})
const emptyData = tabs.filter(d => parseFloat(d.analysis.max) === 0)
this.isNoData = emptyData.length === tabs.length
if (this.isNoData) {
return true
}
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])
}
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))
}
}
},
mounted () {
this.myChart = null
this.chartOption = null
const self = this
self.timer = setTimeout(() => {
if (self.lineTab && self.metric !== metricType.Sessions) {
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
this.valueToRangeValue = null
myChart = null
}
}
</script>

View File

@@ -325,6 +325,73 @@ export const stackedLineChartOption = {
}
]
}
// subscriber的app页签
export const stackedLineWithLegendChartOption = {
color: chartColor3,
tooltip: {
trigger: 'axis',
className: 'echarts-tooltip echarts-tooltip-dark'
},
toolbox: {
show: false
},
brush: {
toolbox: ['lineX'],
xAxisIndex: 'all',
throttleType: 'debounce',
transformable: false
},
legend: {
show: true,
left: 'center',
bottom: 10,
icon: 'roundRect',
itemWidth: 10,
itemHeight: 10
},
grid: {
top: '12%',
left: '2%',
right: '1.5%',
bottom: 40,
containLabel: true
},
xAxis: [
{
type: 'time',
splitNumber: 8,
axisLabel: {
formatter: xAxisTimeFormatter,
rich: xAxisTimeRich
}
}
],
yAxis: [
{
type: 'value',
splitLine: {
show: false
},
axisLabel: {
formatter: function (value) {
return unitConvert(value, unitTypes.number).join('')
}
}
}
],
series: [
{
type: 'line',
symbol: 'circle',
smooth: true,
showSymbol: false,
emphasis: {
focus: 'series'
},
data: []
}
]
}
export const linkTrafficLineChartOption = {
color: chartColor3,
@@ -454,7 +521,6 @@ export const npmLineChartOption = {
xAxis: [
{
type: 'time',
splitNumber: 8,
axisLabel: {
formatter: xAxisTimeFormatter,
rich: xAxisTimeRich