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 3e2c584  network: Egress, PF, FW, VPN, LB tabs (#84)
3e2c584 is described below

commit 3e2c584ee0650d8d096084a533b56b943202b469
Author: Ritchie Vincent <rfcvinc...@gmail.com>
AuthorDate: Tue Jan 21 07:56:30 2020 +0000

    network: Egress, PF, FW, VPN, LB tabs (#84)
    
    Implements the egress, pf, fw, vpn and lb tabs for a guest network (ip).
    
    Signed-off-by: Rohit Yadav <rohit.ya...@shapeblue.com>
    Co-authored-by: Rohit Yadav <ro...@apache.org>
---
 docs/api/apis.remaining                          |   43 -
 legacy/generateOldLayout.js                      |   14 +-
 src/components/view/InfoCard.vue                 |    2 +-
 src/components/view/ResourceView.vue             |   38 +-
 src/config/section/network.js                    |   61 +-
 src/locales/en.json                              |   14 +
 src/utils/device.js                              |    4 +-
 src/views/network/EgressConfigure.vue            |  271 ++++-
 src/views/network/FirewallRules.vue              |  476 ++++++++
 src/views/network/IngressEgressRuleConfigure.vue |    2 +-
 src/views/network/IpConfigure.vue                |   40 -
 src/views/network/LoadBalancing.vue              | 1373 ++++++++++++++++++++++
 src/views/network/PortForwarding.vue             |  675 +++++++++++
 src/views/network/VpnDetails.vue                 |  175 ++-
 14 files changed, 3041 insertions(+), 147 deletions(-)

diff --git a/docs/api/apis.remaining b/docs/api/apis.remaining
index c776794..a9775d3 100644
--- a/docs/api/apis.remaining
+++ b/docs/api/apis.remaining
@@ -2,24 +2,16 @@ addNetworkServiceProvider
 addResourceDetail
 addTrafficType
 assignCertToLoadBalancer
-assignToLoadBalancerRule
 authorizeSamlSso
-authorizeSecurityGroupEgress
-authorizeSecurityGroupIngress
 configureInternalLoadBalancerElement
 configureVirtualRouterElement
-createEgressFirewallRule
-createFirewallRule
 createLBHealthCheckPolicy
-createLBStickinessPolicy
 createLoadBalancer
-createLoadBalancerRule
 createManagementNetworkIpRange
 createNetworkACL
 createNetworkACLList
 createPhysicalNetwork
 createPortableIpRange
-createPortForwardingRule
 createPrivateGateway
 createSecondaryStagingStore
 createSnapshotFromVMSnapshot
@@ -30,25 +22,17 @@ createVpnConnection
 createVpnGateway
 dedicateGuestVlanRange
 dedicatePublicIpRange
-deleteAccountFromProject
-deleteEgressFirewallRule
-deleteFirewallRule
 deleteLBHealthCheckPolicy
-deleteLBStickinessPolicy
-deleteLdapConfiguration
 deleteLoadBalancer
-deleteLoadBalancerRule
 deleteManagementNetworkIpRange
 deleteNetworkACL
 deleteNetworkACLList
 deleteNetworkServiceProvider
 deletePhysicalNetwork
 deletePortableIpRange
-deletePortForwardingRule
 deletePrivateGateway
 deleteProjectInvitation
 deleteSecondaryStagingStore
-deleteSnapshotPolicies
 deleteStaticRoute
 deleteStorageNetworkIpRange
 deleteVlanIpRange
@@ -57,74 +41,47 @@ deleteVpnGateway
 findStoragePoolsForMigration
 importLdapUsers
 ldapCreateAccount
-linkDomainToLdap
 listAffinityGroupTypes
 listAndSwitchSamlAccount
-listCapabilities
 listDedicatedClusters
 listDedicatedGuestVlanRanges
 listDedicatedHosts
 listDedicatedPods
 listDedicatedZones
 listDeploymentPlanners
-listEgressFirewallRules
-listFirewallRules
 listHostHAProviders
 listHostTags
-listHypervisors
 listIdps
 listInternalLoadBalancerElements
 listInternalLoadBalancerVMs
 listLBHealthCheckPolicies
-listLBStickinessPolicies
 listLdapUsers
-listLoadBalancerRuleInstances
-listLoadBalancerRules
 listLoadBalancers
-listNetworkACLLists
 listNetworkACLs
 listNetworkServiceProviders
 listOsCategories
 listPortableIpRanges
-listPortForwardingRules
-listPrivateGateways
-listProjectAccounts
-listProjectInvitations
 listRegisteredServicePackages
-listRemoteAccessVpns
-listResourceLimits
 listSamlAuthorization
 listSecondaryStagingStores
-listSnapshotPolicies
 listStaticRoutes
 listStorageNetworkIpRange
 listStorageProviders
 listStorageTags
 listSupportedNetworkServices
 listTemplateOvfProperties
-listTemplatePermissions
 listTrafficTypes
 listVirtualRouterElements
 listVlanIpRanges
 listVmwareDcs
-listVpnConnections
-listVpnGateways
 moveNetworkAclItem
 releaseDedicatedGuestVlanRange
 releasePublicIpRange
-removeFromLoadBalancerRule
-replaceNetworkACLList
 resetVpnConnection
-revokeSecurityGroupEgress
-revokeSecurityGroupIngress
 startInternalLoadBalancerVM
 stopInternalLoadBalancerVM
-updateLoadBalancerRule
 updateNetworkACLItem
 updateNetworkACLList
 updateNetworkServiceProvider
 updatePhysicalNetwork
-updateProjectInvitation
-updateResourceLimit
 updateTrafficType
-updateVpnCustomerGateway
diff --git a/legacy/generateOldLayout.js b/legacy/generateOldLayout.js
index df7424c..9850162 100644
--- a/legacy/generateOldLayout.js
+++ b/legacy/generateOldLayout.js
@@ -13,8 +13,8 @@ var loadLabel = function (allFields, fieldDict, prefix) {
         allFields[fieldId].components.push(prefix)
       } else {
         allFields[fieldId] = {
-          'labels': [fieldDict[fieldId].label],
-          'components': [prefix]
+          labels: [fieldDict[fieldId].label],
+          components: [prefix]
         }
       }
       cols = cols + "'" + fieldId + "', "
@@ -28,8 +28,8 @@ var loadLabel = function (allFields, fieldDict, prefix) {
             allFields[colId].components.push(prefix)
           } else {
             allFields[colId] = {
-              'labels': [columns[colId].label],
-              'components': [prefix]
+              labels: [columns[colId].label],
+              components: [prefix]
             }
           }
         })
@@ -63,9 +63,9 @@ var loadFields = function (data, prefix) {
       var curActions = []
       $.each(Object.keys(acVal), function (idx, acKey) {
         if (acVal[acKey].createForm) {
-          curActions.push({ 'action': acKey, 'label': acVal[acKey].label, 
'keys': acVal[acKey].createForm.fields })
+          curActions.push({ action: acKey, label: acVal[acKey].label, keys: 
acVal[acKey].createForm.fields })
         } else {
-          curActions.push({ 'action': acKey, 'label': acVal[acKey].label })
+          curActions.push({ action: acKey, label: acVal[acKey].label })
         }
       })
       countActions = countActions + curActions.length
@@ -77,5 +77,5 @@ var loadFields = function (data, prefix) {
       $.extend(actions, recRes.actions)
     }
   })
-  return { 'allFields': allFields, 'columnsOrder': columnsOrder, 'actions': 
actions }
+  return { allFields: allFields, columnsOrder: columnsOrder, actions: actions }
 }
diff --git a/src/components/view/InfoCard.vue b/src/components/view/InfoCard.vue
index 7ac739b..e63a31a 100644
--- a/src/components/view/InfoCard.vue
+++ b/src/components/view/InfoCard.vue
@@ -29,7 +29,7 @@
             </div>
             <slot name="name">
               <h4 class="name">
-                {{ resource.displayname || resource.name }}
+                {{ resource.displayname || resource.name || 
resource.displaytext || resource.hostname || resource.username || 
resource.ipaddress }}
               </h4>
               <console style="margin-left: 10px" :resource="resource" 
size="default" v-if="resource.id" />
             </slot>
diff --git a/src/components/view/ResourceView.vue 
b/src/components/view/ResourceView.vue
index f197628..a1c4bf7 100644
--- a/src/components/view/ResourceView.vue
+++ b/src/components/view/ResourceView.vue
@@ -36,7 +36,7 @@
             v-for="tab in tabs"
             :tab="$t(tab.name)"
             :key="tab.name"
-            v-if="'show' in tab ? tab.show(resource, $route, 
$store.getters.userInfo) : true">
+            v-if="showHideTab(tab)">
             <component :is="tab.component" :resource="resource" 
:loading="loading" :tab="activeTab" />
           </a-tab-pane>
         </a-tabs>
@@ -49,6 +49,7 @@
 import DetailsTab from '@/components/view/DetailsTab'
 import InfoCard from '@/components/view/InfoCard'
 import ResourceLayout from '@/layouts/ResourceLayout'
+import { api } from '@/api'
 
 export default {
   name: 'ResourceView',
@@ -77,12 +78,45 @@ export default {
   },
   data () {
     return {
-      activeTab: ''
+      activeTab: '',
+      networkService: null,
+      vpnEnabled: false
+    }
+  },
+  watch: {
+    resource: function (newItem, oldItem) {
+      this.resource = newItem
+      if (newItem.id === oldItem.id) return
+
+      if (this.resource.associatednetworkid) {
+        api('listNetworks', { id: this.resource.associatednetworkid 
}).then(response => {
+          this.networkService = response.listnetworksresponse.network[0]
+        })
+      }
+
+      if (this.resource.id && this.resource.ipaddress) {
+        api('listRemoteAccessVpns', {
+          publicipid: this.resource.id,
+          listAll: true
+        }).then(response => {
+          this.vpnEnabled = 
response.listremoteaccessvpnsresponse.remoteaccessvpn && 
response.listremoteaccessvpnsresponse.remoteaccessvpn.length > 0
+        })
+      }
     }
   },
   methods: {
     onTabChange (key) {
       this.activeTab = key
+    },
+    showHideTab (tab) {
+      if ('networkServiceFilter' in tab) {
+        return this.networkService && this.networkService.service &&
+          tab.networkServiceFilter(this.networkService.service)
+      } else if ('show' in tab) {
+        return tab.show(this.resource, this.$route, 
this.$store.getters.userInfo)
+      } else {
+        return true
+      }
     }
   }
 }
