This is an automated email from the ASF dual-hosted git repository.
rohit pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cloudstack-primate.git
The following commit(s) were added to refs/heads/master by this push:
new f59b027 infra: zone and physical network, ip ranges tabs for traffic
types (#134)
f59b027 is described below
commit f59b027bae6fa6f1eca591c4839ba77421e6eef1
Author: Ritchie Vincent <[email protected]>
AuthorDate: Sat Feb 8 11:45:19 2020 +0000
infra: zone and physical network, ip ranges tabs for traffic types (#134)
Physical network and systemvms tabs for zone.
IP ranges tabs for traffic type management.
Signed-off-by: Rohit Yadav <[email protected]>
Co-authored-by: Rohit Yadav <[email protected]>
---
src/config/section/infra/zones.js | 18 +-
src/locales/en.json | 4 +-
src/views/infra/network/IpRangesTabManagement.vue | 413 ++++++++++++++++++
src/views/infra/network/IpRangesTabPublic.vue | 489 ++++++++++++++++++++++
src/views/infra/network/IpRangesTabStorage.vue | 398 ++++++++++++++++++
src/views/infra/network/NetworkTab.vue | 62 ++-
src/views/infra/zone/PhysicalNetworksTab.vue | 154 +++++++
src/views/infra/zone/SystemVmsTab.vue | 162 +++++++
src/views/infra/{ => zone}/ZoneResources.vue | 0
src/views/infra/{ => zone}/ZoneWizard.vue | 0
10 files changed, 1678 insertions(+), 22 deletions(-)
diff --git a/src/config/section/infra/zones.js
b/src/config/section/infra/zones.js
index 347c654..ff94ca2 100644
--- a/src/config/section/infra/zones.js
+++ b/src/config/section/infra/zones.js
@@ -23,10 +23,6 @@ export default {
columns: ['name', 'state', 'networktype', 'clusters', 'cpuused',
'cpumaxdeviation', 'cpuallocated', 'cputotal', 'memoryused',
'memorymaxdeviation', 'memoryallocated', 'memorytotal', 'order'],
details: ['name', 'id', 'allocationstate', 'networktype',
'guestcidraddress', 'localstorageenabled', 'securitygroupsenabled', 'dns1',
'dns2', 'internaldns1', 'internaldns2'],
related: [{
- name: 'physicalnetwork',
- title: 'Physical Networks',
- param: 'zoneid'
- }, {
name: 'pod',
title: 'Pods',
param: 'zoneid'
@@ -39,10 +35,6 @@ export default {
title: 'Hosts',
param: 'zoneid'
}, {
- name: 'systemvm',
- title: 'SystemVMs',
- param: 'zoneid'
- }, {
name: 'storagepool',
title: 'Primate Storage',
param: 'zoneid'
@@ -55,8 +47,14 @@ export default {
name: 'details',
component: () => import('@/components/view/DetailsTab.vue')
}, {
+ name: 'Physical Networks',
+ component: () => import('@/views/infra/zone/PhysicalNetworksTab.vue')
+ }, {
+ name: 'System VMs',
+ component: () => import('@/views/infra/zone/SystemVmsTab.vue')
+ }, {
name: 'resources',
- component: () => import('@/views/infra/ZoneResources.vue')
+ component: () => import('@/views/infra/zone/ZoneResources.vue')
}, {
name: 'settings',
component: () => import('@/components/view/SettingsTab.vue')
@@ -68,7 +66,7 @@ export default {
label: 'Add Zone',
listView: true,
popup: true,
- component: () => import('@/views/infra/ZoneWizard.vue')
+ component: () => import('@/views/infra/zone/ZoneWizard.vue')
},
{
api: 'updateZone',
diff --git a/src/locales/en.json b/src/locales/en.json
index 73c7bc0..d669089 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -611,6 +611,7 @@
"label.recover.vm": "Recover VM",
"label.refresh.blades": "Refresh Blades",
"label.reinstall.vm": "Reinstall VM",
+"label.release.account": "Release from Account",
"label.release.dedicated.cluster": "Release Dedicated Cluster",
"label.release.dedicated.host": "Release Dedicated Host",
"label.release.dedicated.pod": "Release Dedicated Pod",
@@ -639,6 +640,7 @@
"label.secondary.storage.vm":"Secondary storage VM",
"label.service.offering":"Service Offering",
"label.set.default.NIC": "Set default NIC",
+"label.set.reservation": "Set Reservation",
"label.shutdown.provider": "Shutdown provider",
"label.snapshot.schedule": "Set up Recurring Snapshot",
"label.standard.us.keyboard": "Standard (US) keyboard",
@@ -1011,7 +1013,7 @@
"vmdisplayname": "VM display name",
"vmipaddress": "VM IP Address",
"vmname": "VM Name",
-"vmstate": "VM state",
+"vmstate": "VM State",
"vmtotal": "Total of VMs",
"vmwaredcId": "VMware Datacenter ID",
"vmwaredcName": "VMware Datacenter Name",
diff --git a/src/views/infra/network/IpRangesTabManagement.vue
b/src/views/infra/network/IpRangesTabManagement.vue
new file mode 100644
index 0000000..3fb324a
--- /dev/null
+++ b/src/views/infra/network/IpRangesTabManagement.vue
@@ -0,0 +1,413 @@
+// 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="componentLoading">
+ <a-button
+ type="dashed"
+ icon="plus"
+ style="margin-bottom: 20px; width: 100%"
+ @click="handleOpenAddIpRangeModal">
+ {{ $t('label.add.ip.range') }}
+ </a-button>
+
+ <a-table
+ style="overflow-y: auto"
+ size="small"
+ :columns="columns"
+ :dataSource="items"
+ :rowKey="record => record.id + record.startip"
+ :pagination="false"
+ >
+ <template slot="forsystemvms" slot-scope="text, record">
+ <a-checkbox :checked="record.forsystemvms" />
+ </template>
+ <template slot="actions" slot-scope="record">
+ <div class="actions">
+ <a-popover placement="bottom">
+ <template slot="content">{{ $t('label.remove.ip.range')
}}</template>
+ <a-button
+ icon="delete"
+ shape="round"
+ type="danger"
+ size="small"
+ @click="handleDeleteIpRange(record)"></a-button>
+ </a-popover>
+ </div>
+ </template>
+ </a-table>
+ <a-pagination
+ class="row-element pagination"
+ size="small"
+ style="overflow-y: auto"
+ :current="page"
+ :pageSize="pageSize"
+ :total="items.length"
+ :showTotal="total => `Total ${total} items`"
+ :pageSizeOptions="['10', '20', '40', '80', '100']"
+ @change="changePage"
+ @showSizeChange="changePageSize"
+ showSizeChanger/>
+
+ <a-modal v-model="addIpRangeModal" :title="$t('label.add.ip.range')"
@ok="handleAddIpRange">
+ <a-form
+ :form="form"
+ @submit="handleAddIpRange"
+ layout="vertical"
+ class="form"
+ >
+ <a-form-item :label="$t('podId')" class="form__item">
+ <a-select
+ v-decorator="['pod', {
+ rules: [{ required: true, message: 'Required' }]
+ }]"
+ >
+ <a-select-option v-for="item in items" :key="item.id"
:value="item.id">{{ item.name }}</a-select-option>
+ </a-select>
+ </a-form-item>
+ <a-form-item :label="$t('gateway')" class="form__item">
+ <a-input
+ v-decorator="['gateway', { rules: [{ required: true, message:
'Required' }] }]">
+ </a-input>
+ </a-form-item>
+ <a-form-item :label="$t('netmask')" class="form__item">
+ <a-input
+ v-decorator="['netmask', { rules: [{ required: true, message:
'Required' }] }]">
+ </a-input>
+ </a-form-item>
+ <a-form-item :label="$t('vlan')" class="form__item">
+ <a-input
+ v-decorator="['vlan']">
+ </a-input>
+ </a-form-item>
+ <a-form-item :label="$t('startip')" class="form__item">
+ <a-input
+ v-decorator="['startip', { rules: [{ required: true, message:
'Required' }] }]">
+ </a-input>
+ </a-form-item>
+ <a-form-item :label="$t('endip')" class="form__item">
+ <a-input
+ v-decorator="['endip', { rules: [{ required: true, message:
'Required' }] }]">
+ </a-input>
+ </a-form-item>
+ <a-form-item :label="$t('System VMs')" class="form__item">
+ <a-checkbox v-decorator="['vms']"></a-checkbox>
+ </a-form-item>
+ </a-form>
+ </a-modal>
+
+ </a-spin>
+</template>
+
+<script>
+import { api } from '@/api'
+
+export default {
+ name: 'IpRangesTabManagement',
+ props: {
+ resource: {
+ type: Object,
+ required: true
+ },
+ loading: {
+ type: Boolean,
+ default: false
+ }
+ },
+ data () {
+ return {
+ componentLoading: false,
+ items: [],
+ domains: [],
+ domainsLoading: false,
+ addIpRangeModal: false,
+ defaultSelectedPod: null,
+ page: 1,
+ pageSize: 10,
+ columns: [
+ {
+ title: this.$t('podid'),
+ dataIndex: 'name'
+ },
+ {
+ title: this.$t('gateway'),
+ dataIndex: 'gateway'
+ },
+ {
+ title: this.$t('netmask'),
+ dataIndex: 'netmask'
+ },
+ {
+ title: this.$t('vlan'),
+ dataIndex: 'vlanid',
+ scopedSlots: { customRender: 'vlan' }
+ },
+ {
+ title: this.$t('startip'),
+ dataIndex: 'startip',
+ scopedSlots: { customRender: 'startip' }
+ },
+ {
+ title: this.$t('endip'),
+ dataIndex: 'endip',
+ scopedSlots: { customRender: 'endip' }
+ },
+ {
+ title: this.$t('System VMs'),
+ dataIndex: 'forsystemvms',
+ scopedSlots: { customRender: 'forsystemvms' }
+ },
+ {
+ title: this.$t('action'),
+ scopedSlots: { customRender: 'actions' }
+ }
+ ]
+ }
+ },
+ beforeCreate () {
+ this.form = this.$form.createForm(this)
+ },
+ mounted () {
+ this.fetchData()
+ },
+ watch: {
+ resource (newItem, oldItem) {
+ if (!newItem || !newItem.id) {
+ return
+ }
+ this.fetchData()
+ }
+ },
+ methods: {
+ fetchData () {
+ this.componentLoading = true
+ api('listPods', {
+ zoneid: this.resource.zoneid,
+ page: this.page,
+ pagesize: this.pageSize
+ }).then(response => {
+ this.items = []
+ const pods = response.listpodsresponse.pod ?
response.listpodsresponse.pod : []
+ for (const pod of pods) {
+ if (pod && pod.startip && pod.startip.length > 0) {
+ for (var idx = 0; idx < pod.startip.length; idx++) {
+ this.items.push({
+ id: pod.id,
+ name: pod.name,
+ gateway: pod.gateway,
+ netmask: pod.netmask,
+ vlanid: pod.vlanid[idx],
+ startip: pod.startip[idx],
+ endip: pod.endip[idx],
+ forsystemvms: pod.forsystemvms[idx] === '1'
+ })
+ }
+ }
+ }
+ }).catch(error => {
+ console.log(error)
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.listpodsresponse
+ ? error.response.data.listpodsresponse.errortext :
error.response.data.errorresponse.errortext
+ })
+ }).finally(() => {
+ this.componentLoading = false
+ })
+ },
+ handleOpenAddIpRangeModal () {
+ this.addIpRangeModal = true
+ setTimeout(() => {
+ if (this.items.length > 0) {
+ this.form.setFieldsValue({
+ pod: this.items[0].id
+ })
+ }
+ }, 200)
+ },
+ handleDeleteIpRange (record) {
+ this.componentLoading = true
+ api('deleteManagementNetworkIpRange', {
+ podid: record.id,
+ startip: record.startip,
+ endip: record.endip,
+ vlan: record.vlanid
+ }).then(response => {
+ this.$store.dispatch('AddAsyncJob', {
+ title: `Successfully removed IP Range`,
+ jobid: response.deletemanagementnetworkiprangeresponse.jobid,
+ status: 'progress'
+ })
+ this.$pollJob({
+ jobId: response.deletemanagementnetworkiprangeresponse.jobid,
+ successMethod: () => {
+ this.componentLoading = false
+ this.fetchData()
+ },
+ errorMessage: 'Removing failed',
+ errorMethod: () => {
+ this.componentLoading = false
+ this.fetchData()
+ },
+ loadingMessage: `Removing IP Range...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => {
+ this.componentLoading = false
+ this.fetchData()
+ }
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description:
error.response.data.deletemanagementnetworkiprangeresponse
+ ?
error.response.data.deletemanagementnetworkiprangeresponse.errortext :
error.response.data.errorresponse.errortext
+ })
+ this.componentLoading = false
+ this.fetchData()
+ })
+ },
+ handleAddIpRange (e) {
+ this.form.validateFields((error, values) => {
+ if (error) return
+
+ this.componentLoading = true
+ this.addIpRangeModal = false
+ api('createManagementNetworkIpRange', {
+ podid: values.pod,
+ gateway: values.gateway,
+ netmask: values.netmask,
+ startip: values.startip,
+ endip: values.endip,
+ forsystemvms: values.vms,
+ vlan: values.vlan || null
+ }).then(response => {
+ this.$store.dispatch('AddAsyncJob', {
+ title: `Successfully added IP Range`,
+ jobid: response.createmanagementnetworkiprangeresponse.jobid,
+ status: 'progress'
+ })
+ this.$pollJob({
+ jobId: response.createmanagementnetworkiprangeresponse.jobid,
+ successMethod: () => {
+ this.componentLoading = false
+ this.fetchData()
+ },
+ errorMessage: 'Adding failed',
+ errorMethod: () => {
+ this.componentLoading = false
+ this.fetchData()
+ },
+ loadingMessage: `Adding IP Range...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => {
+ this.componentLoading = false
+ this.fetchData()
+ }
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description:
error.response.data.createmanagementnetworkiprangeresponse
+ ?
error.response.data.createmanagementnetworkiprangeresponse.errortext :
error.response.data.errorresponse.errortext
+ })
+ }).finally(() => {
+ this.componentLoading = false
+ this.fetchData()
+ })
+ })
+ },
+ changePage (page, pageSize) {
+ this.page = page
+ this.pageSize = pageSize
+ this.fetchData()
+ },
+ changePageSize (currentPage, pageSize) {
+ this.page = currentPage
+ this.pageSize = pageSize
+ this.fetchData()
+ }
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+ .list {
+ &__item {
+ display: flex;
+ }
+
+ &__data {
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ &__col {
+ flex-basis: calc((100% / 3) - 20px);
+ margin-right: 20px;
+ margin-bottom: 10px;
+ }
+
+ &__label {
+ }
+ }
+
+ .ant-list-item {
+ padding-top: 0;
+ padding-bottom: 0;
+
+ &:not(:first-child) {
+ padding-top: 20px;
+ }
+
+ &:not(:last-child) {
+ padding-bottom: 20px;
+ }
+ }
+
+ .actions {
+ button {
+ &:not(:last-child) {
+ margin-bottom: 10px;
+ }
+ }
+ }
+
+ .ant-select {
+ width: 100%;
+ }
+
+ .form {
+ .actions {
+ display: flex;
+ justify-content: flex-end;
+
+ button {
+ &:not(:last-child) {
+ margin-right: 10px;
+ }
+ }
+
+ }
+
+ &__item {
+ }
+ }
+
+ .pagination {
+ margin-top: 20px;
+ }
+</style>
diff --git a/src/views/infra/network/IpRangesTabPublic.vue
b/src/views/infra/network/IpRangesTabPublic.vue
new file mode 100644
index 0000000..7fa4f72
--- /dev/null
+++ b/src/views/infra/network/IpRangesTabPublic.vue
@@ -0,0 +1,489 @@
+// 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="componentLoading">
+ <a-button
+ type="dashed"
+ icon="plus"
+ style="margin-bottom: 20px; width: 100%"
+ @click="handleOpenAddIpRangeModal">
+ {{ $t('label.add.ip.range') }}
+ </a-button>
+
+ <a-table
+ size="small"
+ style="overflow-y: auto"
+ :columns="columns"
+ :dataSource="items"
+ :rowKey="record => record.id"
+ :pagination="false"
+ >
+ <template slot="account" slot-scope="record">
+ <a-button @click="() => handleOpenAccountModal(record)">{{
`[${record.domain}] ${record.account}` }}</a-button>
+ </template>
+ <template slot="actions" slot-scope="record">
+ <div class="actions">
+ <a-popover v-if="record.account === 'system'" placement="bottom">
+ <template slot="content">{{ $t('label.add.account') }}</template>
+ <a-button
+ icon="user-add"
+ shape="round"
+ type="primary"
+ @click="() => handleOpenAddAccountModal(record)"></a-button>
+ </a-popover>
+ <a-popover
+ v-else
+ placement="bottom">
+ <template slot="content">{{ $t('label.release.account')
}}</template>
+ <a-button
+ icon="user-delete"
+ shape="round"
+ type="danger"
+ @click="() => handleRemoveAccount(record.id)"></a-button>
+ </a-popover>
+ <a-popover placement="bottom">
+ <template slot="content">{{ $t('label.remove.ip.range')
}}</template>
+ <a-button icon="delete" shape="round" type="danger"
@click="handleDeleteIpRange(record.id)"></a-button>
+ </a-popover>
+ </div>
+ </template>
+ </a-table>
+ <a-pagination
+ class="row-element pagination"
+ size="small"
+ style="overflow-y: auto"
+ :current="page"
+ :pageSize="pageSize"
+ :total="items.length"
+ :showTotal="total => `Total ${total} items`"
+ :pageSizeOptions="['10', '20', '40', '80', '100']"
+ @change="changePage"
+ @showSizeChange="changePageSize"
+ showSizeChanger/>
+
+ <a-modal v-model="accountModal" v-if="selectedItem" @ok="accountModal =
false">
+ <div>
+ <div style="margin-bottom: 10px;">
+ <div class="list__label">{{ $t('account') }}</div>
+ <div>{{ selectedItem.account }}</div>
+ </div>
+ <div style="margin-bottom: 10px;">
+ <div class="list__label">{{ $t('domain') }}</div>
+ <div>{{ selectedItem.domain }}</div>
+ </div>
+ <div style="margin-bottom: 10px;">
+ <div class="list__label">{{ $t('System VMs') }}</div>
+ <div>{{ selectedItem.forsystemvms }}</div>
+ </div>
+ </div>
+ </a-modal>
+
+ <a-modal :zIndex="1001" v-model="addAccountModal"
:title="$t('label.add.account')" @ok="handleAddAccount">
+ <a-spin :spinning="domainsLoading">
+ <div style="margin-bottom: 10px;">
+ <div class="list__label">{{ $t('account') }}:</div>
+ <a-input v-model="addAccount.account"></a-input>
+ </div>
+ <div>
+ <div class="list__label">{{ $t('domain') }}:</div>
+ <a-select v-model="addAccount.domain">
+ <a-select-option
+ v-for="domain in domains"
+ :key="domain.id"
+ :value="domain.id">{{ domain.name }}
+ </a-select-option>
+ </a-select>
+ </div>
+ </a-spin>
+ </a-modal>
+
+ <a-modal v-model="addIpRangeModal" :title="$t('label.add.ip.range')"
@ok="handleAddIpRange">
+ <a-form
+ :form="form"
+ @submit="handleAddIpRange"
+ layout="vertical"
+ class="form"
+ >
+ <a-form-item :label="$t('gateway')" class="form__item">
+ <a-input
+ v-decorator="['gateway', { rules: [{ required: true, message:
'Required' }] }]">
+ </a-input>
+ </a-form-item>
+ <a-form-item :label="$t('netmask')" class="form__item">
+ <a-input
+ v-decorator="['netmask', { rules: [{ required: true, message:
'Required' }] }]">
+ </a-input>
+ </a-form-item>
+ <a-form-item :label="$t('vlan')" class="form__item">
+ <a-input
+ v-decorator="['vlan']">
+ </a-input>
+ </a-form-item>
+ <a-form-item :label="$t('startip')" class="form__item">
+ <a-input
+ v-decorator="['startip', { rules: [{ required: true, message:
'Required' }] }]">
+ </a-input>
+ </a-form-item>
+ <a-form-item :label="$t('endip')" class="form__item">
+ <a-input
+ v-decorator="['endip', { rules: [{ required: true, message:
'Required' }] }]">
+ </a-input>
+ </a-form-item>
+ <div class="form__item">
+ <div style="color: black;">{{ $t('label.set.reservation') }}</div>
+ <a-switch @change="handleShowAccountFields"></a-switch>
+ </div>
+ <div v-if="showAccountFields" style="margin-top: 20px;">
+ <p>(optional) Please specify an account to be associated with this
IP range.</p>
+ <p>System VMs: Enable dedication of public IP range for SSVM and
CPVM, account field disabled. Reservation strictness defined on
'system.vm.public.ip.reservation.mode.strictness'.</p>
+ <a-form-item :label="$t('System VMs')" class="form__item">
+ <a-switch v-decorator="['forsystemvms']"></a-switch>
+ </a-form-item>
+ <a-spin :spinning="domainsLoading">
+ <a-form-item :label="$t('account')" class="form__item">
+ <a-input v-decorator="['account']"></a-input>
+ </a-form-item>
+ <a-form-item :label="$t('domain')" class="form__item">
+ <a-select v-decorator="['domain']">
+ <a-select-option
+ v-for="domain in domains"
+ :key="domain.id"
+ :value="domain.id">{{ domain.name }}
+ </a-select-option>
+ </a-select>
+ </a-form-item>
+ </a-spin>
+ </div>
+ </a-form>
+ </a-modal>
+
+ </a-spin>
+</template>
+
+<script>
+import { api } from '@/api'
+
+export default {
+ name: 'IpRangesTabPublic',
+ props: {
+ resource: {
+ type: Object,
+ required: true
+ },
+ loading: {
+ type: Boolean,
+ default: false
+ },
+ network: {
+ type: Object,
+ required: true
+ }
+ },
+ data () {
+ return {
+ componentLoading: false,
+ items: [],
+ selectedItem: null,
+ accountModal: false,
+ addAccountModal: false,
+ addAccount: {
+ account: null,
+ domain: null
+ },
+ domains: [],
+ domainsLoading: false,
+ addIpRangeModal: false,
+ showAccountFields: false,
+ page: 1,
+ pageSize: 10,
+ columns: [
+ {
+ title: this.$t('gateway'),
+ dataIndex: 'gateway'
+ },
+ {
+ title: this.$t('netmask'),
+ dataIndex: 'netmask'
+ },
+ {
+ title: this.$t('vlan'),
+ dataIndex: 'vlan'
+ },
+ {
+ title: this.$t('startip'),
+ dataIndex: 'startip'
+ },
+ {
+ title: this.$t('endip'),
+ dataIndex: 'endip'
+ },
+ {
+ title: this.$t('account'),
+ scopedSlots: { customRender: 'account' }
+ },
+ {
+ title: this.$t('action'),
+ scopedSlots: { customRender: 'actions' }
+ }
+ ]
+ }
+ },
+ beforeCreate () {
+ this.form = this.$form.createForm(this)
+ },
+ mounted () {
+ this.fetchData()
+ },
+ watch: {
+ network (newItem, oldItem) {
+ if (!newItem || !newItem.id) {
+ return
+ }
+ this.fetchData()
+ }
+ },
+ methods: {
+ fetchData () {
+ this.componentLoading = true
+ api('listVlanIpRanges', {
+ networkid: this.network.id,
+ zoneid: this.resource.zoneid,
+ page: this.page,
+ pagesize: this.pageSize
+ }).then(response => {
+ this.items = response.listvlaniprangesresponse.vlaniprange ?
response.listvlaniprangesresponse.vlaniprange : []
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.listvlaniprangesresponse
+ ? error.response.data.listvlaniprangesresponse.errortext :
error.response.data.errorresponse.errortext
+ })
+ }).finally(() => {
+ this.componentLoading = false
+ })
+ },
+ fetchDomains () {
+ this.domainsLoading = true
+ api('listDomains', {
+ details: 'min',
+ listAll: true
+ }).then(response => {
+ this.domains = response.listdomainsresponse.domain ?
response.listdomainsresponse.domain : []
+ if (this.domains.length > 0) {
+ this.addAccount.domain = this.domains[0].id
+ this.form.setFieldsValue({ domain: this.domains[0].id })
+ }
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.listdomains
+ ? error.response.data.listdomains.errortext :
error.response.data.errorresponse.errortext
+ })
+ }).finally(() => {
+ this.domainsLoading = false
+ })
+ },
+ handleAddAccount () {
+ this.domainsLoading = true
+
+ if (this.addIpRangeModal === true) {
+ this.addAccountModal = false
+ return
+ }
+
+ api('dedicatePublicIpRange', {
+ id: this.selectedItem.id,
+ zoneid: this.selectedItem.zoneid,
+ domainid: this.addAccount.domain,
+ account: this.addAccount.account
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.dedicatepubliciprangeresponse
+ ? error.response.data.dedicatepubliciprangeresponse.errortext :
error.response.data.errorresponse.errortext
+ })
+ }).finally(() => {
+ this.addAccountModal = false
+ this.domainsLoading = false
+ this.fetchData()
+ })
+ },
+ handleRemoveAccount (id) {
+ this.componentLoading = true
+ api('releasePublicIpRange', { id }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.releasepubliciprangeresponse
+ ? error.response.data.releasepubliciprangeresponse.errortext :
error.response.data.errorresponse.errortext
+ })
+ }).finally(() => {
+ this.fetchData()
+ })
+ },
+ handleOpenAccountModal (item) {
+ this.selectedItem = item
+ this.accountModal = true
+ },
+ handleOpenAddAccountModal (item) {
+ if (!this.addIpRangeModal) {
+ this.selectedItem = item
+ }
+ this.addAccountModal = true
+ this.fetchDomains()
+ },
+ handleShowAccountFields () {
+ if (this.showAccountFields === false) {
+ this.showAccountFields = true
+ this.fetchDomains()
+ return
+ }
+ this.showAccountFields = false
+ },
+ handleOpenAddIpRangeModal () {
+ this.addIpRangeModal = true
+ },
+ handleDeleteIpRange (id) {
+ this.componentLoading = true
+ api('deleteVlanIpRange', { id }).then(() => {
+ this.$notification.success({
+ message: 'Removed IP Range'
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.deletevlaniprangeresponse
+ ? error.response.data.deletevlaniprangeresponse.errortext :
error.response.data.errorresponse.errortext
+ })
+ }).finally(() => {
+ this.componentLoading = false
+ this.fetchData()
+ })
+ },
+ handleAddIpRange (e) {
+ this.form.validateFields((error, values) => {
+ if (error) return
+
+ this.componentLoading = true
+ this.addIpRangeModal = false
+ api('createVlanIpRange', {
+ zoneId: this.resource.zoneid,
+ vlan: values.vlan,
+ gateway: values.gateway,
+ netmask: values.netmask,
+ startip: values.startip,
+ endip: values.endip,
+ forsystemvms: values.forsystemvms,
+ account: values.forsystemvms ? null : values.account,
+ domainid: values.forsystemvms ? null : values.domain,
+ forvirtualnetwork: true
+ }).then(() => {
+ this.$notification.success({
+ message: 'Successfully added IP Range'
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.createvlaniprangeresponse
+ ? error.response.data.createvlaniprangeresponse.errortext :
error.response.data.errorresponse.errortext,
+ duration: 0
+ })
+ }).finally(() => {
+ this.componentLoading = false
+ this.fetchData()
+ })
+ })
+ },
+ changePage (page, pageSize) {
+ this.page = page
+ this.pageSize = pageSize
+ this.fetchData()
+ },
+ changePageSize (currentPage, pageSize) {
+ this.page = currentPage
+ this.pageSize = pageSize
+ this.fetchData()
+ }
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+ .list {
+ &__item {
+ display: flex;
+ }
+
+ &__data {
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ &__col {
+ flex-basis: calc((100% / 3) - 20px);
+ margin-right: 20px;
+ margin-bottom: 10px;
+ }
+
+ &__label {
+ font-weight: bold;
+ }
+ }
+
+ .ant-list-item {
+ padding-top: 0;
+ padding-bottom: 0;
+
+ &:not(:first-child) {
+ padding-top: 20px;
+ }
+
+ &:not(:last-child) {
+ padding-bottom: 20px;
+ }
+ }
+
+ .actions {
+ button {
+ &:not(:last-child) {
+ margin-right: 10px;
+ }
+ }
+ }
+
+ .ant-select {
+ width: 100%;
+ }
+
+ .form {
+ .actions {
+ display: flex;
+ justify-content: flex-end;
+
+ button {
+ &:not(:last-child) {
+ margin-right: 10px;
+ }
+ }
+
+ }
+ }
+
+ .pagination {
+ margin-top: 20px;
+ }
+</style>
diff --git a/src/views/infra/network/IpRangesTabStorage.vue
b/src/views/infra/network/IpRangesTabStorage.vue
new file mode 100644
index 0000000..f506e3f
--- /dev/null
+++ b/src/views/infra/network/IpRangesTabStorage.vue
@@ -0,0 +1,398 @@
+// 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="componentLoading">
+ <a-button
+ type="dashed"
+ icon="plus"
+ style="margin-bottom: 20px; width: 100%"
+ @click="handleOpenAddIpRangeModal">
+ {{ $t('label.add.ip.range') }}
+ </a-button>
+
+ <a-table
+ style="overflow-y: auto"
+ size="small"
+ :columns="columns"
+ :dataSource="items"
+ :rowKey="record => record.id"
+ :pagination="false"
+ >
+ <template slot="name" slot-scope="record">
+ <div>{{ returnPodName(record.podid) }}</div>
+ </template>
+ <template slot="actions" slot-scope="record">
+ <a-popover placement="bottom">
+ <template slot="content">{{ $t('label.remove.ip.range') }}</template>
+ <a-button
+ icon="delete"
+ shape="round"
+ type="danger"
+ @click="handleDeleteIpRange(record.id)"></a-button>
+ </a-popover>
+ </template>
+ </a-table>
+ <a-pagination
+ class="row-element pagination"
+ size="small"
+ style="overflow-y: auto"
+ :current="page"
+ :pageSize="pageSize"
+ :total="items.length"
+ :showTotal="total => `Total ${total} items`"
+ :pageSizeOptions="['10', '20', '40', '80', '100']"
+ @change="changePage"
+ @showSizeChange="changePageSize"
+ showSizeChanger/>
+
+ <a-modal v-model="addIpRangeModal" :title="$t('label.add.ip.range')"
@ok="handleAddIpRange">
+ <a-form
+ :form="form"
+ @submit="handleAddIpRange"
+ layout="vertical"
+ class="form"
+ >
+ <a-form-item :label="$t('podId')" class="form__item">
+ <a-select
+ v-decorator="['pod', {
+ rules: [{ required: true, message: 'Required' }]
+ }]"
+ >
+ <a-select-option v-for="pod in pods" :key="pod.id"
:value="pod.id">{{ pod.name }}</a-select-option>
+ </a-select>
+ </a-form-item>
+ <a-form-item :label="$t('gateway')" class="form__item">
+ <a-input
+ v-decorator="['gateway', { rules: [{ required: true, message:
'Required' }] }]">
+ </a-input>
+ </a-form-item>
+ <a-form-item :label="$t('netmask')" class="form__item">
+ <a-input
+ v-decorator="['netmask', { rules: [{ required: true, message:
'Required' }] }]">
+ </a-input>
+ </a-form-item>
+ <a-form-item :label="$t('vlan')" class="form__item">
+ <a-input
+ v-decorator="['vlan']">
+ </a-input>
+ </a-form-item>
+ <a-form-item :label="$t('startip')" class="form__item">
+ <a-input
+ v-decorator="['startip', { rules: [{ required: true, message:
'Required' }] }]">
+ </a-input>
+ </a-form-item>
+ <a-form-item :label="$t('endip')" class="form__item">
+ <a-input
+ v-decorator="['endip', { rules: [{ required: true, message:
'Required' }] }]">
+ </a-input>
+ </a-form-item>
+ </a-form>
+ </a-modal>
+
+ </a-spin>
+</template>
+
+<script>
+import { api } from '@/api'
+
+export default {
+ name: 'IpRangesTabStorage',
+ props: {
+ resource: {
+ type: Object,
+ required: true
+ },
+ loading: {
+ type: Boolean,
+ default: false
+ }
+ },
+ data () {
+ return {
+ componentLoading: false,
+ items: [],
+ pods: [],
+ domains: [],
+ domainsLoading: false,
+ addIpRangeModal: false,
+ defaultSelectedPod: null,
+ columns: [
+ {
+ title: this.$t('podId'),
+ scopedSlots: { customRender: 'name' }
+ },
+ {
+ title: this.$t('gateway'),
+ dataIndex: 'gateway'
+ },
+ {
+ title: this.$t('netmask'),
+ dataIndex: 'netmask'
+ },
+ {
+ title: this.$t('vlan'),
+ dataIndex: 'vlanid'
+ },
+ {
+ title: this.$t('startip'),
+ dataIndex: 'startip'
+ },
+ {
+ title: this.$t('endip'),
+ dataIndex: 'endip'
+ },
+ {
+ title: this.$t('action'),
+ scopedSlots: { customRender: 'actions' }
+ }
+ ],
+ page: 1,
+ pageSize: 10
+ }
+ },
+ beforeCreate () {
+ this.form = this.$form.createForm(this)
+ },
+ mounted () {
+ this.fetchData()
+ },
+ watch: {
+ resource (newItem, oldItem) {
+ if (!newItem || !newItem.id) {
+ return
+ }
+ this.fetchData()
+ }
+ },
+ methods: {
+ fetchData () {
+ this.fetchPods()
+ this.componentLoading = true
+ api('listStorageNetworkIpRange', {
+ zoneid: this.resource.zoneid,
+ page: this.page,
+ pageSize: this.pageSize
+ }).then(response => {
+ this.items =
response.liststoragenetworkiprangeresponse.storagenetworkiprange ?
response.liststoragenetworkiprangeresponse.storagenetworkiprange : []
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.liststoragenetworkiprangeresponse
+ ? error.response.data.liststoragenetworkiprangeresponse.errortext
: error.response.data.errorresponse.errortext
+ })
+ }).finally(() => {
+ this.componentLoading = false
+ })
+ },
+ fetchPods () {
+ this.componentLoading = true
+ api('listPods', {
+ zoneid: this.resource.zoneid
+ }).then(response => {
+ this.pods = response.listpodsresponse.pod ?
response.listpodsresponse.pod : []
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.listpodsresponse
+ ? error.response.data.listpodsresponse.errortext :
error.response.data.errorresponse.errortext
+ })
+ }).finally(() => {
+ this.componentLoading = false
+ })
+ },
+ returnPodName (id) {
+ const match = this.pods.find(i => i.id === id)
+ return match ? match.name : null
+ },
+ handleOpenAddIpRangeModal () {
+ this.addIpRangeModal = true
+ setTimeout(() => {
+ if (this.items.length > 0) {
+ this.form.setFieldsValue({
+ pod: this.pods[0].id
+ })
+ }
+ }, 200)
+ },
+ handleDeleteIpRange (id) {
+ this.componentLoading = true
+ api('deleteStorageNetworkIpRange', { id }).then(response => {
+ this.$store.dispatch('AddAsyncJob', {
+ title: `Successfully removed IP Range`,
+ jobid: response.deletestoragenetworkiprangeresponse.jobid,
+ status: 'progress'
+ })
+ this.$pollJob({
+ jobId: response.deletestoragenetworkiprangeresponse.jobid,
+ successMethod: () => {
+ this.componentLoading = false
+ this.fetchData()
+ },
+ errorMessage: 'Removing failed',
+ errorMethod: () => {
+ this.componentLoading = false
+ this.fetchData()
+ },
+ loadingMessage: `Removing IP Range...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => {
+ this.componentLoading = false
+ this.fetchData()
+ }
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.deletestoragenetworkiprangeresponse
+ ?
error.response.data.deletestoragenetworkiprangeresponse.errortext :
error.response.data.errorresponse.errortext
+ })
+ this.componentLoading = false
+ this.fetchData()
+ })
+ },
+ handleAddIpRange (e) {
+ this.form.validateFields((error, values) => {
+ if (error) return
+
+ this.componentLoading = true
+ this.addIpRangeModal = false
+ api('createStorageNetworkIpRange', {
+ podid: values.pod,
+ zoneid: this.resource.zoneid,
+ gateway: values.gateway,
+ netmask: values.netmask,
+ startip: values.startip,
+ endip: values.endip,
+ vlan: values.vlan || null
+ }).then(response => {
+ this.$store.dispatch('AddAsyncJob', {
+ title: `Successfully added IP Range`,
+ jobid: response.createstoragenetworkiprangeresponse.jobid,
+ status: 'progress'
+ })
+ this.$pollJob({
+ jobId: response.createstoragenetworkiprangeresponse.jobid,
+ successMethod: () => {
+ this.componentLoading = false
+ this.fetchData()
+ },
+ errorMessage: 'Adding failed',
+ errorMethod: () => {
+ this.componentLoading = false
+ this.fetchData()
+ },
+ loadingMessage: `Adding IP Range...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => {
+ this.componentLoading = false
+ this.fetchData()
+ }
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description:
error.response.data.createstoragenetworkiprangeresponse
+ ?
error.response.data.createstoragenetworkiprangeresponse.errortext :
error.response.data.errorresponse.errortext
+ })
+ }).finally(() => {
+ this.componentLoading = false
+ this.fetchData()
+ })
+ })
+ },
+ changePage (page, pageSize) {
+ this.page = page
+ this.pageSize = pageSize
+ this.fetchData()
+ },
+ changePageSize (currentPage, pageSize) {
+ this.page = currentPage
+ this.pageSize = pageSize
+ this.fetchData()
+ }
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+ .list {
+ &__item {
+ display: flex;
+ }
+
+ &__data {
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ &__col {
+ flex-basis: calc((100% / 3) - 20px);
+ margin-right: 20px;
+ margin-bottom: 10px;
+ }
+
+ &__label {
+ }
+ }
+
+ .ant-list-item {
+ padding-top: 0;
+ padding-bottom: 0;
+
+ &:not(:first-child) {
+ padding-top: 20px;
+ }
+
+ &:not(:last-child) {
+ padding-bottom: 20px;
+ }
+ }
+
+ .actions {
+ button {
+ &:not(:last-child) {
+ margin-bottom: 10px;
+ }
+ }
+ }
+
+ .ant-select {
+ width: 100%;
+ }
+
+ .form {
+ .actions {
+ display: flex;
+ justify-content: flex-end;
+
+ button {
+ &:not(:last-child) {
+ margin-right: 10px;
+ }
+ }
+
+ }
+
+ &__item {
+ }
+ }
+
+ .pagination {
+ margin-top: 20px;
+ }
+</style>
diff --git a/src/views/infra/network/NetworkTab.vue
b/src/views/infra/network/NetworkTab.vue
index 7181bfc..bd23c8a 100644
--- a/src/views/infra/network/NetworkTab.vue
+++ b/src/views/infra/network/NetworkTab.vue
@@ -17,18 +17,34 @@
<template>
<a-spin :spinning="fetchLoading">
- <a-tabs :animated="false" defaultActiveKey="0" tabPosition="left">
+ <a-tabs :tabPosition="device === 'mobile' ? 'top' : 'left'"
:animated="false">
<a-tab-pane v-for="(item, index) in traffictypes"
:tab="item.traffictype" :key="index">
- <div>
- <strong>{{ $t('id') }}</strong> {{ item.id }}
- </div>
- <div v-for="(type, idx) in ['kvmnetworklabel', 'vmwarenetworklabel',
'xennetworklabel', 'hypervnetworklabel', 'ovm3networklabel']" :key="idx">
- <strong>{{ $t(type) }}</strong>
- {{ item[type] || 'Use default gateway' }}
+ <div
+ v-for="(type, idx) in ['kvmnetworklabel', 'vmwarenetworklabel',
'xennetworklabel', 'hypervnetworklabel', 'ovm3networklabel']"
+ :key="idx"
+ style="margin-bottom: 10px;">
+ <div><strong>{{ $t(type) }}</strong></div>
+ <div>{{ item[type] || 'Use default gateway' }}</div>
</div>
<div v-if="item.traffictype === 'Public'">
- Insert here form/component to manage public IP ranges
- <IpRangesTab :resource="resource" />
+ <div style="margin-bottom: 10px;">
+ <div><strong>{{ $t('traffictype') }}</strong></div>
+ <div>{{ publicNetwork.traffictype }}</div>
+ </div>
+ <div style="margin-bottom: 10px;">
+ <div><strong>{{ $t('broadcastdomaintype') }}</strong></div>
+ <div>{{ publicNetwork.broadcastdomaintype }}</div>
+ </div>
+ <a-divider />
+ <IpRangesTabPublic :resource="resource" :loading="loading"
:network="publicNetwork" />
+ </div>
+ <div v-if="item.traffictype === 'Management'">
+ <a-divider />
+ <IpRangesTabManagement :resource="resource" :loading="loading" />
+ </div>
+ <div v-if="item.traffictype === 'Storage'">
+ <a-divider />
+ <IpRangesTabStorage :resource="resource" />
</div>
</a-tab-pane>
<a-tab-pane tab="Service Providers" key="nsp">
@@ -45,15 +61,21 @@
<script>
import { api } from '@/api'
+import { mixinDevice } from '@/utils/mixin.js'
import Status from '@/components/widgets/Status'
-import IpRangesTab from './IpRangesTab'
+import IpRangesTabPublic from './IpRangesTabPublic'
+import IpRangesTabManagement from './IpRangesTabManagement'
+import IpRangesTabStorage from './IpRangesTabStorage'
export default {
name: 'NetworkTab',
components: {
- IpRangesTab,
+ IpRangesTabPublic,
+ IpRangesTabManagement,
+ IpRangesTabStorage,
Status
},
+ mixins: [mixinDevice],
props: {
resource: {
type: Object,
@@ -68,6 +90,7 @@ export default {
return {
traffictypes: [],
nsps: [],
+ publicNetwork: {},
fetchLoading: false
}
},
@@ -96,6 +119,23 @@ export default {
})
this.fetchLoading = true
+ api('listNetworks', {
+ listAll: true,
+ trafficType: 'Public',
+ isSystem: true,
+ zoneId: this.resource.zoneid
+ }).then(json => {
+ this.publicNetwork = json.listnetworksresponse.network[0] || {}
+ }).catch(error => {
+ this.$notification.error({
+ message: 'Request Failed',
+ description: error.response.headers['x-description']
+ })
+ }).finally(() => {
+ this.fetchLoading = false
+ })
+
+ this.fetchLoading = true
api('listNetworkServiceProviders', { physicalnetworkid: this.resource.id
}).then(json => {
this.nsps =
json.listnetworkserviceprovidersresponse.networkserviceprovider
}).catch(error => {
diff --git a/src/views/infra/zone/PhysicalNetworksTab.vue
b/src/views/infra/zone/PhysicalNetworksTab.vue
new file mode 100644
index 0000000..7c0e1e9
--- /dev/null
+++ b/src/views/infra/zone/PhysicalNetworksTab.vue
@@ -0,0 +1,154 @@
+// 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="fetchLoading">
+ <a-list class="list">
+ <a-list-item v-for="network in networks" :key="network.id"
class="list__item">
+ <div class="list__item-outer-container">
+ <div class="list__item-container">
+ <div class="list__col">
+ <div class="list__label">
+ {{ $t('name') }}
+ </div>
+ <div>
+ <router-link :to="{ path: '/physicalnetwork/' + network.id
}">{{ network.name }}</router-link>
+ </div>
+ </div>
+ <div class="list__col">
+ <div class="list__label">{{ $t('state') }}</div>
+ <div><status :text="network.state" displayText></status></div>
+ </div>
+ <div class="list__col">
+ <div class="list__label">
+ {{ $t('isolationmethods') }}
+ </div>
+ <div>
+ {{ network.isolationmethods }}
+ </div>
+ </div>
+ <div class="list__col">
+ <div class="list__label">
+ {{ $t('vlan') }}
+ </div>
+ <div>{{ network.vlan }}</div>
+ </div>
+ <div class="list__col">
+ <div class="list__label">
+ {{ $t('broadcastdomainrange') }}
+ </div>
+ <div>{{ network.broadcastdomainrange }}</div>
+ </div>
+ </div>
+ </div>
+ </a-list-item>
+ </a-list>
+ </a-spin>
+</template>
+
+<script>
+import { api } from '@/api'
+import Status from '@/components/widgets/Status'
+
+export default {
+ name: 'PhysicalNetworksTab',
+ components: {
+ Status
+ },
+ props: {
+ resource: {
+ type: Object,
+ required: true
+ },
+ loading: {
+ type: Boolean,
+ default: false
+ }
+ },
+ data () {
+ return {
+ networks: [],
+ fetchLoading: false
+ }
+ },
+ mounted () {
+ this.fetchData()
+ },
+ watch: {
+ resource (newItem, oldItem) {
+ if (!newItem || !newItem.id) {
+ return
+ }
+ this.fetchData()
+ }
+ },
+ methods: {
+ fetchData () {
+ this.fetchLoading = true
+ api('listPhysicalNetworks', { zoneid: this.resource.id }).then(json => {
+ this.networks = json.listphysicalnetworksresponse.physicalnetwork || []
+ }).catch(error => {
+ this.$notification.error({
+ message: 'Request Failed',
+ description: error.response.headers['x-description']
+ })
+ }).finally(() => {
+ this.fetchLoading = false
+ })
+ }
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+.list {
+
+ &__label {
+ font-weight: bold;
+ }
+
+ &__col {
+ flex: 1;
+
+ @media (min-width: 480px) {
+ &:not(:last-child) {
+ margin-right: 20px;
+ }
+ }
+ }
+
+ &__item {
+ margin-right: -8px;
+ align-items: flex-start;
+
+ &-outer-container {
+ width: 100%;
+ }
+
+ &-container {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+
+ @media (min-width: 480px) {
+ flex-direction: row;
+ margin-bottom: 10px;
+ }
+ }
+ }
+}
+</style>
diff --git a/src/views/infra/zone/SystemVmsTab.vue
b/src/views/infra/zone/SystemVmsTab.vue
new file mode 100644
index 0000000..d1d8a21
--- /dev/null
+++ b/src/views/infra/zone/SystemVmsTab.vue
@@ -0,0 +1,162 @@
+// 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="fetchLoading">
+ <a-list class="list">
+ <a-list-item v-for="vm in vms" :key="vm.id" class="list__item">
+ <div class="list__item-outer-container">
+ <div class="list__item-container">
+ <div class="list__col">
+ <div class="list__label">
+ {{ $t('name') }}
+ </div>
+ <div>
+ <router-link :to="{ path: '/systemvm/' + vm.id }">{{ vm.name
}}</router-link>
+ </div>
+ </div>
+ <div class="list__col">
+ <div class="list__label">{{ $t('vmstate') }}</div>
+ <div><status :text="vm.state" displayText></status></div>
+ </div>
+ <div class="list__col">
+ <div class="list__label">{{ $t('agentstate') }}</div>
+ <div><status :text="vm.agentstate || 'Unknown'"
displayText></status></div>
+ </div>
+ <div class="list__col">
+ <div class="list__label">
+ {{ $t('type') }}
+ </div>
+ <div>
+ {{ vm.systemvmtype == 'consoleproxy' ? 'Console Proxy VM' :
'Secondary Storage VM' }}
+ </div>
+ </div>
+ <div class="list__col">
+ <div class="list__label">
+ {{ $t('publicip') }}
+ </div>
+ <div>
+ {{ vm.publicip }}
+ </div>
+ </div>
+ <div class="list__col">
+ <div class="list__label">
+ {{ $t('hostname') }}
+ </div>
+ <div>
+ <router-link :to="{ path: '/host/' + vm.hostid }">{{
vm.hostname }}</router-link>
+ </div>
+ </div>
+ </div>
+ </div>
+ </a-list-item>
+ </a-list>
+ </a-spin>
+</template>
+
+<script>
+import { api } from '@/api'
+import Status from '@/components/widgets/Status'
+
+export default {
+ name: 'SystemVmsTab',
+ components: {
+ Status
+ },
+ props: {
+ resource: {
+ type: Object,
+ required: true
+ },
+ loading: {
+ type: Boolean,
+ default: false
+ }
+ },
+ data () {
+ return {
+ vms: [],
+ fetchLoading: false
+ }
+ },
+ mounted () {
+ this.fetchData()
+ },
+ watch: {
+ resource (newItem, oldItem) {
+ if (!newItem || !newItem.id) {
+ return
+ }
+ this.fetchData()
+ }
+ },
+ methods: {
+ fetchData () {
+ this.fetchLoading = true
+ api('listSystemVms', { zoneid: this.resource.id }).then(json => {
+ this.vms = json.listsystemvmsresponse.systemvm || []
+ }).catch(error => {
+ this.$notification.error({
+ message: 'Request Failed',
+ description: error.response.headers['x-description']
+ })
+ }).finally(() => {
+ this.fetchLoading = false
+ })
+ }
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+.list {
+
+ &__label {
+ font-weight: bold;
+ }
+
+ &__col {
+ flex: 1;
+
+ @media (min-width: 480px) {
+ &:not(:last-child) {
+ margin-right: 20px;
+ }
+ }
+ }
+
+ &__item {
+ margin-right: -8px;
+ align-items: flex-start;
+
+ &-outer-container {
+ width: 100%;
+ }
+
+ &-container {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+
+ @media (min-width: 480px) {
+ flex-direction: row;
+ margin-bottom: 10px;
+ }
+ }
+ }
+}
+</style>
diff --git a/src/views/infra/ZoneResources.vue
b/src/views/infra/zone/ZoneResources.vue
similarity index 100%
rename from src/views/infra/ZoneResources.vue
rename to src/views/infra/zone/ZoneResources.vue
diff --git a/src/views/infra/ZoneWizard.vue
b/src/views/infra/zone/ZoneWizard.vue
similarity index 100%
rename from src/views/infra/ZoneWizard.vue
rename to src/views/infra/zone/ZoneWizard.vue