This is an automated email from the ASF dual-hosted git repository. min pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/incubator-dubbo-admin.git
The following commit(s) were added to refs/heads/develop by this push: new 1faaec0 add echarts to implement metrics 1faaec0 is described below commit 1faaec0b747604897c4a9af6c67222744231ecd7 Author: nzomkxia <z82507...@gmail.com> AuthorDate: Thu Mar 21 10:05:58 2019 +0800 add echarts to implement metrics --- dubbo-admin-ui/index.html | 1 + dubbo-admin-ui/src/api/chart.js | 96 +++++++++ .../src/components/metrics/ServiceMetrics.vue | 228 +++++++++++++++++++-- dubbo-admin-ui/src/components/public/MiniChart.vue | 86 ++++++++ dubbo-admin-ui/src/lang/en.js | 5 + dubbo-admin-ui/src/lang/zh.js | 5 + dubbo-admin-ui/src/util/echart.js | 215 +++++++++++++++++++ 7 files changed, 622 insertions(+), 14 deletions(-) diff --git a/dubbo-admin-ui/index.html b/dubbo-admin-ui/index.html index 2718725..a4f69a5 100644 --- a/dubbo-admin-ui/index.html +++ b/dubbo-admin-ui/index.html @@ -23,6 +23,7 @@ <title>Dubbo Admin</title> <link href='/static/OpenSans.css' rel="stylesheet" type="text/css"> <link rel="shortcut icon" href="/static/dubbo.ico" type="image/x-icon"> + <script src="https://cdn.bootcss.com/echarts/4.0.4/echarts-en.min.js"></script> </head> <body> <div id="app"></div> diff --git a/dubbo-admin-ui/src/api/chart.js b/dubbo-admin-ui/src/api/chart.js new file mode 100644 index 0000000..4190c7b --- /dev/null +++ b/dubbo-admin-ui/src/api/chart.js @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +const range = (start, end) => new Array(end - start).fill(start).map((el, i) => start + i) + +const shortMonth = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' +] +const monthVisitData = shortMonth.map(m => { + return { + 'month': m, + 'Unique Visit': Math.floor(Math.random() * 1000) + 200, + 'Page View': Math.floor(Math.random() * 1000) + 250 + } +}) + +const campaignData = [ + { + value: 335, + name: 'Website' + }, + { + value: 310, + name: 'Email' + }, + { + value: 234, + name: 'Ads' + }, + { + value: 135, + name: 'Video' + }, + { + value: 1548, + name: 'Search' + } +] +const locationData = [ + { + value: 50, + name: 'China' + }, + { + value: 35, + name: 'USA' + }, + { + value: 25, + name: 'EU' + }, + { + value: 10, + name: 'Russia' + }, + { + value: 10, + name: 'Other' + } +] + +const StackMainData = [220, 182, 191, 234, 290, 330, 310, 123, 442, 321, 90, 149, 210, 122, 133, 334, 198, 123, 125, 220] +const StackData = StackMainData.map((item, key) => { + return { + 'label': key + 'D', + 'max': 500, + 'sales': item + } +}) +const SinData = range(1, 12).map(i => { + return { + 'cate': 'Cat' + i, + 'value': ((Math.sin(i / 5) * (i / 5 - 0.1) + i / 6) * 5) + } +}) + +export { + monthVisitData, + campaignData, + locationData, + StackData, + SinData +} diff --git a/dubbo-admin-ui/src/components/metrics/ServiceMetrics.vue b/dubbo-admin-ui/src/components/metrics/ServiceMetrics.vue index 204083a..e1765a9 100644 --- a/dubbo-admin-ui/src/components/metrics/ServiceMetrics.vue +++ b/dubbo-admin-ui/src/components/metrics/ServiceMetrics.vue @@ -16,27 +16,227 @@ --> <template> - <v-container - fill-height - > - <v-layout align-center> - <v-flex text-xs-center> - <h1 class="display-2 primary--text">{{$t('later.metrics')}}</h1> - <v-btn - href="#/service" - color="primary" - outline - > - {{$t('goIndex')}} - </v-btn> + <v-container grid-list-xl fluid> + <v-layout row wrap> + <v-flex lg12> + <breadcrumb title="metrics" :items="breads"></breadcrumb> + </v-flex> + <v-flex xs12 > + <search id="serviceSearch" v-model="filter" :submit="submit" :label="$t('searchSingleMetrics')"></search> + </v-flex> + <v-flex lg4 sm6 xs12> + <v-card> + <v-card-text> + <mini-chart + title="Monthly Sales" + sub-title="10%" + icon="trending_up" + :data="dataset.monthVisit" + :chart-color="color.blue.base" + type="line" + > + </mini-chart> + <mini-chart + title="Monthly Sales" + sub-title="10%" + icon="trending_up" + :data="dataset.monthVisit" + :chart-color="color.blue.base" + type="line" + > + </mini-chart> + </v-card-text> + </v-card> + </v-flex> + <v-flex lg4 sm6 xs12> + <v-card> + <v-card-text> + <mini-chart + title="Monthly Sales" + sub-title="10%" + icon="trending_up" + :data="dataset.monthVisit" + :chart-color="color.blue.base" + type="line" + > + </mini-chart> + <mini-chart + title="Monthly Sales" + sub-title="10%" + icon="trending_up" + :data="dataset.monthVisit" + :chart-color="color.blue.base" + type="line" + > + </mini-chart> + </v-card-text> + </v-card> + </v-flex> + <v-flex lg4 sm6 xs12> + <v-card> + <v-card-text> + <mini-chart + title="Monthly Sales" + sub-title="10%" + icon="trending_up" + :data="dataset.monthVisit" + :chart-color="color.blue.base" + type="line" + > + </mini-chart> + <mini-chart + title="Monthly Sales" + sub-title="10%" + icon="trending_up" + :data="dataset.monthVisit" + :chart-color="color.blue.base" + type="line" + > + </mini-chart> + </v-card-text> + </v-card> + </v-flex> + <v-flex sm12> + <h3>{{$t('methodMetrics')}}</h3> + </v-flex> + <v-flex lg12 > + <v-tabs + class="elevation-1"> + <v-tab> + {{$t('providers')}} + </v-tab> + <v-tab> + {{$t('consumers')}} + </v-tab> + <v-tab-item> + <v-data-table + class="elevation-1" + :headers="headers" + :items="providerDetails" + > + <template slot="items" slot-scope="props"> + <td>{{props.item.service}}</td> + <td>{{props.item.method}}</td> + <td>{{props.item.qps}}</td> + <td>{{props.item.rt}}</td> + <td>{{props.item.successRate}}</td> + </template> + </v-data-table> + </v-tab-item> + <v-tab-item > + <v-data-table + class="elevation-1" + :headers="headers" + :items="consumerDetails" + > + <template slot="items" slot-scope="props"> + <td>{{props.item.service}}</td> + <td>{{props.item.method}}</td> + <td>{{props.item.qps}}</td> + <td>{{props.item.rt}}</td> + <td>{{props.item.successRate}}</td> + </template> + </v-data-table> + </v-tab-item> + </v-tabs> </v-flex> </v-layout> </v-container> </template> <script> + import EChart from '@/util/echart' + import Material from 'vuetify/es5/util/colors' + import MiniChart from '@/components/public/MiniChart' + import Breadcrumb from '@/components/public/Breadcrumb' + import Search from '@/components/public/Search' + import { + monthVisitData, + campaignData, + locationData, + StackData, + SinData + } from '@/api/chart' export default { - name: 'ServiceMetrics' + name: 'ServiceMetrics', + components: { + MiniChart, + EChart, + Breadcrumb, + Search + }, + data () { + return { + selectedTab: 'tab-1', + filter: '', + headers: [], + providerDetails: [ + { + service: 'a.b.c.d', + method: 'aaaa~ICS', + qps: '0.58', + rt: '111', + successRate: '100%' + }, + { + service: 'a.b.c.f', + method: 'bbbb~ICS', + qps: '0.87', + rt: '120', + successRate: '90%' + } + + ], + consumerDetails: [], + option: null, + dataset: { + sinData: SinData, + monthVisit: monthVisitData, + campaign: campaignData, + location: locationData, + stackData: StackData + }, + color: Material, + breads: [ + { + text: 'metrics', + href: '' + } + ] + + } + }, + methods: { + submit: function () { + }, + setHeaders: function () { + this.headers = [ + { + text: this.$t('service'), + value: 'service' + }, + { + text: this.$t('method'), + value: 'method' + }, + { + text: this.$t('qps'), + value: 'qps' + }, + { + text: this.$t('rt'), + value: 'rt' + }, + { + text: this.$t('successRate'), + value: 'successRate' + } + ] + } + }, + mounted: function () { + this.setHeaders() + } } </script> diff --git a/dubbo-admin-ui/src/components/public/MiniChart.vue b/dubbo-admin-ui/src/components/public/MiniChart.vue new file mode 100644 index 0000000..bc087e9 --- /dev/null +++ b/dubbo-admin-ui/src/components/public/MiniChart.vue @@ -0,0 +1,86 @@ +<!-- + - Licensed to the Apache Software Foundation (ASF) under one or more + - contributor license agreements. See the NOTICE file distributed with + - this work for additional information regarding copyright ownership. + - The ASF licenses this file to You under the Apache License, Version 2.0 + - (the "License"); you may not use this file except in compliance with + - the License. You may obtain a copy of the License at + - + - http://www.apache.org/licenses/LICENSE-2.0 + - + - Unless required by applicable law or agreed to in writing, software + - distributed under the License is distributed on an "AS IS" BASIS, + - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + - See the License for the specific language governing permissions and + - limitations under the License. + --> + +<template> + <div class="layout row ma-0 align-center justify-space-between"> + <div class="text-box"> + <div class="subheading pb-2">{{title}}</div> + <span class="grey--text">{{subTitle}} <v-icon small :color="iconColor">{{icon}}</v-icon></span> + </div> + <div class="chart"> + <e-chart + :path-option="computeChartOption" + height="68px" + width="100%" + > + </e-chart> + </div> + </div> +</template> + +<script> + import EChart from '@/util/echart' + export default { + components: { + EChart + }, + props: { + title: String, + subTitle: String, + icon: String, + iconColor: { + type: String, + default: 'success' + }, + type: String, + chartColor: String, + data: Array + }, + data () { + return { + defaultOption: [ + ['dataset.source', this.data], + ['xAxis.show', false], + ['yAxis.show', false], + ['color', [this.chartColor]] + ] + } + }, + + computed: { + computeChartOption () { + switch (this.type) { + case 'bar': + this.defaultOption.push(['series[0].type', 'bar']) + break + case 'area': + this.defaultOption.push(['series[0].type', 'line']) + this.defaultOption.push(['series[0].areaStyle', {}]) + break + default: + break + } + return this.defaultOption + } + } + + } +</script> + +<style scoped> + +</style> diff --git a/dubbo-admin-ui/src/lang/en.js b/dubbo-admin-ui/src/lang/en.js index af39aff..405fa04 100644 --- a/dubbo-admin-ui/src/lang/en.js +++ b/dubbo-admin-ui/src/lang/en.js @@ -34,6 +34,9 @@ export default { version: 'Version', app: 'Application', ip: 'IP', + qps: 'qps', + rt: 'rt', + successRate: 'success rate', port: 'PORT', timeout: 'timeout(ms)', serialization: 'serialization', @@ -72,11 +75,13 @@ export default { appNameHint: 'Application name the service belongs to', basicInfo: 'BasicInfo', metaData: 'MetaData', + methodMetrics: 'Method Statistics', searchDubboService: 'Search Dubbo Services or applications', serviceSearchHint: 'Service ID, org.apache.dubbo.demo.api.DemoService, * for all services', ipSearchHint: 'Find all services provided by the target server on the specified IP address', appSearchHint: 'Input an application name to find all services provided by one particular application, * for all', searchTagRule: 'Search Tag Rule by application name', + searchSingleMetrics: 'Search Metrics by IP', searchBalanceRule: 'Search Balancing Rule', noMetadataHint: 'There is no metadata available, please update to Dubbo2.7, or check your config center configuration in application.properties, please check ', parameterList: 'parameterList', diff --git a/dubbo-admin-ui/src/lang/zh.js b/dubbo-admin-ui/src/lang/zh.js index 9a03cfe..3f863a1 100644 --- a/dubbo-admin-ui/src/lang/zh.js +++ b/dubbo-admin-ui/src/lang/zh.js @@ -33,6 +33,9 @@ export default { version: '版本', app: '应用', ip: 'IP地址', + qps: 'qps', + rt: 'rt', + successRate: '成功率', serviceInfo: '服务信息', port: '端口', timeout: '超时(毫秒)', @@ -72,11 +75,13 @@ export default { appNameHint: '服务所属的应用名称', basicInfo: '基础信息', metaData: '元数据', + methodMetrics: '服务方法统计', searchDubboService: '搜索Dubbo服务或应用', serviceSearchHint: '服务ID, org.apache.dubbo.demo.api.DemoService, * 代表所有服务', ipSearchHint: '在指定的IP地址上查找目标服务器提供的所有服务', appSearchHint: '输入应用名称以查找由一个特定应用提供的所有服务, * 代表所有', searchTagRule: '根据应用名搜索标签规则', + searchSingleMetrics: '输入IP搜索Metrics信息', searchBalanceRule: '搜索负载均衡规则', parameterList: '参数列表', returnType: '返回值', diff --git a/dubbo-admin-ui/src/util/echart.js b/dubbo-admin-ui/src/util/echart.js new file mode 100644 index 0000000..3660f21 --- /dev/null +++ b/dubbo-admin-ui/src/util/echart.js @@ -0,0 +1,215 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * ECharts Vue Wrapper + * Michael Wang + */ +import colors from 'vuetify/es5/util/colors' +import _object from 'lodash/object' + +const ECharts = window.echarts || undefined +if (ECharts === undefined) { + console.error('ECharts is not defined') +} +// set color palette +const colorPalette = [] + +Object.entries(colors).forEach((item) => { + if (item[1].base) { + colorPalette.push(item[1].base) + } +}); + +(function () { + const throttle = function (type, name, obj) { + obj = obj || window + let running = false + let func = function () { + if (running) { return } + running = true + requestAnimationFrame(function () { + obj.dispatchEvent(new CustomEvent(name)) + running = false + }) + } + obj.addEventListener(type, func) + } + /* init - you can init any event */ + throttle('resize', 'optimizedResize') +})() +export default { + name: 'v-echart', + + render (h) { + const data = { + staticClass: 'v-chart', + style: this.canvasStyle, + ref: 'canvas', + on: this.$listeners + } + return h('div', data) + }, + + props: { + // args of ECharts.init(dom, theme, opts) + width: { type: String, default: 'auto' }, + height: { type: String, default: '400px' }, + merged: { + type: Boolean, + default: true + }, + // instace.setOption + pathOption: [Object, Array], + option: Object, + // general config + textStyle: Object, + title: Object, + legend: [Object, Array], + tooltip: Object, + grid: { type: [Object, Array] }, + xAxis: [Object, Array], + yAxis: [Object, Array], + series: [Object, Array], + axisPointer: Object, + dataset: { type: [Object, Array], default () { return {} } }, // option.dataSet + colors: Array, // echarts.option.color + backgroundColor: [Object, String], + toolbox: { type: [Object, Array] }, + // resize delay + widthChangeDelay: { + type: Number, + default: 450 + } + }, + data: () => ({ + chartInstance: null, + clientWidth: null, + allowedOptions: [ + 'textStyle', 'title', 'legend', 'xAxis', + 'yAxis', 'series', 'tooltip', 'axisPointer', + 'grid', 'dataset', 'colors', 'backgroundColor' + ], + _defaultOption: { + tooltip: { + show: true + }, + title: { + show: true, + textStyle: { + color: 'rgba(0, 0, 0 , .87)', + fontFamily: 'sans-serif' + } + }, + grid: { + containLabel: true + }, + xAxis: { + show: true, + type: 'category', + axisLine: { + lineStyle: { + color: 'rgba(0, 0, 0 , .54)', + type: 'dashed' + } + }, + axisTick: { + show: true, + alignWithLabel: true, + lineStyle: { + show: true, + color: 'rgba(0, 0, 0 , .54)', + type: 'dashed' + } + }, + axisLabel: { + show: false + } + }, + yAxis: { + show: true, + type: 'value', + axisLine: { + lineStyle: { + color: 'rgba(0, 0, 0 , .54)', + type: 'dashed' + } + }, + axisLabel: { + show: false + }, + splitLine: { + lineStyle: { + type: 'dashed' + } + }, + axisTick: { + show: true, + lineStyle: { + show: true, + color: 'rgba(0, 0, 0 , .54)', + type: 'dashed' + } + } + }, + series: [{ + type: 'line' + }] + + } + }), + computed: { + canvasStyle () { + return { + width: this.width, + height: this.height + } + } + }, + methods: { + init () { + // set + if (this.pathOption) { + this.pathOption.forEach((p) => { + _object.set(this.$data._defaultOption, p[0], p[1]) + }) + } + this.chartInstance = ECharts.init(this.$refs.canvas, 'material') + this.chartInstance.setOption(_object.merge(this.option, this.$data._defaultOption)) + window.addEventListener('optimizedResize', (e) => { + setTimeout(_ => { + this.chartInstance.resize() + }, this.widthChangeDelay) + }) + }, + + resize () { + this.chartInstance.resize() + }, + clean () { + window.removeEventListener('resize', this.chartInstance.resize) + this.chartInstance.clear() + } + }, + mounted () { + this.init() + }, + + beforeDestroy () { + this.clean() + } +}