You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by ro...@apache.org on 2020/03/21 20:10:48 UTC

[cloudstack-primate] branch master updated: primate: use a-table with pagination instead of a-list (#151)

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 df189c8  primate: use a-table with pagination instead of a-list (#151)
df189c8 is described below

commit df189c8f75907282b0f662767e533629efb10073
Author: Ritchie Vincent <rf...@gmail.com>
AuthorDate: Sat Mar 21 20:10:40 2020 +0000

    primate: use a-table with pagination instead of a-list (#151)
    
    This converts many components to use a-table with a-pagination than a-list:
    * Convert MigrateWizard
    * Convert Firewalls
    * Convert Port Forwarding
    * Convert Load Balancing
    * Convert Egress Configure
    * Convert IngressEgress Configure
    * Convert Dedicate VLAN
    * Convert VPC Tiers tab
    
    Signed-off-by: Rohit Yadav <ro...@shapeblue.com>
    Co-authored-by: Ritchie Vincent <ri...@ritchievincent.co.uk>
    Co-authored-by: Rohit Yadav <ro...@shapeblue.com>
---
 src/config/section/network.js                      |   2 +-
 src/locales/en.json                                |   1 +
 src/views/compute/MigrateWizard.vue                | 193 +++++++++++--------
 src/views/infra/network/DedicatedVLANTab.vue       | 123 +++++++-----
 .../{EgressConfigure.vue => EgressRulesTab.vue}    | 129 +++++++++----
 src/views/network/FirewallRules.vue                | 121 ++++++++----
 src/views/network/IngressEgressRuleConfigure.vue   | 104 +++++-----
 src/views/network/LoadBalancing.vue                | 210 +++++++++++++--------
 src/views/network/PortForwarding.vue               | 123 ++++++++----
 9 files changed, 649 insertions(+), 357 deletions(-)

diff --git a/src/config/section/network.js b/src/config/section/network.js
index f82e610..70bfa92 100644
--- a/src/config/section/network.js
+++ b/src/config/section/network.js
@@ -40,7 +40,7 @@ export default {
         component: () => import('@/components/view/DetailsTab.vue')
       }, {
         name: 'Egress Rules',
-        component: () => import('@/views/network/EgressConfigure.vue'),
+        component: () => import('@/views/network/EgressRulesTab.vue'),
         show: (record) => { return record.type === 'Isolated' && 'listEgressFirewallRules' in store.getters.apis }
       }, {
         name: 'Public IP Addresses',
diff --git a/src/locales/en.json b/src/locales/en.json
index 25a0f50..8900a05 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -917,6 +917,7 @@
 "snmpPort": "SNMP Port",
 "sockettimeout": "Socket Timeout",
 "sourcecidr": "Source CIDR",
+"destcidr": "Destination CIDR",
 "sourceNat": "Source NAT",
 "sourceipaddress": "Source IP Address",
 "sourceport": "Source Port",
diff --git a/src/views/compute/MigrateWizard.vue b/src/views/compute/MigrateWizard.vue
index 27f0c13..b99125c 100644
--- a/src/views/compute/MigrateWizard.vue
+++ b/src/views/compute/MigrateWizard.vue
@@ -16,60 +16,64 @@
 // under the License.
 
 <template>
-  <a-list :dataSource="hosts" itemLayout="vertical" class="list" :loading="loading">
-    <div slot="header" class="list__header">
-      <a-input-search
-        placeholder="Search"
-        v-model="searchQuery"
-        @search="fetchData" />
-    </div>
-    <a-list-item
-      slot="renderItem"
-      slot-scope="host, index"
-      class="host-item"
-      :class="{ 'host-item--selected' : selectedIndex === index }"
-    >
-      <div class="host-item__row">
-        <div class="host-item__value">
-          <span class="host-item__title">{{ $t('name') }}</span>
-          {{ host.name }}
-        </div>
-        <div class="host-item__value host-item__value--small">
-          <span class="host-item__title">Suitability</span>
-          <a-icon
-            class="host-item__suitability-icon"
-            type="check-circle"
-            theme="twoTone"
-            twoToneColor="#52c41a"
-            v-if="host.suitableformigration" />
-          <a-icon
-            class="host-item__suitability-icon"
-            type="close-circle"
-            theme="twoTone"
-            twoToneColor="#f5222d"
-            v-else />
-        </div>
-        <div class="host-item__value host-item__value--full">
-          <span class="host-item__title">{{ $t('cpuused') }}</span>
-          {{ host.cpuused }}
-        </div>
-        <div class="host-item__value">
-          <span class="host-item__title">{{ $t('memused') }}</span>
-          {{ host.memoryused | byteToGigabyte }} GB
-        </div>
+  <div class="form">
+    <a-input-search
+      placeholder="Search"
+      v-model="searchQuery"
+      style="margin-bottom: 10px;"
+      @search="fetchData" />
+    <a-table
+      size="small"
+      style="overflow-y: auto"
+      :loading="loading"
+      :columns="columns"
+      :dataSource="hosts"
+      :pagination="false"
+      :rowKey="record => record.id">
+      <div slot="suitability" slot-scope="record">
+        <a-icon
+          class="host-item__suitability-icon"
+          type="check-circle"
+          theme="twoTone"
+          twoToneColor="#52c41a"
+          v-if="record.suitableformigration" />
+        <a-icon
+          class="host-item__suitability-icon"
+          type="close-circle"
+          theme="twoTone"
+          twoToneColor="#f5222d"
+          v-else />
+      </div>
+      <div slot="memused" slot-scope="record">
+        {{ record.memoryused | byteToGigabyte }} GB
+      </div>
+      <template slot="select" slot-scope="record">
         <a-radio
           class="host-item__radio"
-          @click="selectedIndex = index"
-          :checked="selectedIndex === index"
-          :disabled="!host.suitableformigration"></a-radio>
-      </div>
-    </a-list-item>
-    <div slot="footer" class="list__footer">
-      <a-button type="primary" :disabled="selectedIndex === null" @click="submitForm">
-        {{ $t('OK') }}
+          @click="selectedHost = record"
+          :checked="record.id === selectedHost.id"
+          :disabled="!record.suitableformigration"></a-radio>
+      </template>
+    </a-table>
+    <a-pagination
+      class="pagination"
+      size="small"
+      :current="page"
+      :pageSize="pageSize"
+      :total="totalCount"
+      :showTotal="total => `Total ${total} items`"
+      :pageSizeOptions="['10', '20', '40', '80', '100']"
+      @change="handleChangePage"
+      @showSizeChange="handleChangePageSize"
+      showSizeChanger/>
+
+    <div style="margin-top: 20px; display: flex; justify-content:flex-end;">
+      <a-button type="primary" :disabled="!selectedHost.id" @click="submitForm">
+        {{ $t('ok') }}
       </a-button>
     </div>
-  </a-list>
+  </div>
+
 </template>
 
 <script>
@@ -87,8 +91,33 @@ export default {
     return {
       loading: true,
       hosts: [],
-      selectedIndex: null,
-      searchQuery: ''
+      selectedHost: {},
+      searchQuery: '',
+      totalCount: 0,
+      page: 1,
+      pageSize: 10,
+      columns: [
+        {
+          title: this.$t('name'),
+          dataIndex: 'name'
+        },
+        {
+          title: this.$t('Suitability'),
+          scopedSlots: { customRender: 'suitability' }
+        },
+        {
+          title: this.$t('cpuused'),
+          dataIndex: 'cpuused'
+        },
+        {
+          title: this.$t('memused'),
+          scopedSlots: { customRender: 'memused' }
+        },
+        {
+          title: this.$t('select'),
+          scopedSlots: { customRender: 'select' }
+        }
+      ]
     }
   },
   mounted () {
@@ -100,20 +129,24 @@ export default {
       api('findHostsForMigration', {
         virtualmachineid: this.resource.id,
         keyword: this.searchQuery,
-        page: 1,
-        pagesize: 500
+        page: this.page,
+        pagesize: this.pageSize
       }).then(response => {
         this.hosts = response.findhostsformigrationresponse.host
-        this.loading = false
+        this.totalCount = response.findhostsformigrationresponse.count
+        if (this.totalCount > 0) {
+          this.totalCount -= 1
+        }
       }).catch(error => {
         this.$message.error('Failed to load hosts: ' + error)
+      }).finally(() => {
+        this.loading = false
       })
     },
     submitForm () {
       this.loading = true
-      const host = this.hosts[this.selectedIndex]
-      api(host.requiresStorageMotion ? 'migrateVirtualMachineWithVolume' : 'migrateVirtualMachine', {
-        hostid: host.id,
+      api(this.selectedHost.requiresStorageMotion ? 'migrateVirtualMachineWithVolume' : 'migrateVirtualMachine', {
+        hostid: this.selectedHost.id,
         virtualmachineid: this.resource.id
       }).then(response => {
         this.$store.dispatch('AddAsyncJob', {
@@ -141,15 +174,23 @@ export default {
         this.$parent.$parent.close()
       }).catch(error => {
         console.error(error)
-        this.$message.error('Failed to migrate host.')
+        this.$message.error(`Failed to migrate VM to host ${this.selectedHost.name}`)
       })
+    },
+    handleChangePage (page, pageSize) {
+      this.page = page
+      this.pageSize = pageSize
+      this.fetchData()
+    },
+    handleChangePageSize (currentPage, pageSize) {
+      this.page = currentPage
+      this.pageSize = pageSize
+      this.fetchData()
     }
   },
   filters: {
     byteToGigabyte: value => {
-      if (!value) return ''
-      value = value / Math.pow(10, 9)
-      return value.toFixed(2)
+      return (value / Math.pow(10, 9)).toFixed(2)
     }
   }
 }
@@ -157,26 +198,10 @@ export default {
 
 <style scoped lang="scss">
 
-  .list {
-    max-height: 95vh;
-    width: 95vw;
-    overflow-y: scroll;
-    margin: -24px;
-
-    @media (min-width: 1000px) {
-      max-height: 70vh;
-      width: 60vw;
-    }
-
-    &__header,
-    &__footer {
-      padding-left: 20px;
-      padding-right: 20px;
-    }
-
-    &__footer {
-      display: flex;
-      justify-content: flex-end;
+  .form {
+    width: 85vw;
+    @media (min-width: 800px) {
+      width: 750px;
     }
   }
 
@@ -230,4 +255,8 @@ export default {
     }
 
   }
+
+  .pagination {
+    margin-top: 20px;
+  }
 </style>
diff --git a/src/views/infra/network/DedicatedVLANTab.vue b/src/views/infra/network/DedicatedVLANTab.vue
index eaa0dc3..48d041d 100644
--- a/src/views/infra/network/DedicatedVLANTab.vue
+++ b/src/views/infra/network/DedicatedVLANTab.vue
@@ -18,41 +18,37 @@
 <template>
   <a-spin :spinning="fetchLoading">
     <a-button type="dashed" icon="plus" style="width: 100%" @click="handleOpenModal">{{ $t('label.dedicate.vlan.vni.range') }}</a-button>
-    <a-list class="list">
-      <a-list-item v-for="item in items" :key="item.id" class="list__item">
-        <div class="list__item-outer-container">
-          <div class="list__item-container">
-            <div class="list__col">
-              <div class="list__label">{{ $t('vlanrange') }}</div>
-              <div>{{ item.guestvlanrange }}</div>
-            </div>
-            <div class="list__col">
-              <div class="list__label">{{ $t('domain') }}</div>
-              <div>{{ item.domain }}</div>
-            </div>
-            <div class="list__col">
-              <div class="list__label">{{ $t('account') }}</div>
-              <div>{{ item.account }}</div>
-            </div>
-          </div>
-          <div class="list__col">
-            <div class="list__label">{{ $t('id') }}</div>
-            <div>{{ item.id }}</div>
-          </div>
-        </div>
-        <div slot="actions">
-          <a-popconfirm
-            :title="`${$t('label.delete')}?`"
-            @confirm="handleDelete(item)"
-            okText="Yes"
-            cancelText="No"
-            placement="top"
-          >
-            <a-button icon="delete" type="danger" shape="round"></a-button>
-          </a-popconfirm>
-        </div>
-      </a-list-item>
-    </a-list>
+    <a-table
+      size="small"
+      style="overflow-y: auto; margin-top: 20px;"
+      :loading="fetchLoading"
+      :columns="columns"
+      :dataSource="items"
+      :pagination="false"
+      :rowKey="record => record.id">
+      <template slot="actions" slot-scope="record">
+        <a-popconfirm
+          :title="`${$t('label.delete')}?`"
+          @confirm="handleDelete(record)"
+          okText="Yes"
+          cancelText="No"
+          placement="top"
+        >
+          <a-button icon="delete" type="danger" shape="round"></a-button>
+        </a-popconfirm>
+      </template>
+    </a-table>
+    <a-pagination
+      class="pagination"
+      size="small"
+      :current="page"
+      :pageSize="pageSize"
+      :total="totalCount"
+      :showTotal="total => `Total ${total} items`"
+      :pageSizeOptions="['10', '20', '40', '80', '100']"
+      @change="handleChangePage"
+      @showSizeChange="handleChangePageSize"
+      showSizeChanger/>
 
     <a-modal v-model="modal" :title="$t('label.dedicate.vlan.vni.range')" @ok="handleSubmit">
       <a-spin :spinning="formLoading">
@@ -147,7 +143,28 @@ export default {
       accounts: [],
       projects: [],
       modal: false,
-      selectedScope: 'account'
+      selectedScope: 'account',
+      totalCount: 0,
+      page: 1,
+      pageSize: 10,
+      columns: [
+        {
+          title: this.$t('vlanrange'),
+          dataIndex: 'guestvlanrange'
+        },
+        {
+          title: this.$t('domain'),
+          dataIndex: 'domain'
+        },
+        {
+          title: this.$t('account'),
+          dataIndex: 'account'
+        },
+        {
+          title: this.$t('action'),
+          scopedSlots: { customRender: 'actions' }
+        }
+      ]
     }
   },
   beforeCreate () {
@@ -166,20 +183,23 @@ export default {
   methods: {
     fetchData () {
       this.form.resetFields()
+      this.formLoading = true
       api('listDedicatedGuestVlanRanges', {
-        physicalnetworkid: this.resource.id
+        physicalnetworkid: this.resource.id,
+        page: this.page,
+        pageSize: this.pageSize
       }).then(response => {
-        this.items = response.listdedicatedguestvlanrangesresponse.dedicatedguestvlanrange
-          ? response.listdedicatedguestvlanrangesresponse.dedicatedguestvlanrange : []
-        this.parentFinishLoading()
-        this.formLoading = false
+        this.items = response.listdedicatedguestvlanrangesresponse.dedicatedguestvlanrange || []
+        this.totalCount = response.listdedicatedguestvlanrangesresponse.count || 0
       }).catch(error => {
         this.$notification.error({
           message: `Error ${error.response.status}`,
-          description: error.response.data.errorresponse.errortext
+          description: error.response.data.errorresponse.errortext,
+          duration: 0
         })
-        this.parentFinishLoading()
+      }).finally(() => {
         this.formLoading = false
+        this.parentFinishLoading()
       })
     },
     fetchDomains () {
@@ -187,8 +207,7 @@ export default {
         details: 'min',
         listAll: true
       }).then(response => {
-        this.domains = response.listdomainsresponse.domain
-          ? response.listdomainsresponse.domain : []
+        this.domains = response.listdomainsresponse.domain || []
         if (this.domains.length > 0) {
           this.form.setFieldsValue({
             domain: this.domains[0].id
@@ -340,6 +359,16 @@ export default {
       this.modal = true
       this.formLoading = true
       this.fetchDomains()
+    },
+    handleChangePage (page, pageSize) {
+      this.page = page
+      this.pageSize = pageSize
+      this.fetchData()
+    },
+    handleChangePageSize (currentPage, pageSize) {
+      this.page = currentPage
+      this.pageSize = pageSize
+      this.fetchData()
     }
   }
 }
@@ -381,4 +410,8 @@ export default {
     }
   }
 }
+
+.pagination {
+  margin-top: 20px;
+}
 </style>
diff --git a/src/views/network/EgressConfigure.vue b/src/views/network/EgressRulesTab.vue
similarity index 70%
rename from src/views/network/EgressConfigure.vue
rename to src/views/network/EgressRulesTab.vue
index 22d9db6..df78788 100644
--- a/src/views/network/EgressConfigure.vue
+++ b/src/views/network/EgressRulesTab.vue
@@ -20,15 +20,15 @@
     <div>
       <div class="form">
         <div class="form__item">
-          <div class="form__label">Source CIDR</div>
+          <div class="form__label">{{ $t('sourcecidr') }}</div>
           <a-input v-model="newRule.cidrlist"></a-input>
         </div>
         <div class="form__item">
-          <div class="form__label">Destination CIDR</div>
+          <div class="form__label">{{ $t('destcidr') }}</div>
           <a-input v-model="newRule.destcidrlist"></a-input>
         </div>
         <div class="form__item">
-          <div class="form__label">Protocol</div>
+          <div class="form__label">{{ $t('protocol') }}</div>
           <a-select v-model="newRule.protocol" style="width: 100%;" @change="resetRulePorts">
             <a-select-option value="tcp">TCP</a-select-option>
             <a-select-option value="udp">UDP</a-select-option>
@@ -37,19 +37,19 @@
           </a-select>
         </div>
         <div v-show="newRule.protocol === 'tcp' || newRule.protocol === 'udp'" class="form__item">
-          <div class="form__label">Start Port</div>
+          <div class="form__label">{{ $t('startport') }}</div>
           <a-input v-model="newRule.startport"></a-input>
         </div>
         <div v-show="newRule.protocol === 'tcp' || newRule.protocol === 'udp'" class="form__item">
-          <div class="form__label">End Port</div>
+          <div class="form__label">{{ $t('endport') }}</div>
           <a-input v-model="newRule.endport"></a-input>
         </div>
         <div v-show="newRule.protocol === 'icmp'" class="form__item">
-          <div class="form__label">ICMP Type</div>
+          <div class="form__label">{{ $t('icmptype') }}</div>
           <a-input v-model="newRule.icmptype"></a-input>
         </div>
         <div v-show="newRule.protocol === 'icmp'" class="form__item">
-          <div class="form__label">ICMP Code</div>
+          <div class="form__label">{{ $t('icmpcode') }}</div>
           <a-input v-model="newRule.icmpcode"></a-input>
         </div>
         <div class="form__item">
@@ -60,35 +60,39 @@
 
     <a-divider/>
 
-    <a-list :loading="loading" style="min-height: 25px;">
-      <a-list-item v-for="rule in egressRules" :key="rule.id" class="rule">
-        <div class="rule-container">
-          <div class="rule__item">
-            <div class="rule__title">Source CIDR</div>
-            <div>{{ rule.cidrlist }}</div>
-          </div>
-          <div class="rule__item">
-            <div class="rule__title">Destination CIDR</div>
-            <div>{{ rule.destcidrlist }}</div>
-          </div>
-          <div class="rule__item">
-            <div class="rule__title">Protocol</div>
-            <div>{{ rule.protocol | capitalise }}</div>
-          </div>
-          <div class="rule__item">
-            <div class="rule__title">{{ rule.protocol === 'icmp' ? 'ICMP Type' : 'Start Port' }}</div>
-            <div>{{ rule.icmptype || rule.startport >= 0 ? rule.icmptype || rule.startport : 'All' }}</div>
-          </div>
-          <div class="rule__item">
-            <div class="rule__title">{{ rule.protocol === 'icmp' ? 'ICMP Code' : 'End Port' }}</div>
-            <div>{{ rule.icmpcode || rule.endport >= 0 ? rule.icmpcode || rule.endport : 'All' }}</div>
-          </div>
-          <div slot="actions">
-            <a-button shape="round" type="danger" icon="delete" @click="deleteRule(rule)" />
-          </div>
-        </div>
-      </a-list-item>
-    </a-list>
+    <a-table
+      size="small"
+      style="overflow-y: auto"
+      :loading="loading"
+      :columns="columns"
+      :dataSource="egressRules"
+      :pagination="false"
+      :rowKey="record => record.id">
+      <template slot="protocol" slot-scope="record">
+        {{ record.protocol | capitalise }}
+      </template>
+      <template slot="startport" slot-scope="record">
+        {{ record.icmptype || record.startport >= 0 ? record.icmptype || record.startport : 'All' }}
+      </template>
+      <template slot="endport" slot-scope="record">
+        {{ record.icmpcode || record.endport >= 0 ? record.icmpcode || record.endport : 'All' }}
+      </template>
+      <template slot="actions" slot-scope="record">
+        <a-button shape="round" type="danger" icon="delete" @click="deleteRule(record)" />
+      </template>
+    </a-table>
+    <a-pagination
+      class="pagination"
+      size="small"
+      :current="page"
+      :pageSize="pageSize"
+      :total="totalCount"
+      :showTotal="total => `Total ${total} items`"
+      :pageSizeOptions="['10', '20', '40', '80', '100']"
+      @change="handleChangePage"
+      @showSizeChange="handleChangePageSize"
+      showSizeChanger/>
+
   </div>
 </template>
 
@@ -96,6 +100,7 @@
 import { api } from '@/api'
 
 export default {
+  name: 'EgressRulesTab',
   props: {
     resource: {
       type: Object,
@@ -115,7 +120,36 @@ export default {
         icmpcode: null,
         startport: null,
         endport: null
-      }
+      },
+      totalCount: 0,
+      page: 1,
+      pageSize: 10,
+      columns: [
+        {
+          title: this.$t('sourcecidr'),
+          dataIndex: 'cidrlist'
+        },
+        {
+          title: this.$t('destcidr'),
+          dataIndex: 'destcidrlist'
+        },
+        {
+          title: this.$t('protocol'),
+          scopedSlots: { customRender: 'protocol' }
+        },
+        {
+          title: `ICMP Type / Start Port`,
+          scopedSlots: { customRender: 'startport' }
+        },
+        {
+          title: `ICMP Code / End Port`,
+          scopedSlots: { customRender: 'endport' }
+        },
+        {
+          title: this.$t('action'),
+          scopedSlots: { customRender: 'actions' }
+        }
+      ]
     }
   },
   mounted () {
@@ -141,9 +175,13 @@ export default {
       this.loading = true
       api('listEgressFirewallRules', {
         listAll: true,
-        networkid: this.resource.id
+        networkid: this.resource.id,
+        page: this.page,
+        pageSize: this.pageSize
       }).then(response => {
-        this.egressRules = response.listegressfirewallrulesresponse.firewallrule
+        this.egressRules = response.listegressfirewallrulesresponse.firewallrule || []
+        this.totalCount = response.listegressfirewallrulesresponse.count || 0
+      }).finally(() => {
         this.loading = false
       })
     },
@@ -211,6 +249,16 @@ export default {
       this.newRule.icmpcode = null
       this.newRule.startport = null
       this.newRule.endport = null
+    },
+    handleChangePage (page, pageSize) {
+      this.page = page
+      this.pageSize = pageSize
+      this.fetchData()
+    },
+    handleChangePageSize (currentPage, pageSize) {
+      this.page = currentPage
+      this.pageSize = pageSize
+      this.fetchData()
     }
   }
 }
@@ -300,4 +348,7 @@ export default {
     }
 
   }
+  .pagination {
+    margin-top: 20px;
+  }
 </style>
diff --git a/src/views/network/FirewallRules.vue b/src/views/network/FirewallRules.vue
index a0a2479..76b815e 100644
--- a/src/views/network/FirewallRules.vue
+++ b/src/views/network/FirewallRules.vue
@@ -55,36 +55,41 @@
 
     <a-divider/>
 
-    <a-list :loading="loading" style="min-height: 25px;">
-      <a-list-item v-for="rule in firewallRules" :key="rule.id" class="rule">
-        <div class="rule-container">
-          <div class="rule__item">
-            <div class="rule__title">{{ $t('sourcecidr') }}</div>
-            <div>{{ rule.cidrlist }}</div>
-          </div>
-          <div class="rule__item">
-            <div class="rule__title">{{ $t('protocol') }}</div>
-            <div>{{ rule.protocol | capitalise }}</div>
-          </div>
-          <div class="rule__item">
-            <div class="rule__title">{{ rule.protocol === 'icmp' ? $t('icmptype') : $t('startport') }}</div>
-            <div>{{ rule.icmptype || rule.startport >= 0 ? rule.icmptype || rule.startport : $t('all') }}</div>
-          </div>
-          <div class="rule__item">
-            <div class="rule__title">{{ rule.protocol === 'icmp' ? 'ICMP Code' : 'End Port' }}</div>
-            <div>{{ rule.icmpcode || rule.endport >= 0 ? rule.icmpcode || rule.endport : $t('all') }}</div>
-          </div>
-          <div class="rule__item">
-            <div class="rule__title">{{ $t('state') }}</div>
-            <div>{{ rule.state }}</div>
-          </div>
-          <div slot="actions">
-            <a-button shape="round" icon="tag" class="rule-action" @click="() => openTagsModal(rule.id)" />
-            <a-button shape="round" type="danger" icon="delete" class="rule-action" @click="deleteRule(rule)" />
-          </div>
+    <a-table
+      size="small"
+      style="overflow-y: auto"
+      :loading="loading"
+      :columns="columns"
+      :dataSource="firewallRules"
+      :pagination="false"
+      :rowKey="record => record.id">
+      <template slot="protocol" slot-scope="record">
+        {{ record.protocol | capitalise }}
+      </template>
+      <template slot="startport" slot-scope="record">
+        {{ record.icmptype || record.startport >= 0 ? record.icmptype || record.startport : $t('all') }}
+      </template>
+      <template slot="endport" slot-scope="record">
+        {{ record.icmpcode || record.endport >= 0 ? record.icmpcode || record.endport : $t('all') }}
+      </template>
+      <template slot="actions" slot-scope="record">
+        <div class="actions">
+          <a-button shape="round" icon="tag" class="rule-action" @click="() => openTagsModal(record.id)" />
+          <a-button shape="round" type="danger" icon="delete" class="rule-action" @click="deleteRule(record)" />
         </div>
-      </a-list-item>
-    </a-list>
+      </template>
+    </a-table>
+    <a-pagination
+      class="pagination"
+      size="small"
+      :current="page"
+      :pageSize="pageSize"
+      :total="totalCount"
+      :showTotal="total => `Total ${total} items`"
+      :pageSizeOptions="['10', '20', '40', '80', '100']"
+      @change="handleChangePage"
+      @showSizeChange="handleChangePageSize"
+      showSizeChanger/>
 
     <a-modal title="Edit Tags" v-model="tagsModalVisible" :footer="null" :afterClose="closeModal">
       <div class="add-tags">
@@ -96,7 +101,7 @@
           <p class="add-tags__label">{{ $t('value') }}</p>
           <a-input v-model="newTag.value"></a-input>
         </div>
-        <a-button type="primary" @click="() => handleAddTag()">{{ $t('add') }}</a-button>
+        <a-button type="primary" @click="() => handleAddTag()" :loading="addTagLoading">{{ $t('add') }}</a-button>
       </div>
 
       <a-divider></a-divider>
@@ -129,6 +134,7 @@ export default {
   data () {
     return {
       loading: true,
+      addTagLoading: false,
       firewallRules: [],
       newRule: {
         protocol: 'tcp',
@@ -145,7 +151,36 @@ export default {
       newTag: {
         key: null,
         value: null
-      }
+      },
+      totalCount: 0,
+      page: 1,
+      pageSize: 10,
+      columns: [
+        {
+          title: this.$t('sourcecidr'),
+          dataIndex: 'cidrlist'
+        },
+        {
+          title: this.$t('protocol'),
+          scopedSlots: { customRender: 'protocol' }
+        },
+        {
+          title: `${this.$t('startport')}/${this.$t('icmptype')}`,
+          scopedSlots: { customRender: 'startport' }
+        },
+        {
+          title: `${this.$t('endport')}/${this.$t('icmpcode')}`,
+          scopedSlots: { customRender: 'endport' }
+        },
+        {
+          title: this.$t('state'),
+          dataIndex: 'state'
+        },
+        {
+          title: this.$t('action'),
+          scopedSlots: { customRender: 'actions' }
+        }
+      ]
     }
   },
   mounted () {
@@ -171,9 +206,12 @@ export default {
       this.loading = true
       api('listFirewallRules', {
         listAll: true,
-        ipaddressid: this.resource.id
+        ipaddressid: this.resource.id,
+        page: this.page,
+        pageSize: this.pageSize
       }).then(response => {
-        this.firewallRules = response.listfirewallrulesresponse.firewallrule
+        this.firewallRules = response.listfirewallrulesresponse.firewallrule || []
+        this.totalCount = response.listfirewallrulesresponse.count || 0
       }).catch(error => {
         this.$notification.error({
           message: `Error ${error.response.status}`,
@@ -271,6 +309,7 @@ export default {
       })
     },
     handleAddTag () {
+      this.addTagLoading = true
       api('createTags', {
         'tags[0].key': this.newTag.key,
         'tags[0].value': this.newTag.value,
@@ -305,6 +344,8 @@ export default {
           description: error.response.data.createtagsresponse.errortext
         })
         this.closeModal()
+      }).finally(() => {
+        this.addTagLoading = false
       })
     },
     handleDeleteTag (tag) {
@@ -343,6 +384,16 @@ export default {
         })
         this.closeModal()
       })
+    },
+    handleChangePage (page, pageSize) {
+      this.page = page
+      this.pageSize = pageSize
+      this.fetchData()
+    },
+    handleChangePageSize (currentPage, pageSize) {
+      this.page = currentPage
+      this.pageSize = pageSize
+      this.fetchData()
     }
   }
 }
@@ -434,7 +485,6 @@ export default {
   }
 
   .rule-action {
-    margin-bottom: 20px;
 
     &:not(:last-of-type) {
       margin-right: 10px;
@@ -472,5 +522,8 @@ export default {
     display: block;
     margin-left: auto;
   }
+  .pagination {
+    margin-top: 20px;
+  }
 
 </style>
diff --git a/src/views/network/IngressEgressRuleConfigure.vue b/src/views/network/IngressEgressRuleConfigure.vue
index 24b6cfc..75432e6 100644
--- a/src/views/network/IngressEgressRuleConfigure.vue
+++ b/src/views/network/IngressEgressRuleConfigure.vue
@@ -69,49 +69,33 @@
       </div>
     </div>
 
-    <a-list>
-      <a-list-item v-for="(rule, index) in rules" :key="index" class="list">
-        <div class="list__col">
-          <div class="list__title">Protocol</div>
-          <div>{{ rule.protocol | capitalise }}</div>
+    <a-table
+      size="small"
+      style="overflow-y: auto"
+      :columns="columns"
+      :dataSource="rules"
+      :pagination="false"
+      :rowKey="record => record.id">
+      <template slot="protocol" slot-scope="record">
+        {{ record.protocol | capitalise }}
+      </template>
+      <template slot="account" slot-scope="record">
+        <div v-if="record.account && record.securitygroupname">
+          {{ record.account }} - {{ record.securitygroupname }}
         </div>
-        <div v-if="rule.startport" class="list__col">
-          <div class="list__title">Start Port</div>
-          <div>{{ rule.startport }}</div>
-        </div>
-        <div v-if="rule.endport" class="list__col">
-          <div class="list__title">End Port</div>
-          <div>{{ rule.endport }}</div>
-        </div>
-        <div v-if="rule.icmptype" class="list__col">
-          <div class="list__title">ICMP Type</div>
-          <div>{{ rule.icmptype }}</div>
-        </div>
-        <div v-if="rule.icmpcode" class="list__col">
-          <div class="list__title">ICMP Code</div>
-          <div>{{ rule.icmpcode }}</div>
-        </div>
-        <div v-if="rule.cidr" class="list__col">
-          <div class="list__title">CIDR</div>
-          <div>{{ rule.cidr }}</div>
-        </div>
-        <div class="list__col" v-if="rule.account && rule.securitygroupname">
-          <div class="list__title">Account, Security Group</div>
-          <div>{{ rule.account }} - {{ rule.securitygroupname }}</div>
-        </div>
-        <div slot="actions" class="actions">
-          <a-button shape="round" icon="tag" class="rule-action" @click="() => openTagsModal(rule)" />
-          <a-popconfirm
-            :title="$t('label.delete') + '?'"
-            @confirm="handleDeleteRule(rule)"
-            okText="Yes"
-            cancelText="No"
-          >
-            <a-button shape="round" type="danger" icon="delete" class="rule-action" />
-          </a-popconfirm>
-        </div>
-      </a-list-item>
-    </a-list>
+      </template>
+      <template slot="actions" slot-scope="record">
+        <a-button shape="round" icon="tag" class="rule-action" @click="() => openTagsModal(record)" />
+        <a-popconfirm
+          :title="$t('label.delete') + '?'"
+          @confirm="handleDeleteRule(record)"
+          okText="Yes"
+          cancelText="No"
+        >
+          <a-button shape="round" type="danger" icon="delete" class="rule-action" />
+        </a-popconfirm>
+      </template>
+    </a-table>
 
     <a-modal title="Edit Tags" v-model="tagsModalVisible" :footer="null" :afterClose="closeModal">
       <a-spin v-if="tagsLoading"></a-spin>
@@ -187,7 +171,41 @@ export default {
       selectedRule: null,
       tagsLoading: false,
       addType: 'cidr',
-      tabType: null
+      tabType: null,
+      columns: [
+        {
+          title: this.$t('protocol'),
+          scopedSlots: { customRender: 'protocol' }
+        },
+        {
+          title: this.$t('startport'),
+          dataIndex: 'startport'
+        },
+        {
+          title: this.$t('endport'),
+          dataIndex: 'endport'
+        },
+        {
+          title: 'ICMP Type',
+          dataIndex: 'icmptype'
+        },
+        {
+          title: 'ICMP Code',
+          dataIndex: 'icmpcode'
+        },
+        {
+          title: 'CIDR',
+          dataIndex: 'cidr'
+        },
+        {
+          title: 'Account, Security Group',
+          scopedSlots: { customRender: 'account' }
+        },
+        {
+          title: this.$t('action'),
+          scopedSlots: { customRender: 'actions' }
+        }
+      ]
     }
   },
   watch: {
diff --git a/src/views/network/LoadBalancing.vue b/src/views/network/LoadBalancing.vue
index 5f175b6..3a043f5 100644
--- a/src/views/network/LoadBalancing.vue
+++ b/src/views/network/LoadBalancing.vue
@@ -59,90 +59,78 @@
 
     <a-divider />
 
-    <a-list :loading="loading" style="min-height: 25px;">
-      <a-list-item v-for="rule in lbRules" :key="rule.id" class="rule custom-ant-list">
-        <div class="rule-container">
-          <div class="rule__row">
-            <div class="rule__item">
-              <div class="rule__title">{{ $t('name') }}</div>
-              <div>{{ rule.name }}</div>
+    <a-table
+      size="small"
+      style="overflow-y: auto"
+      :loading="loading"
+      :columns="columns"
+      :dataSource="lbRules"
+      :pagination="false"
+      :rowKey="record => record.id">
+      <template slot="algorithm" slot-scope="record">
+        {{ returnAlgorithmName(record.algorithm) }}
+      </template>
+      <template slot="protocol" slot-scope="record">
+        {{ record.protocol | capitalise }}
+      </template>
+      <template slot="stickiness" slot-scope="record">
+        <a-button @click="() => openStickinessModal(record.id)">
+          {{ returnStickinessLabel(record.id) }}
+        </a-button>
+      </template>
+      <template slot="add" slot-scope="record">
+        <a-button type="primary" icon="plus" @click="() => { selectedRule = record; handleOpenAddVMModal() }">
+          {{ $t('add') }}
+        </a-button>
+      </template>
+      <template slot="expandedRowRender" slot-scope="record">
+        <div class="rule-instance-list">
+          <div v-for="instance in record.ruleInstances" :key="instance.loadbalancerruleinstance.id">
+            <div v-for="ip in instance.lbvmipaddresses" :key="ip" class="rule-instance-list__item">
+              <div>
+                <a-icon type="desktop" />
+                <router-link :to="{ path: '/vm/' + record.virtualmachineid }">
+                  {{ instance.loadbalancerruleinstance.displayname }}
+                </router-link>
+              </div>
+              <div>{{ ip }}</div>
+              <div><status :text="instance.loadbalancerruleinstance.state" displayText /></div>
+              <a-button
+                size="small"
+                shape="round"
+                type="danger"
+                icon="delete"
+                @click="() => handleDeleteInstanceFromRule(instance, record, ip)" />
             </div>
-            <div class="rule__item">
-              <div class="rule__title">{{ $t('publicport') }}</div>
-              <div>{{ rule.publicport }}</div>
-            </div>
-            <div class="rule__item">
-              <div class="rule__title">{{ $t('privateport') }}</div>
-              <div>{{ rule.privateport }}</div>
-            </div>
-            <div class="rule__item">
-              <div class="rule__title">{{ $t('algorithm') }}</div>
-              <div>{{ returnAlgorithmName(rule.algorithm) }}</div>
-            </div>
-          </div>
-          <div class="rule__row">
-            <div class="rule__item">
-              <div class="rule__title">{{ $t('protocol') }}</div>
-              <div>{{ rule.protocol | capitalise }}</div>
-            </div>
-            <div class="rule__item">
-              <div class="rule__title">{{ $t('state') }}</div>
-              <div>{{ rule.state }}</div>
-            </div>
-            <div class="rule__item">
-              <div class="rule__title">{{ $t('label.action.configure.stickiness') }}</div>
-              <a-button @click="() => openStickinessModal(rule.id)">
-                {{ returnStickinessLabel(rule.id) }}
-              </a-button>
-            </div>
-            <div class="rule__item">
-              <div class="rule__title">{{ $t('label.add.VMs') }}</div>
-              <a-button type="primary" icon="plus" @click="() => { selectedRule = rule; handleOpenAddVMModal() }">
-                {{ $t('add') }}
-              </a-button>
-            </div>
-          </div>
-          <div class="rule__row" v-if="rule.ruleInstances">
-            <a-collapse :bordered="false" class="rule-instance-collapse">
-              <template v-slot:expandIcon="props">
-                <a-icon type="caret-right" :rotate="props.isActive ? 90 : 0" />
-              </template>
-              <a-collapse-panel header="View Instances">
-                <div class="rule-instance-list">
-                  <div v-for="instance in rule.ruleInstances" :key="instance.loadbalancerruleinstance.id">
-                    <div v-for="ip in instance.lbvmipaddresses" :key="ip" class="rule-instance-list__item">
-                      <div>
-                        <a-icon type="desktop" />
-                        <router-link :to="{ path: '/vm/' + rule.virtualmachineid }"> {{ instance.loadbalancerruleinstance.displayname }}</router-link>
-                      </div>
-                      <div>{{ ip }}</div>
-                      <div><status :text="instance.loadbalancerruleinstance.state" displayText /></div>
-                      <a-button
-                        shape="round"
-                        type="danger"
-                        icon="delete"
-                        @click="() => handleDeleteInstanceFromRule(instance, rule, ip)" />
-                    </div>
-                  </div>
-                </div>
-              </a-collapse-panel>
-            </a-collapse>
           </div>
         </div>
-        <div class="rule__item">
-          <a-button shape="circle" icon="edit" class="rule-action" @click="() => openEditRuleModal(rule)"></a-button>
-          <a-button shape="circle" icon="tag" class="rule-action" @click="() => openTagsModal(rule.id)" />
+      </template>
+      <template slot="actions" slot-scope="record">
+        <div class="actions">
+          <a-button size="small" shape="circle" icon="edit" @click="() => openEditRuleModal(record)"></a-button>
+          <a-button size="small" shape="circle" icon="tag" @click="() => openTagsModal(record.id)" />
           <a-popconfirm
             :title="$t('label.delete') + '?'"
-            @confirm="handleDeleteRule(rule)"
+            @confirm="handleDeleteRule(record)"
             okText="Yes"
             cancelText="No"
           >
-            <a-button shape="circle" type="danger" icon="delete" class="rule-action" />
+            <a-button size="small" shape="circle" type="danger" icon="delete" />
           </a-popconfirm>
         </div>
-      </a-list-item>
-    </a-list>
+      </template>
+    </a-table>
+    <a-pagination
+      class="pagination"
+      size="small"
+      :current="page"
+      :pageSize="pageSize"
+      :total="totalCount"
+      :showTotal="total => `Total ${total} items`"
+      :pageSizeOptions="['10', '20', '40', '80', '100']"
+      @change="handleChangePage"
+      @showSizeChange="handleChangePageSize"
+      showSizeChanger/>
 
     <a-modal title="Edit Tags" v-model="tagsModalVisible" :footer="null" :afterClose="closeModal" class="tags-modal">
       <span v-show="tagsModalLoading" class="modal-loading">
@@ -393,7 +381,48 @@ export default {
       addVmModalLoading: false,
       addVmModalNicLoading: false,
       vms: [],
-      nics: []
+      nics: [],
+      totalCount: 0,
+      page: 1,
+      pageSize: 10,
+      columns: [
+        {
+          title: this.$t('name'),
+          dataIndex: 'name'
+        },
+        {
+          title: this.$t('publicport'),
+          dataIndex: 'publicport'
+        },
+        {
+          title: this.$t('privateport'),
+          dataIndex: 'privateport'
+        },
+        {
+          title: this.$t('algorithm'),
+          scopedSlots: { customRender: 'algorithm' }
+        },
+        {
+          title: this.$t('protocol'),
+          scopedSlots: { customRender: 'protocol' }
+        },
+        {
+          title: this.$t('state'),
+          dataIndex: 'state'
+        },
+        {
+          title: this.$t('label.action.configure.stickiness'),
+          scopedSlots: { customRender: 'stickiness' }
+        },
+        {
+          title: this.$t('label.add.VMs'),
+          scopedSlots: { customRender: 'add' }
+        },
+        {
+          title: this.$t('action'),
+          scopedSlots: { customRender: 'actions' }
+        }
+      ]
     }
   },
   mounted () {
@@ -421,11 +450,12 @@ export default {
       this.stickinessPolicies = []
       api('listLoadBalancerRules', {
         listAll: true,
-        publicipid: this.resource.id
+        publicipid: this.resource.id,
+        page: this.page,
+        pageSize: this.pageSize
       }).then(response => {
-        this.lbRules = response.listloadbalancerrulesresponse.loadbalancerrule
-          ? response.listloadbalancerrulesresponse.loadbalancerrule
-          : []
+        this.lbRules = response.listloadbalancerrulesresponse.loadbalancerrule || []
+        this.totalCount = response.listloadbalancerrulesresponse.count || 0
       }).then(() => {
         if (this.lbRules.length > 0) {
           setTimeout(() => {
@@ -1041,6 +1071,16 @@ export default {
       this.newRule.virtualmachineid = []
       this.newTagsForm.resetFields()
       this.stickinessPolicyForm.resetFields()
+    },
+    handleChangePage (page, pageSize) {
+      this.page = page
+      this.pageSize = pageSize
+      this.fetchData()
+    },
+    handleChangePageSize (currentPage, pageSize) {
+      this.page = currentPage
+      this.pageSize = pageSize
+      this.fetchData()
     }
   }
 }
@@ -1370,4 +1410,16 @@ export default {
 
     }
   }
+
+  .pagination {
+    margin-top: 20px;
+  }
+
+  .actions {
+    button {
+      &:not(:last-child) {
+        margin-right: 10px;
+      }
+    }
+  }
 </style>
diff --git a/src/views/network/PortForwarding.vue b/src/views/network/PortForwarding.vue
index eb196a7..fa55d4b 100644
--- a/src/views/network/PortForwarding.vue
+++ b/src/views/network/PortForwarding.vue
@@ -71,37 +71,47 @@
 
     <a-divider/>
 
-    <a-list :loading="loading" style="min-height: 25px;">
-      <a-list-item v-for="rule in portForwardRules" :key="rule.id" class="rule">
-        <div class="rule-container">
-          <div class="rule__item">
-            <div class="rule__title">{{ $t('privateport') }}</div>
-            <div>{{ rule.privateport }} - {{ rule.privateendport }}</div>
-          </div>
-          <div class="rule__item">
-            <div class="rule__title">{{ $t('publicport') }}</div>
-            <div>{{ rule.publicport }} - {{ rule.publicendport }}</div>
-          </div>
-          <div class="rule__item">
-            <div class="rule__title">{{ $t('protocol') }}</div>
-            <div>{{ rule.protocol | capitalise }}</div>
-          </div>
-          <div class="rule__item">
-            <div class="rule__title">{{ $t('state') }}</div>
-            <div>{{ rule.state }}</div>
-          </div>
-          <div class="rule__item">
-            <div class="rule__title">{{ $t('vm') }}</div>
-            <div class="rule__title"></div>
-            <div><a-icon type="desktop"/> <router-link :to="{ path: '/vm/' + rule.virtualmachineid }">{{ rule.virtualmachinename }}</router-link> ({{ rule.vmguestip }})</div>
-          </div>
-          <div slot="actions">
-            <a-button shape="round" icon="tag" class="rule-action" @click="() => openTagsModal(rule.id)" />
-            <a-button shape="round" type="danger" icon="delete" class="rule-action" @click="deleteRule(rule)" />
-          </div>
+    <a-table
+      size="small"
+      style="overflow-y: auto"
+      :loading="loading"
+      :columns="columns"
+      :dataSource="portForwardRules"
+      :pagination="false"
+      :rowKey="record => record.id">
+      <template slot="privateport" slot-scope="record">
+        {{ record.privateport }} - {{ record.privateendport }}
+      </template>
+      <template slot="publicport" slot-scope="record">
+        {{ record.publicport }} - {{ record.publicendport }}
+      </template>
+      <template slot="protocol" slot-scope="record">
+        {{ record.protocol | capitalise }}
+      </template>
+      <template slot="vm" slot-scope="record">
+        <div><a-icon type="desktop"/>
+          <router-link
+            :to="{ path: '/vm/' + record.virtualmachineid }">
+            {{ record.virtualmachinename }}</router-link> ({{ record.vmguestip }})</div>
+      </template>
+      <template slot="actions" slot-scope="record">
+        <div class="actions">
+          <a-button shape="round" icon="tag" class="rule-action" @click="() => openTagsModal(record.id)" />
+          <a-button shape="round" type="danger" icon="delete" class="rule-action" @click="deleteRule(record)" />
         </div>
-      </a-list-item>
-    </a-list>
+      </template>
+    </a-table>
+    <a-pagination
+      class="pagination"
+      size="small"
+      :current="page"
+      :pageSize="pageSize"
+      :total="totalCount"
+      :showTotal="total => `Total ${total} items`"
+      :pageSizeOptions="['10', '20', '40', '80', '100']"
+      @change="handleChangePage"
+      @showSizeChange="handleChangePageSize"
+      showSizeChanger/>
 
     <a-modal title="Edit Tags" v-model="tagsModalVisible" :footer="null" :afterClose="closeModal">
       <span v-show="tagsModalLoading" class="tags-modal-loading">
@@ -227,7 +237,36 @@ export default {
       addVmModalLoading: false,
       addVmModalNicLoading: false,
       vms: [],
-      nics: []
+      nics: [],
+      totalCount: 0,
+      page: 1,
+      pageSize: 10,
+      columns: [
+        {
+          title: this.$t('privateport'),
+          scopedSlots: { customRender: 'privateport' }
+        },
+        {
+          title: this.$t('publicport'),
+          scopedSlots: { customRender: 'publicport' }
+        },
+        {
+          title: this.$t('protocol'),
+          scopedSlots: { customRender: 'protocol' }
+        },
+        {
+          title: this.$t('state'),
+          dataIndex: 'state'
+        },
+        {
+          title: this.$t('vm'),
+          scopedSlots: { customRender: 'vm' }
+        },
+        {
+          title: this.$t('action'),
+          scopedSlots: { customRender: 'actions' }
+        }
+      ]
     }
   },
   mounted () {
@@ -253,9 +292,12 @@ export default {
       this.loading = true
       api('listPortForwardingRules', {
         listAll: true,
-        ipaddressid: this.resource.id
+        ipaddressid: this.resource.id,
+        page: this.page,
+        pageSize: this.pageSize
       }).then(response => {
-        this.portForwardRules = response.listportforwardingrulesresponse.portforwardingrule
+        this.portForwardRules = response.listportforwardingrulesresponse.portforwardingrule || []
+        this.totalCount = response.listportforwardingrulesresponse.count || 0
       }).catch(error => {
         this.$notification.error({
           message: `Error ${error.response.status}`,
@@ -487,6 +529,16 @@ export default {
         })
         this.closeModal()
       })
+    },
+    handleChangePage (page, pageSize) {
+      this.page = page
+      this.pageSize = pageSize
+      this.fetchData()
+    },
+    handleChangePageSize (currentPage, pageSize) {
+      this.page = currentPage
+      this.pageSize = pageSize
+      this.fetchData()
     }
   }
 }
@@ -590,7 +642,6 @@ export default {
   }
 
   .rule-action {
-    margin-bottom: 20px;
 
     &:not(:last-of-type) {
       margin-right: 10px;
@@ -672,4 +723,8 @@ export default {
 
   }
 
+  .pagination {
+    margin-top: 20px;
+  }
+
 </style>