Merge branch 'dev-3.0' into dev-2.0

# Conflicts:
#	nezha-fronted/src/components/common/language/en.js
#	nezha-fronted/src/components/page/dashboard/explore/explore.vue
#	nezha-fronted/src/components/page/dashboard/explore/promqlInput.vue
This commit is contained in:
chenjinsong
2021-08-02 20:29:49 +08:00
14 changed files with 1972 additions and 1148 deletions

View File

@@ -24,7 +24,7 @@
"@topology/sequence-diagram": "^0.3.0", "@topology/sequence-diagram": "^0.3.0",
"axios": "^0.19.0", "axios": "^0.19.0",
"cytoscape": "^3.15.2", "cytoscape": "^3.15.2",
"echarts": "^5.0.1", "echarts": "^5.1.2",
"element-ui": "^2.15.3", "element-ui": "^2.15.3",
"file-saver": "^2.0.2", "file-saver": "^2.0.2",
"leaflet": "^1.7.1", "leaflet": "^1.7.1",

View File

@@ -54,63 +54,63 @@
.top-tool-left { .top-tool-left {
display: flex; display: flex;
} }
.top-tool-btn-group { }
display: flex; .top-tool-btn-group {
.top-tool-btn:not(:last-of-type):not(:first-of-type) { display: flex;
border-left: none; .top-tool-btn:not(:last-of-type):not(:first-of-type) {
border-radius: 0; border-left: none;
} border-radius: 0;
.top-tool-btn:first-of-type:not(:last-of-type) {
border-radius: $--button-border-radius 0 0 $--button-border-radius;
}
.top-tool-btn:last-of-type:not(:first-of-type) {
border-radius: 0 $--button-border-radius $--button-border-radius 0;
border-left: none;
}
} }
.top-tool-btn { .top-tool-btn:first-of-type:not(:last-of-type) {
height: 32px; border-radius: $--button-border-radius 0 0 $--button-border-radius;
width: 36px; }
border: 1px solid $--primary-border-color; .top-tool-btn:last-of-type:not(:first-of-type) {
outline: none; border-radius: 0 $--button-border-radius $--button-border-radius 0;
border-radius: $--button-border-radius; border-left: none;
background-color: $--button-gray-background-color; }
transition: background-color linear .1s; }
.top-tool-btn {
height: 32px;
width: 36px;
border: 1px solid $--primary-border-color;
outline: none;
border-radius: $--button-border-radius;
background-color: $--button-gray-background-color;
transition: background-color linear .1s;
i { i {
font-size: 14px; font-size: 14px;
color: $--button-gray-color; color: $--button-gray-color;
}
} }
.top-tool-btn.top-tool-btn--text { }
padding: 0 8px; .top-tool-btn.top-tool-btn--text {
width: unset; padding: 0 8px;
color: #666; width: unset;
color: #666;
}
.top-tool-btn:hover:not(.nz-btn-disabled) {
background-color: $--button-gray-hover-background-color;
}
.top-tool-btn:focus:not(.nz-btn-disabled), .top-tool-btn.is-focus {
background-color: $--button-gray-hover-background-color;
border: 1px solid #FBCEA4 !important;
i {
color: $--button-gray-active-color;
} }
.top-tool-btn:hover:not(.nz-btn-disabled) { }
background-color: $--button-gray-hover-background-color; .top-tool-btn--delete.top-tool-btn:focus:not(.nz-btn-disabled) {
} background-color: $--button-gray-hover-background-color;
.top-tool-btn:focus:not(.nz-btn-disabled), .top-tool-btn.is-focus { border-color: #FFC4B9;
background-color: $--button-gray-hover-background-color;
border: 1px solid #FBCEA4 !important;
i {
color: $--button-gray-active-color;
}
}
.top-tool-btn--delete.top-tool-btn:focus:not(.nz-btn-disabled) {
background-color: $--button-gray-hover-background-color;
border-color: #FFC4B9;
i { i {
color: #F0745A; color: #F0745A;
}
}
.top-tool-btn--dropdown {
position: relative;
width: auto;
min-width: 36px;
} }
} }
.top-tool-btn--dropdown {
position: relative;
width: auto;
min-width: 36px;
}
.top-tools--sub { .top-tools--sub {
.top-tool-left { .top-tool-left {
height: 100%; height: 100%;

View File

@@ -706,7 +706,6 @@
} }
} }
.top-tool-btn-group { .top-tool-btn-group {
margin-left: 10px;
display: flex; display: flex;
.top-tool-btn:not(:last-of-type):not(:first-of-type) { .top-tool-btn:not(:last-of-type):not(:first-of-type) {
border-left: none; border-left: none;

View File

@@ -59,7 +59,6 @@ export default {
.chart-unit{ .chart-unit{
width: 100px; width: 100px;
margin: 0 20px 0 0;
} }
.chart-unit.el-cascader .el-input.is-focus .el-input__inner { .chart-unit.el-cascader .el-input.is-focus .el-input__inner {
border-color: #FBCEA4; border-color: #FBCEA4;

View File

@@ -13,6 +13,7 @@ const cn = {
oneDay: '1 天', oneDay: '1 天',
twoDay: '2 天', twoDay: '2 天',
week: '1 周', week: '1 周',
time: '时间',
folder: '文件夹', folder: '文件夹',
key: '键名', key: '键名',
logs: '日志', logs: '日志',
@@ -65,6 +66,7 @@ const cn = {
other: '其他', other: '其他',
about: '关于', about: '关于',
query: '查询', query: '查询',
logLabels: '日志标签',
account: '账号', account: '账号',
back: '返回', back: '返回',
unavailable: '不可用', unavailable: '不可用',
@@ -115,6 +117,7 @@ const cn = {
}, },
reset: '重置', reset: '重置',
submit: '保存', submit: '保存',
limit: '限制',
noData: '没有数据', noData: '没有数据',
tag: '标签', tag: '标签',
syncChart: '同步图表', syncChart: '同步图表',
@@ -520,6 +523,10 @@ const cn = {
down: 'down', down: 'down',
prometheus: 'prometheus' prometheus: 'prometheus'
} }
},
explore: {
descending: '降序',
wrapLines: '换行'
} }
}, },
validate: { validate: {

View File

@@ -19,9 +19,10 @@ const en = {
oneDay: '1 day', oneDay: '1 day',
twoDay: '2 days', twoDay: '2 days',
week: '1 week', week: '1 week',
time: 'Time',
folder: 'Folder', folder: 'Folder',
key: 'Key', key: 'Key',
logs: 'Logging', logs: 'Logs',
state: 'State', state: 'State',
projectName: 'Project name', projectName: 'Project name',
startTime: 'Start time', startTime: 'Start time',
@@ -70,6 +71,7 @@ const en = {
about: 'About', // 关于 about: 'About', // 关于
detail: 'Detail', // 详情 detail: 'Detail', // 详情
query: 'Query', // 查询 query: 'Query', // 查询
logLabels: 'Log labels',
account: 'Account', account: 'Account',
back: 'Back', // 返回 back: 'Back', // 返回
unavailable: 'Unavailable', unavailable: 'Unavailable',
@@ -119,6 +121,7 @@ const en = {
}, },
reset: 'Reset', reset: 'Reset',
submit: 'Save', submit: 'Save',
limit: 'Limit',
noData: 'No data', noData: 'No data',
tag: 'Tag', tag: 'Tag',
placeHolder: 'Please enter', placeHolder: 'Please enter',
@@ -529,6 +532,10 @@ const en = {
down: 'down', down: 'down',
prometheus: 'prometheus' prometheus: 'prometheus'
} }
},
explore: {
descending: 'Descending',
wrapLines: 'Wrap lines'
} }
}, },
validate: { // 校验规则 validate: { // 校验规则

View File

@@ -1,9 +1,9 @@
<template> <template>
<div class="interval-refresh"> <div class="interval-refresh">
<time-picker v-if="showTimePicker" ref="timePicker" v-model="searchTime" :default-pick="defaultPick" :show-empty="showEmpty" class="time-picker margin-r-10" size="small" @change="dateChange"></time-picker> <time-picker v-if="showTimePicker" ref="timePicker" v-model="searchTime" :default-pick="defaultPick" :show-empty="showEmpty" class="time-picker margin-r-10" size="small" @change="dateChange"></time-picker>
<multipleTime ref="multipleTime" v-if="showMultiple" :stepSearchTime="searchTime" @change="dateChange(searchTime)" class="multiple-time"/> <multipleTime v-if="showMultiple" ref="multipleTime" :stepSearchTime="searchTime" class="multiple-time margin-r-10" @change="dateChange(searchTime)"/>
<chart-unit v-model="unit" v-if="useChartUnit" style="margin-left: 10px"></chart-unit> <chart-unit v-if="useChartUnit" v-model="unit" class="margin-r-10"></chart-unit>
<div v-show="useRefresh" class="top-tool-btn-group margin-r-10"> <div v-show="useRefresh" class="top-tool-btn-group">
<button :id="id+'-refresh'" class="top-tool-btn top-tool-btn--text" @click="refreshDataFunc"> <button :id="id+'-refresh'" class="top-tool-btn top-tool-btn--text" @click="refreshDataFunc">
<i class="global-active-color nz-icon nz-icon-refresh" style="font-size: 14px"></i>&nbsp; <i class="global-active-color nz-icon nz-icon-refresh" style="font-size: 14px"></i>&nbsp;
<span><slot name="added-text"></slot></span> <span><slot name="added-text"></slot></span>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,404 @@
<template>
<div class="log-detail">
<div id="logChart" class="log-chart">
<!-- <chart ref="logChart" :unit="unit" chart-type="logBar" :show-toolbox="false"></chart>-->
</div>
<div class="log-operations">
<div class="log-operation">
<span class="operation-label">{{$t('overall.time')}}</span>
<el-switch
v-model="time"
:active-color="theme.themeColor"
>
</el-switch>
</div>
<div class="log-operation">
<span class="operation-label">{{$t('dashboard.explore.descending')}}</span>
<el-switch
v-model="operations.descending"
:active-color="theme.themeColor"
>
</el-switch>
</div>
<div class="log-operation">
<span class="operation-label">{{$t('dashboard.explore.wrapLines')}}</span>
<el-switch
v-model="wrapLines"
:active-color="theme.themeColor"
>
</el-switch>
</div>
<div class="log-operation">
<span class="operation-label">{{$t('overall.limit')}}:</span>
<el-select v-model="operations.limit" size="small" style="width: 100px;">
<el-option v-for="option in limitOptions" :key="option" :value="option"></el-option>
</el-select>
</div>
<div class="log-operation">
<span class="operation-label">Result:</span>
<span>{{tableData.length}}</span>
</div>
<div class="log-operation log-operation--right">
<button class="top-tool-btn" type="button" @click="exportLog"><i class="nz-icon nz-icon-download"></i></button>
</div>
</div>
<div class="log-table">
<el-table
:cell-class-name="wrapLines ? '': 'log-row-wrap--no-wrap'"
:data="tableData"
:show-header="false"
class="nz-table2"
size="mini"
>
<el-table-column
type="expand"
>
<template slot-scope="{ row }">
<pre>{{row.labels}}</pre>
</template>
</el-table-column>
<el-table-column
v-if="time"
prop="timestamp"
width="140"
>
<template slot-scope="{ row }">{{row.timestamp}}</template>
</el-table-column>
<el-table-column
prop="message"
>
</el-table-column>
</el-table>
</div>
</div>
</template>
<script>
import * as echarts from 'echarts'
import axios from 'axios'
export default {
name: 'logTab',
props: {
unit: Number,
logData: Array
},
computed: {
tableTimeFormat () {
return this.timeFormat
}
},
data () {
return {
operations: {
levels: [0, 1, 2, 3, 4, 5, 6],
limit: 1000,
descending: true
},
time: true, // 换行和时间不需要处理数据
wrapLines: true,
limitOptions: [300, 1000, 3000, 10000],
levelOptions: [
{
type: 'trace',
keywords: ['trace'],
color: '#6ed0e0'
},
{
type: 'debug',
keywords: ['debug', 'dbug'],
color: '#1f78c1'
},
{
type: 'info',
keywords: ['info', 'information', 'informational', 'notice'],
color: '#7eb26d'
},
{
type: 'warn',
keywords: ['warn', 'warning'],
color: '#ff851b'
},
{
type: 'error',
keywords: ['error', 'err'],
color: '#e24d42'
},
{
type: 'fatal',
keywords: ['emerg', 'critical', 'fatal', 'crit'],
color: '#705da0'
},
{
type: 'unknown',
keywords: [],
color: '#dde4ed'
}
],
tableData: [],
timeFormatData: [],
tableChartData: [],
myChart: null
}
},
methods: {
exportLog () {
this.$emit('exportLog', this.operations)
},
timeFormat (timestamp) {
const timeLength = `${timestamp}`.length
// 判断时间是秒/毫秒/微秒/纳秒
let step = null
switch (timeLength) {
case 10:
step = 1000
break
case 13:
step = 1
break
case 16:
step = 0.001
break
case 19:
step = 0.000001
break
default: break
}
return this.utcTimeToTimezoneStr(timestamp * step)
},
loadChart () {
this.myChart = echarts.init(document.getElementById('logChart'))
if (this.tableChartData.length > 0) {
let series = this.tableChartData.map(d => ({
type: 'bar',
name: d.name,
stack: 'total',
barWidth: 6,
data: d.data.map(item => [item[0], item[1]]),
itemStyle: { color: this.levelOptions.find(l => l.type === d.name).color }
}))
series = series.sort((a, b) => {
return this.levelOptions.findIndex(l => a.name === l.type) - this.levelOptions.findIndex(l => b.name === l.type)
})
const option = {
title: {
show: false
},
grid: {
top: 20,
left: 10,
right: 10,
bottom: 60,
containLabel: true
},
legend: {
bottom: 20,
left: 10
},
series,
xAxis: {
type: 'time',
axisTick: { show: false },
axisLabel: {
rotate: 0,
fontSize: 13 * window.devicePixelRatio,
formatter: '{HH}:{mm}:{ss}'
}
},
tooltip: {
trigger: 'axis'
},
yAxis: {
type: 'value',
axisLabel: {
fontSize: 13 * window.devicePixelRatio,
formatter (value, i) {
let y
if (value < 1000) {
y = value
} else if (value < 1000000) {
y = value / 1000 + ' K'
} else if (value >= 1000000) {
y = value / 1000000 + ' M'
} else {
y = value / 1000000000 + ' G'
}
return y
}
}
},
useUTC: false // 使用本地时间
}
this.myChart.setOption(option)
/* 点击legend
* 1.当前如果是全高亮状态则全部置灰只留被点击的legend高亮
* 2.如果点击的是唯一高亮的legend则变为全高亮状态
* 3.否则只改变被点击的legend状态
* */
this.myChart.on('legendselectchanged', ({ name, selected }) => {
const selectedLevel = []
const unselectedLevel = []
for (const n in selected) {
selected[n] ? selectedLevel.push(n) : unselectedLevel.push(n)
}
// 1.
if (selectedLevel.length + unselectedLevel.length > 1) {
if (unselectedLevel.length === 1 && unselectedLevel[0] === name) {
selectedLevel.forEach(l => {
this.myChart.dispatchAction({
type: 'legendInverseSelect'
})
})
} else if (selectedLevel.length === 0) { // 2.
this.myChart.dispatchAction({
type: 'legendAllSelect'
})
}
}
})
}
},
applyFilter (allTableData, filter) {
let data = [...allTableData]
// 过滤level
data = data.filter(d => {
const hit = filter.levels.some(l => {
return this.levelOptions[l].type === d.level.toLowerCase()
})
return hit
})
// limit
data = data.slice(0, filter.limit)
// 升降序
data = data.sort((a, b) => {
return filter.descending ? b.timestamp - a.timestamp : a.timestamp - b.timestamp
})
// logs内部上方的图表数据
const tableChartData = {}
data.forEach(d => {
tableChartData[d.level] || (tableChartData[d.level] = {})
tableChartData[d.level][`${d.timestamp}`] ? tableChartData[d.level][`${d.timestamp}`]++ : tableChartData[d.level][`${d.timestamp}`] = 1
})
const temp = []
for (const d in tableChartData) {
const level = { name: d, data: [] }
for (const time in tableChartData[d]) {
level.data.push([time, tableChartData[d][time]])
}
temp.push(level)
}
return { tableData: data, tableChartData: temp }
},
filterLogType (data) {
const logData = data.filter(l => l.resultType === 'streamsFormat')
let allTableData = []
// 合并
logData.forEach(d => {
allTableData = [...allTableData, ...d.result]
})
// 去重
const temp = []
allTableData = allTableData.reduce((cur, next) => {
if (temp.indexOf(next.uuid) === -1) {
temp.push(next.uuid)
cur.push(next)
}
return cur
}, [])
// 把时间调整为毫秒,并合并同毫秒的数据
allTableData = allTableData.map(d => ({
...d,
timestamp: this.timeFormat(d.timestamp)
}))
return { logData: allTableData }
}
},
watch: {
logData: {
deep: true,
immediate: true,
handler (n, o) {
const { logData } = this.filterLogType(n) // 过滤出不同resultType合并去重
const { tableData, tableChartData } = this.applyFilter(logData, this.operations) // 应用operation区域的过滤项
this.tableData = tableData
this.tableChartData = tableChartData
this.loadChart()
}
},
operations: {
deep: true,
handler (n, o) {
const { logData } = this.filterLogType(this.logData)
const { tableData, tableChartData } = this.applyFilter(logData, this.operations) // 应用operation区域的过滤项
this.tableData = tableData
this.tableChartData = tableChartData
this.loadChart()
}
}
}
}
</script>
<style lang="scss">
.log-detail {
* {
box-sizing: border-box;
}
.log-table .nz-table2 {
padding: 10px 0 0 0;
.el-table__body {
border-collapse: separate;
border-spacing: 0 6px;
td {
vertical-align: top;
padding: 1px 0;
border: none;
}
td.el-table__expanded-cell {
padding: 0 0 0 60px;
background-color: #fafafa;
}
// 左侧边框
td:first-child {
border-left: 3px solid $--right-box-border-color;
}
td.el-table__expanded-cell:first-child {
border-left: none;
}
}
}
.log-chart {
height: 300px;
width: 100%;
}
.log-operations {
display: flex;
align-items: center;
height: 50px;
width: 100%;
padding: 0 10px 0 20px;
border: 1px solid #E4E8EB;
border-radius: 2px;
.log-operation {
display: flex;
align-items: center;
&.log-operation--right {
margin-left: auto;
}
&:not(:last-of-type) {
margin-right: 30px;
}
.operation-label {
padding-right: 10px;
color: #666666;
}
}
}
}
</style>

View File

@@ -413,6 +413,7 @@ export default {
defaultTooltipFormatter: function (params) { defaultTooltipFormatter: function (params) {
let minusFlag = true let minusFlag = true
let str = '<div>' let str = '<div>'
params instanceof Array || (params = [params])
params.forEach((item, i) => { params.forEach((item, i) => {
const alias = this.queryAlias(i) const alias = this.queryAlias(i)
if (i === 0 && alias.indexOf('Previous ') === -1) { if (i === 0 && alias.indexOf('Previous ') === -1) {

View File

@@ -54,7 +54,7 @@ const commonOption = {
tooltip: { // 和 option.tooltip 的配置项相同 tooltip: { // 和 option.tooltip 的配置项相同
show: true, show: true,
position: 'top', position: 'top',
formatter: function (param) { formatter (param) {
return param.title // 自定义的 DOM 结构 return param.title // 自定义的 DOM 结构
}, },
backgroundColor: 'rgba(255,255,255,0)', backgroundColor: 'rgba(255,255,255,0)',
@@ -103,7 +103,7 @@ const commonOption = {
axisLabel: { axisLabel: {
interval: 0, interval: 0,
rotate: 0, rotate: 0,
formatter: function (value, index) { formatter (value, index) {
const tData = new Date(value) const tData = new Date(value)
return [tData.getFullYear(), tData.getMonth() + 1, tData.getDate()].join('-') + '\n' + return [tData.getFullYear(), tData.getMonth() + 1, tData.getDate()].join('-') + '\n' +
[tData.getHours(), tData.getMinutes()].join(':') [tData.getHours(), tData.getMinutes()].join(':')
@@ -251,7 +251,7 @@ const alertMessageBarByRule = {
axisLabel: { axisLabel: {
show: true, show: true,
fontSize: 12, fontSize: 12,
formatter: function (value) { formatter (value) {
if (value.length > 15) { if (value.length > 15) {
return '...' + value.substring(value.length - 12, value.length) return '...' + value.substring(value.length - 12, value.length)
} }
@@ -305,7 +305,7 @@ const alertMessageBarByAsset = {
axisLabel: { axisLabel: {
show: true, show: true,
fontSize: 12, fontSize: 12,
formatter: function (value) { formatter (value) {
let r = value let r = value
if (r.length > 4) { if (r.length > 4) {
r = value.substring(0, 3) + '...' r = value.substring(0, 3) + '...'
@@ -316,7 +316,62 @@ const alertMessageBarByAsset = {
triggerEvent: true triggerEvent: true
} }
} }
const logBar = {
title: {
show: false
},
xAxis: {
type: 'time',
axisLabel: {
rotate: 0,
fontSize: 13 * window.devicePixelRatio
},
axisPointer: { // y轴上显示指针对应的值
show: true
},
splitLine: {
show: false
},
axisLine: {
show: false
},
axisTick: {
show: false
}
},
tooltip: {
show: true
},
yAxis: {
type: 'value',
splitLine: {
show: true
},
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
fontSize: 13 * window.devicePixelRatio,
formatter (value, i) {
let y
if (value < 1000) {
} else if (value < 1000000) {
y = value / 1000 + ' K'
} else if (value >= 1000000) {
y = value / 1000000 + ' M'
} else {
y = value / 1000000000 + ' G'
}
return y
}
}
},
useUTC: false, // 使用本地时间
series: []
}
const overviewLine = { const overviewLine = {
title: { title: {
show: false show: false
@@ -332,7 +387,7 @@ const overviewLine = {
trigger: 'axis', trigger: 'axis',
confine: false, confine: false,
extraCssText: 'z-index:1000;', extraCssText: 'z-index:1000;',
formatter: function (v, i) { formatter (v, i) {
return v return v
} }
}, },
@@ -375,7 +430,7 @@ const overviewLine = {
}, },
axisLabel: { axisLabel: {
fontSize: 13 * window.devicePixelRatio, fontSize: 13 * window.devicePixelRatio,
formatter: function (value, i) { formatter (value, i) {
let y let y
if (value < 1000) { if (value < 1000) {
y = value + ' Bs' y = value + ' Bs'
@@ -524,22 +579,23 @@ const chartTypes = {
bar: { name: 'alertMessage', option: alertMessageBarByAsset }, bar: { name: 'alertMessage', option: alertMessageBarByAsset },
ruleBar: { name: 'ruleMessage', option: alertMessageBarByRule }, ruleBar: { name: 'ruleMessage', option: alertMessageBarByRule },
assetBar: { name: 'assetMessage', option: alertMessageBarByAsset }, assetBar: { name: 'assetMessage', option: alertMessageBarByAsset },
logBar: { name: 'logBar', option: logBar },
noData: { name: 'noData', option: noDataOption }, noData: { name: 'noData', option: noDataOption },
tooltipPie: { option: tooltipPieOption }, tooltipPie: { option: tooltipPieOption },
topoPie: { option: topoPieOption } topoPie: { option: topoPieOption }
} }
export default { export default {
getOption: function (type) { getOption (type) {
return JSON.parse(JSON.stringify(chartTypes[type].option)) return JSON.parse(JSON.stringify(chartTypes[type].option))
}, },
getOptionNoData: function (type) { getOptionNoData (type) {
chartTypes[type].option.xAxis.data = createTempTimes() chartTypes[type].option.xAxis.data = createTempTimes()
return JSON.parse(JSON.stringify(chartTypes[type].option)) return JSON.parse(JSON.stringify(chartTypes[type].option))
}, },
setMap: function (map) { setMap (map) {
mapOptions.geo.map = map mapOptions.geo.map = map
}, },
getBgColorList: function () { getBgColorList () {
return Object.assign([], bgColorList) return Object.assign([], bgColorList)
} }
} }

View File

@@ -30,7 +30,7 @@
</el-input> </el-input>
</div> </div>
<pick-time id="panel" ref="pickTime" v-model="searchTime" :refresh-data-func="dateChange" :use-chart-unit="false"></pick-time> <pick-time id="panel" ref="pickTime" v-model="searchTime" :refresh-data-func="dateChange" :use-chart-unit="false" class="margin-r-10"></pick-time>
<button id="panel-add-chart" v-has="'panel_chart_add'" :title="$t('overall.createChart')" class="top-tool-btn margin-r-10" <button id="panel-add-chart" v-has="'panel_chart_add'" :title="$t('overall.createChart')" class="top-tool-btn margin-r-10"
type="button" @click="addChart"> type="button" @click="addChart">

View File

@@ -27,7 +27,7 @@ export default new Router({
}, },
{ {
path: '/explore', path: '/explore',
component: resolve => require(['../components/page/dashboard/explore/explore.vue'], resolve) component: resolve => require(['@/components/page/dashboard/explore/explore'], resolve)
}, },
{ {
path: '/overview', path: '/overview',