diff --git a/src/config/section/network.js b/src/config/section/network.js
index 6fbb781..a2a429a 100644
--- a/src/config/section/network.js
+++ b/src/config/section/network.js
@@ -45,8 +45,9 @@ export default {
         name: 'details',
         component: () => import('@/components/view/DetailsTab.vue')
       }, {
-        name: 'egress-rules',
-        component: () => import('@/views/network/EgressConfigure.vue')
+        name: 'Egress Rules',
+        component: () => import('@/views/network/EgressConfigure.vue'),
+        show: () => true
       }],
       actions: [
         {
@@ -227,14 +228,23 @@ export default {
       columns: ['ipaddress', 'state', 'associatednetworkname', 
'virtualmachinename', 'allocated', 'account', 'zonename'],
       details: ['ipaddress', 'id', 'associatednetworkname', 
'virtualmachinename', 'networkid', 'issourcenat', 'isstaticnat', 
'virtualmachinename', 'vmipaddress', 'vlan', 'allocated', 'account', 
'zonename'],
       tabs: [{
-        name: 'configure',
-        component: () => import('@/views/network/IpConfigure.vue')
-      }, {
-        name: 'vpn',
-        component: () => import('@/views/network/VpnDetails.vue')
-      }, {
         name: 'details',
         component: () => import('@/components/view/DetailsTab.vue')
+      }, {
+        name: 'Firewall',
+        component: () => import('@/views/network/FirewallRules.vue'),
+        networkServiceFilter: networkService => networkService.filter(x => 
x.name === 'Firewall').length > 0
+      }, {
+        name: 'Port Forwarding',
+        component: () => import('@/views/network/PortForwarding.vue'),
+        networkServiceFilter: networkService => networkService.filter(x => 
x.name === 'PortForwarding').length > 0
+      }, {
+        name: 'Load Balancing',
+        component: () => import('@/views/network/LoadBalancing.vue'),
+        networkServiceFilter: networkService => networkService.filter(x => 
x.name === 'Lb').length > 0
+      }, {
+        name: 'VPN',
+        component: () => import('@/views/network/VpnDetails.vue')
       }],
       actions: [
         {
@@ -245,39 +255,6 @@ export default {
           args: ['networkid']
         },
         {
-          api: 'createRemoteAccessVpn',
-          icon: 'link',
-          label: 'Enable Remote Access VPN',
-          dataView: true,
-          args: ['publicipid', 'domainid', 'account'],
-          mapping: {
-            publicipid: {
-              value: (record) => { return record.id }
-            },
-            domainid: {
-              value: (record) => { return record.domainid }
-            },
-            account: {
-              value: (record) => { return record.account }
-            }
-          }
-        },
-        {
-          api: 'deleteRemoteAccessVpn',
-          icon: 'disconnect',
-          label: 'Disable Remove Access VPN',
-          dataView: true,
-          args: ['publicipid', 'domainid'],
-          mapping: {
-            publicipid: {
-              value: (record) => { return record.id }
-            },
-            domainid: {
-              value: (record) => { return record.domainid }
-            }
-          }
-        },
-        {
           api: 'enableStaticNat',
           icon: 'plus-circle',
           label: 'Enable Static NAT',
@@ -306,7 +283,7 @@ export default {
         {
           api: 'disassociateIpAddress',
           icon: 'delete',
-          label: 'Delete IP',
+          label: 'Release IP',
           dataView: true,
           show: (record) => { return !record.issourcenat }
         }
diff --git a/src/locales/en.json b/src/locales/en.json
index 86d1684..f4abfaf 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -2,6 +2,7 @@
 "Accounts": "Accounts",
 "Affinity Groups": "Affinity Groups",
 "Alerts": "Alerts",
+"cancel": "Cancel",
 "CPU Sockets": "CPU Sockets",
 "Cloudian Storage": "Cloudian Storage",
 "Clusters": "Clusters",
@@ -13,6 +14,7 @@
 "Dashboard": "Dashboard",
 "Disk Offerings": "Disk Offerings",
 "Domains": "Domains",
+"done": "Done",
 "Events": "Events",
 "Global Settings": "Global Settings",
 "Hosts": "Hosts",
@@ -77,6 +79,7 @@
 "agentUsername": "Agent Username",
 "agentstate": "Agent State",
 "algorithm": "Algorithm",
+"all": "All",
 "allocatediops": "IOPS Allocated",
 "allocationstate": "Allocation State",
 "annotation": "Annotation",
@@ -256,6 +259,7 @@
 "hypervisortype": "Hypervisor",
 "hypervisorversion": "Hypervisor version",
 "hypervnetworklabel": "HyperV Traffic Label",
+"icmp": "ICMP",
 "icmpcode": "ICMP Code",
 "icmptype": "ICMP Type",
 "id": "ID",
@@ -340,6 +344,7 @@
 "label.action.cancel.maintenance.mode": "Cancel Maintenance Mode",
 "label.action.change.password": "Change Password",
 "label.action.configure.samlauthorization": "Configure SAML SSO Authorization",
+"label.action.configure.stickiness": "Stickiness",
 "label.action.copy.ISO": "Copy ISO",
 "label.action.copy.template": "Copy Template",
 "label.action.create.volume": "Create Volume",
@@ -430,6 +435,8 @@
 "label.add.OpenDaylight.device": "Add OpenDaylight Controller",
 "label.add.PA.device": "Add Palo Alto device",
 "label.add.SRX.device": "Add SRX device",
+"label.add.VM": "Add VM",
+"label.add.VMs": "Add VMs",
 "label.add.VM.to.tier": "Add VM to tier",
 "label.add.account": "Add Account",
 "label.add.acl.list": "Add ACL List",
@@ -460,8 +467,10 @@
 "label.add.primary.storage": "Add Primary Storage",
 "label.add.region": "Add Region",
 "label.add.role": "Add Role",
+"label.add.rule": "Add Rule",
 "label.add.secondary.storage": "Add Secondary Storage",
 "label.add.security.group": "Add Security Group",
+"label.add.setting": "Add Setting",
 "label.add.system.service.offering": "Add System Service Offering",
 "label.add.ucs.manager": "Add UCS Manager",
 "label.add.user": "Add User",
@@ -840,6 +849,7 @@
 "secretkey": "Secret Key",
 "securityGroups": "Security Groups",
 "securitygroup": "Security Group",
+"select": "Select",
 "sent": "Date",
 "sentbytes": "Bytes Sent",
 "server": "Server",
@@ -872,6 +882,7 @@
 "snmpCommunity": "SNMP Community",
 "snmpPort": "SNMP Port",
 "sockettimeout": "Socket Timeout",
+"sourcecidr": "Source CIDR",
 "sourceNat": "Source NAT",
 "sourceipaddress": "Source IP Address",
 "sourceport": "Source Port",
@@ -905,6 +916,7 @@
 "systemvmtype": "System VM Type",
 "tags": "Tags",
 "tariffValue": "Tariff Value",
+"tcp": "TCP",
 "template": "Select a template",
 "templateFileUpload": "Local file",
 "templateLimit": "Template Limits",
@@ -926,6 +938,7 @@
 "traffictype": "Traffic Type",
 "transportzoneuuid": "Transport Zone Uuid",
 "type": "Type",
+"udp": "UDP",
 "unit": "Usage Unit",
 "url": "URL",
 "usageName": "Usage Type",
@@ -964,6 +977,7 @@
 "vlanRange": "VLAN/VNI Range",
 "vlanname": "VLAN",
 "vlanrange": "VLAN/VNI Range",
+"vm": "VM",
 "vmLimit": "Instance Limits",
 "vmTotal": "Instances",
 "vmdisplayname": "VM display name",
diff --git a/src/utils/device.js b/src/utils/device.js
index 8c31370..731270d 100644
--- a/src/utils/device.js
+++ b/src/utils/device.js
@@ -45,6 +45,6 @@ export const deviceEnquire = function (callback) {
   // screen and (max-width: 1087.99px)
   enquireJs
     .register('screen and (max-width: 576px)', matchMobile)
-    .register('screen and (min-width: 576px) and (max-width: 1366px)', 
matchTablet)
-    .register('screen and (min-width: 1367px)', matchDesktop)
+    .register('screen and (min-width: 576px) and (max-width: 1280px)', 
matchTablet)
+    .register('screen and (min-width: 1281px)', matchDesktop)
 }
diff --git a/src/views/network/EgressConfigure.vue 
b/src/views/network/EgressConfigure.vue
index 1bd3670..22d9db6 100644
--- a/src/views/network/EgressConfigure.vue
+++ b/src/views/network/EgressConfigure.vue
@@ -17,24 +17,287 @@
 
 <template>
   <div>
-    TODO: Egress view for isolated network
+    <div>
+      <div class="form">
+        <div class="form__item">
+          <div class="form__label">Source CIDR</div>
+          <a-input v-model="newRule.cidrlist"></a-input>
+        </div>
+        <div class="form__item">
+          <div class="form__label">Destination CIDR</div>
+          <a-input v-model="newRule.destcidrlist"></a-input>
+        </div>
+        <div class="form__item">
+          <div class="form__label">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>
+            <a-select-option value="icmp">ICMP</a-select-option>
+            <a-select-option value="all">All</a-select-option>
+          </a-select>
+        </div>
+        <div v-show="newRule.protocol === 'tcp' || newRule.protocol === 'udp'" 
class="form__item">
+          <div class="form__label">Start Port</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>
+          <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>
+          <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>
+          <a-input v-model="newRule.icmpcode"></a-input>
+        </div>
+        <div class="form__item">
+          <a-button type="primary" icon="plus" @click="addRule">{{ $t('add') 
}}</a-button>
+        </div>
+      </div>
+    </div>
+
+    <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>
   </div>
 </template>
 
 <script>
+import { api } from '@/api'
 
 export default {
-  name: '',
-  components: {
+  props: {
+    resource: {
+      type: Object,
+      required: true
+    }
   },
   data () {
     return {
+      loading: true,
+      egressRules: [],
+      newRule: {
+        protocol: 'tcp',
+        cidrlist: null,
+        destcidrlist: null,
+        networkid: this.resource.id,
+        icmptype: null,
+        icmpcode: null,
+        startport: null,
+        endport: null
+      }
+    }
+  },
+  mounted () {
+    this.fetchData()
+  },
+  filters: {
+    capitalise: val => {
+      if (val === 'all') return 'All'
+      return val.toUpperCase()
+    }
+  },
+  watch: {
+    resource: function (newItem, oldItem) {
+      if (!newItem || !newItem.id) {
+        return
+      }
+      this.resource = newItem
+      this.fetchData()
     }
   },
   methods: {
+    fetchData () {
+      this.loading = true
+      api('listEgressFirewallRules', {
+        listAll: true,
+        networkid: this.resource.id
+      }).then(response => {
+        this.egressRules = 
response.listegressfirewallrulesresponse.firewallrule
+        this.loading = false
+      })
+    },
+    deleteRule (rule) {
+      this.loading = true
+      api('deleteEgressFirewallRule', { id: rule.id }).then(response => {
+        this.$pollJob({
+          jobId: response.deleteegressfirewallruleresponse.jobid,
+          successMessage: `Successfully removed Egress rule`,
+          successMethod: () => this.fetchData(),
+          errorMessage: 'Removing Egress rule failed',
+          errorMethod: () => this.fetchData(),
+          loadingMessage: `Deleting Egress rule...`,
+          catchMessage: 'Error encountered while fetching async job result',
+          catchMethod: () => this.fetchData()
+        })
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.errorresponse.errortext
+        })
+        this.fetchData()
+      })
+    },
+    addRule () {
+      this.loading = true
+      api('createEgressFirewallRule', { ...this.newRule }).then(response => {
+        this.$pollJob({
+          jobId: response.createegressfirewallruleresponse.jobid,
+          successMessage: `Successfully added new Egress rule`,
+          successMethod: () => {
+            this.resetAllRules()
+            this.fetchData()
+          },
+          errorMessage: 'Adding new Egress rule failed',
+          errorMethod: () => {
+            this.resetAllRules()
+            this.fetchData()
+          },
+          loadingMessage: `Adding new Egress rule...`,
+          catchMessage: 'Error encountered while fetching async job result',
+          catchMethod: () => {
+            this.resetAllRules()
+            this.fetchData()
+          }
+        })
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: 
error.response.data.createegressfirewallruleresponse.errortext
+        })
+        this.resetAllRules()
+        this.fetchData()
+      })
+    },
+    resetAllRules () {
+      this.newRule.protocol = 'tcp'
+      this.newRule.cidrlist = null
+      this.newRule.destcidrlist = null
+      this.newRule.networkid = this.resource.id
+      this.resetRulePorts()
+    },
+    resetRulePorts () {
+      this.newRule.icmptype = null
+      this.newRule.icmpcode = null
+      this.newRule.startport = null
+      this.newRule.endport = null
+    }
   }
 }
 </script>
 
-<style scoped>
+<style scoped lang="scss">
+  .rule {
+
+    &-container {
+      display: flex;
+      width: 100%;
+      flex-wrap: wrap;
+      margin-right: -20px;
+      margin-bottom: -10px;
+    }
+
+    &__item {
+      padding-right: 20px;
+      margin-bottom: 20px;
+
+      @media (min-width: 760px) {
+        flex: 1;
+      }
+
+    }
+
+    &__title {
+      font-weight: bold;
+    }
+
+  }
+
+  .add-btn {
+    width: 100%;
+    padding-top: 15px;
+    padding-bottom: 15px;
+    height: auto;
+  }
+
+  .add-actions {
+    display: flex;
+    justify-content: flex-end;
+    margin-right: -20px;
+    margin-bottom: 20px;
+
+    @media (min-width: 760px) {
+      margin-top: 20px;
+    }
+
+    button {
+      margin-right: 20px;
+    }
+
+  }
+
+  .form {
+    display: flex;
+    margin-right: -20px;
+    margin-bottom: 20px;
+    flex-direction: column;
+    align-items: flex-end;
+
+    @media (min-width: 760px) {
+      flex-direction: row;
+    }
+
+    &__item {
+      display: flex;
+      flex-direction: column;
+      flex: 1;
+      padding-right: 20px;
+      margin-bottom: 20px;
+
+      @media (min-width: 760px) {
+        margin-bottom: 0;
+      }
+
+      input,
+      .ant-select {
+        margin-top: auto;
+      }
+
+    }
+
+    &__label {
+      font-weight: bold;
+    }
+
+  }
 </style>
diff --git a/src/views/network/FirewallRules.vue 
b/src/views/network/FirewallRules.vue
new file mode 100644
index 0000000..a0a2479
--- /dev/null
+++ b/src/views/network/FirewallRules.vue
@@ -0,0 +1,476 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+<template>
+  <div>
+    <div>
+      <div class="form">
+        <div class="form__item">
+          <div class="form__label">{{ $t('sourcecidr') }}</div>
+          <a-input v-model="newRule.cidrlist"></a-input>
+        </div>
+        <div class="form__item">
+          <div class="form__label">{{ $t('protocol') }}</div>
+          <a-select v-model="newRule.protocol" style="width: 100%;" 
@change="resetRulePorts">
+            <a-select-option value="tcp">{{ $t('tcp') }}</a-select-option>
+            <a-select-option value="udp">{{ $t('udp') }}</a-select-option>
+            <a-select-option value="icmp">{{ $t('icmp') }}</a-select-option>
+          </a-select>
+        </div>
+        <div v-show="newRule.protocol === 'tcp' || newRule.protocol === 'udp'" 
class="form__item">
+          <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">{{ $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">{{ $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">{{ $t('icmpcode') }}</div>
+          <a-input v-model="newRule.icmpcode"></a-input>
+        </div>
+        <div class="form__item" style="margin-left: auto;">
+          <a-button type="primary" @click="addRule">{{ $t('add') }}</a-button>
+        </div>
+      </div>
+    </div>
+
+    <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>
+        </div>
+      </a-list-item>
+    </a-list>
+
+    <a-modal title="Edit Tags" v-model="tagsModalVisible" :footer="null" 
:afterClose="closeModal">
+      <div class="add-tags">
+        <div class="add-tags__input">
+          <p class="add-tags__label">{{ $t('key') }}</p>
+          <a-input v-model="newTag.key"></a-input>
+        </div>
+        <div class="add-tags__input">
+          <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>
+      </div>
+
+      <a-divider></a-divider>
+
+      <div class="tags-container">
+        <div class="tags" v-for="(tag, index) in tags" :key="index">
+          <a-tag :key="index" :closable="true" :afterClose="() => 
handleDeleteTag(tag)">
+            {{ tag.key }} = {{ tag.value }}
+          </a-tag>
+        </div>
+      </div>
+
+      <a-button class="add-tags-done" @click="tagsModalVisible = false" 
type="primary">{{ $t('done') }}</a-button>
+    </a-modal>
+
+  </div>
+</template>
+
+<script>
+import { api } from '@/api'
+
+export default {
+  props: {
+    resource: {
+      type: Object,
+      required: true
+    }
+  },
+  inject: ['parentFetchData', 'parentToggleLoading'],
+  data () {
+    return {
+      loading: true,
+      firewallRules: [],
+      newRule: {
+        protocol: 'tcp',
+        cidrlist: null,
+        ipaddressid: this.resource.id,
+        icmptype: null,
+        icmpcode: null,
+        startport: null,
+        endport: null
+      },
+      tagsModalVisible: false,
+      selectedRule: null,
+      tags: [],
+      newTag: {
+        key: null,
+        value: null
+      }
+    }
+  },
+  mounted () {
+    this.fetchData()
+  },
+  filters: {
+    capitalise: val => {
+      if (val === 'all') return 'All'
+      return val.toUpperCase()
+    }
+  },
+  watch: {
+    resource: function (newItem, oldItem) {
+      if (!newItem || !newItem.id) {
+        return
+      }
+      this.resource = newItem
+      this.fetchData()
+    }
+  },
+  methods: {
+    fetchData () {
+      this.loading = true
+      api('listFirewallRules', {
+        listAll: true,
+        ipaddressid: this.resource.id
+      }).then(response => {
+        this.firewallRules = response.listfirewallrulesresponse.firewallrule
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.errorresponse.errortext
+        })
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    deleteRule (rule) {
+      this.loading = true
+      api('deleteFirewallRule', { id: rule.id }).then(response => {
+        this.$pollJob({
+          jobId: response.deletefirewallruleresponse.jobid,
+          successMessage: `Successfully removed Firewall rule`,
+          successMethod: () => this.fetchData(),
+          errorMessage: 'Removing Firewall rule failed',
+          errorMethod: () => this.fetchData(),
+          loadingMessage: `Deleting Firewall rule...`,
+          catchMessage: 'Error encountered while fetching async job result',
+          catchMethod: () => this.fetchData()
+        })
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.errorresponse.errortext
+        })
+        this.fetchData()
+      })
+    },
+    addRule () {
+      this.loading = true
+      api('createFirewallRule', { ...this.newRule }).then(response => {
+        this.$pollJob({
+          jobId: response.createfirewallruleresponse.jobid,
+          successMessage: `Successfully added new Firewall rule`,
+          successMethod: () => {
+            this.resetAllRules()
+            this.fetchData()
+          },
+          errorMessage: 'Adding new Firewall rule failed',
+          errorMethod: () => {
+            this.resetAllRules()
+            this.fetchData()
+          },
+          loadingMessage: `Adding new Firewall rule...`,
+          catchMessage: 'Error encountered while fetching async job result',
+          catchMethod: () => {
+            this.resetAllRules()
+            this.fetchData()
+          }
+        })
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.createfirewallruleresponse.errortext
+        })
+        this.resetAllRules()
+        this.fetchData()
+      })
+    },
+    resetAllRules () {
+      this.newRule.protocol = 'tcp'
+      this.newRule.cidrlist = null
+      this.newRule.networkid = this.resource.id
+      this.resetRulePorts()
+    },
+    resetRulePorts () {
+      this.newRule.icmptype = null
+      this.newRule.icmpcode = null
+      this.newRule.startport = null
+      this.newRule.endport = null
+    },
+    closeModal () {
+      this.selectedRule = null
+      this.tagsModalVisible = false
+      this.newTag.key = null
+      this.newTag.value = null
+    },
+    openTagsModal (id) {
+      this.selectedRule = id
+      this.tagsModalVisible = true
+      api('listTags', {
+        resourceId: id,
+        resourceType: 'FirewallRule',
+        listAll: true
+      }).then(response => {
+        this.tags = response.listtagsresponse.tag
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.errorresponse.errortext
+        })
+        this.closeModal()
+      })
+    },
+    handleAddTag () {
+      api('createTags', {
+        'tags[0].key': this.newTag.key,
+        'tags[0].value': this.newTag.value,
+        resourceIds: this.selectedRule,
+        resourceType: 'FirewallRule'
+      }).then(response => {
+        this.$pollJob({
+          jobId: response.createtagsresponse.jobid,
+          successMessage: `Successfully added tag`,
+          successMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.openTagsModal(this.selectedRule)
+          },
+          errorMessage: 'Failed to add new tag',
+          errorMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.closeModal()
+          },
+          loadingMessage: `Adding tag...`,
+          catchMessage: 'Error encountered while fetching async job result',
+          catchMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.closeModal()
+          }
+        })
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.createtagsresponse.errortext
+        })
+        this.closeModal()
+      })
+    },
+    handleDeleteTag (tag) {
+      api('deleteTags', {
+        'tags[0].key': tag.key,
+        'tags[0].value': tag.value,
+        resourceIds: this.selectedRule,
+        resourceType: 'FirewallRule'
+      }).then(response => {
+        this.$pollJob({
+          jobId: response.deletetagsresponse.jobid,
+          successMessage: `Successfully removed tag`,
+          successMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.openTagsModal(this.selectedRule)
+          },
+          errorMessage: 'Failed to remove tag',
+          errorMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.closeModal()
+          },
+          loadingMessage: `Removing tag...`,
+          catchMessage: 'Error encountered while fetching async job result',
+          catchMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.closeModal()
+          }
+        })
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.deletetagsresponse.errortext
+        })
+        this.closeModal()
+      })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+  .rule {
+
+    &-container {
+      display: flex;
+      width: 100%;
+      flex-wrap: wrap;
+      margin-right: -20px;
+      margin-bottom: -10px;
+    }
+
+    &__item {
+      padding-right: 20px;
+      margin-bottom: 20px;
+
+      @media (min-width: 760px) {
+        flex: 1;
+      }
+
+    }
+
+    &__title {
+      font-weight: bold;
+    }
+
+  }
+
+  .add-btn {
+    width: 100%;
+    padding-top: 15px;
+    padding-bottom: 15px;
+    height: auto;
+  }
+
+  .add-actions {
+    display: flex;
+    justify-content: flex-end;
+    margin-right: -20px;
+    margin-bottom: 20px;
+
+    @media (min-width: 760px) {
+      margin-top: 20px;
+    }
+
+    button {
+      margin-right: 20px;
+    }
+
+  }
+
+  .form {
+    display: flex;
+    align-items: flex-end;
+    margin-right: -20px;
+    flex-direction: column;
+    margin-bottom: 20px;
+
+    @media (min-width: 760px) {
+      flex-direction: row;
+    }
+
+    &__item {
+      display: flex;
+      flex-direction: column;
+      /*flex: 1;*/
+      padding-right: 20px;
+      margin-bottom: 20px;
+
+      @media (min-width: 760px) {
+        margin-bottom: 0;
+      }
+
+      input,
+      .ant-select {
+        margin-top: auto;
+      }
+
+    }
+
+    &__label {
+      font-weight: bold;
+    }
+
+  }
+
+  .rule-action {
+    margin-bottom: 20px;
+
+    &:not(:last-of-type) {
+      margin-right: 10px;
+    }
+
+  }
+
+  .tags {
+    margin-bottom: 10px;
+  }
+
+  .add-tags {
+    display: flex;
+    align-items: flex-end;
+    justify-content: space-between;
+
+    &__input {
+      margin-right: 10px;
+    }
+
+    &__label {
+      margin-bottom: 5px;
+      font-weight: bold;
+    }
+
+  }
+
+  .tags-container {
+    display: flex;
+    flex-wrap: wrap;
+    margin-bottom: 10px;
+  }
+
+  .add-tags-done {
+    display: block;
+    margin-left: auto;
+  }
+
+</style>
diff --git a/src/views/network/IngressEgressRuleConfigure.vue 
b/src/views/network/IngressEgressRuleConfigure.vue
index 8d17bba..24b6cfc 100644
--- a/src/views/network/IngressEgressRuleConfigure.vue
+++ b/src/views/network/IngressEgressRuleConfigure.vue
@@ -107,7 +107,7 @@
             okText="Yes"
             cancelText="No"
           >
