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

Reply via email to