UI: Admin, account and project dashboard improvements (#7956)

This PR aims at improving the CloudStack dashboard and introduces the following:

    Admin dashboard: six cards that are responsive to screen sizes and show zone specific compute, storage and network allocation, as well as instance/hosts stats, alerts and events. Now, by default, the admin dashboard shows aggegate data from all zones, with option for admin to select individual zone to see individual zone stats
    Account/project dashboard: six cards that are responsive to screen sizes and show account or project specific resource lists/counts, and limits shown in three cards as (a) compute (with running stopped instances), (b) storage and (c) network allocation, an admin-defined links/docs card (via config.json) and events cards. Admin is allowed to configure project limits on project dashboards.
    A global create button on the top global header/user-menu to allow for quick actions such as to deploy a VM, CKS cluster and create a volume (more actions can be added as desired via code changes) etc.

Doc PR - apache/cloudstack-documentation#349

---------

Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
This commit is contained in:
Rohit Yadav 2023-10-06 13:40:22 +05:30 committed by GitHub
parent 82b981854b
commit 5d9ae31f1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1369 additions and 1024 deletions

688
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -34,13 +34,13 @@
"test:unit": "vue-cli-service test:unit"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.3.0",
"@fortawesome/free-brands-svg-icons": "^5.15.2",
"@fortawesome/free-solid-svg-icons": "^5.15.2",
"@fortawesome/vue-fontawesome": "^3.0.0-4",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-brands-svg-icons": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/vue-fontawesome": "^3.0.3",
"@vue-js-cron/ant": "^1.1.3",
"@vue-js-cron/core": "^3.7.1",
"ant-design-vue": "^3.2.9",
"ant-design-vue": "^3.2.20",
"antd": "^4.21.4",
"antd-theme-webpack-plugin": "^1.3.9",
"axios": "^0.21.1",

30
ui/public/config.json vendored
View File

@ -59,6 +59,36 @@
"jp": "label.japanese.keyboard",
"sc": "label.simplified.chinese.keyboard"
},
"userCard": {
"title": "label.help",
"icon": "question-circle-outlined",
"links": [
{
"title": "Documentation",
"text": "CloudStack documentation website",
"link": "https://docs.cloudstack.apache.org/en/latest/",
"icon": "read-outlined"
},
{
"title": "API Documentation",
"text": "Refer to API documentation",
"link": "https://cloudstack.apache.org/api.html",
"icon": "api-outlined"
},
{
"title": "Email Support",
"text": "Join CloudStack users mailing list to seek and provide support",
"link": "mailto:users-subscribe@cloudstack.apache.org",
"icon": "mail-outlined"
},
{
"title": "Report Issue",
"text": "Submit a bug or improvement request",
"link": "https://github.com/apache/cloudstack/issues/new",
"icon": "bug-outlined"
}
]
},
"plugins": [],
"basicZoneEnabled": true,
"multipleServer": false,

View File

@ -538,6 +538,7 @@
"label.cpuused": "CPU utilized",
"label.cpuusedghz": "CPU used",
"label.create": "Create",
"label.create.instance": "Create cloud server",
"label.create.account": "Create account",
"label.create.backup": "Start backup",
"label.create.network": "Create new network",
@ -2193,6 +2194,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