-            <a-button shape="round" type="danger" icon="close-circle" 
class="rule-action" />
+            <a-button shape="round" type="danger" icon="delete" 
class="rule-action" />
           </a-popconfirm>
         </div>
       </a-list-item>
diff --git a/src/views/network/IpConfigure.vue 
b/src/views/network/IpConfigure.vue
deleted file mode 100644
index d0c6213..0000000
--- a/src/views/network/IpConfigure.vue
+++ /dev/null
@@ -1,40 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-<template>
-  <div>
-    TODO: IP configure view: firewall, pf, lb
-  </div>
-</template>
-
-<script>
-
-export default {
-  name: '',
-  components: {
-  },
-  data () {
-    return {
-    }
-  },
-  methods: {
-  }
-}
-</script>
-
-<style scoped>
-</style>
diff --git a/src/views/network/LoadBalancing.vue 
b/src/views/network/LoadBalancing.vue
new file mode 100644
index 0000000..5f175b6
--- /dev/null
+++ b/src/views/network/LoadBalancing.vue
@@ -0,0 +1,1373 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+<template>
+  <div>
+    <div>
+      <div class="form">
+        <div class="form__item" ref="newRuleName">
+          <div class="form__label"><span class="form__required">*</span>{{ 
$t('name') }}</div>
+          <a-input v-model="newRule.name"></a-input>
+          <span class="error-text">Required</span>
+        </div>
+        <div class="form__item" ref="newRulePublicPort">
+          <div class="form__label"><span class="form__required">*</span>{{ 
$t('publicport') }}</div>
+          <a-input v-model="newRule.publicport"></a-input>
+          <span class="error-text">Required</span>
+        </div>
+        <div class="form__item" ref="newRulePrivatePort">
+          <div class="form__label"><span class="form__required">*</span>{{ 
$t('privateport') }}</div>
+          <a-input v-model="newRule.privateport"></a-input>
+          <span class="error-text">Required</span>
+        </div>
+        <div class="form__item">
+          <div class="form__label">{{ $t('algorithm') }}</div>
+          <a-select v-model="newRule.algorithm">
+            <a-select-option value="roundrobin">Round-robin</a-select-option>
+            <a-select-option value="leastconn">Least 
connections</a-select-option>
+            <a-select-option value="source">Source</a-select-option>
+          </a-select>
+        </div>
+        <div class="form__item">
+          <div class="form__label">{{ $t('protocol') }}</div>
+          <a-select v-model="newRule.protocol" style="min-width: 100px">
+            <a-select-option value="tcp-proxy">TCP Proxy</a-select-option>
+            <a-select-option value="tcp">TCP</a-select-option>
+            <a-select-option value="udp">UDP</a-select-option>
+          </a-select>
+        </div>
+        <div class="form__item">
+          <div class="form__label" style="white-space: nowrap;">{{ 
$t('label.add.VMs') }}</div>
+          <a-button type="primary" @click="handleOpenAddVMModal">Add</a-button>
+        </div>
+      </div>
+    </div>
+
+    <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>
+            </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)" />
+          <a-popconfirm
+            :title="$t('label.delete') + '?'"
+            @confirm="handleDeleteRule(rule)"
+            okText="Yes"
+            cancelText="No"
+          >
+            <a-button shape="circle" type="danger" icon="delete" 
class="rule-action" />
+          </a-popconfirm>
+        </div>
+      </a-list-item>
+    </a-list>
+
+    <a-modal title="Edit Tags" v-model="tagsModalVisible" :footer="null" 
:afterClose="closeModal" class="tags-modal">
+      <span v-show="tagsModalLoading" class="modal-loading">
+        <a-icon type="loading"></a-icon>
+      </span>
+
+      <a-form :form="newTagsForm" class="add-tags" @submit="handleAddTag">
+        <div class="add-tags__input">
+          <p class="add-tags__label">{{ $t('key') }}</p>
+          <a-form-item>
+            <a-input v-decorator="['key', { rules: [{ required: true, message: 
'Please specify a tag key'}] }]" />
+          </a-form-item>
+        </div>
+        <div class="add-tags__input">
+          <p class="add-tags__label">{{ $t('value') }}</p>
+          <a-form-item>
+            <a-input v-decorator="['value', { rules: [{ required: true, 
message: 'Please specify a tag value'}] }]" />
+          </a-form-item>
+        </div>
+        <a-button type="primary" html-type="submit">{{ $t('label.add') 
}}</a-button>
+      </a-form>
+
+      <a-divider></a-divider>
+
+      <div v-show="!tagsModalLoading" class="tags-container">
+        <div class="tags" v-for="(tag, index) in tags" :key="index">
+          <a-tag :key="index" :closable="true" :afterClose="() => 
handleDeleteTag(tag)">
+            {{ tag.key }} = {{ tag.value }}
+          </a-tag>
+        </div>
+      </div>
+
+      <a-button class="add-tags-done" @click="tagsModalVisible = false" 
type="primary">{{ $t('done') }}</a-button>
+    </a-modal>
+
+    <a-modal
+      title="Configure Sticky Policy"
+      v-model="stickinessModalVisible"
+      :footer="null"
+      :afterClose="closeModal"
+      :okButtonProps="{ props: {htmlType: 'submit'}}">
+
+      <span v-show="stickinessModalLoading" class="modal-loading">
+        <a-icon type="loading"></a-icon>
+      </span>
+
+      <a-form :form="stickinessPolicyForm" 
@submit="handleSubmitStickinessForm" class="custom-ant-form">
+        <a-form-item label="Stickiness method">
+          <a-select v-decorator="['methodname']" 
@change="handleStickinessMethodSelectChange">
+            <a-select-option value="LbCookie">LbCookie</a-select-option>
+            <a-select-option value="AppCookie">AppCookie</a-select-option>
+            <a-select-option value="SourceBased">SourceBased</a-select-option>
+            <a-select-option value="none">None</a-select-option>
+          </a-select>
+        </a-form-item>
+        <a-form-item
+          label="Sticky Name"
+          v-show="stickinessPolicyMethod === 'LbCookie' || 
stickinessPolicyMethod ===
+            'AppCookie' || stickinessPolicyMethod === 'SourceBased'">
+          <a-input v-decorator="['name', { rules: [{ required: true, message: 
'Please specify a sticky name'}] }]" />
+        </a-form-item>
+        <a-form-item
+          label="Cookie name"
+          v-show="stickinessPolicyMethod === 'LbCookie' || 
stickinessPolicyMethod ===
+            'AppCookie'">
+          <a-input v-decorator="['cookieName']" />
+        </a-form-item>
+        <a-form-item
+          label="Mode"
+          v-show="stickinessPolicyMethod === 'LbCookie' || 
stickinessPolicyMethod ===
+            'AppCookie'">
+          <a-input v-decorator="['mode']" />
+        </a-form-item>
+        <a-form-item label="No cache" v-show="stickinessPolicyMethod === 
'LbCookie'">
+          <a-checkbox v-decorator="['nocache']" 
v-model="stickinessNoCache"></a-checkbox>
+        </a-form-item>
+        <a-form-item label="Indirect" v-show="stickinessPolicyMethod === 
'LbCookie'">
+          <a-checkbox v-decorator="['indirect']" 
v-model="stickinessIndirect"></a-checkbox>
+        </a-form-item>
+        <a-form-item label="Post only" v-show="stickinessPolicyMethod === 
'LbCookie'">
+          <a-checkbox v-decorator="['postonly']" 
v-model="stickinessPostOnly"></a-checkbox>
+        </a-form-item>
+        <a-form-item label="Domain" v-show="stickinessPolicyMethod === 
'LbCookie'">
+          <a-input v-decorator="['domain']" />
+        </a-form-item>
+        <a-form-item label="Length" v-show="stickinessPolicyMethod === 
'AppCookie'">
+          <a-input v-decorator="['length']" type="number" />
+        </a-form-item>
+        <a-form-item label="Hold time" v-show="stickinessPolicyMethod === 
'AppCookie'">
+          <a-input v-decorator="['holdtime']" type="number" />
+        </a-form-item>
+        <a-form-item label="Request learn" v-show="stickinessPolicyMethod === 
'AppCookie'">
+          <a-checkbox v-decorator="['requestLearn']" 
v-model="stickinessRequestLearn"></a-checkbox>
+        </a-form-item>
+        <a-form-item label="Prefix" v-show="stickinessPolicyMethod === 
'AppCookie'">
+          <a-checkbox v-decorator="['prefix']" 
v-model="stickinessPrefix"></a-checkbox>
+        </a-form-item>
+        <a-form-item label="Table size" v-show="stickinessPolicyMethod === 
'SourceBased'">
+          <a-input v-decorator="['tablesize']" />
+        </a-form-item>
+        <a-form-item label="Expires" v-show="stickinessPolicyMethod === 
'SourceBased'">
+          <a-input v-decorator="['expire']" />
+        </a-form-item>
+        <a-button type="primary" html-type="submit">OK</a-button>
+      </a-form>
+    </a-modal>
+
+    <a-modal title="Edit rule" v-model="editRuleModalVisible" 
:afterClose="closeModal" @ok="handleSubmitEditForm">
+      <span v-show="editRuleModalLoading" class="modal-loading">
+        <a-icon type="loading"></a-icon>
+      </span>
+
+      <div class="edit-rule" v-if="selectedRule">
+        <div class="edit-rule__item">
+          <p class="edit-rule__label">{{ $t('name') }}</p>
+          <a-input v-model="editRuleDetails.name" />
+        </div>
+        <div class="edit-rule__item">
+          <p class="edit-rule__label">{{ $t('algorithm') }}</p>
+          <a-select v-model="editRuleDetails.algorithm">
+            <a-select-option value="roundrobin">Round-robin</a-select-option>
+            <a-select-option value="leastconn">Least 
connections</a-select-option>
+            <a-select-option value="source">Source</a-select-option>
+          </a-select>
+        </div>
+        <div class="edit-rule__item">
+          <p class="edit-rule__label">{{ $t('protocol') }}</p>
+          <a-select v-model="editRuleDetails.protocol">
+            <a-select-option value="tcp-proxy">TCP proxy</a-select-option>
+            <a-select-option value="tcp">TCP</a-select-option>
+            <a-select-option value="udp">UDP</a-select-option>
+          </a-select>
+        </div>
+      </div>
+    </a-modal>
+
+    <a-modal
+      title="Add VMs"
+      v-model="addVmModalVisible"
+      class="vm-modal"
+      width="60vw"
+      @ok="handleAddNewRule"
+      :okButtonProps="{ props:
+        {disabled: newRule.virtualmachineid === [] } }"
+      @cancel="closeModal"
+    >
+
+      <a-icon v-if="addVmModalLoading" type="loading"></a-icon>
+
+      <div v-else>
+        <div class="vm-modal__header">
+          <span style="min-width: 200px;">{{ $t('name') }}</span>
+          <span>{{ $t('instancename') }}</span>
+          <span>{{ $t('displayname') }}</span>
+          <span>{{ $t('ip') }}</span>
+          <span>{{ $t('account') }}</span>
+          <span>{{ $t('zonenamelabel') }}</span>
+          <span>{{ $t('state') }}</span>
+          <span>{{ $t('select') }}</span>
+        </div>
+
+        <a-checkbox-group style="width: 100%;">
+          <div v-for="(vm, index) in vms" :key="index" class="vm-modal__item">
+            <span style="min-width: 200px;">
+              <span>
+                {{ vm.name }}
+              </span>
+              <a-icon v-if="addVmModalNicLoading" type="loading"></a-icon>
+              <a-select
+                v-else-if="!addVmModalNicLoading && 
newRule.virtualmachineid[index] === vm.id"
+                mode="multiple"
+                v-model="newRule.vmguestip[index]"
+              >
+                <a-select-option v-for="(nic, nicIndex) in nics[index]" 
:key="nic" :value="nic">
+                  {{ nic }}{{ nicIndex === 0 ? ' (Primary)' : null }}
+                </a-select-option>
+              </a-select>
+            </span>
+            <span>{{ vm.instancename }}</span>
+            <span>{{ vm.displayname }}</span>
+            <span></span>
+            <span>{{ vm.account }}</span>
+            <span>{{ vm.zonename }}</span>
+            <span>{{ vm.state }}</span>
+            <a-checkbox :value="vm.id" @change="e => fetchNics(e, index)" />
+          </div>
+        </a-checkbox-group>
+      </div>
+
+    </a-modal>
+
+  </div>
+</template>
+
+<script>
+import { api } from '@/api'
+import Status from '@/components/widgets/Status'
+
+export default {
+  name: 'LoadBalancing',
+  components: {
+    Status
+  },
+  props: {
+    resource: {
+      type: Object,
+      required: true
+    }
+  },
+  inject: ['parentFetchData', 'parentToggleLoading'],
+  data () {
+    return {
+      loading: true,
+      lbRules: [],
+      newTagsForm: this.$form.createForm(this),
+      tagsModalVisible: false,
+      tagsModalLoading: false,
+      tags: [],
+      selectedRule: null,
+      stickinessModalVisible: false,
+      stickinessPolicies: [],
+      stickinessPolicyForm: this.$form.createForm(this),
+      stickinessModalLoading: false,
+      selectedStickinessPolicy: null,
+      stickinessPolicyMethod: 'LbCookie',
+      stickinessNoCache: null,
+      stickinessIndirect: null,
+      stickinessPostOnly: null,
+      stickinessRequestLearn: null,
+      stickinessPrefix: null,
+      editRuleModalVisible: false,
+      editRuleModalLoading: false,
+      editRuleDetails: {
+        name: '',
+        algorithm: '',
+        protocol: ''
+      },
+      newRule: {
+        algorithm: 'roundrobin',
+        name: '',
+        privateport: '',
+        publicport: '',
+        protocol: 'tcp',
+        virtualmachineid: [],
+        vmguestip: []
+      },
+      addVmModalVisible: false,
+      addVmModalLoading: false,
+      addVmModalNicLoading: false,
+      vms: [],
+      nics: []
+    }
+  },
+  mounted () {
+    this.fetchData()
+  },
+  watch: {
+    resource: function (newItem, oldItem) {
+      if (!newItem || !newItem.id) {
+        return
+      }
+      this.resource = newItem
+      this.fetchData()
+    }
+  },
+  filters: {
+    capitalise: val => {
+      if (val === 'all') return 'All'
+      return val.toUpperCase()
+    }
+  },
+  methods: {
+    fetchData () {
+      this.loading = true
+      this.lbRules = []
+      this.stickinessPolicies = []
+      api('listLoadBalancerRules', {
+        listAll: true,
+        publicipid: this.resource.id
+      }).then(response => {
+        this.lbRules = response.listloadbalancerrulesresponse.loadbalancerrule
+          ? response.listloadbalancerrulesresponse.loadbalancerrule
+          : []
+      }).then(() => {
+        if (this.lbRules.length > 0) {
+          setTimeout(() => {
+            this.fetchLBRuleInstances()
+          }, 100)
+          this.fetchLBStickinessPolicies()
+          return
+        }
+        this.loading = false
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.errorresponse.errortext
+        })
+        this.loading = false
+      })
+    },
+    fetchLBRuleInstances () {
+      for (const rule of this.lbRules) {
+        this.loading = true
+        api('listLoadBalancerRuleInstances', {
+          listAll: true,
+          lbvmips: true,
+          id: rule.id
+        }).then(response => {
+          this.$set(rule, 'ruleInstances', 
response.listloadbalancerruleinstancesresponse.lbrulevmidip)
+        }).catch(error => {
+          this.$notification.error({
+            message: `Error ${error.response.status}`,
+            description: error.response.data.errorresponse.errortext
+          })
+        }).finally(() => {
+          this.loading = false
+        })
+      }
+    },
+    fetchLBStickinessPolicies () {
+      this.loading = true
+      this.lbRules.forEach(rule => {
+        api('listLBStickinessPolicies', {
+          listAll: true,
+          lbruleid: rule.id
+        }).then(response => {
+          
this.stickinessPolicies.push(...response.listlbstickinesspoliciesresponse.stickinesspolicies)
+        }).catch(error => {
+          this.$notification.error({
+            message: `Error ${error.response.status}`,
+            description: error.response.data.errorresponse.errortext
+          })
+        }).finally(() => {
+          this.loading = false
+        })
+      })
+    },
+    returnAlgorithmName (name) {
+      switch (name) {
+        case 'leastconn':
+          return 'Least connections'
+        case 'roundrobin' :
+          return 'Round-robin'
+        case 'source':
+          return 'Source'
+        default :
+          return ''
+      }
+    },
+    returnStickinessLabel (id) {
+      const match = this.stickinessPolicies.filter(policy => policy.lbruleid 
=== id)
+      if (match.length > 0 && match[0].stickinesspolicy.length > 0) {
+        return match[0].stickinesspolicy[0].methodname
+      }
+      return 'Configure'
+    },
+    openTagsModal (id) {
+      this.tagsModalLoading = true
+      this.tagsModalVisible = true
+      this.tags = []
+      this.selectedRule = id
+      this.newTagsForm.resetFields()
+      api('listTags', {
+        resourceId: id,
+        resourceType: 'LoadBalancer',
+        listAll: true
+      }).then(response => {
+        this.tags = response.listtagsresponse.tag
+        this.tagsModalLoading = false
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.errorresponse.errortext
+        })
+        this.closeModal()
+      })
+    },
+    handleAddTag (e) {
+      this.tagsModalLoading = true
+
+      e.preventDefault()
+      this.newTagsForm.validateFields((err, values) => {
+        if (err) {
+          this.tagsModalLoading = false
+          return
+        }
+
+        api('createTags', {
+          'tags[0].key': values.key,
+          'tags[0].value': values.value,
+          resourceIds: this.selectedRule,
+          resourceType: 'LoadBalancer'
+        }).then(response => {
+          this.$pollJob({
+            jobId: response.createtagsresponse.jobid,
+            successMessage: `Successfully added tag`,
+            successMethod: () => {
+              this.parentFetchData()
+              this.parentToggleLoading()
+              this.openTagsModal(this.selectedRule)
+            },
+            errorMessage: 'Failed to add new tag',
+            errorMethod: () => {
+              this.parentFetchData()
+              this.parentToggleLoading()
+              this.closeModal()
+            },
+            loadingMessage: `Adding tag...`,
+            catchMessage: 'Error encountered while fetching async job result',
+            catchMethod: () => {
+              this.parentFetchData()
+              this.parentToggleLoading()
+              this.closeModal()
+            }
+          })
+        }).catch(error => {
+          this.$notification.error({
+            message: `Error ${error.response.status}`,
+            description: error.response.data.createtagsresponse.errortext
+          })
+          this.closeModal()
+        })
+      })
+    },
+    handleDeleteTag (tag) {
+      this.tagsModalLoading = true
+      api('deleteTags', {
+        'tags[0].key': tag.key,
+        'tags[0].value': tag.value,
+        resourceIds: tag.resourceid,
+        resourceType: 'LoadBalancer'
+      }).then(response => {
+        this.$pollJob({
+          jobId: response.deletetagsresponse.jobid,
+          successMessage: `Successfully removed tag`,
+          successMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.openTagsModal(this.selectedRule)
+          },
+          errorMessage: 'Failed to remove tag',
+          errorMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.closeModal()
+          },
+          loadingMessage: `Removing tag...`,
+          catchMessage: 'Error encountered while fetching async job result',
+          catchMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.closeModal()
+          }
+        })
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.deletetagsresponse.errortext
+        })
+        this.closeModal()
+      })
+    },
+    openStickinessModal (id) {
+      this.stickinessModalVisible = true
+      this.selectedRule = id
+      const match = this.stickinessPolicies.find(policy => policy.lbruleid === 
id)
+
+      if (match && match.stickinesspolicy.length > 0) {
+        this.selectedStickinessPolicy = match.stickinesspolicy[0]
+        this.stickinessPolicyMethod = this.selectedStickinessPolicy.methodname
+        this.$nextTick(() => {
+          this.stickinessPolicyForm.setFieldsValue({ methodname: 
this.selectedStickinessPolicy.methodname })
+          this.stickinessPolicyForm.setFieldsValue({ name: 
this.selectedStickinessPolicy.name })
+          this.stickinessPolicyForm.setFieldsValue({ cookieName: 
this.selectedStickinessPolicy.params['cookie-name'] })
+          this.stickinessPolicyForm.setFieldsValue({ mode: 
this.selectedStickinessPolicy.params.mode })
+          this.stickinessPolicyForm.setFieldsValue({ domain: 
this.selectedStickinessPolicy.params.domain })
+          this.stickinessPolicyForm.setFieldsValue({ length: 
this.selectedStickinessPolicy.params.length })
+          this.stickinessPolicyForm.setFieldsValue({ holdtime: 
this.selectedStickinessPolicy.params.holdtime })
+          this.stickinessNoCache = 
!!this.selectedStickinessPolicy.params.nocache
+          this.stickinessIndirect = 
!!this.selectedStickinessPolicy.params.indirect
+          this.stickinessPostOnly = 
!!this.selectedStickinessPolicy.params.postonly
+          this.stickinessRequestLearn = 
!!this.selectedStickinessPolicy.params['request-learn']
+          this.stickinessPrefix = !!this.selectedStickinessPolicy.params.prefix
+        })
+      }
+    },
+    handleAddStickinessPolicy (data, values) {
+      api('createLBStickinessPolicy', {
+        ...data,
+        lbruleid: this.selectedRule,
+        name: values.name,
+        methodname: values.methodname
+      }).then(response => {
+        this.$pollJob({
+          jobId: response.createLBStickinessPolicy.jobid,
+          successMessage: `Successfully configured sticky policy`,
+          successMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.fetchData()
+            this.closeModal()
+          },
+          errorMessage: 'Failed to configure sticky policy',
+          errorMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.fetchData()
+            this.closeModal()
+          },
+          loadingMessage: `Updating sticky policy...`,
+          catchMessage: 'Error encountered while fetching async job result',
+          catchMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.fetchData()
+            this.closeModal()
+          }
+        })
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.createLBStickinessPolicy.errortext
+        })
+        this.closeModal()
+      })
+    },
+    handleDeleteStickinessPolicy () {
+      this.stickinessModalLoading = true
+      api('deleteLBStickinessPolicy', { id: this.selectedStickinessPolicy.id 
}).then(response => {
+        this.$pollJob({
+          jobId: response.deleteLBstickinessrruleresponse.jobid,
+          successMessage: `Successfully removed sticky policy`,
+          successMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.fetchData()
+            this.closeModal()
+          },
+          errorMessage: 'Failed to remove sticky policy',
+          errorMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.fetchData()
+            this.closeModal()
+          },
+          loadingMessage: `Removing sticky policy...`,
+          catchMessage: 'Error encountered while fetching async job result',
+          catchMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.fetchData()
+            this.closeModal()
+          }
+        })
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.errorresponse.errortext
+        })
+        this.closeModal()
+      })
+    },
+    handleSubmitStickinessForm (e) {
+      this.stickinessModalLoading = true
+      e.preventDefault()
+      this.stickinessPolicyForm.validateFields((err, values) => {
+        if (err) {
+          this.stickinessModalLoading = false
+          return
+        }
+        if (values.methodname === 'none') {
+          this.handleDeleteStickinessPolicy()
+          return
+        }
+
+        values.nocache = this.stickinessNoCache
+        values.indirect = this.stickinessIndirect
+        values.postonly = this.stickinessPostOnly
+        values.requestLearn = this.stickinessRequestLearn
+        values.prefix = this.stickinessPrefix
+
+        let data = {}
+        let count = 0
+        Object.entries(values).forEach(([key, val]) => {
+          if (val && key !== 'name' && key !== 'methodname') {
+            if (key === 'cookieName') {
+              data = { ...data, ...{ [`param[${count}].name`]: 'cookie-name' } 
}
+            } else if (key === 'requestLearn') {
+              data = { ...data, ...{ [`param[${count}].name`]: 'request-learn' 
} }
+            } else {
+              data = { ...data, ...{ [`param[${count}].name`]: key } }
+            }
+            data = { ...data, ...{ [`param[${count}].value`]: val } }
+            count++
+          }
+        })
+
+        this.handleAddStickinessPolicy(data, values)
+      })
+    },
+    handleStickinessMethodSelectChange (e) {
+      this.stickinessPolicyMethod = e
+    },
+    handleDeleteInstanceFromRule (instance, rule, ip) {
+      this.loading = true
+      api('removeFromLoadBalancerRule', {
+        id: rule.id,
+        'vmidipmap[0].vmid': instance.loadbalancerruleinstance.id,
+        'vmidipmap[0].vmip': ip
+      }).then(response => {
+        this.$pollJob({
+          jobId: response.removefromloadbalancerruleresponse.jobid,
+          successMessage: `Successfully removed instance from rule`,
+          successMethod: () => {
+            this.fetchData()
+          },
+          errorMessage: 'Failed to remove instance',
+          errorMethod: () => {
+            this.fetchData()
+          },
+          loadingMessage: `Removing...`,
+          catchMessage: 'Error encountered while fetching async job result',
+          catchMethod: () => {
+            this.fetchData()
+          }
+        })
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.errorresponse.errortext
+        })
+        this.fetchData()
+      })
+    },
+    openEditRuleModal (rule) {
+      this.selectedRule = rule
+      this.editRuleModalVisible = true
+      this.editRuleDetails.name = this.selectedRule.name
+      this.editRuleDetails.algorithm = this.selectedRule.algorithm
+      this.editRuleDetails.protocol = this.selectedRule.protocol
+    },
+    handleSubmitEditForm () {
+      this.loading = true
+      this.editRuleModalLoading = true
+      api('updateLoadBalancerRule', {
+        ...this.editRuleDetails,
+        id: this.selectedRule.id
+      }).then(response => {
+        this.$pollJob({
+          jobId: response.updateloadbalancerruleresponse.jobid,
+          successMessage: `Successfully edited rule`,
+          successMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.fetchData()
+            this.closeModal()
+          },
+          errorMessage: 'Failed to edit rule',
+          errorMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.fetchData()
+            this.closeModal()
+          },
+          loadingMessage: `Updating rule...`,
+          catchMessage: 'Error encountered while fetching async job result',
+          catchMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.fetchData()
+            this.closeModal()
+          }
+        })
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.errorresponse.errortext
+        })
+        this.loading = false
+        this.closeModal()
+      })
+    },
+    handleDeleteRule (rule) {
+      this.loading = true
+      api('deleteLoadBalancerRule', {
+        id: rule.id
+      }).then(response => {
+        this.$pollJob({
+          jobId: response.deleteloadbalancerruleresponse.jobid,
+          successMessage: `Successfully deleted rule`,
+          successMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.fetchData()
+            this.closeModal()
+          },
+          errorMessage: 'Failed to delete rule',
+          errorMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.fetchData()
+            this.closeModal()
+          },
+          loadingMessage: `Deleting rule...`,
+          catchMessage: 'Error encountered while fetching async job result',
+          catchMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.fetchData()
+            this.closeModal()
+          }
+        })
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.errorresponse.errortext
+        })
+        this.loading = false
+        this.closeModal()
+      })
+    },
+    handleOpenAddVMModal () {
+      if (!this.selectedRule) {
+        if (!this.newRule.name) {
+          this.$refs.newRuleName.classList.add('error')
+        } else {
+          this.$refs.newRuleName.classList.remove('error')
+        }
+        if (!this.newRule.publicport) {
+          this.$refs.newRulePublicPort.classList.add('error')
+        } else {
+          this.$refs.newRulePublicPort.classList.remove('error')
+        }
+        if (!this.newRule.privateport) {
+          this.$refs.newRulePrivatePort.classList.add('error')
+        } else {
+          this.$refs.newRulePrivatePort.classList.remove('error')
+        }
+        if (!this.newRule.name || !this.newRule.publicport || 
!this.newRule.privateport) return
+      }
+      this.addVmModalVisible = true
+      this.addVmModalLoading = true
+      api('listVirtualMachines', {
+        listAll: true,
+        page: 1,
+        pagesize: 500,
+        networkid: this.resource.associatednetworkid,
+        account: this.resource.account,
+        domainid: this.resource.domainid
+      }).then(response => {
+        this.vms = response.listvirtualmachinesresponse.virtualmachine
+        this.vms.forEach((vm, index) => {
+          this.newRule.virtualmachineid[index] = null
+          this.nics[index] = null
+          this.newRule.vmguestip[index] = null
+        })
+        this.addVmModalLoading = false
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.errorresponse.errortext
+        })
+        this.closeModal()
+      })
+    },
+    fetchNics (e, index) {
+      if (!e.target.checked) {
+        this.newRule.virtualmachineid[index] = null
+        this.nics[index] = null
+        this.newRule.vmguestip[index] = null
+        return
+      }
+      this.newRule.virtualmachineid[index] = e.target.value
+      this.addVmModalNicLoading = true
+
+      api('listNics', {
+        virtualmachineid: e.target.value,
+        networkid: this.resource.associatednetworkid
+      }).then(response => {
+        if (!response.listnicsresponse.nic[0]) return
+        const newItem = []
+        newItem.push(response.listnicsresponse.nic[0].ipaddress)
+        if (response.listnicsresponse.nic[0].secondaryip) {
+          newItem.push(...response.listnicsresponse.nic[0].secondaryip.map(ip 
=> ip.ipaddress))
+        }
+        this.nics[index] = newItem
+        this.newRule.vmguestip[index] = this.nics[index][0]
+        this.addVmModalNicLoading = false
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.errorresponse.errortext
+        })
+        this.closeModal()
+      })
+    },
+    handleAssignToLBRule (data) {
+      const vmIDIpMap = {}
+
+      let count = 0
+      let innerCount = 0
+      this.newRule.vmguestip.forEach(ip => {
+        if (Array.isArray(ip)) {
+          ip.forEach(i => {
+            vmIDIpMap[`vmidipmap[${innerCount}].vmid`] = 
this.newRule.virtualmachineid[count]
+            vmIDIpMap[`vmidipmap[${innerCount}].vmip`] = i
+            innerCount++
+          })
+        } else {
+          vmIDIpMap[`vmidipmap[${innerCount}].vmid`] = 
this.newRule.virtualmachineid[count]
+          vmIDIpMap[`vmidipmap[${innerCount}].vmip`] = ip
+          innerCount++
+        }
+        count++
+      })
+
+      this.loading = true
+      api('assignToLoadBalancerRule', {
+        id: data,
+        ...vmIDIpMap
+      }).then(response => {
+        this.$pollJob({
+          jobId: response.assigntoloadbalancerruleresponse.jobid,
+          successMessage: `Successfully assigned VM`,
+          successMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.fetchData()
+            this.closeModal()
+          },
+          errorMessage: 'Failed to assign VM',
+          errorMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.fetchData()
+            this.closeModal()
+          },
+          loadingMessage: `Assigning VM...`,
+          catchMessage: 'Error encountered while fetching async job result',
+          catchMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.fetchData()
+            this.closeModal()
+          }
+        })
+      })
+    },
+    handleAddNewRule () {
+      this.loading = true
+
+      if (this.selectedRule) {
+        this.handleAssignToLBRule(this.selectedRule.id)
+        return
+      }
+
+      api('createLoadBalancerRule', {
+        openfirewall: false,
+        networkid: this.resource.associatednetworkid,
+        publicipid: this.resource.id,
+        algorithm: this.newRule.algorithm,
+        name: this.newRule.name,
+        privateport: this.newRule.privateport,
+        protocol: this.newRule.protocol,
+        publicport: this.newRule.publicport
+      }).then(response => {
+        this.addVmModalVisible = false
+        this.handleAssignToLBRule(response.createloadbalancerruleresponse.id)
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: 
error.response.data.createloadbalancerruleresponse.errortext
+        })
+        this.loading = false
+      })
+
+      // assigntoloadbalancerruleresponse.jobid
+    },
+    closeModal () {
+      this.selectedRule = null
+      this.tagsModalVisible = false
+      this.stickinessModalVisible = false
+      this.stickinessModalLoading = false
+      this.selectedStickinessPolicy = null
+      this.stickinessPolicyMethod = 'LbCookie'
+      this.stickinessNoCache = null
+      this.stickinessIndirect = null
+      this.stickinessPostOnly = null
+      this.editRuleModalVisible = false
+      this.editRuleModalLoading = false
+      this.addVmModalLoading = false
+      this.addVmModalNicLoading = false
+      this.vms = []
+      this.nics = []
+      this.addVmModalVisible = false
+      this.newRule.virtualmachineid = []
+      this.newTagsForm.resetFields()
+      this.stickinessPolicyForm.resetFields()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+  .rule {
+
+    &-container {
+      display: flex;
+      flex-direction: column;
+      width: 100%;
+
+      @media (min-width: 760px) {
+        margin-right: -20px;
+        margin-bottom: -10px;
+      }
+
+    }
+
+    &__row {
+      display: flex;
+      flex-wrap: wrap;
+    }
+
+    &__item {
+      padding-right: 20px;
+      margin-bottom: 20px;
+
+      @media (min-width: 760px) {
+        flex: 1;
+      }
+
+    }
+
+    &__title {
+      font-weight: bold;
+    }
+
+  }
+
+  .add-btn {
+    width: 100%;
+    padding-top: 15px;
+    padding-bottom: 15px;
+    height: auto;
+  }
+
+  .add-actions {
+    display: flex;
+    justify-content: flex-end;
+    margin-right: -20px;
+    margin-bottom: 20px;
+
+    @media (min-width: 760px) {
+      margin-top: 20px;
+    }
+
+    button {
+      margin-right: 20px;
+    }
+
+  }
+
+  .form {
+    display: flex;
+    margin-right: -20px;
+    flex-direction: column;
+    align-items: flex-start;
+
+    @media (min-width: 760px) {
+      flex-direction: row;
+    }
+
+    &__required {
+      margin-right: 5px;
+      color: red;
+    }
+
+    .error-text {
+      display: none;
+      color: red;
+      font-size: 0.8rem;
+    }
+
+    .error {
+
+      input {
+        border-color: red;
+      }
+
+      .error-text {
+        display: block;
+      }
+
+    }
+
+    &--column {
+      flex-direction: column;
+      margin-right: 0;
+      align-items: flex-end;
+
+      .form__item {
+        width: 100%;
+        padding-right: 0;
+      }
+
+    }
+
+    &__item {
+      display: flex;
+      flex-direction: column;
+      padding-right: 20px;
+      margin-bottom: 20px;
+
+      @media (min-width: 1200px) {
+        margin-bottom: 0;
+        flex: 1;
+      }
+
+      input,
+      .ant-select {
+        margin-top: auto;
+      }
+
+      &__input-container {
+        display: flex;
+
+        input {
+
+          &:not(:last-child) {
+            margin-right: 10px;
+          }
+
+        }
+
+      }
+
+    }
+
+    &__label {
+      font-weight: bold;
+    }
+
+  }
+
+  .rule-action {
+    margin-bottom: 10px;
+  }
+
+  .tags-modal {
+
+    .ant-divider {
+      margin-top: 0;
+    }
+
+  }
+
+  .tags {
+    margin-bottom: 10px;
+  }
+
+  .add-tags {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+
+    &__input {
+      margin-right: 10px;
+    }
+
+    &__label {
+      margin-bottom: 5px;
+      font-weight: bold;
+    }
+
+  }
+
+  .tags-container {
+    display: flex;
+    flex-wrap: wrap;
+    margin-bottom: 10px;
+  }
+
+  .add-tags-done {
+    display: block;
+    margin-left: auto;
+  }
+
+  .modal-loading {
+    position: absolute;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background-color: rgba(0,0,0,0.5);
+    z-index: 1;
+    color: #1890ff;
+    font-size: 2rem;
+  }
+
+  .ant-list-item {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+
+    @media (min-width: 760px) {
+      flex-direction: row;
+      align-items: center;
+    }
+
+  }
+
+  .rule-instance-collapse {
+    width: 100%;
+    margin-left: -15px;
+
+    .ant-collapse-item {
+      border: 0;
+    }
+
+  }
+
+  .rule-instance-list {
+    display: flex;
+    flex-direction: column;
+
+    &__item {
+      display: flex;
+      flex-wrap: wrap;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 10px;
+
+      div {
+        margin-left: 25px;
+        margin-bottom: 10px;
+      }
+    }
+  }
+
+  .edit-rule {
+
+    .ant-select {
+      width: 100%;
+    }
+
+    &__item {
+      margin-bottom: 10px;
+    }
+
+    &__label {
+      margin-bottom: 5px;
+      font-weight: bold;
+    }
+
+  }
+
+  .vm-modal {
+
+    &__header {
+      display: flex;
+
+      span {
+        flex: 1;
+        font-weight: bold;
+        margin-right: 10px;
+      }
+
+    }
+
+    &__item {
+      display: flex;
+      margin-top: 10px;
+
+      span,
+      label {
+        display: block;
+        flex: 1;
+        margin-right: 10px;
+      }
+
+    }
+
+  }
+
+  .custom-ant-form {
+    .ant-form-item-label {
+      font-weight: bold;
+      line-height: 1;
+    }
+    .ant-form-item {
+      margin-bottom: 10px;
+    }
+  }
+
+  .custom-ant-list {
+    .ant-list-item-action {
+      margin-top: 10px;
+      margin-left: 0;
+
+      @media (min-width: 760px) {
+        margin-top: 0;
+        margin-left: 24px;
+      }
+
+    }
+  }
+
+  .rule-instance-collapse {
+    .ant-collapse-header,
+    .ant-collapse-content {
+      margin-left: -12px;
+    }
+  }
+
+  .rule {
+    .ant-list-item-content-single {
+      width: 100%;
+
+      @media (min-width: 760px) {
+        width: auto;
+      }
+
+    }
+  }
+</style>
diff --git a/src/views/network/PortForwarding.vue 
b/src/views/network/PortForwarding.vue
new file mode 100644
index 0000000..eb196a7
--- /dev/null
+++ b/src/views/network/PortForwarding.vue
@@ -0,0 +1,675 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+<template>
+  <div>
+    <div>
+      <div class="form">
+        <div class="form__item">
+          <div class="form__label">{{ $t('privateport') }}</div>
+          <a-input-group class="form__item__input-container" compact>
+            <a-input
+              v-model="newRule.privateport"
+              placeholder="Start"
+              style="border-right: 0; width: 60px; margin-right: 0;"></a-input>
+            <a-input
+              placeholder="-"
+              disabled
+              style="width: 30px; border-left: 0; border-right: 0; 
pointer-events: none; backgroundColor: #fff; text-align:
+              center; margin-right: 0;"></a-input>
+            <a-input
+              v-model="newRule.privateendport"
+              placeholder="End"
+              style="border-left: 0; width: 60px; text-align: right; 
margin-right: 0;"></a-input>
+          </a-input-group>
+        </div>
+        <div class="form__item">
+          <div class="form__label">{{ $t('publicport') }}</div>
+          <a-input-group class="form__item__input-container" compact>
+            <a-input
+              v-model="newRule.publicport"
+              placeholder="Start"
+              style="border-right: 0; width: 60px; margin-right: 0;"></a-input>
+            <a-input
+              placeholder="-"
+              disabled
+              style="width: 30px; border-left: 0; border-right: 0; 
pointer-events: none; backgroundColor: #fff;
+              text-align: center; margin-right: 0;"></a-input>
+            <a-input
+              v-model="newRule.publicendport"
+              placeholder="End"
+              style="border-left: 0; width: 60px; text-align: right; 
margin-right: 0;"></a-input>
+          </a-input-group>
+        </div>
+        <div class="form__item">
+          <div class="form__label">{{ $t('protocol') }}</div>
+          <a-select v-model="newRule.protocol" style="width: 100%;">
+            <a-select-option value="tcp">{{ $t('tcp') }}</a-select-option>
+            <a-select-option value="udp">{{ $t('udp') }}</a-select-option>
+          </a-select>
+        </div>
+        <div class="form__item" style="margin-left: auto;">
+          <div class="form__label">{{ $t('label.add.VM') }}</div>
+          <a-button type="primary" @click="openAddVMModal">{{ $t('add') 
}}</a-button>
+        </div>
+      </div>
+    </div>
+
+    <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>
+        </div>
+      </a-list-item>
+    </a-list>
+
+    <a-modal title="Edit Tags" v-model="tagsModalVisible" :footer="null" 
:afterClose="closeModal">
+      <span v-show="tagsModalLoading" class="tags-modal-loading">
+        <a-icon type="loading"></a-icon>
+      </span>
+
+      <div class="add-tags">
+        <div class="add-tags__input">
+          <p class="add-tags__label">{{ $t('key') }}</p>
+          <a-input v-model="newTag.key"></a-input>
+        </div>
+        <div class="add-tags__input">
+          <p class="add-tags__label">{{ $t('value') }}</p>
+          <a-input v-model="newTag.value"></a-input>
+        </div>
+        <a-button type="primary" @click="() => handleAddTag()">{{ 
$t('label.add') }}</a-button>
+      </div>
+
+      <a-divider></a-divider>
+
+      <div v-show="!tagsModalLoading" class="tags-container">
+        <div class="tags" v-for="(tag, index) in tags" :key="index">
+          <a-tag :key="index" :closable="true" :afterClose="() => 
handleDeleteTag(tag)">
+            {{ tag.key }} = {{ tag.value }}
+          </a-tag>
+        </div>
+      </div>
+
+      <a-button class="add-tags-done" @click="tagsModalVisible = false" 
type="primary">{{ $t('done') }}</a-button>
+    </a-modal>
+
+    <a-modal
+      title="Add VM"
+      v-model="addVmModalVisible"
+      class="vm-modal"
+      width="60vw"
+      @ok="addRule"
+      :okButtonProps="{ props:
+        {disabled: newRule.virtualmachineid === null } }"
+      @cancel="closeModal"
+    >
+
+      <a-icon v-if="addVmModalLoading" type="loading"></a-icon>
+
+      <div v-else>
+        <div class="vm-modal__header">
+          <span style="min-width: 200px;">{{ $t('name') }}</span>
+          <span>{{ $t('instancename') }}</span>
+          <span>{{ $t('displayname') }}</span>
+          <span>{{ $t('ip') }}</span>
+          <span>{{ $t('account') }}</span>
+          <span>{{ $t('zone') }}</span>
+          <span>{{ $t('state') }}</span>
+          <span>{{ $t('select') }}</span>
+        </div>
+
+        <a-radio-group v-model="newRule.virtualmachineid" style="width: 100%;" 
@change="fetchNics">
+          <div v-for="(vm, index) in vms" :key="index" class="vm-modal__item">
+
+            <span style="min-width: 200px;">
+              <span>
+                {{ vm.name }}
+              </span>
+              <a-icon v-if="addVmModalNicLoading" type="loading"></a-icon>
+              <a-select
+                v-else-if="!addVmModalNicLoading && newRule.virtualmachineid 
=== vm.id"
+                v-model="newRule.vmguestip">
+                <a-select-option v-for="(nic, nicIndex) in nics" :key="nic" 
:value="nic">
+                  {{ nic }}{{ nicIndex === 0 ? ' (Primary)' : null }}
+                </a-select-option>
+              </a-select>
+            </span>
+            <span>{{ vm.instancename }}</span>
+            <span>{{ vm.displayname }}</span>
+            <span></span>
+            <span>{{ vm.account }}</span>
+            <span>{{ vm.zonename }}</span>
+            <span>{{ vm.state }}</span>
+            <a-radio :value="vm.id" />
+          </div>
+        </a-radio-group>
+      </div>
+
+    </a-modal>
+
+  </div>
+</template>
+
+<script>
+import { api } from '@/api'
+
+export default {
+  props: {
+    resource: {
+      type: Object,
+      required: true
+    }
+  },
+  inject: ['parentFetchData', 'parentToggleLoading'],
+  data () {
+    return {
+      loading: true,
+      portForwardRules: [],
+      newRule: {
+        protocol: 'tcp',
+        privateport: null,
+        privateendport: null,
+        publicport: null,
+        publicendport: null,
+        openfirewall: false,
+        vmguestip: null,
+        virtualmachineid: null
+      },
+      tagsModalVisible: false,
+      selectedRule: null,
+      tags: [],
+      newTag: {
+        key: null,
+        value: null
+      },
+      tagsModalLoading: false,
+      addVmModalVisible: false,
+      addVmModalLoading: false,
+      addVmModalNicLoading: false,
+      vms: [],
+      nics: []
+    }
+  },
+  mounted () {
+    this.fetchData()
+  },
+  watch: {
+    resource: function (newItem, oldItem) {
+      if (!newItem || !newItem.id) {
+        return
+      }
+      this.resource = newItem
+      this.fetchData()
+    }
+  },
+  filters: {
+    capitalise: val => {
+      if (val === 'all') return 'All'
+      return val.toUpperCase()
+    }
+  },
+  methods: {
+    fetchData () {
+      this.loading = true
+      api('listPortForwardingRules', {
+        listAll: true,
+        ipaddressid: this.resource.id
+      }).then(response => {
+        this.portForwardRules = 
response.listportforwardingrulesresponse.portforwardingrule
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.errorresponse.errortext
+        })
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    deleteRule (rule) {
+      this.loading = true
+      api('deletePortForwardingRule', { id: rule.id }).then(response => {
+        this.$pollJob({
+          jobId: response.deleteportforwardingruleresponse.jobid,
+          successMessage: `Successfully removed Port Forwarding rule`,
+          successMethod: () => this.fetchData(),
+          errorMessage: 'Removing Port Forwarding rule failed',
+          errorMethod: () => this.fetchData(),
+          loadingMessage: `Deleting Port Forwarding rule...`,
+          catchMessage: 'Error encountered while fetching async job result',
+          catchMethod: () => this.fetchData()
+        })
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.errorresponse.errortext
+        })
+        this.fetchData()
+      })
+    },
+    addRule () {
+      this.loading = true
+      this.addVmModalVisible = false
+      api('createPortForwardingRule', {
+        ...this.newRule,
+        ipaddressid: this.resource.id,
+        networkid: this.resource.associatednetworkid
+      }).then(response => {
+        this.$pollJob({
+          jobId: response.createportforwardingruleresponse.jobid,
+          successMessage: `Successfully added new Port Forwarding rule`,
+          successMethod: () => {
+            this.closeModal()
+            this.fetchData()
+          },
+          errorMessage: 'Adding new Port Forwarding rule failed',
+          errorMethod: () => {
+            this.closeModal()
+            this.fetchData()
+          },
+          loadingMessage: `Adding new Port Forwarding rule...`,
+          catchMessage: 'Error encountered while fetching async job result',
+          catchMethod: () => {
+            this.closeModal()
+            this.fetchData()
+          }
+        })
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: 
error.response.data.createportforwardingruleresponse.errortext
+        })
+        this.closeModal()
+        this.fetchData()
+      })
+    },
+    resetAllRules () {
+      this.newRule.protocol = 'tcp'
+      this.newRule.privateport = null
+      this.newRule.privateendport = null
+      this.newRule.publicport = null
+      this.newRule.publicendport = null
+      this.newRule.openfirewall = false
+      this.newRule.vmguestip = null
+      this.newRule.virtualmachineid = null
+    },
+    resetTagInputs () {
+      this.newTag.key = null
+      this.newTag.value = null
+    },
+    closeModal () {
+      this.selectedRule = null
+      this.tagsModalVisible = false
+      this.addVmModalVisible = false
+      this.newRule.virtualmachineid = null
+      this.addVmModalLoading = false
+      this.addVmModalNicLoading = false
+      this.nics = []
+      this.resetTagInputs()
+      this.resetAllRules()
+    },
+    openTagsModal (id) {
+      this.tagsModalLoading = true
+      this.selectedRule = id
+      this.tagsModalVisible = true
+      this.tags = []
+      this.resetTagInputs()
+      api('listTags', {
+        resourceId: id,
+        resourceType: 'PortForwardingRule',
+        listAll: true
+      }).then(response => {
+        this.tags = response.listtagsresponse.tag
+        this.tagsModalLoading = false
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.errorresponse.errortext
+        })
+        this.closeModal()
+      })
+    },
+    handleAddTag () {
+      this.tagsModalLoading = true
+      api('createTags', {
+        'tags[0].key': this.newTag.key,
+        'tags[0].value': this.newTag.value,
+        resourceIds: this.selectedRule,
+        resourceType: 'PortForwardingRule'
+      }).then(response => {
+        this.$pollJob({
+          jobId: response.createtagsresponse.jobid,
+          successMessage: `Successfully added tag`,
+          successMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.openTagsModal(this.selectedRule)
+          },
+          errorMessage: 'Failed to add new tag',
+          errorMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.closeModal()
+          },
+          loadingMessage: `Adding tag...`,
+          catchMessage: 'Error encountered while fetching async job result',
+          catchMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.closeModal()
+          }
+        })
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.createtagsresponse.errortext
+        })
+        this.closeModal()
+      })
+    },
+    handleDeleteTag (tag) {
+      this.tagsModalLoading = true
+      api('deleteTags', {
+        'tags[0].key': tag.key,
+        'tags[0].value': tag.value,
+        resourceIds: this.selectedRule,
+        resourceType: 'PortForwardingRule'
+      }).then(response => {
+        this.$pollJob({
+          jobId: response.deletetagsresponse.jobid,
+          successMessage: `Successfully removed tag`,
+          successMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.openTagsModal(this.selectedRule)
+          },
+          errorMessage: 'Failed to remove tag',
+          errorMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.closeModal()
+          },
+          loadingMessage: `Removing tag...`,
+          catchMessage: 'Error encountered while fetching async job result',
+          catchMethod: () => {
+            this.parentFetchData()
+            this.parentToggleLoading()
+            this.closeModal()
+          }
+        })
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.deletetagsresponse.errortext
+        })
+        this.closeModal()
+      })
+    },
+    openAddVMModal () {
+      this.addVmModalVisible = true
+      this.addVmModalLoading = true
+      api('listVirtualMachines', {
+        listAll: true,
+        page: 1,
+        pagesize: 500,
+        networkid: this.resource.associatednetworkid,
+        account: this.resource.account,
+        domainid: this.resource.domainid
+      }).then(response => {
+        this.vms = response.listvirtualmachinesresponse.virtualmachine
+        this.addVmModalLoading = false
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.errorresponse.errortext
+        })
+        this.closeModal()
+      })
+    },
+    fetchNics (e) {
+      this.addVmModalNicLoading = true
+      api('listNics', {
+        virtualmachineid: e.target.value,
+        networkid: this.resource.associatednetworkid
+      }).then(response => {
+        if (!response.listnicsresponse.nic || 
response.listnicsresponse.nic.length < 1) return
+        const nic = response.listnicsresponse.nic[0]
+        this.nics.push(nic.ipaddress)
+        if (nic.secondaryip && nic.secondaryip.length > 0) {
+          this.nics.push(...nic.secondaryip.map(ip => ip.ipaddress))
+        }
+        this.newRule.vmguestip = this.nics[0]
+        this.addVmModalNicLoading = false
+      }).catch(error => {
+        console.log(error)
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.errorresponse.errortext
+        })
+        this.closeModal()
+      })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+  .rule {
+
+    &-container {
+      display: flex;
+      width: 100%;
+      flex-wrap: wrap;
+      margin-right: -20px;
+      margin-bottom: -10px;
+    }
+
+    &__item {
+      padding-right: 20px;
+      margin-bottom: 20px;
+
+      @media (min-width: 760px) {
+        flex: 1;
+      }
+
+    }
+
+    &__title {
+      font-weight: bold;
+    }
+
+  }
+
+  .add-btn {
+    width: 100%;
+    padding-top: 15px;
+    padding-bottom: 15px;
+    height: auto;
+  }
+
+  .add-actions {
+    display: flex;
+    justify-content: flex-end;
+    margin-right: -20px;
+    margin-bottom: 20px;
+
+    @media (min-width: 760px) {
+      margin-top: 20px;
+    }
+
+    button {
+      margin-right: 20px;
+    }
+
+  }
+
+  .form {
+    display: flex;
+    margin-right: -20px;
+    margin-bottom: 20px;
+    flex-direction: column;
+
+    @media (min-width: 760px) {
+      flex-direction: row;
+    }
+
+    &__item {
+      display: flex;
+      flex-direction: column;
+      /*flex: 1;*/
+      padding-right: 20px;
+      margin-bottom: 20px;
+
+      @media (min-width: 760px) {
+        margin-bottom: 0;
+      }
+
+      input,
+      .ant-select {
+        margin-top: auto;
+      }
+
+      &__input-container {
+        display: flex;
+
+        input {
+
+          &:not(:last-child) {
+            margin-right: 10px;
+          }
+
+        }
+
+      }
+
+    }
+
+    &__label {
+      font-weight: bold;
+    }
+
+  }
+
+  .rule-action {
+    margin-bottom: 20px;
+
+    &:not(:last-of-type) {
+      margin-right: 10px;
+    }
+
+  }
+
+  .tags {
+    margin-bottom: 10px;
+  }
+
+  .add-tags {
+    display: flex;
+    align-items: flex-end;
+    justify-content: space-between;
+
+    &__input {
+      margin-right: 10px;
+    }
+
+    &__label {
+      margin-bottom: 5px;
+      font-weight: bold;
+    }
+
+  }
+
+  .tags-container {
+    display: flex;
+    flex-wrap: wrap;
+    margin-bottom: 10px;
+  }
+
+  .add-tags-done {
+    display: block;
+    margin-left: auto;
+  }
+
+  .tags-modal-loading {
+    position: absolute;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background-color: rgba(0,0,0,0.5);
+    z-index: 1;
+    color: #1890ff;
+    font-size: 2rem;
+  }
+
+  .vm-modal {
+
+    &__header {
+      display: flex;
+
+      span {
+        flex: 1;
+        font-weight: bold;
+        margin-right: 10px;
+      }
+
+    }
+
+    &__item {
+      display: flex;
+      margin-top: 10px;
+
+      span,
+      label {
+        display: block;
+        flex: 1;
+        margin-right: 10px;
+      }
+
+    }
+
+  }
+
+</style>
diff --git a/src/views/network/VpnDetails.vue b/src/views/network/VpnDetails.vue
index a0b23ad..f630d18 100644
--- a/src/views/network/VpnDetails.vue
+++ b/src/views/network/VpnDetails.vue
@@ -16,25 +16,190 @@
 // under the License.
 
 <template>
