ui: Admin, account and project dashboard improvements (#8193)

Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
This commit is contained in:
Rohit Yadav 2023-11-08 14:38:05 +05:30 committed by GitHub
parent e65c9ffe70
commit b2e83271f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 891 additions and 269 deletions

View File

@ -2148,6 +2148,7 @@
"label.volumetotal": "Volume",
"label.volumetype": "Volume Type",
"label.vpc": "VPC",
"label.vpcs": "VPCs",
"label.vpc.id": "VPC ID",
"label.vpc.offerings": "VPC offerings",
"label.vpc.virtual.router": "VPC virtual router",

View File

@ -93,7 +93,6 @@ export default {
}
&-footer {
border-top: 1px solid #e8e8e8;
padding-top: 9px;
margin-top: 8px;
}

View File

@ -134,7 +134,7 @@ export default {
text-align: center;
transition: all 0.5s;
cursor: pointer;
top: calc(50% - 45px);
top: calc(100% - 45px);
z-index: 100;
&.left{

View File

@ -20,7 +20,7 @@ import { UserLayout, BasicLayout, RouteView } from '@/layouts'
import AutogenView from '@/views/AutogenView.vue'
import IFramePlugin from '@/views/plugins/IFramePlugin.vue'
import { shallowRef, defineAsyncComponent } from 'vue'
import { shallowRef } from 'vue'
import { vueProps } from '@/vue-app'
import compute from '@/config/section/compute'
@ -201,26 +201,7 @@ export function asyncRouterMap () {
name: 'dashboard',
meta: {
title: 'label.dashboard',
icon: 'DashboardOutlined',
tabs: [
{
name: 'dashboard',
component: shallowRef(defineAsyncComponent(() => import('@/views/dashboard/UsageDashboardChart')))
},
{
name: 'accounts',
show: (record, route, user) => { return record.account === user.account || ['Admin', 'DomainAdmin'].includes(user.roletype) },
component: shallowRef(defineAsyncComponent(() => import('@/views/project/AccountsTab')))
},
{
name: 'limits',
params: {
projectid: 'id'
},
show: (record, route, user) => { return ['Admin'].includes(user.roletype) },
component: shallowRef(defineAsyncComponent(() => import('@/components/view/ResourceLimitTab.vue')))
}
]
icon: 'DashboardOutlined'
},
component: () => import('@/views/dashboard/Dashboard')
},

View File

@ -63,7 +63,8 @@ import {
Slider,
AutoComplete,
Collapse,
Space
Space,
Statistic
} from 'ant-design-vue'
import VueClipboard from 'vue3-clipboard'
import VueCropper from 'vue-cropper'
@ -127,5 +128,6 @@ export default {
app.use(Collapse)
app.use(Descriptions)
app.use(Space)
app.use(Statistic)
}
}

View File

@ -36,7 +36,7 @@
.dark-mode {
background: @dark-bgColor;
h1, h2, h3, h4, h5, h6 {
h1, h2, h3, h4, h5, h6, .ant-statistic-title, .ant-statistic-content {
color: @dark-text-color-3;
}
@ -959,4 +959,4 @@
.button-clear-notification {
background-color: @dark-secondary-bgColor;
}
}
}

View File

@ -43,7 +43,7 @@ export const deviceEnquire = function (callback) {
}
enquireJs
.register('screen and (max-width: 800px)', matchMobile)
.register('screen and (min-width: 800px) and (max-width: 1366px)', matchTablet)
.register('screen and (min-width: 1367px)', matchDesktop)
.register('screen and (max-width: 765px)', matchMobile)
.register('screen and (min-width: 766px) and (max-width: 1279px)', matchTablet)
.register('screen and (min-width: 1280px)', matchDesktop)
}

View File

@ -16,8 +16,8 @@
// under the License.
<template>
<a-row class="capacity-dashboard" :gutter="12">
<a-col :xl="18">
<a-row class="capacity-dashboard" :gutter="[12,12]">
<a-col :span="24">
<div class="capacity-dashboard-wrapper">
<div class="capacity-dashboard-select">
<a-select
@ -41,91 +41,283 @@
<div class="capacity-dashboard-button">
<a-button
shape="round"
@click="() => { listCapacity(zoneSelected, true); listEvents() }">
@click="() => { updateData(zoneSelected); listAlerts(); listEvents(); }">
<reload-outlined/>
{{ $t('label.fetch.latest') }}
</a-button>
</div>
</div>
<a-row :gutter="12">
<a-col
:xs="12"
:sm="8"
:md="6"
:style="{ marginBottom: '12px' }"
v-for="stat in stats"
:key="stat.type">
<chart-card :loading="loading">
<router-link :to="{ path: '/zone/' + zoneSelected.id }">
<div class="capacity-dashboard-chart-card-inner">
<h3>{{ $t(ts[stat.name]) }}</h3>
<a-progress
type="dashboard"
:status="getStatus(parseFloat(stat.percentused))"
:percent="parseFloat(stat.percentused)"
:format="percent => `${parseFloat(stat.percentused).toFixed(2)}%`"
:strokeColor="getStrokeColour(parseFloat(stat.percentused))"
:width="100" />
</div>
</router-link>
<template #footer>
<div class="center">{{ displayData(stat.name, stat.capacityused) }} / {{ displayData(stat.name, stat.capacitytotal) }}</div>
</template>
</chart-card>
</a-col>
</a-row>
</a-col>
<a-col :xl="6" class="dashboard-event">
<chart-card :loading="loading">
<div style="text-align: center">
<a-tooltip placement="bottom" class="capacity-dashboard-button-wrapper">
<template #title>
{{ $t('label.view') + ' ' + $t('label.host.alerts') }}
</template>
<a-button type="primary" danger shape="circle">
<router-link :to="{ name: 'host', query: {'state': 'Alert'} }">
<desktop-outlined class="capacity-dashboard-button-icon" />
</router-link>
</a-button>
</a-tooltip>
<a-tooltip placement="bottom" class="capacity-dashboard-button-wrapper">
<template #title>
{{ $t('label.view') + ' ' + $t('label.alerts') }}
</template>
<a-button shape="circle">
<router-link :to="{ name: 'alert' }">
<flag-outlined class="capacity-dashboard-button-icon" />
</router-link>
</a-button>
</a-tooltip>
<a-tooltip placement="bottom" class="capacity-dashboard-button-wrapper">
<template #title>
{{ $t('label.view') + ' ' + $t('label.events') }}
</template>
<a-button shape="circle">
<router-link :to="{ name: 'event' }">
<schedule-outlined class="capacity-dashboard-button-icon" />
</router-link>
</a-button>
</a-tooltip>
</div>
<template #footer>
<div class="capacity-dashboard-footer">
<a-timeline>
<a-timeline-item
v-for="event in events"
:key="event.id"
:color="getEventColour(event)">
<span :style="{ color: '#999' }"><small>{{ $toLocaleDate(event.created) }}</small></span><br/>
<span :style="{ color: '#666' }"><small><router-link :to="{ path: '/event/' + event.id }">{{ event.type }}</router-link></small></span><br/>
<resource-label :resourceType="event.resourcetype" :resourceId="event.resourceid" :resourceName="event.resourcename" />
<span :style="{ color: '#aaa' }">({{ event.username }}) {{ event.description }}</span>
</a-timeline-item>
</a-timeline>
<a-col :xs="{ span: 24 }" :lg="{ span: 12 }" :xl="{ span: 8 }" :xxl="{ span: 8 }">
<chart-card :loading="loading" class="dashboard-card">
<template #title>
<div class="center">
<router-link :to="{ path: '/infrasummary' }" v-if="!zoneSelected.id">
<h3>
<bank-outlined />
{{ $t('label.infrastructure') }}
</h3>
</router-link>
<router-link :to="{ path: '/zone/' + zoneSelected.id }" v-else>
<h3>
<global-outlined />
{{ $t('label.zone') }}
</h3>
</router-link>
</div>
</template>
<a-divider style="margin: 0px 0px; border-width: 0px"/>
<a-row :gutter="[12, 12]">
<a-col :span="12">
<router-link :to="{ path: '/pod', query: { zoneid: zoneSelected.id } }">
<a-statistic
:title="$t('label.pods')"
:value="data.pods"
:value-style="{ color: $config.theme['@primary-color'] }">
<template #prefix>
<appstore-outlined/>&nbsp;
</template>
</a-statistic>
</router-link>
</a-col>
<a-col :span="12">
<router-link :to="{ path: '/cluster', query: { zoneid: zoneSelected.id } }">
<a-statistic
:title="$t('label.clusters')"
:value="data.clusters"
:value-style="{ color: $config.theme['@primary-color'] }">
<template #prefix>
<cluster-outlined/>&nbsp;
</template>
</a-statistic>
</router-link>
</a-col>
<a-col :span="12">
<router-link :to="{ path: '/host', query: { zoneid: zoneSelected.id } }">
<a-statistic
:title="$t('label.hosts')"
:value="data.totalHosts"
:value-style="{ color: $config.theme['@primary-color'] }">
<template #prefix>
<database-outlined/>&nbsp;
</template>
</a-statistic>
</router-link>
</a-col>
<a-col :span="12">
<router-link :to="{ path: '/host', query: { zoneid: zoneSelected.id, state: 'alert' } }">
<a-statistic
:title="$t('label.host.alerts')"
:value="data.alertHosts"
:value-style="{ color: $config.theme['@primary-color'] }">
<template #prefix>
<database-outlined/>
<a-badge v-if="data.alertHosts > 0" count="!" style="margin-left: -5px" />
<a-badge v-else count="✓" style="margin-left: -5px" :number-style="{ backgroundColor: '#52c41a' }" />
</template>
</a-statistic>
</router-link>
</a-col>
<a-col :span="12">
<router-link :to="{ path: '/storagepool', query: { zoneid: zoneSelected.id } }">
<a-statistic
:title="$t('label.primary.storage')"
:value="data.pools"
:value-style="{ color: $config.theme['@primary-color'] }">
<template #prefix>
<hdd-outlined/>&nbsp;
</template>
</a-statistic>
</router-link>
</a-col>
<a-col :span="12">
<router-link :to="{ path: '/systemvm', query: { zoneid: zoneSelected.id } }">
<a-statistic
:title="$t('label.system.vms')"
:value="data.systemvms"
:value-style="{ color: $config.theme['@primary-color'] }">
<template #prefix>
<thunderbolt-outlined/>&nbsp;
</template>
</a-statistic>
</router-link>
</a-col>
<a-col :span="12">
<router-link :to="{ path: '/router', query: { zoneid: zoneSelected.id } }">
<a-statistic
:title="$t('label.virtual.routers')"
:value="data.routers"
:value-style="{ color: $config.theme['@primary-color'] }">
<template #prefix>
<fork-outlined/>&nbsp;
</template>
</a-statistic>
</router-link>
</a-col>
<a-col :span="12">
<router-link :to="{ path: '/vm', query: { zoneid: zoneSelected.id, projectid: '-1' } }">
<a-statistic
:title="$t('label.instances')"
:value="data.instances"
:value-style="{ color: $config.theme['@primary-color'] }">
<template #prefix>
<cloud-server-outlined/>&nbsp;
</template>
</a-statistic>
</router-link>
</a-col>
</a-row>
</chart-card>
</a-col>
<a-col :xs="{ span: 24 }" :lg="{ span: 12 }" :xl="{ span: 8 }" :xxl="{ span: 8 }">
<chart-card :loading="loading" class="dashboard-card">
<template #title>
<div class="center">
<h3><cloud-outlined /> {{ $t('label.compute') }}</h3>
</div>
</template>
<div>
<div v-for="ctype in ['MEMORY', 'CPU', 'CPU_CORE', 'GPU']" :key="ctype" >
<div v-if="statsMap[ctype]">
<div>
<strong>{{ $t(ts[ctype]) }}</strong>
</div>
<a-progress
status="active"
:percent="statsMap[ctype]?.capacitytotal > 0 ? parseFloat(100.0 * statsMap[ctype]?.capacityused / statsMap[ctype]?.capacitytotal).toFixed(2) : 0"
:format="p => statsMap[ctype]?.capacitytotal > 0 ? parseFloat(100.0 * statsMap[ctype]?.capacityused / statsMap[ctype]?.capacitytotal).toFixed(2) + '%' : '0%'"
stroke-color="#52c41a"
size="small"
style="width:95%; float: left"
/>
<br/>
<div style="text-align: center">
{{ displayData(ctype, statsMap[ctype]?.capacityused) }} {{ $t('label.allocated') }} | {{ displayData(ctype, statsMap[ctype]?.capacitytotal) }} {{ $t('label.total') }}
</div>
</div>
</div>
</div>
</chart-card>
</a-col>
<a-col :xs="{ span: 24 }" :lg="{ span: 12 }" :xl="{ span: 8 }" :xxl="{ span: 8 }">
<chart-card :loading="loading" class="dashboard-card">
<template #title>
<div class="center">
<h3><hdd-outlined /> {{ $t('label.storage') }}</h3>
</div>
</template>
<div>
<div v-for="ctype in ['STORAGE', 'STORAGE_ALLOCATED', 'LOCAL_STORAGE', 'SECONDARY_STORAGE']" :key="ctype" >
<div v-if="statsMap[ctype]">
<div>
<strong>{{ $t(ts[ctype]) }}</strong>
</div>
<a-progress
status="active"
:percent="statsMap[ctype]?.capacitytotal > 0 ? parseFloat(100.0 * statsMap[ctype]?.capacityused / statsMap[ctype]?.capacitytotal).toFixed(2) : 0"
:format="p => statsMap[ctype]?.capacitytotal > 0 ? parseFloat(100.0 * statsMap[ctype]?.capacityused / statsMap[ctype]?.capacitytotal).toFixed(2) + '%' : '0%'"
stroke-color="#52c41a"
size="small"
style="width:95%; float: left"
/>
<br/>
<div style="text-align: center">
{{ displayData(ctype, statsMap[ctype]?.capacityused) }} <span v-if="ctype !== 'STORAGE'">{{ $t('label.allocated') }}</span><span v-else>{{ $t('label.used') }}</span> | {{ displayData(ctype, statsMap[ctype]?.capacitytotal) }} {{ $t('label.total') }}
</div>
</div>
</div>
</div>
</chart-card>
</a-col>
<a-col :xs="{ span: 24 }" :lg="{ span: 12 }" :xl="{ span: 8 }" :xxl="{ span: 8 }">
<chart-card :loading="loading" class="dashboard-card">
<template #title>
<div class="center">
<h3><apartment-outlined /> {{ $t('label.network') }}</h3>
</div>
</template>
<div>
<div v-for="ctype in ['VLAN', 'VIRTUAL_NETWORK_PUBLIC_IP', 'VIRTUAL_NETWORK_IPV6_SUBNET', 'DIRECT_ATTACHED_PUBLIC_IP', 'PRIVATE_IP']" :key="ctype" >
<div v-if="statsMap[ctype]">
<div>
<strong>{{ $t(ts[ctype]) }}</strong>
</div>
<a-progress
status="active"
:percent="statsMap[ctype]?.capacitytotal > 0 ? parseFloat(100.0 * statsMap[ctype]?.capacityused / statsMap[ctype]?.capacitytotal).toFixed(2) : 0"
:format="p => statsMap[ctype]?.capacitytotal > 0 ? parseFloat(100.0 * statsMap[ctype]?.capacityused / statsMap[ctype]?.capacitytotal).toFixed(2) + '%' : '0%'"
stroke-color="#52c41a"
size="small"
style="width:95%; float: left"
/>
<br/>
<div style="text-align: center">
{{ displayData(ctype, statsMap[ctype]?.capacityused) }} {{ $t('label.allocated') }} | {{ displayData(ctype, statsMap[ctype]?.capacitytotal) }} {{ $t('label.total') }}
</div>
</div>
</div>
</div>
</chart-card>
</a-col>
<a-col :xs="{ span: 24 }" :lg="{ span: 12 }" :xl="{ span: 8 }" :xxl="{ span: 8 }">
<router-link :to="{ path: '/alert' }">
<a-card :loading="loading" :bordered="false" class="dashboard-card dashboard-event">
<div class="center" style="margin-top: -8px">
<h3>
<flag-outlined />
{{ $t('label.alerts') }}
</h3>
</div>
<a-divider style="margin: 6px 0px; border-width: 0px"/>
<a-timeline>
<a-timeline-item
v-for="alert in alerts"
:key="alert.id"
color="red">
<span :style="{ color: '#999' }"><small>{{ $toLocaleDate(alert.sent) }}</small></span>&nbsp;
<span :style="{ color: '#666' }"><small><router-link :to="{ path: '/alert/' + alert.id }">{{ alert.name }}</router-link></small></span><br/>
<span :style="{ color: '#aaa' }">{{ alert.description }}</span>
</a-timeline-item>
</a-timeline>
<router-link :to="{ path: '/alert' }">
<a-button>
{{ $t('label.view') }} {{ $t('label.alerts') }}
</a-button>
</router-link>
</a-card>
</router-link>
</a-col>
<a-col :xs="{ span: 24 }" :lg="{ span: 12 }" :xl="{ span: 8 }" :xxl="{ span: 8 }">
<router-link :to="{ path: '/event' }">
<a-card :loading="loading" :bordered="false" class="dashboard-card dashboard-event">
<div class="center" style="margin-top: -8px">
<h3>
<schedule-outlined />
{{ $t('label.events') }}
</h3>
</div>
<a-divider style="margin: 6px 0px; border-width: 0px"/>
<a-timeline>
<a-timeline-item
v-for="event in events"
:key="event.id"
:color="getEventColour(event)">
<span :style="{ color: '#999' }"><small>{{ $toLocaleDate(event.created) }}</small></span>&nbsp;
<span :style="{ color: '#666' }"><small><router-link :to="{ path: '/event/' + event.id }">{{ event.type }}</router-link></small></span><br/>
<span>
<resource-label :resourceType="event.resourcetype" :resourceId="event.resourceid" :resourceName="event.resourcename" />
</span>
<span :style="{ color: '#aaa' }">({{ event.username }}) {{ event.description }}</span>
</a-timeline-item>
</a-timeline>
<router-link :to="{ path: '/event' }">
<a-button>
{{ $t('label.view') }} {{ $t('label.events') }}
</a-button>
</router-link>
</a-card>
</router-link>
</a-col>
</a-row>
</template>
@ -135,21 +327,35 @@ import { api } from '@/api'
import ChartCard from '@/components/widgets/ChartCard'
import ResourceIcon from '@/components/view/ResourceIcon'
import ResourceLabel from '@/components/widgets/ResourceLabel'
import Status from '@/components/widgets/Status'
export default {
name: 'CapacityDashboard',
components: {
ChartCard,
ResourceIcon,
ResourceLabel
ResourceLabel,
Status
},
data () {
return {
loading: true,
tabKey: 'alerts',
alerts: [],
events: [],
zones: [],
zoneSelected: {},
stats: [],
statsMap: {},
data: {
pods: 0,
clusters: 0,
totalHosts: 0,
alertHosts: 0,
pools: 0,
instances: 0,
systemvms: 0,
routers: 0
},
ts: {
CPU: 'label.cpu',
CPU_CORE: 'label.cpunumber',
@ -159,8 +365,8 @@ export default {
MEMORY: 'label.memory',
PRIVATE_IP: 'label.management.ips',
SECONDARY_STORAGE: 'label.secondary.storage',
STORAGE: 'label.storage',
STORAGE_ALLOCATED: 'label.primary.storage',
STORAGE: 'label.primary.storage.used',
STORAGE_ALLOCATED: 'label.primary.storage.allocated',
VIRTUAL_NETWORK_PUBLIC_IP: 'label.public.ips',
VLAN: 'label.vlan',
VIRTUAL_NETWORK_IPV6_SUBNET: 'label.ipv6.subnets'
@ -196,13 +402,10 @@ export default {
}
return 'normal'
},
getStrokeColour (value) {
if (value >= 80) {
return this.$config.theme['@graph-exception-color'] || 'red'
}
return this.$config.theme['@graph-normal-color'] || 'primary'
},
displayData (dataType, value) {
if (!value) {
value = 0
}
switch (dataType) {
case 'CPU':
value = parseFloat(value / 1000.0, 10).toFixed(2) + ' GHz'
@ -214,9 +417,9 @@ export default {
case 'LOCAL_STORAGE':
value = parseFloat(value / (1024 * 1024 * 1024.0), 10).toFixed(2)
if (value >= 1024.0) {
value = parseFloat(value / 1024.0).toFixed(2) + ' TB'
value = parseFloat(value / 1024.0).toFixed(2) + ' TiB'
} else {
value = value + ' GB'
value = value + ' GiB'
}
break
}
@ -224,26 +427,134 @@ export default {
},
fetchData () {
this.listZones()
this.listAlerts()
this.listEvents()
},
listCapacity (zone, latest = false) {
const params = {
zoneid: zone.id,
fetchlatest: latest
listCapacity (zone, latest = false, additive = false) {
this.loading = true
api('listCapacity', { zoneid: zone.id, fetchlatest: latest }).then(json => {
this.loading = false
let stats = []
if (json && json.listcapacityresponse && json.listcapacityresponse.capacity) {
stats = json.listcapacityresponse.capacity
}
for (const stat of stats) {
if (additive) {
for (const [key, value] of Object.entries(stat)) {
if (stat.name in this.statsMap) {
if (key in this.statsMap[stat.name]) {
this.statsMap[stat.name][key] += value
} else {
this.statsMap[stat.name][key] = value
}
} else {
this.statsMap[stat.name] = { key: value }
}
}
} else {
this.statsMap[stat.name] = stat
}
}
})
},
updateData (zone) {
if (!zone.id) {
this.statsMap = {}
for (const zone of this.zones.slice(1)) {
this.listCapacity(zone, true, true)
}
} else {
this.statsMap = {}
this.listCapacity(this.zoneSelected, true)
}
this.data = {
pods: 0,
clusters: 0,
totalHosts: 0,
alertHosts: 0,
pools: 0,
instances: 0,
systemvms: 0,
routers: 0
}
this.loading = true
api('listCapacity', params).then(json => {
this.stats = []
api('listPods', { zoneid: zone.id }).then(json => {
this.loading = false
if (json && json.listcapacityresponse && json.listcapacityresponse.capacity) {
this.stats = json.listcapacityresponse.capacity
this.data.pods = json?.listpodsresponse?.count
if (!this.data.pods) {
this.data.pods = 0
}
})
api('listClusters', { zoneid: zone.id }).then(json => {
this.loading = false
this.data.clusters = json?.listclustersresponse?.count
if (!this.data.clusters) {
this.data.clusters = 0
}
})
api('listHosts', { zoneid: zone.id, listall: true, details: 'min', type: 'routing', page: 1, pagesize: 1 }).then(json => {
this.loading = false
this.data.totalHosts = json?.listhostsresponse?.count
if (!this.data.totalHosts) {
this.data.totalHosts = 0
}
})
api('listHosts', { zoneid: zone.id, listall: true, details: 'min', type: 'routing', state: 'alert', page: 1, pagesize: 1 }).then(json => {
this.loading = false
this.data.alertHosts = json?.listhostsresponse?.count
if (!this.data.alertHosts) {
this.data.alertHosts = 0
}
})
api('listStoragePools', { zoneid: zone.id }).then(json => {
this.loading = false
this.data.pools = json?.liststoragepoolsresponse?.count
if (!this.data.pools) {
this.data.pools = 0
}
})
api('listSystemVms', { zoneid: zone.id }).then(json => {
this.loading = false
this.data.systemvms = json?.listsystemvmsresponse?.count
if (!this.data.systemvms) {
this.data.systemvms = 0
}
})
api('listRouters', { zoneid: zone.id, listall: true }).then(json => {
this.loading = false
this.data.routers = json?.listroutersresponse?.count
if (!this.data.routers) {
this.data.routers = 0
}
})
api('listVirtualMachines', { zoneid: zone.id, listall: true, projectid: '-1', details: 'min', page: 1, pagesize: 1 }).then(json => {
this.loading = false
this.data.instances = json?.listvirtualmachinesresponse?.count
if (!this.data.instances) {
this.data.instances = 0
}
})
},
listAlerts () {
const params = {
page: 1,
pagesize: 8,
listall: true
}
this.loading = true
api('listAlerts', params).then(json => {
this.alerts = []
this.loading = false
if (json && json.listalertsresponse && json.listalertsresponse.alert) {
this.alerts = json.listalertsresponse.alert
}
})
},
listEvents () {
const params = {
page: 1,
pagesize: 6,
pagesize: 8,
listall: true
}
this.loading = true
@ -269,18 +580,19 @@ export default {
if (json && json.listzonesresponse && json.listzonesresponse.zone) {
this.zones = json.listzonesresponse.zone
if (this.zones.length > 0) {
this.zones.splice(0, 0, { name: this.$t('label.all.zone') })
this.zoneSelected = this.zones[0]
this.listCapacity(this.zones[0])
this.updateData(this.zones[0])
}
}
})
},
changeZone (index) {
this.zoneSelected = this.zones[index]
this.listCapacity(this.zoneSelected)
this.updateData(this.zoneSelected)
},
filterZone (input, option) {
return option.children[0].children.toLowerCase().indexOf(input.toLowerCase()) >= 0
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
}
}
@ -290,7 +602,6 @@ export default {
.capacity-dashboard {
&-wrapper {
display: flex;
margin-bottom: 12px;
}
&-chart-card-inner {
@ -313,7 +624,7 @@ export default {
&-button {
width: auto;
padding-left: 12px;
padding-left: 8px;
}
&-button-icon {
@ -321,21 +632,28 @@ export default {
padding: 2px;
}
&-footer {
&-title {
padding-top: 12px;
padding-left: 3px;
white-space: normal;
}
}
.dashboard-card {
width: 100%;
min-height: 370px;
}
.dashboard-event {
width: 100%;
overflow-x:hidden;
overflow-y: auto;
max-height: 370px;
}
.center {
display: block;
text-align: center;
}
@media (max-width: 1200px) {
.dashboard-event {
width: 100%;
}
}
</style>

View File

@ -16,82 +16,303 @@
// under the License.
<template>
<a-row class="usage-dashboard" :gutter="12">
<a-col :xl="16" style="padding-left: 0; padding-right: 0;">
<a-row>
<a-card style="width: 100%">
<a-tabs
v-if="showProject"
:animated="false"
@change="onTabChange">
<template v-for="tab in $route.meta.tabs" :key="tab.name">
<a-tab-pane
v-if="'show' in tab ? tab.show(project, $route, $store.getters.userInfo) : true"
:tab="$t('label.' + tab.name)"
:key="tab.name">
<keep-alive>
<component
:is="tab.component"
:resource="project"
:loading="loading"
:bordered="false"
:stats="stats" />
</keep-alive>
</a-tab-pane>
</template>
</a-tabs>
<a-row :gutter="24" v-else>
<a-col
class="usage-dashboard-chart-tile"
:xs="12"
:md="8"
v-for="stat in stats"
:key="stat.type">
<a-card
class="usage-dashboard-chart-card"
:bordered="false"
:loading="loading"
:style="stat.bgcolor ? { 'background': stat.bgcolor } : {}">
<router-link v-if="stat.path" :to="{ path: stat.path, query: stat.query }">
<div
class="usage-dashboard-chart-card-inner">
<h3>{{ stat.name }}</h3>
<h2>
<render-icon :icon="stat.icon" />
{{ stat.count == undefined ? 0 : stat.count }}
</h2>
</div>
</router-link>
</a-card>
</a-col>
</a-row>
</a-card>
</a-row>
</a-col>
<a-col :xl="8">
<chart-card :loading="loading" >
<div class="usage-dashboard-chart-card-inner">
<a-button>
<router-link :to="{ name: 'event' }">
{{ $t('label.view') + ' ' + $t('label.events') }}
</router-link>
</a-button>
</div>
<template #footer>
<div class="usage-dashboard-chart-footer">
<a-timeline>
<a-timeline-item
v-for="event in events"
:key="event.id"
:color="getEventColour(event)">
<span :style="{ color: '#999' }"><small>{{ $toLocaleDate(event.created) }}</small></span><br/>
<span :style="{ color: '#666' }"><small><router-link :to="{ path: '/event/' + event.id }">{{ event.type }}</router-link></small></span><br/>
<resource-label :resourceType="event.resourcetype" :resourceId="event.resourceid" :resourceName="event.resourcename" />
<span :style="{ color: '#aaa' }">({{ event.username }}) {{ event.description }}</span>
</a-timeline-item>
</a-timeline>
<a-row class="capacity-dashboard" :gutter="[12,12]">
<a-col :xs="{ span: 24 }" :lg="{ span: 12 }" :xl="{ span: 8 }" :xxl="{ span: 8 }">
<chart-card :loading="loading" class="dashboard-card">
<template #title>
<div class="center">
<h3>
<dashboard-outlined /> {{ $t('label.resources') }}
<span style="float: right" v-if="showProject">
<a-dropdown>
<template #overlay>
<a-menu>
<a-menu-item>
<router-link :to="{ path: '/project/' + project.id }">
<project-outlined/>
{{ $t('label.view') }} {{ $t('label.project') }}
</router-link>
</a-menu-item>
<a-menu-item v-if="showProject && ['Admin'].includes($store.getters.userInfo.roletype)">
<router-link :to="{ path: '/project/' + project.id, query: { tab: 'limits.configure' } }">
<setting-outlined/>
{{ $t('label.configure') }} {{ $t('label.project') }} {{ $t('label.limits') }}
</router-link>
</a-menu-item>
</a-menu>
</template>
<a-button size="small" type="text">
<more-outlined />
</a-button>
</a-dropdown>
</span>
</h3>
</div>
</template>
<a-divider style="margin: 6px 0px; border-width: 0px"/>
<a-row :gutter="[10, 10]">
<a-col :span="12">
<router-link :to="{ path: '/vm' }">
<a-statistic
:title="$t('label.instances')"
:value="data.instances"
:value-style="{ color: $config.theme['@primary-color'] }">
<template #prefix>
<cloud-server-outlined/>&nbsp;
</template>
</a-statistic>
</router-link>
</a-col>
<a-col :span="12">
<router-link :to="{ path: '/kubernetes' }">
<a-statistic
:title="$t('label.kubernetes.cluster')"
:value="data.kubernetes"
:value-style="{ color: $config.theme['@primary-color'] }">
<template #prefix>
<cluster-outlined/>&nbsp;
</template>
</a-statistic>
</router-link>
</a-col>
<a-col :span="12">
<router-link :to="{ path: '/volume' }">
<a-statistic
:title="$t('label.volumes')"
:value="data.volumes"
:value-style="{ color: $config.theme['@primary-color'] }">
<template #prefix>
<hdd-outlined/>&nbsp;
</template>
</a-statistic>
</router-link>
</a-col>
<a-col :span="12">
<router-link :to="{ path: '/snapshot' }">
<a-statistic
:title="$t('label.snapshots')"
:value="data.snapshots"
:value-style="{ color: $config.theme['@primary-color'] }">
<template #prefix>
<build-outlined/>&nbsp;
</template>
</a-statistic>
</router-link>
</a-col>
<a-col :span="12">
<router-link :to="{ path: '/guestnetwork' }">
<a-statistic
:title="$t('label.guest.networks')"
:value="data.networks"
:value-style="{ color: $config.theme['@primary-color'] }">
<template #prefix>
<apartment-outlined/>&nbsp;
</template>
</a-statistic>
</router-link>
</a-col>
<a-col :span="12">
<router-link :to="{ path: '/vpc' }">
<a-statistic
:title="$t('label.vpcs')"
:value="data.vpcs"
:value-style="{ color: $config.theme['@primary-color'] }">
<template #prefix>
<deployment-unit-outlined/>&nbsp;
</template>
</a-statistic>
</router-link>
</a-col>
<a-col :span="12">
<router-link :to="{ path: '/publicip' }">
<a-statistic
:title="$t('label.public.ips')"
:value="data.ips"
:value-style="{ color: $config.theme['@primary-color'] }">
<template #prefix>
<environment-outlined/>&nbsp;
</template>
</a-statistic>
</router-link>
</a-col>
<a-col :span="12">
<router-link :to="{ path: '/template', query: { templatefilter: 'self', filter: 'self' } }">
<a-statistic
:title="$t('label.templates')"
:value="data.templates"
:value-style="{ color: $config.theme['@primary-color'] }">
<template #prefix>
<picture-outlined/>&nbsp;
</template>
</a-statistic>
</router-link>
</a-col>
</a-row>
</chart-card>
</a-col>
<a-col :xs="{ span: 24 }" :lg="{ span: 12 }" :xl="{ span: 8 }" :xxl="{ span: 8 }">
<chart-card :loading="loading" class="dashboard-card">
<template #title>
<div class="center">
<h3>
<cloud-outlined /> {{ $t('label.compute') }}
</h3>
</div>
</template>
<a-divider style="margin: 6px 0px; border-width: 0px"/>
<a-row>
<a-col :span="12">
<router-link :to="{ path: '/vm', query: { state: 'running', filter: 'running' } }">
<a-statistic
:title="$t('label.running') + ' ' + $t('label.instances')"
:value="data.running"
:value-style="{ color: $config.theme['@primary-color'] }">
<template #prefix>
<status class="status" text="Running"/>
</template>
</a-statistic>
</router-link>
</a-col>
<a-col :span="12">
<router-link :to="{ path: '/vm', query: { state: 'stopped', filter: 'stopped' } }">
<a-statistic
:title="$t('label.stopped') + ' ' + $t('label.instances')"
:value="data.stopped"
:value-style="{ color: $config.theme['@primary-color'] }">
<template #prefix>
<status class="status" text="Stopped"/>
</template>
</a-statistic>
</router-link>
</a-col>
</a-row>
<a-divider style="margin: 1px 0px; border-width: 0px;"/>
<div
v-for="usageType in ['vm', 'cpu', 'memory', 'project']"
:key="usageType">
<div v-if="usageType + 'total' in entity">
<div>
<strong>
{{ $t(getLabel(usageType)) }}
</strong>
<span style="float: right">
{{ getValue(usageType, entity[usageType + 'total']) }} {{ $t('label.used') }}
</span>
</div>
<a-progress
status="active"
:percent="parseFloat(getPercentUsed(entity[usageType + 'total'], entity[usageType + 'limit']))"
:format="p => resource[item + 'limit'] !== '-1' && resource[item + 'limit'] !== 'Unlimited' ? p.toFixed(0) + '%' : ''"
stroke-color="#52c41a"
size="small"
/>
<br/>
<div style="text-align: center">
{{ entity[usageType + 'available'] === 'Unlimited' ? $t('label.unlimited') : getValue(usageType, entity[usageType + 'available']) }} {{ $t('label.available') }}
{{ entity[usageType + 'limit'] === 'Unlimited' ? '' : (' | ' + getValue(usageType, entity[usageType + 'limit']) + ' ' + $t('label.limit')) }}
</div>
</div>
</div>
</chart-card>
</a-col>
<a-col :xs="{ span: 24 }" :lg="{ span: 12 }" :xl="{ span: 8 }" :xxl="{ span: 8 }">
<chart-card :loading="loading" class="dashboard-card">
<template #title>
<div class="center">
<h3><hdd-outlined /> {{ $t('label.storage') }}</h3>
</div>
</template>
<a-divider style="margin: 6px 0px; border-width: 0px"/>
<div
v-for="usageType in ['volume', 'snapshot', 'template', 'primarystorage', 'secondarystorage']"
:key="usageType">
<div>
<div>
<strong>
{{ $t(getLabel(usageType)) }}
</strong>
<span style="float: right">
{{ getValue(usageType, entity[usageType + 'total']) }} {{ $t('label.used') }}
</span>
</div>
<a-progress
status="active"
:percent="parseFloat(getPercentUsed(entity[usageType + 'total'], entity[usageType + 'limit']))"
:format="p => resource[item + 'limit'] !== '-1' && resource[item + 'limit'] !== 'Unlimited' ? p.toFixed(0) + '%' : ''"
stroke-color="#52c41a"
size="small"
/>
<br/>
<div style="text-align: center">
{{ entity[usageType + 'available'] === 'Unlimited' ? $t('label.unlimited') : getValue(usageType, entity[usageType + 'available']) }} {{ $t('label.available') }}
{{ entity[usageType + 'limit'] === 'Unlimited' ? '' : (' | ' + getValue(usageType, entity[usageType + 'limit']) + ' ' + $t('label.limit')) }}
</div>
</div>
</div>
</chart-card>
</a-col>
<a-col :xs="{ span: 24 }" :lg="{ span: 12 }" :xl="{ span: 8 }" :xxl="{ span: 8 }" class="dashboard-card">
<chart-card :loading="loading" class="dashboard-card">
<template #title>
<div class="center">
<h3><apartment-outlined /> {{ $t('label.network') }}</h3>
</div>
</template>
<a-divider style="margin: 6px 0px; border-width: 0px"/>
<div
v-for="usageType in ['ip', 'network', 'vpc']"
:key="usageType">
<div>
<div>
<strong>
{{ $t(getLabel(usageType)) }}
</strong>
<span style="float: right">
{{ getValue(usageType, entity[usageType + 'total']) }} {{ $t('label.used') }}
</span>
</div>
<a-progress
status="active"
:percent="parseFloat(getPercentUsed(entity[usageType + 'total'], entity[usageType + 'limit']))"
:format="p => resource[item + 'limit'] !== '-1' && resource[item + 'limit'] !== 'Unlimited' ? p.toFixed(0) + '%' : ''"
stroke-color="#52c41a"
size="small"
/>
<br/>
<div style="text-align: center">
{{ entity[usageType + 'available'] === 'Unlimited' ? $t('label.unlimited') : getValue(usageType, entity[usageType + 'available']) }} {{ $t('label.available') }}
{{ entity[usageType + 'limit'] === 'Unlimited' ? '' : (' | ' + getValue(usageType, entity[usageType + 'limit']) + ' ' + $t('label.limit')) }}
</div>
</div>
</div>
</chart-card>
</a-col>
<a-col :xs="{ span: 24 }" :lg="{ span: 12 }" :xl="{ span: 8 }" :xxl="{ span: 8 }">
<chart-card :loading="loading" class="dashboard-card dashboard-event">
<template #title>
<div class="center">
<h3><schedule-outlined /> {{ $t('label.events') }}</h3>
</div>
</template>
<a-divider style="margin: 6px 0px; border-width: 0px"/>
<a-timeline>
<a-timeline-item
v-for="event in events"
:key="event.id"
:color="getEventColour(event)">
<span :style="{ color: '#999' }"><small>{{ $toLocaleDate(event.created) }}</small></span>&nbsp;
<span :style="{ color: '#666' }"><small><router-link :to="{ path: '/event/' + event.id }">{{ event.type }}</router-link></small></span><br/>
<span>
<resource-label :resourceType="event.resourcetype" :resourceId="event.resourceid" :resourceName="event.resourcename" />
</span>
<span :style="{ color: '#aaa' }">({{ event.username }}) {{ event.description }}</span>
</a-timeline-item>
</a-timeline>
<router-link :to="{ path: '/event' }">
<a-button>
{{ $t('label.view') }} {{ $t('label.events') }}
</a-button>
</router-link>
</chart-card>
</a-col>
</a-row>
@ -104,13 +325,15 @@ import store from '@/store'
import ChartCard from '@/components/widgets/ChartCard'
import UsageDashboardChart from '@/views/dashboard/UsageDashboardChart'
import ResourceLabel from '@/components/widgets/ResourceLabel'
import Status from '@/components/widgets/Status'
export default {
name: 'UsageDashboard',
components: {
ChartCard,
UsageDashboardChart,
ResourceLabel
ResourceLabel,
Status
},
props: {
resource: {
@ -129,9 +352,29 @@ export default {
loading: false,
showAction: false,
showAddAccount: false,
project: {},
account: {},
events: [],
stats: [],
project: {}
data: {
running: 0,
stopped: 0,
instances: 0,
kubernetes: 0,
volumes: 0,
snapshots: 0,
networks: 0,
vpcs: 0,
ips: 0,
templates: 0
}
}
},
computed: {
entity: function () {
if (this.showProject) {
return this.project
}
return this.account
}
},
created () {
@ -158,6 +401,9 @@ export default {
deep: true,
handler (newData, oldData) {
this.project = newData
if (newData.id) {
this.fetchData()
}
}
},
'$i18n.global.locale' (to, from) {
@ -168,61 +414,95 @@ export default {
},
methods: {
fetchData () {
this.stats = [{}, {}, {}, {}, {}, {}]
api('listVirtualMachines', { state: 'Running', listall: true }).then(json => {
var count = 0
if (json && json.listvirtualmachinesresponse) {
count = json.listvirtualmachinesresponse.count
if (store.getters.project.id) {
this.listProject()
} else {
this.listAccount()
}
this.updateData()
},
listAccount () {
this.loading = true
api('listAccounts', { id: this.$store.getters.userInfo.accountid }).then(json => {
this.loading = false
if (json && json.listaccountsresponse && json.listaccountsresponse.account) {
this.account = json.listaccountsresponse.account[0]
}
var tileColor = this.$config.theme['@dashboard-tile-runningvms-bg'] || '#dfe9cc'
this.stats.splice(0, 1, { name: this.$t('label.running.vms'), count: count, icon: 'desktop-outlined', bgcolor: tileColor, path: '/vm', query: { state: 'running', filter: 'running' } })
})
api('listVirtualMachines', { state: 'Stopped', listall: true }).then(json => {
var count = 0
if (json && json.listvirtualmachinesresponse) {
count = json.listvirtualmachinesresponse.count
},
listProject () {
this.loading = true
api('listProjects', { id: store.getters.project.id }).then(json => {
this.loading = false
if (json && json.listprojectsresponse && json.listprojectsresponse.project) {
this.project = json.listprojectsresponse.project[0]
}
var tileColor = this.$config.theme['@dashboard-tile-stoppedvms-bg'] || '#edcbce'
this.stats.splice(1, 1, { name: this.$t('label.stopped.vms'), count: count, icon: 'poweroff-outlined', bgcolor: tileColor, path: '/vm', query: { state: 'stopped', filter: 'stopped' } })
})
api('listVirtualMachines', { listall: true }).then(json => {
var count = 0
if (json && json.listvirtualmachinesresponse) {
count = json.listvirtualmachinesresponse.count
}
var tileColor = this.$config.theme['@dashboard-tile-totalvms-bg'] || '#ffffff'
this.stats.splice(2, 1, { name: this.$t('label.total.vms'), count: count, icon: 'number-outlined', bgcolor: tileColor, path: '/vm' })
})
api('listVolumes', { listall: true }).then(json => {
var count = 0
if (json && json.listvolumesresponse) {
count = json.listvolumesresponse.count
}
var tileColor = this.$config.theme['@dashboard-tile-totalvolumes-bg'] || '#ffffff'
this.stats.splice(3, 1, { name: this.$t('label.total.volume'), count: count, icon: 'database-outlined', bgcolor: tileColor, path: '/volume' })
})
api('listNetworks', { listall: true }).then(json => {
var count = 0
if (json && json.listnetworksresponse) {
count = json.listnetworksresponse.count
}
var tileColor = this.$config.theme['@dashboard-tile-totalnetworks-bg'] || '#ffffff'
this.stats.splice(4, 1, { name: this.$t('label.total.network'), count: count, icon: 'apartment-outlined', bgcolor: tileColor, path: '/guestnetwork' })
})
api('listPublicIpAddresses', { listall: true }).then(json => {
var count = 0
if (json && json.listpublicipaddressesresponse) {
count = json.listpublicipaddressesresponse.count
}
var tileColor = this.$config.theme['@dashboard-tile-totalips-bg'] || '#ffffff'
this.stats.splice(5, 1, { name: this.$t('label.public.ip.addresses'), count: count, icon: 'environment-outlined', bgcolor: tileColor, path: '/publicip' })
})
},
updateData () {
this.data = {
running: 0,
stopped: 0,
instances: 0,
kubernetes: 0,
volumes: 0,
snapshots: 0,
networks: 0,
vpcs: 0,
ips: 0,
templates: 0
}
this.listInstances()
this.listEvents()
this.loading = true
api('listKubernetesClusters', { listall: true, page: 1, pagesize: 1 }).then(json => {
this.loading = false
this.data.kubernetes = json?.listkubernetesclustersresponse?.count
})
api('listVolumes', { listall: true, page: 1, pagesize: 1 }).then(json => {
this.loading = false
this.data.volumes = json?.listvolumesresponse?.count
})
api('listSnapshots', { listall: true, page: 1, pagesize: 1 }).then(json => {
this.loading = false
this.data.snapshots = json?.listsnapshotsresponse?.count
})
api('listNetworks', { listall: true, page: 1, pagesize: 1 }).then(json => {
this.loading = false
this.data.networks = json?.listnetworksresponse?.count
})
api('listVPCs', { listall: true, page: 1, pagesize: 1 }).then(json => {
this.loading = false
this.data.vpcs = json?.listvpcsresponse?.count
})
api('listPublicIpAddresses', { listall: true, page: 1, pagesize: 1 }).then(json => {
this.loading = false
this.data.ips = json?.listpublicipaddressesresponse?.count
})
api('listTemplates', { templatefilter: 'self', listall: true, page: 1, pagesize: 1 }).then(json => {
this.loading = false
this.data.templates = json?.listtemplatesresponse?.count
})
},
listInstances (zone) {
this.loading = true
api('listVirtualMachines', { listall: true, details: 'min', page: 1, pagesize: 1 }).then(json => {
this.loading = false
this.data.instances = json?.listvirtualmachinesresponse?.count
})
api('listVirtualMachines', { listall: true, details: 'min', state: 'running', page: 1, pagesize: 1 }).then(json => {
this.loading = false
this.data.running = json?.listvirtualmachinesresponse?.count
})
api('listVirtualMachines', { listall: true, details: 'min', state: 'stopped', page: 1, pagesize: 1 }).then(json => {
this.loading = false
this.data.stopped = json?.listvirtualmachinesresponse?.count
})
},
listEvents () {
const params = {
page: 1,
pagesize: 6,
pagesize: 8,
listall: true
}
this.loading = true
@ -234,6 +514,37 @@ export default {
}
})
},
getLabel (usageType) {
switch (usageType) {
case 'vm':
return 'label.instances'
case 'cpu':
return 'label.cpunumber'
case 'memory':
return 'label.memory'
case 'primarystorage':
return 'label.primary.storage'
case 'secondarystorage':
return 'label.secondary.storage'
case 'ip':
return 'label.public.ips'
}
return 'label.' + usageType + 's'
},
getValue (usageType, value) {
switch (usageType) {
case 'memory':
return parseFloat(value / 1024.0).toFixed(2) + ' GiB'
case 'primarystorage':
return parseFloat(value).toFixed(2) + ' GiB'
case 'secondarystorage':
return parseFloat(value).toFixed(2) + ' GiB'
}
return value
},
getPercentUsed (total, limit) {
return (limit === 'Unlimited') ? 0 : (total / limit) * 100
},
getEventColour (event) {
if (event.level === 'ERROR') {
return 'red'
@ -242,13 +553,6 @@ export default {
return 'green'
}
return 'blue'
},
onTabChange (key) {
this.showAddAccount = false
if (key !== 'Dashboard') {
this.showAddAccount = true
}
}
}
}
@ -276,6 +580,23 @@ export default {
}
}
.dashboard-card {
width: 100%;
min-height: 420px;
}
.dashboard-event {
width: 100%;
overflow-x:hidden;
overflow-y: scroll;
max-height: 420px;
}
.center {
display: block;
text-align: center;
}
@media (max-width: 1200px) {
.ant-col-xl-8 {
width: 100%;