1
0
Fork 0

Add a new dashboard page.

This commit is contained in:
Jorge Gonzalez 2019-08-26 18:15:41 +02:00 committed by Ludovic Fernandez
parent 89150e1164
commit fd24b1898e
133 changed files with 17303 additions and 11112 deletions

View file

@ -1,79 +0,0 @@
<template>
<canvas />
</template>
<script>
import Chart from "chart.js";
Chart.plugins.register({
afterDraw: function(chart) {
if (chart.data.datasets[0].data.reduce((acc, it) => acc + it, 0) === 0) {
var ctx = chart.chart.ctx;
var width = chart.chart.width;
var height = chart.chart.height;
chart.clear();
ctx.save();
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = "16px normal 'Helvetica Nueue'";
ctx.fillText(`No ${chart.options.title.text}`, width / 2, height / 2);
ctx.restore();
}
}
});
export default {
name: "EntityStateDoughnut",
props: {
entity: {
type: Object,
default: () => ({
errors: 0,
warnings: 0,
total: 0
})
},
title: {
type: String,
required: true
}
},
computed: {
data() {
return [
this.entity.errors,
this.entity.warnings,
this.entity.total - (this.entity.errors + this.entity.warnings)
];
}
},
mounted() {
new Chart(this.$el, {
type: "doughnut",
data: {
datasets: [
{
data: this.data,
backgroundColor: [
"hsl(348, 100%, 61%)",
"hsl(48, 100%, 67%)",
"hsl(141, 71%, 48%)"
]
}
],
labels: ["errors", "warnings", "success"]
},
options: {
title: {
display: true,
text: this.title
},
legend: {
display: false
}
}
});
}
};
</script>

View file

@ -1,22 +0,0 @@
<template>
<div class="tile is-parent">
<div class="tile is-child notification" :class="modifier">
<p class="title">{{ title }}</p>
</div>
</div>
</template>
<script>
export default {
props: {
modifier: {
type: Object,
default: () => ({})
},
title: {
type: String,
required: true
}
}
};
</script>

View file

@ -0,0 +1,27 @@
<template>
<q-avatar :color="state" text-color="white">
<q-icon v-if="state === 'positive'" name="eva-checkmark-circle-2" />
<q-icon v-if="state === 'warning'" name="eva-alert-circle" />
<q-icon v-if="state === 'negative'" name="eva-alert-triangle" />
</q-avatar>
</template>
<script>
export default {
name: 'AvatarState',
props: ['state']
}
</script>
<style scoped lang="scss">
@import "../../css/sass/variables";
.q-avatar{
font-size: 32px;
border-radius: 4px;
.q-icon {
font-size: 22px;
margin: 0 0 0 1px;
}
}
</style>

View file

@ -0,0 +1,30 @@
<script>
import { Doughnut } from 'vue-chartjs'
export default {
extends: Doughnut,
props: {
chartdata: {
type: Object,
default: null
},
options: {
type: Object,
default: null
}
},
watch: {
chartdata: function (newData, oldData) {
// TODO - bug, 'update()' not update the chart, remplace for renderChart()
// console.log('new data from watcher...', newData, oldData, this.$_.isEqual(newData.datasets[0].data, oldData.datasets[0].data))
if (!this.$_.isEqual(newData.datasets[0].data, oldData.datasets[0].data)) {
// this.$data._chart.update()
this.renderChart(this.chartdata, this.options)
}
}
},
mounted () {
this.renderChart(this.chartdata, this.options)
}
}
</script>

View file