@ -1,153 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:ns4="http://ns.adobe.com/SaveForWeb/1.0/"
xmlns:ns3="http://ns.adobe.com/Variables/1.0/"
xmlns:ns2="http://ns.adobe.com/AdobeIllustrator/10.0/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
ns2:viewOrigin="262 450"
ns2:rulerOrigin="0 0"
ns2:pageBounds="0 792 612 0"
viewBox="0 0 55.999999 56.000069"
overflow="visible"
enable-background="new 0 0 87.041 108.445"
xml:space="preserve"
version="1.1"
id="svg31"
sodipodi:docname="debian.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
style="overflow:visible"><defs
id="defs35" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1866"
inkscape:window-height="1017"
id="namedview33"
showgrid="false"
inkscape:zoom="2.1762184"
inkscape:cx="-62.475298"
inkscape:cy="28.002047"
inkscape:window-x="54"
inkscape:window-y="26"
inkscape:window-maximized="1"
inkscape:current-layer="g28"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<metadata
id="metadata2">
<ns3:variableSets>
<ns3:variableSet
varSetName="binding1"
locked="none">
<ns3:variables />
<ns3:sampleDataSets />
</ns3:variableSet>
</ns3:variableSets>
<ns4:sfw>
<ns4:slices />
<ns4:sliceSourceBounds
y="341.555"
x="262"
width="87.041"
height="108.445"
bottomLeftOrigin="true" />
</ns4:sfw>
<rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata>
<g
id="Layer_1"
ns2:layer="yes"
ns2:dimmedPercent="50"
ns2:rgbTrio="#4F008000FFFF"
transform="translate(-20.985947,-26.22447)">
<g
id="g28">
<path
ns2:knockout="Off"
d="m 53.872479,55.811055 c -0.927921,0.01291 0.175567,0.478161 1.386977,0.664571 0.334609,-0.261284 0.638236,-0.525667 0.908815,-0.78282 -0.75442,0.184861 -1.522266,0.188992 -2.295792,0.118249"
id="path4"
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
inkscape:connector-curvature="0" />
<path
ns2:knockout="Off"
d="m 58.852891,54.569696 c 0.552518,-0.762682 0.955289,-1.597656 1.097291,-2.461031 -0.123929,0.615516 -0.458022,1.146863 -0.772493,1.707644 -1.734495,1.092127 -0.163174,-0.648564 -10e-4,-1.310037 -1.865137,2.347429 -0.256121,1.407631 -0.323766,2.063424"
id="path6"
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
inkscape:connector-curvature="0" />
<path
ns2:knockout="Off"
d="m 60.691176,49.786022 c 0.112053,-1.670981 -0.328929,-1.142732 -0.477128,-0.505012 0.172985,0.08985 0.309824,1.177846 0.477128,0.505012"
id="path8"
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
inkscape:connector-curvature="0" />
<path
ns2:knockout="Off"
d="m 50.353918,26.946873 c 0.495201,0.08882 1.069924,0.156977 0.98937,0.275226 0.541674,-0.118765 0.664571,-0.228236 -0.98937,-0.275226"
id="path10"
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
inkscape:connector-curvature="0" />
<path
ns2:knockout="Off"
d="m 51.343288,27.222099 -0.350101,0.07229 0.325831,-0.02892 0.02427,-0.04338"
id="path12"
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
inkscape:connector-curvature="0" />
<path
ns2:knockout="Off"
d="m 66.784887,50.419611 c 0.05525,1.500578 -0.438917,2.228663 -0.884546,3.517529 l -0.801926,0.400705 c -0.65631,1.274407 0.06351,0.809155 -0.406386,1.822794 -1.024482,0.910881 -3.109077,2.850376 -3.776231,3.027491 -0.486939,-0.01085 0.329962,-0.574722 0.436851,-0.79573 -1.371485,0.941864 -1.100389,1.413828 -3.197894,1.985969 l -0.06145,-0.136323 c -5.173018,2.433663 -12.358856,-2.389255 -12.26436,-8.969904 -0.05525,0.417745 -0.156977,0.313438 -0.271612,0.482292 -0.266964,-3.385854 1.563576,-6.786682 4.650966,-8.175207 3.019746,-1.494898 6.559995,-0.881448 8.723078,1.13447 -1.188172,-1.556347 -3.553158,-3.206156 -6.356027,-3.051761 -2.745552,0.04337 -5.313988,1.788197 -6.171166,3.682251 -1.406598,0.885579 -1.569772,3.413738 -2.182706,3.876408 -0.824647,6.060662 1.551183,8.679186 5.570109,11.759347 0.632556,0.426524 0.178148,0.49107 0.263866,0.815869 -1.335339,-0.625327 -2.558109,-1.569256 -3.563486,-2.724897 0.533413,0.780755 1.109168,1.539822 1.853261,2.136232 -1.258916,-0.426523 -2.940741,-3.050728 -3.431811,-3.157617 2.170313,3.885702 8.805182,6.814566 12.279335,5.361494 -1.607467,0.05938 -3.64972,0.03305 -5.455991,-0.634621 -0.758551,-0.390378 -1.790263,-1.199017 -1.605918,-1.350314 4.741331,1.771157 9.639123,1.341535 13.741702,-1.94724 1.043588,-0.81277 2.183738,-2.195615 2.513184,-2.214721 -0.496234,0.746158 0.08469,0.358879 -0.296398,1.01777 1.039974,-1.677178 -0.451826,-0.682645 1.075087,-2.896333 l 0.563879,0.776624 c -0.209647,-1.39214 1.728815,-3.082743 1.532077,-5.284555 0.444596,-0.673349 0.496234,0.724471 0.02427,2.273588 0.654761,-1.718487 0.172469,-1.994746 0.340806,-3.412705 0.181763,0.476612 0.420327,0.983173 0.542707,1.48612 -0.426523,-1.660654 0.437884,-2.796673 0.651662,-3.761773 -0.21068,-0.09346 -0.658374,0.734282 -0.760616,-1.227417 0.01497,-0.852014 0.237015,-0.446662 0.322733,-0.656309 -0.167305,-0.09605 -0.606222,-0.749257 -0.873186,-2.001976 0.19364,-0.294332 0.517405,0.763198 0.780755,0.806574 -0.16937,-0.996083 -0.461121,-1.755666 -0.472997,-2.519897 -0.769395,-1.607984 -0.272128,0.214294 -0.896423,-0.69039 -0.818966,-2.554494 0.679547,-0.592796 0.780755,-1.753601 1.24136,1.798525 1.949306,4.585903 2.274104,5.740512 -0.247858,-1.407631 -0.648563,-2.771371 -1.137568,-4.090702 0.376952,0.158526 -0.607254,-2.896333 0.490037,-0.873186 -1.172165,-4.312742 -5.016557,-8.342512 -8.553191,-10.233467 0.43272,0.396057 0.979042,0.893324 0.78282,0.971296 -1.758764,-1.047203 -1.449457,-1.12879 -1.701447,-1.571321 -1.432933,-0.582984 -1.526913,0.04699 -2.476005,0.001 -2.70063,-1.432419 -3.221133,-1.280089 -5.706433,-2.177544 l 0.113085,0.528249 c -1.78923,-0.595894 -2.084595,0.226171 -4.018409,0.0021 -0.117733,-0.09191 0.619646,-0.332544 1.226384,-0.420843 -1.729847,0.228236 -1.648777,-0.340806 -3.341446,0.063 0.417229,-0.292783 0.858211,-0.486423 1.303324,-0.735314 -1.410729,0.08572 -3.36778,0.821032 -2.763625,0.15233 -2.300955,1.026548 -6.387526,2.467743 -8.680735,4.617918 l -0.07229,-0.481776 c -1.050818,1.261498 -4.582289,3.767453 -4.863712,5.401255 l -0.280906,0.06558 c -0.546839,0.925856 -0.900553,1.975125 -1.334306,2.927832 -0.715176,1.218638 -1.048236,0.468866 -0.946511,0.659924 -1.406598,2.851924 -2.10525,5.248408 -2.708889,7.213721 0.430138,0.642884 0.01033,3.870211 0.172985,6.453106 -0.706398,12.756463 8.952863,25.142168 19.511129,28.001841 1.547568,0.553548 3.849039,0.532381 5.806607,0.589182 -2.309733,-0.660445 -2.608197,-0.350104 -4.858031,-1.134472 -1.622958,-0.764231 -1.978739,-1.636901 -3.128184,-2.634532 l 0.454924,0.803992 c -2.25448,-0.7978 -1.311068,-0.987308 -3.145223,-1.568227 l 0.485907,-0.634622 c -0.730667,-0.05525 -1.935364,-1.231548 -2.26481,-1.882693 l -0.799344,0.0315 c -0.960453,-1.185074 -1.472178,-2.039154 -1.434999,-2.700627 l -0.258186,0.460088 C 37.555113,72.462514 34.31436,68.520528 35.995668,69.438122 35.683263,69.152568 35.2681,68.973386 34.817823,68.155453 l 0.342355,-0.391411 c -0.809156,-1.041006 -1.489218,-2.375312 -1.437581,-2.819909 0.431688,0.582984 0.731184,0.691939 1.027581,0.791599 -2.043285,-5.069744 -2.15792,-0.279358 -3.705488,-5.160626 l 0.32738,-0.02634 c -0.250956,-0.377984 -0.403286,-0.7885 -0.605188,-1.191271 l 0.142519,-1.420024 c -1.471145,-1.70093 -0.411549,-7.232311 -0.19932,-10.265999 0.147166,-1.233613 1.227933,-2.546748 2.049998,-4.606041 l -0.500881,-0.08623 c 0.957354,-1.669948 5.466318,-6.706644 7.554528,-6.447425 1.011573,-1.270793 -0.200869,-0.0046 -0.39864,-0.324799 2.22195,-2.299406 2.920602,-1.624507 4.420148,-2.038121 1.617278,-0.959937 -1.388009,0.37437 -0.621196,-0.366108 2.79564,-0.714143 1.98132,-1.623475 5.628458,-1.985968 0.384698,0.218941 -0.892807,0.338223 -1.213475,0.622228 2.329356,-1.139634 7.371216,-0.880415 10.646049,0.632556 3.799984,1.775805 8.069351,7.025246 8.237688,11.964348 l 0.191575,0.05164 c -0.09708,1.963248 0.300528,4.233737 -0.388312,6.319365 l 0.468866,-0.987304"
id="path14"
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
inkscape:connector-curvature="0" />
<path
ns2:knockout="Off"
d="m 43.744352,57.084946 -0.130126,0.650629 c 0.609836,0.828261 1.093677,1.725716 1.872366,2.373247 -0.560264,-1.093677 -0.97646,-1.545502 -1.74224,-3.023876"
id="path16"
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
inkscape:connector-curvature="0" />
<path
ns2:knockout="Off"
d="m 45.186064,57.028145 c -0.322733,-0.356814 -0.513791,-0.786435 -0.727569,-1.214508 0.204483,0.752354 0.623261,1.398853 1.013123,2.056195 l -0.285554,-0.841687"
id="path18"
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
inkscape:connector-curvature="0" />
<path
ns2:knockout="Off"
d="m 70.696924,51.483338 -0.136323,0.341839 c -0.249924,1.775288 -0.789533,3.531987 -1.616762,5.160625 0.91398,-1.718487 1.505226,-3.598082 1.753085,-5.502464"
id="path20"
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
inkscape:connector-curvature="0" />
<path
ns2:knockout="Off"
d="m 50.53723,26.50176 c 0.627393,-0.229786 1.542405,-0.125995 2.208009,-0.277292 -0.867506,0.07281 -1.730881,0.116184 -2.583411,0.226171 l 0.375402,0.05112"
id="path22"
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
inkscape:connector-curvature="0" />
<path
ns2:knockout="Off"
d="m 28.511368,38.214118 c 0.144584,1.338437 -1.006926,1.857908 0.255088,0.975427 0.676447,-1.523815 -0.264383,-0.420843 -0.255088,-0.975427"
id="path24"
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
inkscape:connector-curvature="0" />
<path
ns2:knockout="Off"
d="m 27.028346,44.408521 c 0.290718,-0.892292 0.343388,-1.428286 0.454408,-1.944659 -0.803476,1.027065 -0.369723,1.246007 -0.454408,1.944659"
id="path26"
style="fill:#000000;fill-opacity:1;stroke-width:0.51637238"
inkscape:connector-curvature="0" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -0,0 +1,152 @@
// 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>
<a-dropdown>
<template #overlay>
<a-menu>
<a-menu-item style="width: 100%; padding: 12px">
<router-link :to="{ path: '/action/deployVirtualMachine'}">
<a-row>
<a-col style="margin-right: 12px">
<a-avatar :style="{ backgroundColor: $config.theme['@primary-color'] }">
<template #icon>
<cloud-server-outlined/>
</template>
</a-avatar>
</a-col>
<a-col>
<h3 style="margin-bottom: 0px;">
{{ $t('label.instance') }}
</h3>
<small>{{ $t('label.create.instance') }}</small>
</a-col>
</a-row>
</router-link>
</a-menu-item>
<a-menu-item style="width: 100%; padding: 12px" v-if="'listKubernetesClusters' in $store.getters.apis">
<router-link :to="{ path: '/kubernetes', query: { action: 'createKubernetesCluster' } }">
<a-row>
<a-col style="margin-right: 12px">
<a-avatar :style="{ backgroundColor: $config.theme['@primary-color'] }">
<template #icon>
<font-awesome-icon :icon="['fa-solid', 'fa-dharmachakra']" />
</template>
</a-avatar>
</a-col>
<a-col>
<h3 style="margin-bottom: 0px;">
{{ $t('label.kubernetes') }}
</h3>
<small>{{ $t('label.kubernetes.cluster.create') }}</small>
</a-col>
</a-row>
</router-link>
</a-menu-item>
<a-menu-item style="width: 100%; padding: 12px">
<router-link :to="{ path: '/volume', query: { action: 'createVolume' } }">
<a-row>
<a-col style="margin-right: 12px">
<a-avatar :style="{ backgroundColor: $config.theme['@primary-color'] }">
<template #icon>
<hdd-outlined />
</template>
</a-avatar>
</a-col>
<a-col>
<h3 style="margin-bottom: 0px;">
{{ $t('label.volume') }}
</h3>
<small>{{ $t('label.action.create.volume') }}</small>
</a-col>
</a-row>
</router-link>
</a-menu-item>
<a-menu-item style="width: 100%; padding: 12px">
<router-link :to="{ path: '/guestnetwork', query: { action: 'createNetwork' } }">
<a-row>
<a-col style="margin-right: 12px">
<a-avatar :style="{ backgroundColor: $config.theme['@primary-color'] }">
<template #icon>
<apartment-outlined />
</template>
</a-avatar>
</a-col>
<a-col>
<h3 style="margin-bottom: 0px;">
{{ $t('label.network') }}
</h3>
<small>{{ $t('label.add.network') }}</small>
</a-col>
</a-row>
</router-link>
</a-menu-item>
<a-menu-item style="width: 100%; padding: 12px">
<router-link :to="{ path: '/vpc', query: { action: 'createVPC' } }">
<a-row>
<a-col style="margin-right: 12px">
<a-avatar :style="{ backgroundColor: $config.theme['@primary-color'] }">
<template #icon>
<deployment-unit-outlined />
</template>
</a-avatar>
</a-col>
<a-col>
<h3 style="margin-bottom: 0px;">
{{ $t('label.vpc') }}
</h3>
<small>{{ $t('label.add.vpc') }}</small>
</a-col>
</a-row>
</router-link>
</a-menu-item>
<a-menu-item style="width: 100%; padding: 12px">
<router-link :to="{ path: '/template', query: { action: 'registerTemplate' } }">
<a-row>
<a-col style="margin-right: 12px">
<a-avatar :style="{ backgroundColor: $config.theme['@primary-color'] }">
<template #icon>
<picture-outlined />
</template>
</a-avatar>
</a-col>
<a-col>
<h3 style="margin-bottom: 0px;">
{{ $t('label.templatename') }}
</h3>
<small>{{ $t('label.action.register.template') }}</small>
</a-col>
</a-row>
</router-link>
</a-menu-item>
</a-menu>
</template>
<a-button type="primary">
{{ $t('label.create') }}
<DownOutlined />
</a-button>
</a-dropdown>
</template>
<script>
export default {
name: 'CreateMenu',
components: {
}
}
</script>