-  <div>
-    TODO: VPN configure/detail tab view
+  <div v-if="remoteAccessVpn">
+    <div>
+      <p>Your Remote Access VPN is currently enabled and can be accessed via 
the IP <strong>{{ remoteAccessVpn.publicip }}</strong></p>
+      <p>Your IPSec pre-shared key is <strong>{{ remoteAccessVpn.presharedkey 
}}</strong></p>
+      <a-divider/>
+      <a-button><router-link :to="{ path: '/vpnuser'}">Manage VPN 
Users</router-link></a-button>
+      <a-button style="margin-left: 10px" type="danger" @click="disableVpn = 
true">Disable VPN</a-button>
+    </div>
+
+    <a-modal v-model="disableVpn" :footer="null" oncancel="disableVpn = false" 
title="Disable Remove Access VPN">
+      <p>Are you sure you want to disable VPN?</p>
+
+      <a-divider></a-divider>
+
+      <div class="actions">
+        <a-button @click="() => disableVpn = false">Cancel</a-button>
+        <a-button type="primary" @click="handleDisableVpn">Yes</a-button>
+      </div>
+    </a-modal>
+
+  </div>
+  <div v-else>
+    <a-button type="primary" @click="enableVpn = true">Enable VPN</a-button>
+
+    <a-modal v-model="enableVpn" :footer="null" onCancel="enableVpn = false" 
title="Enable Remote Access VPN">
+      <p>Please confirm that you want Remote Access VPN enabled for this IP 
address.</p>
+
+      <a-divider></a-divider>
+
+      <div class="actions">
+        <a-button @click="() => enableVpn = false">Cancel</a-button>
+        <a-button type="primary" @click="handleCreateVpn">Yes</a-button>
+      </div>
+    </a-modal>
+
   </div>
 </template>
 
 <script>