@ -0,0 +1,97 @@
<template>
<q-header class="shadow-1">
<section class="app-section bg-primary text-white">
<div class="app-section-wrap app-boxed app-boxed-xl">
<q-toolbar class="row no-wrap items-center">
<div class="q-pr-md logo">
<img alt="logo" src="~assets/logo.svg">
</div>
<q-tabs align="left" inline-label indicator-color="transparent" active-color="white" stretch>
<q-route-tab to="/" icon="eva-home-outline" no-caps label="Dashboard" />
</q-tabs>
<q-space />
<q-btn type="a" :href="`https://docs.traefik.io/${parsedVersion}`" target="_blank" stretch flat no-caps label="Documentation" class="btn-menu" />
<q-btn v-if="version" type="a" href="https://github.com/containous/traefik/" target="_blank" stretch flat no-caps :label="`${name} ${version}`" class="btn-menu" />
</q-toolbar>
</div>
</section>
<section class="app-section bg-white text-black">
<div class="app-section-wrap app-boxed app-boxed-xl">
<slot />
</div>
</section>
</q-header>
</template>
<script>
import config from '../../../package'
import { mapActions, mapGetters } from 'vuex'
export default {
name: 'NavBar',
data () {
return {
}
},
computed: {
...mapGetters('core', { coreVersion: 'version' }),
version () {
return this.coreVersion.Version
},
parsedVersion () {
if (this.version === undefined) {
return 'master'
}
if (this.version === 'dev') {
return 'master'
}
const matches = this.version.match(/^(v?\d+\.\d+)/)
return matches ? 'v' + matches[1] : 'master'
},
name () {
return config.productName
}
},
methods: {
...mapActions('core', { getVersion: 'getVersion' })
},
created () {
this.getVersion()
}
}
</script>
<style scoped lang="scss">
@import "../../css/sass/variables";
.q-toolbar {
min-height: 64px;
}
.logo {
display: inline-block;
img {
height: 24px;
}
}
.q-tabs {
color: rgba( $app-text-white, .4 );
/deep/ .q-tabs__content {
.q-tab__content{
min-width: 100%;
.q-tab__label {
font-size: 16px;
font-weight: 600;
}
}
}
}
.btn-menu{
color: rgba( $app-text-white, .4 );
font-size: 16px;
font-weight: 600;
}
</style>

View file

@ -0,0 +1,15 @@
<template>
<q-page>
<slot/>
</q-page>
</template>
<script>
export default {
name: 'PageDefault'
}
</script>
<style scoped lang="scss">
</style>

View file