View File

@ -81,7 +81,7 @@ export default {
const projects = []
const getNextPage = () => {
this.loading = true
api('listProjects', { listAll: true, details: 'min', page: page, pageSize: 500, showIcon: true }).then(json => {
api('listProjects', { listAll: true, page: page, pageSize: 500, showIcon: true }).then(json => {
if (json?.listprojectsresponse?.project) {
projects.push(...json.listprojectsresponse.project)
}
@ -130,7 +130,7 @@ export default {
<style lang="less" scoped>
.project {
&-select {
width: 30vw;
width: 27vw;
}
&-icon {

View File

@ -17,6 +17,9 @@
<template>
<div class="user-menu">
<span class="action">
<create-menu v-if="device === 'desktop'" />
</span>
<external-link class="action"/>
<translation-menu class="action"/>
<header-notice class="action"/>
@ -69,6 +72,7 @@
<script>
import { api } from '@/api'
import CreateMenu from './CreateMenu'
import ExternalLink from './ExternalLink'
import HeaderNotice from './HeaderNotice'
import TranslationMenu from './TranslationMenu'
@ -80,11 +84,19 @@ import { SERVER_MANAGER } from '@/store/mutation-types'
export default {
name: 'UserMenu',
components: {
CreateMenu,
ExternalLink,
TranslationMenu,
HeaderNotice,
ResourceIcon
},
props: {
device: {
type: String,
required: false,
default: 'desktop'
}
},
data () {
return {
image: '',

View File

@ -41,6 +41,11 @@
<render-icon
v-if="children.meta.icon && typeof (children.meta.icon) === 'string'"
:icon="children.meta.icon" />
<font-awesome-icon
v-else-if="children.meta.icon && Array.isArray(children.meta.icon)"
:icon="children.meta.icon"
class="anticon"
:style="[$store.getters.darkMode ? { color: 'rgba(255, 255, 255, 0.65)' } : { color: '#888' }]" />
<render-icon v-else :svgIcon="children.meta.icon" />
<span>{{ $t(children.meta.title) }}</span>
</router-link>

View File

@ -18,7 +18,7 @@
<template>
<a-layout-header v-if="!headerBarFixed" :class="[fixedHeader && 'ant-header-fixedHeader', sidebarOpened ? 'ant-header-side-opened' : 'ant-header-side-closed', theme ]" :style="{ padding: '0' }">
<div v-if="mode === 'sidemenu'" class="header">
<template v-if="device==='mobile'">
<template v-if="device === 'mobile'">
<menu-fold-outlined class="trigger" v-if="collapsed" @click="toggle" />
<menu-unfold-outlined class="trigger" v-else @click="toggle" />
</template>
@ -28,7 +28,7 @@
</template>
<project-menu v-if="device !== 'mobile'" />
<saml-domain-switcher style="margin-left: 20px" />
<user-menu></user-menu>
<user-menu :device="device"></user-menu>
</div>
<div v-else :class="['top-nav-header-index', theme]">
<div class="header-index-wide">

View File

@ -279,6 +279,11 @@ export default {
&.dark {
.ant-drawer-content {
background-color: rgb(0, 21, 41);
max-width: 256px;
}
.ant-drawer-content-wrapper {
width: 256px !important;;
}
}
@ -287,11 +292,16 @@ export default {
.ant-drawer-content {
background-color: #fff;
max-width: 256px;
}
.ant-drawer-content-wrapper {
width: 256px !important;
}
}
.ant-drawer-body {
padding: 0
padding: 0;
}
}

View File

@ -36,6 +36,12 @@
<span v-else>
<os-logo v-if="resource.ostypeid || resource.ostypename" :osId="resource.ostypeid" :osName="resource.ostypename" size="4x" @update-osname="setResourceOsType"/>
<render-icon v-else-if="typeof $route.meta.icon ==='string'" style="font-size: 36px" :icon="$route.meta.icon" />
<font-awesome-icon
v-else-if="$route.meta.icon && Array.isArray($route.meta.icon)"
:icon="$route.meta.icon"
size="4x"
class="anticon"
:style="[$store.getters.darkMode ? { color: 'rgba(255, 255, 255, 0.65)' } : { color: '#888' }]" />
<render-icon v-else style="font-size: 36px" :svgIcon="$route.meta.icon" />
</span>
</slot>

View File

@ -37,7 +37,7 @@
</keep-alive>
<a-tabs
v-else
style="width: 100%"
style="width: 100%; margin-top: -12px"
:animated="false"
:activeKey="activeTab || tabs[0].name"
@change="onTabChange" >

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

@ -24,29 +24,15 @@
:icon="['fab', logo]"
:size="size"
:style="[$store.getters.darkMode ? { color: 'rgba(255, 255, 255, 0.65)' } : { color: '#666' }]"
v-if="logo !== 'debian'" />
<debian-icon
v-else-if="logo === 'debian'"
:width="size === '4x' ? 56 : 16"
:height="size === '4x' ? 56 : 16"
:style="{
height: size === '4x' ? '56px' : '16px',
width: size === '4x' ? '56px' : '16px',
marginBottom: '-4px',
background: $store.getters.darkMode ? 'rgba(255, 255, 255, 0.65)' : ''
}" />
/>
</a-tooltip>
</template>
<script>
import { api } from '@/api'
import DebianIcon from '@/assets/icons/debian.svg?inline'
export default {
name: 'OsLogo',
components: {
DebianIcon
},
props: {
osId: {
type: String,

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

@ -16,7 +16,6 @@
// under the License.
import { shallowRef, defineAsyncComponent } from 'vue'
import kubernetes from '@/assets/icons/kubernetes.svg?inline'
import store from '@/store'
export default {
@ -27,7 +26,7 @@ export default {
{
name: 'vm',
title: 'label.instances',
icon: 'desktop-outlined',
icon: 'cloud-server-outlined',
docHelp: 'adminguide/virtual_machines.html',
permission: ['listVirtualMachinesMetrics'],
resourceType: 'UserVm',
@ -456,7 +455,7 @@ export default {
{
name: 'kubernetes',
title: 'label.kubernetes',
icon: shallowRef(kubernetes),
icon: ['fa-solid', 'fa-dharmachakra'],
docHelp: 'plugins/cloudstack-kubernetes-service.html',
permission: ['listKubernetesClusters'],
columns: (store) => {
@ -557,7 +556,7 @@ export default {
{
name: 'autoscalevmgroup',
title: 'label.autoscale.vm.groups',
icon: 'ordered-list-outlined',
icon: 'fullscreen-outlined',
docHelp: 'adminguide/autoscale_without_netscaler.html',
resourceType: 'AutoScaleVmGroup',
permission: ['listAutoScaleVmGroups'],

View File

@ -16,7 +16,6 @@
// under the License.
import { shallowRef, defineAsyncComponent } from 'vue'
import kubernetes from '@/assets/icons/kubernetes.svg?inline'
import store from '@/store'
export default {
@ -340,7 +339,7 @@ export default {
{
name: 'kubernetesiso',
title: 'label.kubernetes.isos',
icon: shallowRef(kubernetes),
icon: ['fa-solid', 'fa-dharmachakra'],
docHelp: 'plugins/cloudstack-kubernetes-service.html#kubernetes-supported-versions',
permission: ['listKubernetesSupportedVersions'],
columns: ['name', 'state', 'semanticversion', 'isostate', 'mincpunumber', 'minmemory', 'zonename'],

View File

@ -21,7 +21,7 @@ import store from '@/store'
export default {
name: 'host',
title: 'label.hosts',
icon: 'desktop-outlined',
icon: 'database-outlined',
docHelp: 'conceptsandterminology/concepts.html#about-hosts',
permission: ['listHostsMetrics'],
resourceType: 'Host',

View File

@ -21,7 +21,7 @@ import store from '@/store'
export default {
name: 'storagepool',
title: 'label.primary.storage',
icon: 'database-outlined',
icon: 'hdd-outlined',
docHelp: 'adminguide/storage.html#primary-storage',
permission: ['listStoragePoolsMetrics'],
columns: () => {

View File

@ -21,7 +21,7 @@ import store from '@/store'
export default {
name: 'storage',
title: 'label.storage',
icon: 'database-outlined',
icon: 'hdd-outlined',
children: [
{
name: 'volume',

View File

@ -22,11 +22,11 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
// import { fas } from '@fortawesome/free-solid-svg-icons'
// import { far } from '@fortawesome/free-regular-svg-icons'
import { faCentos, faUbuntu, faSuse, faRedhat, faFedora, faLinux, faFreebsd, faApple, faWindows, faJava } from '@fortawesome/free-brands-svg-icons'
import { faCompactDisc, faCameraRetro } from '@fortawesome/free-solid-svg-icons'
import { faCentos, faUbuntu, faDebian, faSuse, faRedhat, faFedora, faLinux, faFreebsd, faApple, faWindows, faJava } from '@fortawesome/free-brands-svg-icons'
import { faCompactDisc, faCameraRetro, faDharmachakra } from '@fortawesome/free-solid-svg-icons'
library.add(faCentos, faUbuntu, faSuse, faRedhat, faFedora, faLinux, faFreebsd, faApple, faWindows, faJava)
library.add(faCompactDisc, faCameraRetro)
library.add(faCentos, faUbuntu, faDebian, faSuse, faRedhat, faFedora, faLinux, faFreebsd, faApple, faWindows, faJava)
library.add(faCompactDisc, faCameraRetro, faDharmachakra)
export default {
install: (app) => {

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'
@ -131,5 +132,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;
}

View File

@ -519,6 +519,11 @@ a {
padding: 16px;
}
.ant-tabs-left>.ant-tabs-nav .ant-tabs-tab {
padding: 8px 24px 8px 8px;
text-align: center;
}
.ant-steps {
&-item-container {
&:hover {

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

@ -393,8 +393,8 @@
</a-modal>
</div>
<div :style="this.$store.getters.shutdownTriggered ? 'margin-top: 25px;' : null">
<div v-if="dataView" style="margin-top: -10px">
<div :style="this.$store.getters.shutdownTriggered ? 'margin-top: 24px; margin-bottom: 12px' : null">
<div v-if="dataView">
<slot name="resource" v-if="$route.path.startsWith('/quotasummary') || $route.path.startsWith('/publicip')"></slot>
<resource-view
v-else
@ -1059,6 +1059,19 @@ export default {
this.loading = false
this.searchParams = params
})
if ('action' in this.$route.query) {
const actionName = this.$route.query.action
for (const action of this.actions) {
if (action.listView && action.api === actionName) {
this.execAction(action, false)
const query = Object.assign({}, this.$route.query)
delete query.action
this.$router.replace({ query })
break
}
}
}
},
closeAction () {
this.actionLoading = false

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,282 @@
<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/>
<status class="status" text="Alert" style="margin-left: -10px"/>
</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 +326,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 +364,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 +401,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 +416,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 +426,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,15 +579,16 @@ 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.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
@ -290,7 +601,6 @@ export default {
.capacity-dashboard {
&-wrapper {
display: flex;
margin-bottom: 12px;
}
&-chart-card-inner {
@ -313,7 +623,7 @@ export default {
&-button {
width: auto;
padding-left: 12px;
padding-left: 8px;
}
&-button-icon {
@ -321,21 +631,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,331 @@
// 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 }" class="dashboard-card">
<chart-card :loading="loading" class="dashboard-card">
<template #title>
<div class="center">
<h3><render-icon :icon="$config.userCard.icon" /> {{ $t($config.userCard.title) }}</h3>
</div>
</template>
<a-divider style="margin: 6px 0px; border-width: 0px"/>
<a-list item-layout="horizontal" :data-source="$config.userCard.links">
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta :description="item.text">
<template #title>
<a :href="item.link" target="_blank"><h4>{{ item.title }}</h4></a>
</template>
<template #avatar>
<a-avatar :style="{ backgroundColor: $config.theme['@primary-color'] }">
<template #icon>
<render-icon :icon="item.icon" />
</template>
</a-avatar>
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</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 +353,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 +380,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 +429,9 @@ export default {
deep: true,
handler (newData, oldData) {
this.project = newData
if (newData.id) {
this.fetchData()
}
}
},
'$i18n.global.locale' (to, from) {
@ -168,61 +442,95 @@ export default {
},
methods: {
fetchData () {
this.stats = [{}, {}, {}, {}, {}, {}]
api('listVirtualMachines', { state: 'Running', listall: true, retrieveonlyresourcecount: 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, retrieveonlyresourcecount: 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, retrieveonlyresourcecount: 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, retrieveonlyresourcecount: 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, retrieveonlyresourcecount: 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, retrieveonlyresourcecount: 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 +542,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 +581,6 @@ export default {
return 'green'
}
return 'blue'
},
onTabChange (key) {
this.showAddAccount = false
if (key !== 'Dashboard') {
this.showAddAccount = true
}
}
}
}
@ -276,6 +608,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%;

View File

@ -293,11 +293,11 @@
</div>
<a-form-item v-if="selectedNetworkOfferingSupportsSourceNat" name="sourcenatipaddress" ref="sourcenatipaddress">
<template #label>
<tooltip-label :title="$t('label.routerip')" :tooltip="apiParams.sourcenatipaddress.description"/>
<tooltip-label :title="$t('label.routerip')" :tooltip="apiParams.sourcenatipaddress?.description"/>
</template>
<a-input
v-model:value="form.sourcenatipaddress"
:placeholder="apiParams.sourcenatipaddress.description"/>
:placeholder="apiParams.sourcenatipaddress?.description"/>
</a-form-item>
<a-form-item
ref="networkdomain"

View File

@ -106,7 +106,8 @@ export default {
fetchActionZoneData () {
this.loading = true
const params = {}
if (this.resource.zoneid && this.$route.name === 'deployVirtualMachine') {
console.log(this.resource)
if (this.$route.name === 'deployVirtualMachine' && this.resource.zoneid) {
params.id = this.resource.zoneid
}
this.actionZoneLoading = true

View File

@ -157,11 +157,11 @@
</a-row>
<a-form-item v-if="selectedNetworkOfferingSupportsSourceNat" name="sourcenatipaddress" ref="sourcenatipaddress">
<template #label>
<tooltip-label :title="$t('label.routerip')" :tooltip="apiParams.sourcenatipaddress.description"/>
<tooltip-label :title="$t('label.routerip')" :tooltip="apiParams.sourcenatipaddress?.description"/>
</template>
<a-input
v-model:value="form.sourcenatipaddress"
:placeholder="apiParams.sourcenatipaddress.description"/>
:placeholder="apiParams.sourcenatipaddress?.description"/>
</a-form-item>
<a-form-item name="start" ref="start">
<template #label>