+import { api } from '@/api'
 
 export default {
-  name: '',
-  components: {
+  props: {
+    resource: {
+      type: Object,
+      required: true
+    }
   },
   data () {
     return {
+      remoteAccessVpn: null,
+      enableVpn: false,
+      disableVpn: false
+    }
+  },
+  inject: ['parentFetchData', 'parentToggleLoading'],
+  mounted () {
+    this.fetchData()
+  },
+  watch: {
+    resource: function (newItem, oldItem) {
+      if (!newItem || !newItem.id) {
+        return
+      }
+      this.resource = newItem
+      this.fetchData()
     }
   },
   methods: {
+    fetchData () {
+      api('listRemoteAccessVpns', {
+        publicipid: this.resource.id,
+        listAll: true
+      }).then(response => {
+        this.remoteAccessVpn = 
response.listremoteaccessvpnsresponse.remoteaccessvpn
+          ? response.listremoteaccessvpnsresponse.remoteaccessvpn[0] : null
+      }).catch(error => {
+        console.log(error)
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.errorresponse.errortext
+        })
+      })
+    },
+    handleCreateVpn () {
+      this.parentToggleLoading()
+      this.enableVpn = false
+      api('createRemoteAccessVpn', {
+        publicipid: this.resource.id,
+        domainid: this.resource.domainid,
+        account: this.resource.account
+      }).then(response => {
+        this.$pollJob({
+          jobId: response.createremoteaccessvpnresponse.jobid,
+          successMethod: result => {
+            const res = result.jobresult.remoteaccessvpn
+            this.$notification.success({
+              message: 'Status',
+              description:
+                `Your Remote Access VPN is currently enabled and can be 
accessed via the IP ${res.publicip}. Your IPSec pre-shared key is 
${res.presharedkey}`,
+              duration: 0
+            })
+            this.fetchData()
+            this.parentFetchData()
+            this.parentToggleLoading()
+          },
+          errorMessage: 'Failed to enable VPN',
+          errorMethod: () => {
+            this.fetchData()
+            this.parentFetchData()
+            this.parentToggleLoading()
+          },
+          loadingMessage: `Enabling VPN...`,
+          catchMessage: 'Error encountered while fetching async job result',
+          catchMethod: () => {
+            this.fetchData()
+            this.parentFetchData()
+            this.parentToggleLoading()
+          }
+        })
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.createremoteaccessvpnresponse
+            ? error.response.data.createremoteaccessvpnresponse.errortext : 
error.response.data.errorresponse.errortext
+        })
+        this.fetchData()
+        this.parentFetchData()
+        this.parentToggleLoading()
+      })
+    },
+    handleDisableVpn () {
+      this.parentToggleLoading()
+      this.disableVpn = false
+      api('deleteRemoteAccessVpn', {
+        publicipid: this.resource.id,
+        domainid: this.resource.domainid
+      }).then(response => {
+        this.$pollJob({
+          jobId: response.deleteremoteaccessvpnresponse.jobid,
+          successMessage: 'Successfully disabled VPN',
+          successMethod: () => {
+            this.fetchData()
+            this.parentFetchData()
+            this.parentToggleLoading()
+          },
+          errorMessage: 'Failed to disable VPN',
+          errorMethod: () => {
+            this.fetchData()
+            this.parentFetchData()
+            this.parentToggleLoading()
+          },
+          loadingMessage: `Disabling VPN...`,
+          catchMessage: 'Error encountered while fetching async job result',
+          catchMethod: () => {
+            this.fetchData()
+            this.parentFetchData()
+            this.parentToggleLoading()
+          }
+        })
+      }).catch(error => {
+        this.$notification.error({
+          message: `Error ${error.response.status}`,
+          description: error.response.data.deleteremoteaccessvpnresponse
+            ? error.response.data.deleteremoteaccessvpnresponse.errortext : 
error.response.data.errorresponse.errortext
+        })
+        this.fetchData()
+        this.parentFetchData()
+        this.parentToggleLoading()
+      })
+    }
   }
 }
 </script>
 
-<style scoped>
+<style scoped lang="scss">
+  .actions {
+    display: flex;
+    justify-content: flex-end;
+
+    button {
+      &:not(:last-child) {
+        margin-right: 20px;
+      }
+    }
+  }
 </style>

Reply via email to