@ -0,0 +1,72 @@
<template>
<span
:style="{ height, width: computedWidth }"
v-bind:class="['SkeletonBox']"
/>
</template>
<script>
export default {
name: `SkeletonBox`,
props: {
maxWidth: {
default: 100,
type: Number
},
minWidth: {
default: 80,
type: Number
},
height: {
default: `2em`,
type: String
},
width: {
default: null,
type: String
}
},
computed: {
computedWidth () {
return this.width || `${Math.floor((Math.random() * (this.maxWidth - this.minWidth)) + this.minWidth)}%`
}
}
}
</script>
<style scoped lang="scss">
.SkeletonBox {
display: inline-block;
position: relative;
vertical-align: middle;
overflow: hidden;
background-color: #E0E0E0;
border-radius: 4px;
&.dark{
background-color: #9E9E9E;
}
&::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
will-change: transform;
transform: translateX(-100%) translateZ(0);
background-image: linear-gradient(
90deg,
rgba(#fff, 0) 0,
rgba(#fff, 0.2) 20%,
rgba(#fff, 0.5) 60%,
rgba(#fff, 0)
);
// TODO - fix high cpu usage
// animation: shimmer 5s infinite;
content: '';
}
@keyframes shimmer {
from { transform: translateX(-100%) translateZ(0); }
to { transform: translateX(100%) translateZ(0); }
}
}
</style>

View file

@ -0,0 +1,196 @@
<template>
<q-card flat bordered>
<q-card-section>
<div class="row items-center no-wrap">
<div class="col">
<div class="text-h6 text-weight-bold">{{getName}}</div>
</div>
</div>
</q-card-section>
<q-card-section>
<div class="row items-center q-col-gutter-md">
<div class="col-12 col-sm-6">
<ChartDoughnut
:chartdata="getChartdata()"
:options="options"/>
</div>
<div class="col-12 col-sm-6">
<q-list>
<q-item class="label-state">
<q-item-section avatar>
<avatar-state state="positive"/>
</q-item-section>
<q-item-section class="label-state-text">
<q-item-label>Success</q-item-label>
<q-item-label caption lines="1">{{getSuccess(true)}}%</q-item-label>
</q-item-section>
<q-item-section side class="label-state-side">
{{getSuccess()}}
</q-item-section>
</q-item>
<q-item class="label-state">
<q-item-section avatar>
<avatar-state state="warning"/>
</q-item-section>
<q-item-section class="label-state-text">
<q-item-label>Warnings</q-item-label>
<q-item-label caption lines="1">{{getWarnings(true)}}%</q-item-label>
</q-item-section>
<q-item-section side class="label-state-side">
{{getWarnings()}}
</q-item-section>
</q-item>
<q-item class="label-state">
<q-item-section avatar>
<avatar-state state="negative"/>
</q-item-section>
<q-item-section class="label-state-text">
<q-item-label>Errors</q-item-label>
<q-item-label caption lines="1">{{getErrors(true)}}%</q-item-label>
</q-item-section>
<q-item-section side class="label-state-side">
{{getErrors()}}
</q-item-section>
</q-item>
</q-list>
</div>
</div>
</q-card-section>
</q-card>
</template>
<script>
import Helps from '../../_helpers/Helps'
import ChartDoughnut from '../_commons/ChartDoughnut'
import AvatarState from '../_commons/AvatarState'
export default {
name: 'PanelChart',
props: ['name', 'data', 'type'],
components: {
ChartDoughnut,
AvatarState
},
data () {
return {
loading: true,
options: {
legend: {
display: false
},
animation: {
duration: 1000
},
tooltips: {
enabled: true
}
}
}
},
computed: {
getName () {
return Helps.capFirstLetter(this.name)
},
getUrl () {
return `/${this.type}/${this.getName.toLowerCase()}`
}
},
methods: {
getSuccess (inPercent = false) {
const num = this.data.total - (this.data.errors + this.data.warnings)
let result = 0
if (inPercent) {
result = Helps.getPercent(num, this.data.total).toFixed(0)
} else {
result = num
}
return isNaN(result) ? 0 : result
},
getWarnings (inPercent = false) {
const num = this.data.warnings
let result = 0
if (inPercent) {
result = Helps.getPercent(num, this.data.total).toFixed(0)
} else {
result = num
}
return isNaN(result) ? 0 : result
},
getErrors (inPercent = false) {
const num = this.data.errors
let result = 0
if (inPercent) {
result = Helps.getPercent(num, this.data.total).toFixed(0)
} else {
result = num
}
return isNaN(result) ? 0 : result
},
getData () {
return [this.getSuccess(), this.getWarnings(), this.getErrors()]
},
getChartdata () {
if (this.getData()[0] === 0 && this.getData()[1] === 0 && this.getData()[2] === 0) {
this.options.tooltips.enabled = false
return {
datasets: [{
backgroundColor: [
'#f2f3f5'
],
data: [1]
}]
}
} else {
this.options.tooltips.enabled = true
return {
datasets: [{
backgroundColor: [
'#00a697',
'#db7d11',
'#ff0039'
],
data: this.getData()
}],
labels: [
'Success',
'Warnings',
'Errors'
]
}
}
}
}
}
</script>
<style scoped lang="scss">
@import "../../css/sass/variables";
.label-state {
min-height: 32px;
padding: 8px;
.q-item__section--avatar{
min-width: 32px;
padding: 0 8px 0 0;
}
&-text{
.q-item__label{
font-size: 16px;
line-height: 16px !important;
font-weight: 600;
}
.q-item__label--caption{
font-size: 14px;
line-height: 14px !important;
font-weight: 500;
color: $app-text-grey;
}
}
&-side{
font-size: 22px;
font-weight: 700;
padding: 0 0 0 8px;
color: inherit;
}
}
</style>

View file

@ -0,0 +1,25 @@
<template>
<q-card flat bordered>
<q-card-section>
<div class="row items-center no-wrap">
<div class="col">
<div class="text-subtitle2 text-uppercase text-center text-app-grey" style="letter-spacing: 3px;">{{name}}</div>
</div>
</div>
</q-card-section>
<q-card-section>
<div class="text-h3 text-center text-weight-bold">{{address}}</div>
</q-card-section>
</q-card>
</template>
<script>
export default {
name: 'PanelEntry',
props: ['address', 'name']
}
</script>
<style scoped>
</style>

View file

@ -0,0 +1,78 @@
<template>
<q-card flat bordered>
<q-card-section>
<div class="row items-center no-wrap">
<div class="col">
<div class="text-subtitle2 text-uppercase text-center text-app-grey" style="letter-spacing: 3px;">{{featureKey}}</div>
</div>
</div>
</q-card-section>
<q-card-section>
<div class="text-h3 text-center text-weight-bold">
<q-chip
outline
color="primary"
text-color="white"
v-bind:class="['feature-chip', {'feature-chip-string':isString}, {'feature-chip-boolean':isBoolean}, {'feature-chip-boolean-true':isTrue}]">
{{getVal}}
</q-chip>
</div>
</q-card-section>
</q-card>
</template>
<script>
export default {
name: 'PanelFeature',
props: ['featureKey', 'featureVal'],
computed: {
isString () {
return this.$_.isString(this.featureVal)
},
isBoolean () {
return this.$_.isBoolean(this.featureVal) || this.featureVal === ''
},
isTrue () {
return this.isBoolean && this.featureVal === true
},
getVal () {
if (this.featureVal === true) {
return 'ON'
} else if (this.featureVal === false || this.featureVal === '') {
return 'OFF'
} else {
return this.featureVal
}
}
}
}
</script>
<style scoped lang="scss">
@import "../../css/sass/variables";
.feature-chip {
border-radius: 12px;
border-width: 2px;
height: 56px;
padding: 12px 24px;
&-string{
border-color: $app-text-grey;
font-size: 24px;
color: $app-text-grey !important;
background-color: rgba( $app-text-grey, .1 );
}
&-boolean{
font-size: 40px;
font-weight: 700;
border-color: $negative;
color: $negative !important;
background-color: rgba( $negative, .1 );
&-true{
border-color: $positive;
color: $positive !important;
background-color: rgba( $positive, .1 );
}
}
}
</style>

View file

@ -0,0 +1,46 @@
<template>
<q-card flat bordered>
<q-card-section>
<div class="row items-center no-wrap">
<div class="col text-center">
<q-avatar class="provider-logo">
<q-icon :name="`img:statics/providers/${getNameLogo}.svg`" />
</q-avatar>
</div>
</div>
</q-card-section>
<q-card-section>
<div class="text-h6 text-center text-weight-bold">
{{getName}}
</div>
</q-card-section>
</q-card>
</template>
<script>
export default {
name: 'PanelProvider',
props: ['name'],
computed: {
getName () {
return this.name
},
getNameLogo () {
return this.getName.toLowerCase()
}
}
}
</script>
<style scoped lang="scss">
@import "../../css/sass/variables";
.provider-logo {
width: 52px;
height: 52px;
img {
width: 100%;
height: 100%;
}
}
</style>

View file

@ -0,0 +1,65 @@
<template>
<q-toolbar class="row no-wrap items-center">
<q-tabs align="left" inline-label indicator-color="transparent" stretch>
<q-route-tab :to="`/http/routers`" no-caps label="HTTP Routers">
<q-badge align="middle" label="10" class="q-ml-sm"/>
</q-route-tab>
<q-route-tab :to="`/http/services`" no-caps label="HTTP Services">
<q-badge align="middle" label="12" class="q-ml-sm"/>
</q-route-tab>
<q-route-tab :to="`/http/middlewares`" no-caps label="HTTP Middlewares">
<q-badge align="middle" label="8" class="q-ml-sm"/>
</q-route-tab>
</q-tabs>
</q-toolbar>
</template>
<script>
export default {
name: 'HTTPToolBar',
data () {
return {
}
}
}
</script>
<style scoped lang="scss">
@import "../../css/sass/variables";
.q-toolbar {
min-height: 48px;
padding: 0;
}
.q-tabs {
/deep/ .q-tabs__content {
.q-tab__label {
color: $app-text-grey;
font-size: 16px;
font-weight: 700;
}
.q-badge {
font-size: 13px;
font-weight: 700;
border-radius: 12px;
text-align: center;
align-items: center;
justify-content: center;
min-width: 30px;
padding: 6px;
color: $app-text-grey;
background-color: rgba( $app-text-grey, .1 );
}
.q-tab--active {
.q-tab__label {
color: $accent;
}
.q-badge {
color: $accent;
background-color: rgba( $accent, .1 );
}
}
}
}
</style>

View file

@ -0,0 +1,62 @@
<template>
<q-toolbar class="row no-wrap items-center">
<q-tabs align="left" inline-label indicator-color="transparent" stretch>
<q-route-tab :to="`/tcp/routers`" no-caps label="TCP Routers">
<q-badge align="middle" label="10" class="q-ml-sm"/>
</q-route-tab>
<q-route-tab :to="`/tcp/services`" no-caps label="TCP Services">
<q-badge align="middle" label="12" class="q-ml-sm"/>
</q-route-tab>
</q-tabs>
</q-toolbar>
</template>
<script>
export default {
name: 'TCPToolBar',
data () {
return {
}
}
}
</script>
<style scoped lang="scss">
@import "../../css/sass/variables";
.q-toolbar {
min-height: 48px;
padding: 0;
}
.q-tabs {
/deep/ .q-tabs__content {
.q-tab__label {
color: $app-text-grey;
font-size: 16px;
font-weight: 700;
}
.q-badge {
font-size: 13px;
font-weight: 700;
border-radius: 12px;
text-align: center;
align-items: center;
justify-content: center;
min-width: 30px;
padding: 6px;
color: $app-text-grey;
background-color: rgba( $app-text-grey, .1 );
}
.q-tab--active {
.q-tab__label {
color: $accent;
}
.q-badge {
color: $accent;
background-color: rgba( $accent, .1 );
}
}
}
}
</style>