mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 08:42:29 +01:00
project: dashboard, custom actions and tabs (#73)
This fixes #41 Adds project specific dashboard tabs, custom actions and tabs for project view. Also adds quickview and other list/details view improvements. Co-authored-by: hoangnm <hoangcit92@gmail.com> Co-authored-by: Rohit Yadav <rohit@apache.org> Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
This commit is contained in:
parent
b866f233af
commit
a8bdc99757
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB |
@ -106,7 +106,7 @@ export default {
|
|||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.project {
|
.project {
|
||||||
&-select {
|
&-select {
|
||||||
width: 40%;
|
width: 30vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-icon {
|
&-icon {
|
||||||
|
|||||||
@ -85,7 +85,23 @@ export default {
|
|||||||
height: auto;
|
height: auto;
|
||||||
|
|
||||||
/deep/ .ant-layout-sider-children {
|
/deep/ .ant-layout-sider-children {
|
||||||
overflow-y: auto;
|
overflow-y: hidden;
|
||||||
|
&:hover {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .ant-menu-vertical .ant-menu-item {
|
||||||
|
margin-top: 0px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .ant-menu-inline .ant-menu-item:not(:last-child) {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .ant-menu-inline .ant-menu-item {
|
||||||
|
margin-top: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.ant-fixed-sidemenu {
|
&.ant-fixed-sidemenu {
|
||||||
@ -99,14 +115,14 @@ export default {
|
|||||||
|
|
||||||
.ant-menu-light {
|
.ant-menu-light {
|
||||||
border-right-color: transparent;
|
border-right-color: transparent;
|
||||||
padding: 10px 0;
|
padding: 14px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.dark {
|
&.dark {
|
||||||
.ant-menu-dark {
|
.ant-menu-dark {
|
||||||
border-right-color: transparent;
|
border-right-color: transparent;
|
||||||
padding: 10px 0;
|
padding: 14px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,8 +18,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<div class="links">
|
<div class="links">
|
||||||
<a href="https://github.com/apache/cloudstack-primate" target="_blank">
|
CloudStack Server {{ $store.getters.features.cloudstackversion }}
|
||||||
|
<a-divider type="vertical" />
|
||||||
|
<a href="https://github.com/apache/cloudstack-primate/issues/new/choose" target="_blank">
|
||||||
<a-icon type="github"/>
|
<a-icon type="github"/>
|
||||||
|
Report Bug
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -51,9 +54,6 @@ export default {
|
|||||||
color: rgba(0, 0, 0, .65);
|
color: rgba(0, 0, 0, .65);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:last-child) {
|
|
||||||
margin-right: 40px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.copyright {
|
.copyright {
|
||||||
|
|||||||
170
ui/src/components/view/ActionButton.vue
Normal file
170
ui/src/components/view/ActionButton.vue
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
// 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>
|
||||||
|
<span class="row-action-button">
|
||||||
|
<a-tooltip
|
||||||
|
v-for="(action, actionIndex) in actions"
|
||||||
|
:key="actionIndex"
|
||||||
|
arrowPointAtCenter
|
||||||
|
placement="bottomRight">
|
||||||
|
<template slot="title">
|
||||||
|
{{ $t(action.label) }}
|
||||||
|
</template>
|
||||||
|
<a-badge
|
||||||
|
class="button-action-badge"
|
||||||
|
:overflowCount="9"
|
||||||
|
:count="actionBadge[action.api] ? actionBadge[action.api].badgeNum : 0"
|
||||||
|
v-if="action.api in $store.getters.apis &&
|
||||||
|
action.showBadge &&
|
||||||
|
((!dataView && (action.listView || action.groupAction && selectedRowKeys.length > 0)) || (dataView && action.dataView)) &&
|
||||||
|
('show' in action ? action.show(resource, $store.getters.userInfo) : true)">
|
||||||
|
<a-button
|
||||||
|
:icon="action.icon"
|
||||||
|
:type="action.icon === 'delete' ? 'danger' : (action.icon === 'plus' ? 'primary' : 'default')"
|
||||||
|
shape="circle"
|
||||||
|
style="margin-right: 5px"
|
||||||
|
@click="execAction(action)" />
|
||||||
|
</a-badge>
|
||||||
|
<a-button
|
||||||
|
v-if="action.api in $store.getters.apis &&
|
||||||
|
!action.showBadge &&
|
||||||
|
((!dataView && (action.listView || action.groupAction && selectedRowKeys.length > 0)) || (dataView && action.dataView)) &&
|
||||||
|
('show' in action ? action.show(resource, $store.getters.userInfo) : true)"
|
||||||
|
:icon="action.icon"
|
||||||
|
:type="action.icon === 'delete' ? 'danger' : (action.icon === 'plus' ? 'primary' : 'default')"
|
||||||
|
shape="circle"
|
||||||
|
style="margin-left: 5px"
|
||||||
|
@click="execAction(action)" />
|
||||||
|
</a-tooltip>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { api } from '@/api'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ActionButton',
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
actionBadge: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.handleShowBadge()
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
actions: {
|
||||||
|
type: Array,
|
||||||
|
default () {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resource: {
|
||||||
|
type: Object,
|
||||||
|
default () {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataView: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
selectedRowKeys: {
|
||||||
|
type: Array,
|
||||||
|
default () {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
resource (newItem, oldItem) {
|
||||||
|
if (!newItem || !newItem.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.handleShowBadge()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
execAction (action) {
|
||||||
|
this.$emit('exec-action', action)
|
||||||
|
},
|
||||||
|
handleShowBadge () {
|
||||||
|
const dataBadge = {}
|
||||||
|
const arrAsync = []
|
||||||
|
const actionBadge = this.actions.filter(action => action.showBadge === true)
|
||||||
|
|
||||||
|
if (actionBadge && actionBadge.length > 0) {
|
||||||
|
const dataLength = actionBadge.length
|
||||||
|
|
||||||
|
for (let i = 0; i < dataLength; i++) {
|
||||||
|
const action = actionBadge[i]
|
||||||
|
|
||||||
|
arrAsync.push(new Promise((resolve, reject) => {
|
||||||
|
api(action.api, action.param).then(json => {
|
||||||
|
let responseJsonName
|
||||||
|
const response = {}
|
||||||
|
|
||||||
|
response.api = action.api
|
||||||
|
response.count = 0
|
||||||
|
|
||||||
|
for (const key in json) {
|
||||||
|
if (key.includes('response')) {
|
||||||
|
responseJsonName = key
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json[responseJsonName].count && json[responseJsonName].count > 0) {
|
||||||
|
response.count = json[responseJsonName].count
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(response)
|
||||||
|
}).catch(error => {
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.all(arrAsync).then(response => {
|
||||||
|
for (let j = 0; j < response.length; j++) {
|
||||||
|
this.$set(dataBadge, response[j].api, {})
|
||||||
|
this.$set(dataBadge[response[j].api], 'badgeNum', response[j].count)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.actionBadge = dataBadge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped >
|
||||||
|
.button-action-badge {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/.button-action-badge .ant-badge-count {
|
||||||
|
right: 10px;
|
||||||
|
z-index: 8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -43,27 +43,6 @@
|
|||||||
<a-list-item-meta>
|
<a-list-item-meta>
|
||||||
<span slot="title">
|
<span slot="title">
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
<a-button shape="circle" size="small" @click="updateDetail(index)" v-if="item.edit">
|
|
||||||
<a-icon type="check-circle" theme="twoTone" twoToneColor="#52c41a" />
|
|
||||||
</a-button>
|
|
||||||
<a-button shape="circle" size="small" @click="hideEditDetail(index)" v-if="item.edit" style="margin-left: 5px">
|
|
||||||
<a-icon type="close-circle" theme="twoTone" twoToneColor="#f5222d" />
|
|
||||||
</a-button>
|
|
||||||
<a-button shape="circle" size="small" @click="showEditDetail(index)" v-if="!item.edit">
|
|
||||||
<a-icon type="edit" />
|
|
||||||
</a-button>
|
|
||||||
<a-divider type="vertical" />
|
|
||||||
<a-popconfirm
|
|
||||||
title="Delete setting?"
|
|
||||||
@confirm="deleteDetail(index)"
|
|
||||||
okText="Yes"
|
|
||||||
cancelText="No"
|
|
||||||
placement="right"
|
|
||||||
>
|
|
||||||
<a-button shape="circle" size="small">
|
|
||||||
<a-icon type="delete" theme="twoTone" twoToneColor="#f5222d" />
|
|
||||||
</a-button>
|
|
||||||
</a-popconfirm>
|
|
||||||
</span>
|
</span>
|
||||||
<span slot="description" style="word-break: break-all">
|
<span slot="description" style="word-break: break-all">
|
||||||
<span v-if="item.edit" style="display: flex">
|
<span v-if="item.edit" style="display: flex">
|
||||||
@ -77,6 +56,30 @@
|
|||||||
<span v-else @click="showEditDetail(index)">{{ item.value }}</span>
|
<span v-else @click="showEditDetail(index)">{{ item.value }}</span>
|
||||||
</span>
|
</span>
|
||||||
</a-list-item-meta>
|
</a-list-item-meta>
|
||||||
|
<div slot="actions">
|
||||||
|
<a-button shape="circle" size="default" @click="updateDetail(index)" v-if="item.edit">
|
||||||
|
<a-icon type="check-circle" theme="twoTone" twoToneColor="#52c41a" />
|
||||||
|
</a-button>
|
||||||
|
<a-button shape="circle" size="default" @click="hideEditDetail(index)" v-if="item.edit">
|
||||||
|
<a-icon type="close-circle" theme="twoTone" twoToneColor="#f5222d" />
|
||||||
|
</a-button>
|
||||||
|
<a-button shape="circle" @click="showEditDetail(index)" v-if="!item.edit">
|
||||||
|
<a-icon type="edit" />
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<div slot="actions">
|
||||||
|
<a-popconfirm
|
||||||
|
title="Delete setting?"
|
||||||
|
@confirm="deleteDetail(index)"
|
||||||
|
okText="Yes"
|
||||||
|
cancelText="No"
|
||||||
|
placement="left"
|
||||||
|
>
|
||||||
|
<a-button shape="circle">
|
||||||
|
<a-icon type="delete" theme="twoTone" twoToneColor="#f5222d" />
|
||||||
|
</a-button>
|
||||||
|
</a-popconfirm>
|
||||||
|
</div>
|
||||||
</a-list-item>
|
</a-list-item>
|
||||||
</a-list>
|
</a-list>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
|
|||||||
@ -430,6 +430,7 @@
|
|||||||
:value="annotation"
|
:value="annotation"
|
||||||
placeholder="Add Note" />
|
placeholder="Add Note" />
|
||||||
<a-button
|
<a-button
|
||||||
|
style="margin-top: 10px"
|
||||||
@click="saveNote"
|
@click="saveNote"
|
||||||
type="primary"
|
type="primary"
|
||||||
>
|
>
|
||||||
@ -643,12 +644,15 @@ export default {
|
|||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
|
||||||
|
/deep/ .ant-card-body {
|
||||||
|
padding: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
.resource-details {
|
.resource-details {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
& > .avatar {
|
& > .avatar {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding-top: 20px;
|
|
||||||
width: 104px;
|
width: 104px;
|
||||||
//height: 104px;
|
//height: 104px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|||||||
@ -34,7 +34,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div slot="expandedRowRender" slot-scope="resource">
|
<div slot="expandedRowRender" slot-scope="resource">
|
||||||
<info-card :resource="resource" style="margin-right: 50px">
|
<info-card :resource="resource" style="margin-left: 0px; width: 50%">
|
||||||
<div slot="actions" style="padding-top: 12px">
|
<div slot="actions" style="padding-top: 12px">
|
||||||
<a-tooltip
|
<a-tooltip
|
||||||
v-for="(action, actionIndex) in $route.meta.actions"
|
v-for="(action, actionIndex) in $route.meta.actions"
|
||||||
@ -48,12 +48,10 @@
|
|||||||
('show' in action ? action.show(resource, $store.getters.userInfo) : true)"
|
('show' in action ? action.show(resource, $store.getters.userInfo) : true)"
|
||||||
:icon="action.icon"
|
:icon="action.icon"
|
||||||
:type="action.icon === 'delete' ? 'danger' : (action.icon === 'plus' ? 'primary' : 'default')"
|
:type="action.icon === 'delete' ? 'danger' : (action.icon === 'plus' ? 'primary' : 'default')"
|
||||||
shape="round"
|
shape="circle"
|
||||||
size="small"
|
|
||||||
style="margin-right: 5px; margin-top: 12px"
|
style="margin-right: 5px; margin-top: 12px"
|
||||||
@click="$parent.execAction(action)"
|
@click="$parent.execAction(action)"
|
||||||
>
|
>
|
||||||
{{ $t(action.label) }}
|
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -36,7 +36,7 @@
|
|||||||
v-for="tab in tabs"
|
v-for="tab in tabs"
|
||||||
:tab="$t(tab.name)"
|
:tab="$t(tab.name)"
|
||||||
:key="tab.name"
|
:key="tab.name"
|
||||||
v-if="'show' in tab ? tab.show(resource, $route) : true">
|
v-if="'show' in tab ? tab.show(resource, $route, $store.getters.userInfo) : true">
|
||||||
<component :is="tab.component" :resource="resource" :loading="loading" :tab="activeTab" />
|
<component :is="tab.component" :resource="resource" :loading="loading" :tab="activeTab" />
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
@ -46,7 +46,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import DetailsTab from '@/components/view/DetailsTab'
|
import DetailsTab from '@/components/view/DetailsTab'
|
||||||
import InfoCard from '@/components/view/InfoCard'
|
import InfoCard from '@/components/view/InfoCard'
|
||||||
import ResourceLayout from '@/layouts/ResourceLayout'
|
import ResourceLayout from '@/layouts/ResourceLayout'
|
||||||
|
|||||||
@ -31,18 +31,22 @@
|
|||||||
<span v-else>
|
<span v-else>
|
||||||
{{ $t(item.meta.title) }}
|
{{ $t(item.meta.title) }}
|
||||||
</span>
|
</span>
|
||||||
<a-tooltip v-if="index === (breadList.length - 1)" placement="bottom">
|
<span v-if="index === (breadList.length - 1)" style="margin-left: 5px">
|
||||||
<template slot="title">
|
<a-tooltip placement="bottom">
|
||||||
{{ "Open Documentation" }}
|
<template slot="title">
|
||||||
</template>
|
{{ "Open Documentation" }}
|
||||||
<a
|
</template>
|
||||||
v-if="item.meta.docHelp"
|
<a
|
||||||
style="margin-right: 5px"
|
v-if="item.meta.docHelp"
|
||||||
:href="docBase + '/' + $route.meta.docHelp"
|
style="margin-right: 10px"
|
||||||
target="_blank">
|
:href="docBase + '/' + $route.meta.docHelp"
|
||||||
<a-icon type="question-circle-o"></a-icon>
|
target="_blank">
|
||||||
</a>
|
<a-icon type="question-circle-o"></a-icon>
|
||||||
</a-tooltip>
|
</a>
|
||||||
|
</a-tooltip>
|
||||||
|
<slot name="end">
|
||||||
|
</slot>
|
||||||
|
</span>
|
||||||
</a-breadcrumb-item>
|
</a-breadcrumb-item>
|
||||||
</a-breadcrumb>
|
</a-breadcrumb>
|
||||||
</template>
|
</template>
|
||||||
@ -72,6 +76,9 @@ export default {
|
|||||||
this.name = this.$route.name
|
this.name = this.$route.name
|
||||||
this.breadList = []
|
this.breadList = []
|
||||||
this.$route.matched.forEach((item) => {
|
this.$route.matched.forEach((item) => {
|
||||||
|
if (item && item.parent && item.parent.name !== 'index' && !item.path.endsWith(':id')) {
|
||||||
|
this.breadList.pop()
|
||||||
|
}
|
||||||
this.breadList.push(item)
|
this.breadList.push(item)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -90,7 +97,6 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ant-breadcrumb .anticon {
|
.ant-breadcrumb .anticon {
|
||||||
margin-left: 8px;
|
|
||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -66,6 +66,7 @@ export default {
|
|||||||
case 'Down':
|
case 'Down':
|
||||||
case 'Error':
|
case 'Error':
|
||||||
case 'Stopped':
|
case 'Stopped':
|
||||||
|
case 'Declined':
|
||||||
case 'Disconnected':
|
case 'Disconnected':
|
||||||
status = 'error'
|
status = 'error'
|
||||||
break
|
break
|
||||||
@ -78,6 +79,7 @@ export default {
|
|||||||
case 'Alert':
|
case 'Alert':
|
||||||
case 'Allocated':
|
case 'Allocated':
|
||||||
case 'Created':
|
case 'Created':
|
||||||
|
case 'Pending':
|
||||||
status = 'warning'
|
status = 'warning'
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@ -167,7 +167,27 @@ export const asyncRouterMap = [
|
|||||||
{
|
{
|
||||||
path: '/dashboard',
|
path: '/dashboard',
|
||||||
name: 'dashboard',
|
name: 'dashboard',
|
||||||
meta: { title: 'Dashboard', keepAlive: true, icon: 'dashboard' },
|
meta: {
|
||||||
|
title: 'Dashboard',
|
||||||
|
keepAlive: true,
|
||||||
|
icon: 'dashboard',
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'Dashboard',
|
||||||
|
component: () => import('@/views/dashboard/UsageDashboardChart')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'accounts',
|
||||||
|
show: (record, route, user) => { return record.account === user.account || ['Admin', 'DomainAdmin'].includes(user.roletype) },
|
||||||
|
component: () => import('@/views/project/AccountsTab')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'resources',
|
||||||
|
show: (record, route, user) => { return ['Admin'].includes(user.roletype) },
|
||||||
|
component: () => import('@/views/project/ResourcesTab.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
component: () => import('@/views/dashboard/Dashboard')
|
component: () => import('@/views/dashboard/Dashboard')
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,22 @@ export default {
|
|||||||
resourceType: 'Project',
|
resourceType: 'Project',
|
||||||
columns: ['name', 'state', 'displaytext', 'account', 'domain'],
|
columns: ['name', 'state', 'displaytext', 'account', 'domain'],
|
||||||
details: ['name', 'id', 'displaytext', 'projectaccountname', 'vmtotal', 'cputotal', 'memorytotal', 'volumetotal', 'iptotal', 'vpctotal', 'templatetotal', 'primarystoragetotal', 'account', 'domain'],
|
details: ['name', 'id', 'displaytext', 'projectaccountname', 'vmtotal', 'cputotal', 'memorytotal', 'volumetotal', 'iptotal', 'vpctotal', 'templatetotal', 'primarystoragetotal', 'account', 'domain'],
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'details',
|
||||||
|
component: () => import('@/components/view/DetailsTab.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'accounts',
|
||||||
|
show: (record, route, user) => { return record.account === user.account || ['Admin', 'DomainAdmin'].includes(user.roletype) },
|
||||||
|
component: () => import('@/views/project/AccountsTab.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'resources',
|
||||||
|
show: (record, route, user) => { return ['Admin'].includes(user.roletype) },
|
||||||
|
component: () => import('@/views/project/ResourcesTab.vue')
|
||||||
|
}
|
||||||
|
],
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
api: 'createProject',
|
api: 'createProject',
|
||||||
@ -31,6 +47,27 @@ export default {
|
|||||||
listView: true,
|
listView: true,
|
||||||
args: ['name', 'displaytext']
|
args: ['name', 'displaytext']
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
api: 'updateProjectInvitation',
|
||||||
|
icon: 'key',
|
||||||
|
label: 'label.enter.token',
|
||||||
|
listView: true,
|
||||||
|
popup: true,
|
||||||
|
component: () => import('@/views/project/InvitationTokenTemplate.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'listProjectInvitations',
|
||||||
|
icon: 'team',
|
||||||
|
label: 'label.project.invitation',
|
||||||
|
listView: true,
|
||||||
|
popup: true,
|
||||||
|
showBadge: true,
|
||||||
|
badgeNum: 0,
|
||||||
|
param: {
|
||||||
|
state: 'Pending'
|
||||||
|
},
|
||||||
|
component: () => import('@/views/project/InvitationsTemplate.vue')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
api: 'updateProject',
|
api: 'updateProject',
|
||||||
icon: 'edit',
|
icon: 'edit',
|
||||||
@ -58,6 +95,7 @@ export default {
|
|||||||
label: 'Add Account to Project',
|
label: 'Add Account to Project',
|
||||||
dataView: true,
|
dataView: true,
|
||||||
args: ['projectid', 'account', 'email'],
|
args: ['projectid', 'account', 'email'],
|
||||||
|
show: (record, user) => { return record.account === user.account || ['Admin', 'DomainAdmin'].includes(user.roletype) },
|
||||||
mapping: {
|
mapping: {
|
||||||
projectid: {
|
projectid: {
|
||||||
value: (record) => { return record.id }
|
value: (record) => { return record.id }
|
||||||
|
|||||||
@ -7,6 +7,8 @@
|
|||||||
"Clusters": "Clusters",
|
"Clusters": "Clusters",
|
||||||
"Compute": "Compute",
|
"Compute": "Compute",
|
||||||
"Compute Offerings": "Compute Offerings",
|
"Compute Offerings": "Compute Offerings",
|
||||||
|
"confirmacceptinvitation": "Please confirm you wish to join this project",
|
||||||
|
"confirmdeclineinvitation": "Are you sure you want to decline this project invitation?",
|
||||||
"Configuration": "Configuration",
|
"Configuration": "Configuration",
|
||||||
"Dashboard": "Dashboard",
|
"Dashboard": "Dashboard",
|
||||||
"Disk Offerings": "Disk Offerings",
|
"Disk Offerings": "Disk Offerings",
|
||||||
@ -263,6 +265,7 @@
|
|||||||
"internaldns2": "Internal DNS 2",
|
"internaldns2": "Internal DNS 2",
|
||||||
"interval": "Polling Interval (in sec)",
|
"interval": "Polling Interval (in sec)",
|
||||||
"intervaltype": "Interval Type",
|
"intervaltype": "Interval Type",
|
||||||
|
"invitations": "Invitations",
|
||||||
"ip": "IP Address",
|
"ip": "IP Address",
|
||||||
"ip4Netmask": "IPv4 Netmask",
|
"ip4Netmask": "IPv4 Netmask",
|
||||||
"ip4dns1": "IPv4 DNS1",
|
"ip4dns1": "IPv4 DNS1",
|
||||||
@ -380,6 +383,7 @@
|
|||||||
"label.action.manage.cluster": "Manage Cluster",
|
"label.action.manage.cluster": "Manage Cluster",
|
||||||
"label.action.migrate.router": "Migrate Router",
|
"label.action.migrate.router": "Migrate Router",
|
||||||
"label.action.migrate.systemvm": "Migrate System VM",
|
"label.action.migrate.systemvm": "Migrate System VM",
|
||||||
|
"label.action.project.add.account": "Add Account to Project",
|
||||||
"label.action.reboot.instance": "Reboot Instance",
|
"label.action.reboot.instance": "Reboot Instance",
|
||||||
"label.action.reboot.router": "Reboot Router",
|
"label.action.reboot.router": "Reboot Router",
|
||||||
"label.action.reboot.systemvm": "Reboot System VM",
|
"label.action.reboot.systemvm": "Reboot System VM",
|
||||||
@ -550,6 +554,7 @@
|
|||||||
"label.outofbandmanagement.configure": "Configure Out-of-band Management",
|
"label.outofbandmanagement.configure": "Configure Out-of-band Management",
|
||||||
"label.outofbandmanagement.disable": "Disable Out-of-band Management",
|
"label.outofbandmanagement.disable": "Disable Out-of-band Management",
|
||||||
"label.outofbandmanagement.enable": "Enable Out-of-band Management",
|
"label.outofbandmanagement.enable": "Enable Out-of-band Management",
|
||||||
|
"label.project.invitation": "Project Invitations",
|
||||||
"label.quota.add.credits": "Add Credits",
|
"label.quota.add.credits": "Add Credits",
|
||||||
"label.quota.dates": "Update Dates",
|
"label.quota.dates": "Update Dates",
|
||||||
"label.recover.vm": "Recover VM",
|
"label.recover.vm": "Recover VM",
|
||||||
@ -613,6 +618,18 @@
|
|||||||
"makeredundant": "Make redundant",
|
"makeredundant": "Make redundant",
|
||||||
"managedstate": "Managed State",
|
"managedstate": "Managed State",
|
||||||
"managementServers": "Number of Management Servers",
|
"managementServers": "Number of Management Servers",
|
||||||
|
"maxuser_vm": "Max. user VMs",
|
||||||
|
"maxpublic_ip": "Max. public IPs",
|
||||||
|
"maxvolume": "Max. volumes",
|
||||||
|
"maxsnapshot": "Max. snapshots",
|
||||||
|
"maxtemplate": "Max. templates",
|
||||||
|
"maxproject": "Max. projects",
|
||||||
|
"maxnetwork": "Max. networks",
|
||||||
|
"maxvpc": "Max. VPCs",
|
||||||
|
"maxcpu": "Max. CPU cores",
|
||||||
|
"maxmemory": "Max. memory (MiB)",
|
||||||
|
"maxprimary_storage": "Max. primary (GiB)",
|
||||||
|
"maxsecondary_storage": "Max. secondary (GiB)",
|
||||||
"maxCPUNumber": "Max CPU Cores",
|
"maxCPUNumber": "Max CPU Cores",
|
||||||
"maxInstance": "Max Instances",
|
"maxInstance": "Max Instances",
|
||||||
"maxIops": "Max IOPS",
|
"maxIops": "Max IOPS",
|
||||||
@ -770,10 +787,12 @@
|
|||||||
"reservedSystemNetmask": "Reserved system netmask",
|
"reservedSystemNetmask": "Reserved system netmask",
|
||||||
"reservedSystemStartIp": "Start Reserved system IP",
|
"reservedSystemStartIp": "Start Reserved system IP",
|
||||||
"reservediprange": "Reserved IP Range",
|
"reservediprange": "Reserved IP Range",
|
||||||
|
"resources": "Resources",
|
||||||
"resourceid": "Resource ID",
|
"resourceid": "Resource ID",
|
||||||
"resourcename": "Resource Name",
|
"resourcename": "Resource Name",
|
||||||
"resourcestate": "Resource state",
|
"resourcestate": "Resource state",
|
||||||
"restartrequired": "Restart required",
|
"restartrequired": "Restart required",
|
||||||
|
"revokeinvitationconfirm": "Please confirm that you would like to revoke this invitation?",
|
||||||
"role": "Role",
|
"role": "Role",
|
||||||
"rolename": "Role",
|
"rolename": "Role",
|
||||||
"roletype": "Role Type",
|
"roletype": "Role Type",
|
||||||
|
|||||||
@ -25,6 +25,7 @@ const getters = {
|
|||||||
nickname: state => state.user.name,
|
nickname: state => state.user.name,
|
||||||
welcome: state => state.user.welcome,
|
welcome: state => state.user.welcome,
|
||||||
apis: state => state.user.apis,
|
apis: state => state.user.apis,
|
||||||
|
features: state => state.user.features,
|
||||||
userInfo: state => state.user.info,
|
userInfo: state => state.user.info,
|
||||||
addRouters: state => state.permission.addRouters,
|
addRouters: state => state.permission.addRouters,
|
||||||
multiTab: state => state.app.multiTab,
|
multiTab: state => state.app.multiTab,
|
||||||
|
|||||||
@ -29,6 +29,7 @@ const user = {
|
|||||||
avatar: '',
|
avatar: '',
|
||||||
info: {},
|
info: {},
|
||||||
apis: {},
|
apis: {},
|
||||||
|
features: {},
|
||||||
project: {},
|
project: {},
|
||||||
asyncJobIds: []
|
asyncJobIds: []
|
||||||
},
|
},
|
||||||
@ -54,6 +55,9 @@ const user = {
|
|||||||
SET_APIS: (state, apis) => {
|
SET_APIS: (state, apis) => {
|
||||||
state.apis = apis
|
state.apis = apis
|
||||||
},
|
},
|
||||||
|
SET_FEATURES: (state, features) => {
|
||||||
|
state.features = features
|
||||||
|
},
|
||||||
SET_ASYNC_JOB_IDS: (state, jobsJsonArray) => {
|
SET_ASYNC_JOB_IDS: (state, jobsJsonArray) => {
|
||||||
Vue.ls.set(ASYNC_JOB_IDS, jobsJsonArray)
|
Vue.ls.set(ASYNC_JOB_IDS, jobsJsonArray)
|
||||||
state.asyncJobIds = jobsJsonArray
|
state.asyncJobIds = jobsJsonArray
|
||||||
@ -86,7 +90,6 @@ const user = {
|
|||||||
|
|
||||||
GetInfo ({ commit }) {
|
GetInfo ({ commit }) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Discover allowed APIs
|
|
||||||
api('listApis').then(response => {
|
api('listApis').then(response => {
|
||||||
const apis = {}
|
const apis = {}
|
||||||
const apiList = response.listapisresponse.api
|
const apiList = response.listapisresponse.api
|
||||||
@ -104,7 +107,6 @@ const user = {
|
|||||||
reject(error)
|
reject(error)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Find user info
|
|
||||||
api('listUsers').then(response => {
|
api('listUsers').then(response => {
|
||||||
const result = response.listusersresponse.user[0]
|
const result = response.listusersresponse.user[0]
|
||||||
commit('SET_INFO', result)
|
commit('SET_INFO', result)
|
||||||
@ -117,6 +119,13 @@ const user = {
|
|||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
reject(error)
|
reject(error)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
api('listCapabilities').then(response => {
|
||||||
|
const result = response.listcapabilitiesresponse.capability
|
||||||
|
commit('SET_FEATURES', result)
|
||||||
|
}).catch(error => {
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
Logout ({ commit, state }) {
|
Logout ({ commit, state }) {
|
||||||
|
|||||||
@ -19,46 +19,36 @@
|
|||||||
<div>
|
<div>
|
||||||
<a-card class="breadcrumb-card">
|
<a-card class="breadcrumb-card">
|
||||||
<a-row>
|
<a-row>
|
||||||
<a-col :span="24" style="display: flex">
|
<a-col :span="14" style="padding-left: 6px">
|
||||||
<breadcrumb />
|
<breadcrumb>
|
||||||
<a-tooltip placement="bottom">
|
<a-tooltip placement="bottom" slot="end">
|
||||||
<template slot="title">
|
|
||||||
{{ "Refresh" }}
|
|
||||||
</template>
|
|
||||||
<a-button
|
|
||||||
style="margin-left: 8px"
|
|
||||||
:loading="loading"
|
|
||||||
shape="round"
|
|
||||||
size="small"
|
|
||||||
icon="sync"
|
|
||||||
@click="fetchData()">
|
|
||||||
{{ $t('refresh') }}
|
|
||||||
</a-button>
|
|
||||||
</a-tooltip>
|
|
||||||
</a-col>
|
|
||||||
<a-col :span="24" style="padding-top: 12px">
|
|
||||||
<span>
|
|
||||||
<a-tooltip
|
|
||||||
v-for="(action, actionIndex) in actions"
|
|
||||||
:key="actionIndex"
|
|
||||||
placement="bottom">
|
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
{{ $t(action.label) }}
|
{{ "Refresh" }}
|
||||||
</template>
|
</template>
|
||||||
<a-button
|
<a-button
|
||||||
v-if="action.api in $store.getters.apis &&
|
style="margin-top: 4px"
|
||||||
((!dataView && (action.listView || action.groupAction && selectedRowKeys.length > 0)) || (dataView && action.dataView)) &&
|
:loading="loading"
|
||||||
('show' in action ? action.show(resource) : true)"
|
|
||||||
:icon="action.icon"
|
|
||||||
:type="action.icon === 'delete' ? 'danger' : (action.icon === 'plus' ? 'primary' : 'default')"
|
|
||||||
shape="circle"
|
shape="circle"
|
||||||
style="margin-right: 5px"
|
size="small"
|
||||||
@click="execAction(action)"
|
type="dashed"
|
||||||
>
|
icon="reload"
|
||||||
|
@click="fetchData()">
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
|
</breadcrumb>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="10">
|
||||||
|
<span style="float: right">
|
||||||
|
<action-button
|
||||||
|
style="margin-bottom: 5px"
|
||||||
|
:loading="loading"
|
||||||
|
:actions="actions"
|
||||||
|
:selectedRowKeys="selectedRowKeys"
|
||||||
|
:dataView="dataView"
|
||||||
|
:resource="resource"
|
||||||
|
@exec-action="execAction"/>
|
||||||
<a-input-search
|
<a-input-search
|
||||||
style="width: 50%; padding-left: 6px"
|
style="width: 25vw; margin-left: 10px"
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
v-if="!dataView && !treeView"
|
v-if="!dataView && !treeView"
|
||||||
@ -81,7 +71,14 @@
|
|||||||
centered
|
centered
|
||||||
width="auto"
|
width="auto"
|
||||||
>
|
>
|
||||||
<component :is="currentAction.component" :resource="resource" :loading="loading" v-bind="{currentAction}" />
|
<component
|
||||||
|
:is="currentAction.component"
|
||||||
|
:resource="resource"
|
||||||
|
:loading="loading"
|
||||||
|
v-bind="{currentAction}"
|
||||||
|
@refresh-data="fetchData"
|
||||||
|
@poll-action="pollActionCompletion"
|
||||||
|
@close-action="closeAction"/>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
<a-modal
|
<a-modal
|
||||||
@ -137,13 +134,18 @@
|
|||||||
</a-select-option>
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="field.type==='uuid' || field.name==='account' || field.name==='keypair'">
|
<span v-else-if="field.type==='uuid' || (field.name==='account' && !['addAccountToProject'].includes(currentAction.api)) || field.name==='keypair'">
|
||||||
<a-select
|
<a-select
|
||||||
:loading="field.loading"
|
showSearch
|
||||||
|
optionFilterProp="children"
|
||||||
v-decorator="[field.name, {
|
v-decorator="[field.name, {
|
||||||
rules: [{ required: field.required, message: 'Please select option' }]
|
rules: [{ required: field.required, message: 'Please select option' }]
|
||||||
}]"
|
}]"
|
||||||
|
:loading="field.loading"
|
||||||
:placeholder="field.description"
|
:placeholder="field.description"
|
||||||
|
:filterOption="(input, option) => {
|
||||||
|
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<a-select-option v-for="(opt, optIndex) in field.opts" :key="optIndex">
|
<a-select-option v-for="(opt, optIndex) in field.opts" :key="optIndex">
|
||||||
{{ opt.name || opt.description }}
|
{{ opt.name || opt.description }}
|
||||||
@ -253,6 +255,7 @@ import Status from '@/components/widgets/Status'
|
|||||||
import ListView from '@/components/view/ListView'
|
import ListView from '@/components/view/ListView'
|
||||||
import ResourceView from '@/components/view/ResourceView'
|
import ResourceView from '@/components/view/ResourceView'
|
||||||
import TreeView from '@/components/view/TreeView'
|
import TreeView from '@/components/view/TreeView'
|
||||||
|
import ActionButton from '@/components/view/ActionButton'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Resource',
|
name: 'Resource',
|
||||||
@ -262,7 +265,8 @@ export default {
|
|||||||
ResourceView,
|
ResourceView,
|
||||||
ListView,
|
ListView,
|
||||||
TreeView,
|
TreeView,
|
||||||
Status
|
Status,
|
||||||
|
ActionButton
|
||||||
},
|
},
|
||||||
mixins: [mixinDevice],
|
mixins: [mixinDevice],
|
||||||
provide: function () {
|
provide: function () {
|
||||||
@ -360,6 +364,7 @@ export default {
|
|||||||
if (this.$route.meta.columns) {
|
if (this.$route.meta.columns) {
|
||||||
this.columnKeys = this.$route.meta.columns
|
this.columnKeys = this.$route.meta.columns
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.$route.meta.actions) {
|
if (this.$route.meta.actions) {
|
||||||
this.actions = this.$route.meta.actions
|
this.actions = this.$route.meta.actions
|
||||||
}
|
}
|
||||||
@ -625,7 +630,11 @@ export default {
|
|||||||
} else if (param.type === 'list') {
|
} else if (param.type === 'list') {
|
||||||
params[key] = input.map(e => { return param.opts[e].id }).reduce((str, name) => { return str + ',' + name })
|
params[key] = input.map(e => { return param.opts[e].id }).reduce((str, name) => { return str + ',' + name })
|
||||||
} else if (param.name === 'account' || param.name === 'keypair') {
|
} else if (param.name === 'account' || param.name === 'keypair') {
|
||||||
params[key] = param.opts[input].name
|
if (['addAccountToProject'].includes(this.currentAction.api)) {
|
||||||
|
params[key] = input
|
||||||
|
} else {
|
||||||
|
params[key] = param.opts[input].name
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
params[key] = input
|
params[key] = input
|
||||||
}
|
}
|
||||||
@ -673,7 +682,7 @@ export default {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.currentAction.icon === 'delete') {
|
if (this.currentAction.icon === 'delete' && this.dataView) {
|
||||||
this.$router.go(-1)
|
this.$router.go(-1)
|
||||||
} else {
|
} else {
|
||||||
if (!hasJobId) {
|
if (!hasJobId) {
|
||||||
|
|||||||
@ -175,8 +175,7 @@ export default {
|
|||||||
},
|
},
|
||||||
loginSuccess (res) {
|
loginSuccess (res) {
|
||||||
this.$router.push({ name: 'dashboard' })
|
this.$router.push({ name: 'dashboard' })
|
||||||
this.$message.success('Login Successful')
|
this.$message.loading('Login Successful. Discoverying Features...', 5)
|
||||||
this.$message.loading('Discoverying Features', 4)
|
|
||||||
},
|
},
|
||||||
requestFailed (err) {
|
requestFailed (err) {
|
||||||
if (err && err.response && err.response.data && err.response.data.loginresponse) {
|
if (err && err.response && err.response.data && err.response.data.loginresponse) {
|
||||||
|
|||||||
@ -665,8 +665,13 @@ export default {
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss">
|
<style scoped>
|
||||||
.wide-modal {
|
.wide-modal {
|
||||||
min-width: 50vw;
|
min-width: 50vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/deep/ .ant-list-item {
|
||||||
|
padding-top: 12px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
<capacity-dashboard/>
|
<capacity-dashboard/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<usage-dashboard/>
|
<usage-dashboard :resource="$store.getters.project" :showProject="project" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -17,24 +17,43 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a-row class="usage-dashboard" :gutter="12">
|
<a-row class="usage-dashboard" :gutter="12">
|
||||||
<a-col
|
<a-col :xl="16">
|
||||||
:xl="16">
|
<a-row>
|
||||||
<a-row :gutter="12">
|
<a-card>
|
||||||
<a-col
|
<a-tabs
|
||||||
class="usage-dashboard-chart-tile"
|
v-if="showProject"
|
||||||
:xs="12"
|
:animated="false"
|
||||||
:md="8"
|
@change="onTabChange">
|
||||||
v-for="stat in stats"
|
<a-tab-pane
|
||||||
:key="stat.type">
|
v-for="tab in $route.meta.tabs"
|
||||||
<chart-card class="usage-dashboard-chart-card" :loading="loading">
|
:tab="$t(tab.name)"
|
||||||
<router-link :to="{ name: stat.path }">
|
:key="tab.name"
|
||||||
<div class="usage-dashboard-chart-card-inner">
|
v-if="'show' in tab ? tab.show(project, $route, $store.getters.userInfo) : true">
|
||||||
<h4>{{ stat.name }}</h4>
|
<component
|
||||||
<h1>{{ stat.count == undefined ? 0 : stat.count }}</h1>
|
:is="tab.component"
|
||||||
</div>
|
:resource="project"
|
||||||
</router-link>
|
:loading="loading"
|
||||||
</chart-card>
|
:bordered="false"
|
||||||
</a-col>
|
:stats="stats" />
|
||||||
|
</a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
<a-col
|
||||||
|
v-else
|
||||||
|
class="usage-dashboard-chart-tile"
|
||||||
|
:xs="12"
|
||||||
|
:md="8"
|
||||||
|
v-for="stat in stats"
|
||||||
|
:key="stat.type">
|
||||||
|
<chart-card class="usage-dashboard-chart-card" :loading="loading">
|
||||||
|
<router-link :to="{ name: stat.path }">
|
||||||
|
<div class="usage-dashboard-chart-card-inner">
|
||||||
|
<h4>{{ stat.name }}</h4>
|
||||||
|
<h1>{{ stat.count == undefined ? 0 : stat.count }}</h1>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
</chart-card>
|
||||||
|
</a-col>
|
||||||
|
</a-card>
|
||||||
</a-row>
|
</a-row>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col
|
<a-col
|
||||||
@ -64,22 +83,44 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { api } from '@/api'
|
import { api } from '@/api'
|
||||||
|
import store from '@/store'
|
||||||
|
|
||||||
import ChartCard from '@/components/widgets/ChartCard'
|
import ChartCard from '@/components/widgets/ChartCard'
|
||||||
|
import UsageDashboardChart from '@/views/dashboard/UsageDashboardChart'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'UsageDashboard',
|
name: 'UsageDashboard',
|
||||||
components: {
|
components: {
|
||||||
ChartCard
|
ChartCard,
|
||||||
|
UsageDashboardChart
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
resource: {
|
||||||
|
type: Object,
|
||||||
|
default () {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showProject: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
|
showAction: false,
|
||||||
|
showAddAccount: false,
|
||||||
events: [],
|
events: [],
|
||||||
stats: []
|
stats: [],
|
||||||
|
project: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
beforeCreate () {
|
||||||
|
this.form = this.$form.createForm(this)
|
||||||
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
this.project = store.getters.project
|
||||||
this.fetchData()
|
this.fetchData()
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@ -87,6 +128,9 @@ export default {
|
|||||||
if (to.name === 'dashboard') {
|
if (to.name === 'dashboard') {
|
||||||
this.fetchData()
|
this.fetchData()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
resource (newData, oldData) {
|
||||||
|
this.project = newData
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -159,6 +203,13 @@ export default {
|
|||||||
return 'green'
|
return 'green'
|
||||||
}
|
}
|
||||||
return 'blue'
|
return 'blue'
|
||||||
|
},
|
||||||
|
onTabChange (key) {
|
||||||
|
this.showAddAccount = false
|
||||||
|
|
||||||
|
if (key !== 'Dashboard') {
|
||||||
|
this.showAddAccount = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
ui/src/views/dashboard/UsageDashboardChart.vue
Normal file
59
ui/src/views/dashboard/UsageDashboardChart.vue
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
// Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
// or more contributor license agreements. See the NOTICE file
|
||||||
|
// distributed with this work for additional information
|
||||||
|
// regarding copyright ownership. The ASF licenses this file
|
||||||
|
// to you under the Apache License, Version 2.0 (the
|
||||||
|
// "License"); you may not use this file except in compliance
|
||||||
|
// with the License. You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing,
|
||||||
|
// software distributed under the License is distributed on an
|
||||||
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
// KIND, either express or implied. See the License for the
|
||||||
|
// specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<a-col
|
||||||
|
class="usage-dashboard-chart-tile"
|
||||||
|
:xs="12"
|
||||||
|
:md="8"
|
||||||
|
v-for="stat in stats"
|
||||||
|
:key="stat.type">
|
||||||
|
<chart-card class="usage-dashboard-chart-card" :loading="loading">
|
||||||
|
<router-link :to="{ name: stat.path }">
|
||||||
|
<div class="usage-dashboard-chart-card-inner">
|
||||||
|
<h4>{{ stat.name }}</h4>
|
||||||
|
<h1>{{ stat.count == undefined ? 0 : stat.count }}</h1>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
</chart-card>
|
||||||
|
</a-col>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ChartCard from '@/components/widgets/ChartCard'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'UsageDashboardChart',
|
||||||
|
components: {
|
||||||
|
ChartCard
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
stats: {
|
||||||
|
type: Array,
|
||||||
|
default () {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -16,11 +16,11 @@
|
|||||||
// under the License.
|
// under the License.
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a-row :gutter="24">
|
<a-row :gutter="12">
|
||||||
<a-col :md="24">
|
<a-col :md="24">
|
||||||
<a-card class="breadcrumb-card">
|
<a-card class="breadcrumb-card">
|
||||||
<a-col :md="24" style="display: flex">
|
<a-col :md="24" style="display: flex">
|
||||||
<breadcrumb style="padding-top: 6px" />
|
<breadcrumb style="padding-top: 6px; padding-left: 8px" />
|
||||||
<a-button
|
<a-button
|
||||||
style="margin-left: 12px; margin-top: 4px"
|
style="margin-left: 12px; margin-top: 4px"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@ -141,7 +141,7 @@
|
|||||||
</a-col>
|
</a-col>
|
||||||
<a-col
|
<a-col
|
||||||
:md="6"
|
:md="6"
|
||||||
:style="{ marginBottom: '12px', marginTop: '12px' }"
|
style="margin-bottom: 12px"
|
||||||
v-for="(section, index) in sections"
|
v-for="(section, index) in sections"
|
||||||
v-if="routes[section]"
|
v-if="routes[section]"
|
||||||
:key="index">
|
:key="index">
|
||||||
|
|||||||
267
ui/src/views/project/AccountsTab.vue
Normal file
267
ui/src/views/project/AccountsTab.vue
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
// Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
// or more contributor license agreements. See the NOTICE file
|
||||||
|
// distributed with this work for additional information
|
||||||
|
// regarding copyright ownership. The ASF licenses this file
|
||||||
|
// to you under the Apache License, Version 2.0 (the
|
||||||
|
// "License"); you may not use this file except in compliance
|
||||||
|
// with the License. You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing,
|
||||||
|
// software distributed under the License is distributed on an
|
||||||
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
// KIND, either express or implied. See the License for the
|
||||||
|
// specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<a-row :gutter="12">
|
||||||
|
<a-col :md="24" :lg="24">
|
||||||
|
<a-table
|
||||||
|
size="small"
|
||||||
|
:loading="loading"
|
||||||
|
:columns="columns"
|
||||||
|
:dataSource="dataSource"
|
||||||
|
:pagination="false"
|
||||||
|
:rowKey="record => record.accountid || record.account"
|
||||||
|
>
|
||||||
|
<span slot="action" v-if="record.role!==owner" slot-scope="text, record" class="account-button-action">
|
||||||
|
<a-tooltip placement="top">
|
||||||
|
<template slot="title">
|
||||||
|
{{ $t('label.make.project.owner') }}
|
||||||
|
</template>
|
||||||
|
<a-button type="default" shape="circle" icon="user" size="small" @click="onMakeProjectOwner(record)" />
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip placement="top">
|
||||||
|
<template slot="title">
|
||||||
|
{{ $t('label.remove.project.account') }}
|
||||||
|
</template>
|
||||||
|
<a-button
|
||||||
|
type="danger"
|
||||||
|
shape="circle"
|
||||||
|
icon="delete"
|
||||||
|
size="small"
|
||||||
|
@click="onShowConfirmDelete(record)"/>
|
||||||
|
</a-tooltip>
|
||||||
|
</span>
|
||||||
|
</a-table>
|
||||||
|
<a-pagination
|
||||||
|
class="row-element"
|
||||||
|
size="small"
|
||||||
|
:current="page"
|
||||||
|
:pageSize="pageSize"
|
||||||
|
:total="itemCount"
|
||||||
|
:showTotal="total => `Total ${total} items`"
|
||||||
|
:pageSizeOptions="['10', '20', '40', '80', '100']"
|
||||||
|
@change="changePage"
|
||||||
|
@showSizeChange="changePageSize"
|
||||||
|
showSizeChanger/>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { api } from '@/api'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AccountsTab',
|
||||||
|
props: {
|
||||||
|
resource: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
columns: [],
|
||||||
|
dataSource: [],
|
||||||
|
loading: false,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
itemCount: 0,
|
||||||
|
owner: 'Admin'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.columns = [
|
||||||
|
{
|
||||||
|
title: this.$t('account'),
|
||||||
|
dataIndex: 'account',
|
||||||
|
width: '35%',
|
||||||
|
scopedSlots: { customRender: 'account' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: this.$t('role'),
|
||||||
|
dataIndex: 'role',
|
||||||
|
scopedSlots: { customRender: 'role' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: this.$t('action'),
|
||||||
|
dataIndex: 'action',
|
||||||
|
fixed: 'right',
|
||||||
|
width: 100,
|
||||||
|
scopedSlots: { customRender: 'action' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
this.page = 1
|
||||||
|
this.pageSize = 10
|
||||||
|
this.itemCount = 0
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
resource (newItem, oldItem) {
|
||||||
|
if (!newItem || !newItem.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.resource = newItem
|
||||||
|
this.fetchData()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchData () {
|
||||||
|
const params = {}
|
||||||
|
params.projectId = this.resource.id
|
||||||
|
params.page = this.page
|
||||||
|
params.pageSize = this.pageSize
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
api('listProjectAccounts', params).then(json => {
|
||||||
|
const listProjectAccount = json.listprojectaccountsresponse.projectaccount
|
||||||
|
const itemCount = json.listprojectaccountsresponse.count
|
||||||
|
|
||||||
|
if (!listProjectAccount || listProjectAccount.length === 0) {
|
||||||
|
this.dataSource = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.itemCount = itemCount
|
||||||
|
this.dataSource = listProjectAccount
|
||||||
|
}).catch(error => {
|
||||||
|
this.$notification.error({
|
||||||
|
message: 'Request Failed',
|
||||||
|
description: error.response.headers['x-description']
|
||||||
|
})
|
||||||
|
}).finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
changePage (page, pageSize) {
|
||||||
|
this.page = page
|
||||||
|
this.pageSize = pageSize
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
changePageSize (currentPage, pageSize) {
|
||||||
|
this.page = currentPage
|
||||||
|
this.pageSize = pageSize
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
onMakeProjectOwner (record) {
|
||||||
|
const title = this.$t('label.make.project.owner')
|
||||||
|
const loading = this.$message.loading(title + 'in progress for ' + record.account, 0)
|
||||||
|
const params = {}
|
||||||
|
|
||||||
|
params.id = this.resource.id
|
||||||
|
params.account = record.account
|
||||||
|
|
||||||
|
api('updateProject', params).then(json => {
|
||||||
|
const hasJobId = this.checkForAddAsyncJob(json, title, record.account)
|
||||||
|
|
||||||
|
if (hasJobId) {
|
||||||
|
this.fetchData()
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
// show error
|
||||||
|
this.$notification.error({
|
||||||
|
message: 'Request Failed',
|
||||||
|
description: error.response.headers['x-description']
|
||||||
|
})
|
||||||
|
}).finally(() => {
|
||||||
|
setTimeout(loading, 1000)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onShowConfirmDelete (record) {
|
||||||
|
const self = this
|
||||||
|
let title = this.$t('deleteconfirm')
|
||||||
|
title = title.replace('{name}', this.$t('account'))
|
||||||
|
|
||||||
|
this.$confirm({
|
||||||
|
title: title,
|
||||||
|
okText: 'OK',
|
||||||
|
okType: 'danger',
|
||||||
|
cancelText: 'Cancel',
|
||||||
|
onOk () {
|
||||||
|
self.removeAccount(record)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
removeAccount (record) {
|
||||||
|
const title = this.$t('label.remove.project.account')
|
||||||
|
const loading = this.$message.loading(title + 'in progress for ' + record.account, 0)
|
||||||
|
const params = {}
|
||||||
|
|
||||||
|
params.account = record.account
|
||||||
|
params.projectid = this.resource.id
|
||||||
|
|
||||||
|
api('deleteAccountFromProject', params).then(json => {
|
||||||
|
const hasJobId = this.checkForAddAsyncJob(json, title, record.account)
|
||||||
|
|
||||||
|
if (hasJobId) {
|
||||||
|
this.fetchData()
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
// show error
|
||||||
|
this.$notification.error({
|
||||||
|
message: 'Request Failed',
|
||||||
|
description: error.response.headers['x-description']
|
||||||
|
})
|
||||||
|
}).finally(() => {
|
||||||
|
setTimeout(loading, 1000)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
checkForAddAsyncJob (json, title, description) {
|
||||||
|
let hasJobId = false
|
||||||
|
|
||||||
|
for (const obj in json) {
|
||||||
|
if (obj.includes('response')) {
|
||||||
|
for (const res in json[obj]) {
|
||||||
|
if (res === 'jobid') {
|
||||||
|
hasJobId = true
|
||||||
|
const jobId = json[obj][res]
|
||||||
|
this.$store.dispatch('AddAsyncJob', {
|
||||||
|
title: title,
|
||||||
|
jobid: jobId,
|
||||||
|
description: description,
|
||||||
|
status: 'progress'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasJobId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/deep/.ant-table-fixed-right {
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-element {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-button-action button {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
132
ui/src/views/project/InvitationTokenTemplate.vue
Normal file
132
ui/src/views/project/InvitationTokenTemplate.vue
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
// Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
// or more contributor license agreements. See the NOTICE file
|
||||||
|
// distributed with this work for additional information
|
||||||
|
// regarding copyright ownership. The ASF licenses this file
|
||||||
|
// to you under the Apache License, Version 2.0 (the
|
||||||
|
// "License"); you may not use this file except in compliance
|
||||||
|
// with the License. You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing,
|
||||||
|
// software distributed under the License is distributed on an
|
||||||
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
// KIND, either express or implied. See the License for the
|
||||||
|
// specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="row-project-invitation">
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<a-form
|
||||||
|
:form="form"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
layout="vertical">
|
||||||
|
<a-form-item :label="$t('projectid')">
|
||||||
|
<a-input
|
||||||
|
v-decorator="['projectid', {
|
||||||
|
rules: [{ required: true, message: 'Please enter input' }]
|
||||||
|
}]"
|
||||||
|
:placeholder="$t('project.projectid.description')"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item :label="$t('token')">
|
||||||
|
<a-input
|
||||||
|
v-decorator="['token', {
|
||||||
|
rules: [{ required: true, message: 'Please enter input' }]
|
||||||
|
}]"
|
||||||
|
:placeholder="$t('project.token.description')"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<div class="card-footer">
|
||||||
|
<!-- ToDo extract as component -->
|
||||||
|
<a-button @click="() => this.$router.back()">{{ this.$t('cancel') }}</a-button>
|
||||||
|
<a-button :loading="loading" type="primary" @click="handleSubmit">{{ this.$t('OK') }}</a-button>
|
||||||
|
</div>
|
||||||
|
</a-form>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { api } from '@/api'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'InvitationTokenTemplate',
|
||||||
|
beforeCreate () {
|
||||||
|
this.form = this.$form.createForm(this)
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
loading: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleSubmit (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.form.validateFields((err, values) => {
|
||||||
|
if (err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = this.$t('label.accept.project.invitation')
|
||||||
|
const description = this.$t('projectid') + ' ' + values.projectid
|
||||||
|
const loading = this.$message.loading(title + 'in progress for ' + description, 0)
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
api('updateProjectInvitation', values).then(json => {
|
||||||
|
this.checkForAddAsyncJob(json, title, description)
|
||||||
|
this.$emit('close-action')
|
||||||
|
}).catch(error => {
|
||||||
|
this.$notification.error({
|
||||||
|
message: 'Request Failed',
|
||||||
|
description: error.response.headers['x-description']
|
||||||
|
})
|
||||||
|
}).finally(() => {
|
||||||
|
this.$emit('refresh-data')
|
||||||
|
this.loading = false
|
||||||
|
setTimeout(loading, 1000)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
checkForAddAsyncJob (json, title, description) {
|
||||||
|
let hasJobId = false
|
||||||
|
|
||||||
|
for (const obj in json) {
|
||||||
|
if (obj.includes('response')) {
|
||||||
|
for (const res in json[obj]) {
|
||||||
|
if (res === 'jobid') {
|
||||||
|
hasJobId = true
|
||||||
|
const jobId = json[obj][res]
|
||||||
|
this.$store.dispatch('AddAsyncJob', {
|
||||||
|
title: title,
|
||||||
|
jobid: jobId,
|
||||||
|
description: description,
|
||||||
|
status: 'progress'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasJobId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.row-project-invitation {
|
||||||
|
min-width: 450px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
button + button {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
326
ui/src/views/project/InvitationsTemplate.vue
Normal file
326
ui/src/views/project/InvitationsTemplate.vue
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
// Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
// or more contributor license agreements. See the NOTICE file
|
||||||
|
// distributed with this work for additional information
|
||||||
|
// regarding copyright ownership. The ASF licenses this file
|
||||||
|
// to you under the Apache License, Version 2.0 (the
|
||||||
|
// "License"); you may not use this file except in compliance
|
||||||
|
// with the License. You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing,
|
||||||
|
// software distributed under the License is distributed on an
|
||||||
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
// KIND, either express or implied. See the License for the
|
||||||
|
// specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="row-invitation">
|
||||||
|
<a-row :gutter="12">
|
||||||
|
<a-col :md="24" :lg="24">
|
||||||
|
<a-input-search
|
||||||
|
class="input-search-invitation"
|
||||||
|
style="width: unset"
|
||||||
|
placeholder="Search"
|
||||||
|
v-model="searchQuery"
|
||||||
|
@search="onSearch" />
|
||||||
|
</a-col>
|
||||||
|
<a-col :md="24" :lg="24">
|
||||||
|
<a-table
|
||||||
|
size="small"
|
||||||
|
:loading="loading"
|
||||||
|
:columns="columns"
|
||||||
|
:dataSource="dataSource"
|
||||||
|
:pagination="false"
|
||||||
|
:rowKey="record => record.id || record.account"
|
||||||
|
@change="onChangeTable">
|
||||||
|
<template slot="state" slot-scope="text">
|
||||||
|
<status :text="text ? text : ''" displayText />
|
||||||
|
</template>
|
||||||
|
<span slot="action" v-if="record.state===stateAllow" slot-scope="text, record" class="account-button-action">
|
||||||
|
<a-tooltip placement="top">
|
||||||
|
<template slot="title">
|
||||||
|
{{ $t('label.accept.project.invitation') }}
|
||||||
|
</template>
|
||||||
|
<a-button
|
||||||
|
type="success"
|
||||||
|
shape="circle"
|
||||||
|
icon="check"
|
||||||
|
size="small"
|
||||||
|
@click="onShowConfirmAcceptInvitation(record)"/>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip placement="top">
|
||||||
|
<template slot="title">
|
||||||
|
{{ $t('label.decline.invitation') }}
|
||||||
|
</template>
|
||||||
|
<a-button
|
||||||
|
type="danger"
|
||||||
|
shape="circle"
|
||||||
|
icon="close"
|
||||||
|
size="small"
|
||||||
|
@click="onShowConfirmRevokeInvitation(record)"/>
|
||||||
|
</a-tooltip>
|
||||||
|
</span>
|
||||||
|
</a-table>
|
||||||
|
<a-pagination
|
||||||
|
class="row-element"
|
||||||
|
size="small"
|
||||||
|
:current="page"
|
||||||
|
:pageSize="pageSize"
|
||||||
|
:total="itemCount"
|
||||||
|
:showTotal="total => `Total ${total} items`"
|
||||||
|
:pageSizeOptions="['10', '20', '40', '80', '100']"
|
||||||
|
@change="changePage"
|
||||||
|
@showSizeChange="changePageSize"
|
||||||
|
showSizeChanger/>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { api } from '@/api'
|
||||||
|
import Status from '@/components/widgets/Status'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'InvitationsTemplate',
|
||||||
|
components: {
|
||||||
|
Status
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
columns: [],
|
||||||
|
dataSource: [],
|
||||||
|
listDomains: [],
|
||||||
|
loading: false,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
itemCount: 0,
|
||||||
|
state: undefined,
|
||||||
|
domainid: undefined,
|
||||||
|
projectid: undefined,
|
||||||
|
searchQuery: undefined,
|
||||||
|
stateAllow: 'Pending'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.columns = [
|
||||||
|
{
|
||||||
|
title: this.$t('project'),
|
||||||
|
dataIndex: 'project',
|
||||||
|
scopedSlots: { customRender: 'project' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: this.$t('domain'),
|
||||||
|
dataIndex: 'domain',
|
||||||
|
scopedSlots: { customRender: 'domain' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: this.$t('state'),
|
||||||
|
dataIndex: 'state',
|
||||||
|
width: 130,
|
||||||
|
scopedSlots: { customRender: 'state' },
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
text: this.$t('Pending'),
|
||||||
|
value: 'Pending'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$t('Completed'),
|
||||||
|
value: 'Completed'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$t('Declined'),
|
||||||
|
value: 'Declined'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
filterMultiple: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: this.$t('action'),
|
||||||
|
dataIndex: 'action',
|
||||||
|
width: 80,
|
||||||
|
scopedSlots: { customRender: 'action' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
this.page = 1
|
||||||
|
this.pageSize = 10
|
||||||
|
this.itemCount = 0
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchData () {
|
||||||
|
const params = {}
|
||||||
|
|
||||||
|
params.page = this.page
|
||||||
|
params.pageSize = this.pageSize
|
||||||
|
params.state = this.state
|
||||||
|
params.domainid = this.domainid
|
||||||
|
params.projectid = this.projectid
|
||||||
|
params.keyword = this.searchQuery
|
||||||
|
params.listAll = true
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
this.dataSource = []
|
||||||
|
this.itemCount = 0
|
||||||
|
|
||||||
|
api('listProjectInvitations', params).then(json => {
|
||||||
|
const listProjectInvitations = json.listprojectinvitationsresponse.projectinvitation
|
||||||
|
const itemCount = json.listprojectinvitationsresponse.count
|
||||||
|
|
||||||
|
if (!listProjectInvitations || listProjectInvitations.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dataSource = listProjectInvitations
|
||||||
|
this.itemCount = itemCount
|
||||||
|
}).catch(error => {
|
||||||
|
this.$notification.error({
|
||||||
|
message: 'Request Failed',
|
||||||
|
description: error.response.headers['x-description']
|
||||||
|
})
|
||||||
|
}).finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
changePage (page, pageSize) {
|
||||||
|
this.page = page
|
||||||
|
this.pageSize = pageSize
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
changePageSize (currentPage, pageSize) {
|
||||||
|
this.page = currentPage
|
||||||
|
this.pageSize = pageSize
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
onShowConfirmAcceptInvitation (record) {
|
||||||
|
const self = this
|
||||||
|
const title = this.$t('confirmacceptinvitation')
|
||||||
|
|
||||||
|
this.$confirm({
|
||||||
|
title: title,
|
||||||
|
okText: 'OK',
|
||||||
|
okType: 'danger',
|
||||||
|
cancelText: 'Cancel',
|
||||||
|
onOk () {
|
||||||
|
self.updateProjectInvitation(record, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateProjectInvitation (record, state) {
|
||||||
|
let title = ''
|
||||||
|
|
||||||
|
if (state) {
|
||||||
|
title = this.$t('label.accept.project.invitation')
|
||||||
|
} else {
|
||||||
|
title = this.$t('label.decline.invitation')
|
||||||
|
}
|
||||||
|
|
||||||
|
const loading = this.$message.loading(title + 'in progress for ' + record.project, 0)
|
||||||
|
const params = {}
|
||||||
|
|
||||||
|
params.projectid = record.projectid
|
||||||
|
params.account = record.account
|
||||||
|
params.domainid = record.domainid
|
||||||
|
params.accept = state
|
||||||
|
|
||||||
|
api('updateProjectInvitation', params).then(json => {
|
||||||
|
const hasJobId = this.checkForAddAsyncJob(json, title, record.project)
|
||||||
|
|
||||||
|
if (hasJobId) {
|
||||||
|
this.fetchData()
|
||||||
|
this.$emit('refresh-data')
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
// show error
|
||||||
|
this.$notification.error({
|
||||||
|
message: 'Request Failed',
|
||||||
|
description: error.response.headers['x-description']
|
||||||
|
})
|
||||||
|
}).finally(() => {
|
||||||
|
setTimeout(loading, 1000)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onShowConfirmRevokeInvitation (record) {
|
||||||
|
const self = this
|
||||||
|
const title = this.$t('confirmdeclineinvitation')
|
||||||
|
|
||||||
|
this.$confirm({
|
||||||
|
title: title,
|
||||||
|
okText: 'OK',
|
||||||
|
okType: 'danger',
|
||||||
|
cancelText: 'Cancel',
|
||||||
|
onOk () {
|
||||||
|
self.updateProjectInvitation(record, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onChangeTable (pagination, filters, sorter) {
|
||||||
|
if (!filters || Object.keys(filters).length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = filters.state && filters.state.length > 0 ? filters.state[0] : undefined
|
||||||
|
this.domainid = filters.domain && filters.domain.length > 0 ? filters.domain[0] : undefined
|
||||||
|
this.projectid = filters.project && filters.project.length > 0 ? filters.project[0] : undefined
|
||||||
|
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
onSearch (value) {
|
||||||
|
this.searchQuery = value
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
checkForAddAsyncJob (json, title, description) {
|
||||||
|
let hasJobId = false
|
||||||
|
|
||||||
|
for (const obj in json) {
|
||||||
|
if (obj.includes('response')) {
|
||||||
|
for (const res in json[obj]) {
|
||||||
|
if (res === 'jobid') {
|
||||||
|
hasJobId = true
|
||||||
|
const jobId = json[obj][res]
|
||||||
|
this.$store.dispatch('AddAsyncJob', {
|
||||||
|
title: title,
|
||||||
|
jobid: jobId,
|
||||||
|
description: description,
|
||||||
|
status: 'progress'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasJobId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/deep/.ant-table-fixed-right {
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-invitation {
|
||||||
|
min-width: 500px;
|
||||||
|
max-width: 768px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-element {
|
||||||
|
margin-top: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-button-action button {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-search-invitation {
|
||||||
|
float: right;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
178
ui/src/views/project/ResourcesTab.vue
Normal file
178
ui/src/views/project/ResourcesTab.vue
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
// 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-spin :spinning="loading || formLoading">
|
||||||
|
<a-form
|
||||||
|
:form="form"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
layout="vertical"
|
||||||
|
>
|
||||||
|
<a-form-item
|
||||||
|
v-for="(item, index) in dataResource"
|
||||||
|
v-if="dataSource.includes(item.resourcetypename)"
|
||||||
|
:key="index"
|
||||||
|
:v-bind="item.resourcetypename"
|
||||||
|
:label="$t('max' + item.resourcetypename)">
|
||||||
|
<a-input-number
|
||||||
|
style="width: 100%;"
|
||||||
|
v-decorator="[item.resourcetype, {
|
||||||
|
initialValue: item.max
|
||||||
|
}]"
|
||||||
|
:placeholder="$t('project.' + item.resourcetypename + '.description')"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<div class="card-footer">
|
||||||
|
<!-- ToDo extract as component -->
|
||||||
|
<a-button :loading="formLoading" type="primary" @click="handleSubmit">{{ this.$t('apply') }}</a-button>
|
||||||
|
</div>
|
||||||
|
</a-form>
|
||||||
|
</a-spin>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { api } from '@/api'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ResourceTab',
|
||||||
|
props: {
|
||||||
|
resource: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeCreate () {
|
||||||
|
this.form = this.$form.createForm(this)
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
formLoading: false,
|
||||||
|
dataResource: [],
|
||||||
|
dataSource: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.dataSource = [
|
||||||
|
'network',
|
||||||
|
'volume',
|
||||||
|
'public_ip',
|
||||||
|
'template',
|
||||||
|
'user_vm',
|
||||||
|
'snapshot',
|
||||||
|
'vpc', 'cpu',
|
||||||
|
'memory',
|
||||||
|
'primary_storage',
|
||||||
|
'secondary_storage'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.fetchData()
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
resource (newData, oldData) {
|
||||||
|
if (!newData || !newData.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resource = newData
|
||||||
|
this.fetchData()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchData () {
|
||||||
|
const params = {}
|
||||||
|
params.projectid = this.resource.id
|
||||||
|
|
||||||
|
this.formLoading = true
|
||||||
|
|
||||||
|
api('listResourceLimits', params).then(json => {
|
||||||
|
if (json.listresourcelimitsresponse.resourcelimit) {
|
||||||
|
this.dataResource = json.listresourcelimitsresponse.resourcelimit
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
this.$notification.error({
|
||||||
|
message: 'Request Failed',
|
||||||
|
description: error.response.headers['x-description']
|
||||||
|
})
|
||||||
|
}).finally(() => {
|
||||||
|
this.formLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleSubmit (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.form.validateFields((err, values) => {
|
||||||
|
if (err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrAsync = []
|
||||||
|
const params = {}
|
||||||
|
params.projectid = this.resource.id
|
||||||
|
|
||||||
|
// create parameter from form
|
||||||
|
for (const key in values) {
|
||||||
|
const input = values[key]
|
||||||
|
|
||||||
|
if (input === undefined) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
params.resourcetype = key
|
||||||
|
params.max = input
|
||||||
|
|
||||||
|
arrAsync.push(new Promise((resolve, reject) => {
|
||||||
|
api('updateResourceLimit', params).then(json => {
|
||||||
|
resolve()
|
||||||
|
}).catch(error => {
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
this.formLoading = true
|
||||||
|
|
||||||
|
Promise.all(arrAsync).then(() => {
|
||||||
|
this.$message.success('Apply Successful')
|
||||||
|
this.fetchData()
|
||||||
|
}).catch(error => {
|
||||||
|
this.$notification.error({
|
||||||
|
message: 'Request Failed',
|
||||||
|
description: error.response.headers['x-description']
|
||||||
|
})
|
||||||
|
}).finally(() => {
|
||||||
|
this.formLoading = false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.card-footer {
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
button + button {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
x
Reference in New Issue
Block a user