You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by ro...@apache.org on 2020/05/09 05:16:02 UTC

[cloudstack-primate] branch master updated: image: handle copy and delete actions for template/iso zone tab (#284)

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 c84af39  image: handle copy and delete actions for template/iso zone tab (#284)
c84af39 is described below

commit c84af39d1d6697fb50c92cd61b6250dd22b4969d
Author: Abhishek Kumar <ab...@gmail.com>
AuthorDate: Sat May 9 10:45:52 2020 +0530

    image: handle copy and delete actions for template/iso zone tab (#284)
    
    Fixes #279
    
    Implements copy and delete actions in the template/iso zones tab.
    Also implements template/iso filter by dropdown.
    
    Signed-off-by: Abhishek Kumar <ab...@gmail.com>
    Signed-off-by: Rohit Yadav <ro...@shapeblue.com>
    Co-authored-by: Rohit Yadav <ro...@shapeblue.com>
---
 src/config/router.js                         |   2 +
 src/config/section/image.js                  |  37 +---
 src/locales/en.json                          |   2 +
 src/views/AutogenView.vue                    |  68 +++++--
 src/views/image/IsoZones.vue                 | 274 ++++++++++++++++++++++-----
 src/views/image/RegisterOrUploadTemplate.vue |  25 ++-
 src/views/image/TemplateZones.vue            | 267 +++++++++++++++++++++-----
 7 files changed, 520 insertions(+), 155 deletions(-)

diff --git a/src/config/router.js b/src/config/router.js
index b718773..58bc558 100644
--- a/src/config/router.js
+++ b/src/config/router.js
@@ -61,6 +61,7 @@ export function generateRouterMap (section) {
           docHelp: child.docHelp,
           permission: child.permission,
           resourceType: child.resourceType,
+          filters: child.filters,
           params: child.params ? child.params : {},
           columns: child.columns,
           details: child.details,
@@ -122,6 +123,7 @@ export function generateRouterMap (section) {
     map.meta.resourceType = section.resourceType
     map.meta.details = section.details
     map.meta.actions = section.actions
+    map.meta.filters = section.filters
     map.meta.treeView = section.treeView ? section.treeView : false
     map.meta.tabs = section.treeView ? section.tabs : {}
 
diff --git a/src/config/section/image.js b/src/config/section/image.js
index 39031b8..39df4f2 100644
--- a/src/config/section/image.js
+++ b/src/config/section/image.js
@@ -27,8 +27,9 @@ export default {
       title: 'Templates',
       icon: 'save',
       permission: ['listTemplates'],
-      params: { templatefilter: 'executable' },
+      params: { templatefilter: 'self' },
       resourceType: 'Template',
+      filters: ['self', 'shared', 'featured', 'community'],
       columns: ['name', 'ostypename', 'status', 'hypervisor', 'account', 'domain', 'order'],
       details: ['name', 'id', 'displaytext', 'checksum', 'hypervisor', 'format', 'ostypename', 'size', 'isready', 'passwordenabled', 'directdownload', 'isextractable', 'isdynamicallyscalable', 'ispublic', 'isfeatured', 'crosszones', 'type', 'account', 'domain', 'created'],
       related: [{
@@ -95,21 +96,6 @@ export default {
           popup: true,
           show: (record, store) => { return (['Admin', 'DomainAdmin'].includes(store.userInfo.roletype) && (record.domainid === store.userInfo.domainid && record.account === store.userInfo.account) || record.templatetype !== 'BUILTIN') },
           component: () => import('@/views/image/UpdateTemplateIsoPermissions')
-        },
-        {
-          api: 'copyTemplate',
-          icon: 'copy',
-          label: 'Copy Template',
-          args: ['sourcezoneid', 'destzoneids'],
-          dataView: true
-        },
-        {
-          api: 'deleteTemplate',
-          icon: 'delete',
-          label: 'Delete Template',
-          args: ['zoneid'],
-          dataView: true,
-          groupAction: true
         }
       ]
     },
@@ -118,8 +104,9 @@ export default {
       title: 'ISOs',
       icon: 'usb',
       permission: ['listIsos'],
-      params: { isofilter: 'executable' },
+      params: { isofilter: 'self' },
       resourceType: 'ISO',
+      filters: ['self', 'shared', 'featured', 'community'],
       columns: ['name', 'ostypename', 'account', 'domain'],
       details: ['name', 'id', 'displaytext', 'checksum', 'ostypename', 'size', 'bootable', 'isready', 'directdownload', 'isextractable', 'ispublic', 'isfeatured', 'crosszones', 'account', 'domain', 'created'],
       related: [{
@@ -180,24 +167,10 @@ export default {
           icon: 'reconciliation',
           label: 'Update ISO Permissions',
           dataView: true,
+          args: ['op', 'accounts', 'projectids'],
           popup: true,
           show: (record, store) => { return (['Admin', 'DomainAdmin'].includes(store.userInfo.roletype) && (record.domainid === store.userInfo.domainid && record.account === store.userInfo.account) || record.templatetype !== 'BUILTIN') },
           component: () => import('@/views/image/UpdateTemplateIsoPermissions')
-        },
-        {
-          api: 'copyIso',
-          icon: 'copy',
-          label: 'Copy ISO',
-          args: ['sourcezoneid', 'destzoneids'],
-          dataView: true
-        },
-        {
-          api: 'deleteIso',
-          icon: 'delete',
-          label: 'Delete ISO',
-          args: ['zoneid'],
-          dataView: true,
-          groupAction: true
         }
       ]
     },
diff --git a/src/locales/en.json b/src/locales/en.json
index ae303d1..1905f31 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -1279,7 +1279,9 @@
 "filter": "Filter",
 "featured": "Featured",
 "community": "Community",
+"self": "Mine",
 "selfexecutable": "Self",
+"shared": "Shared",
 "sharedexecutable": "Shared",
 "fixed": "Fixed Offering",
 "customconstrained": "Custom Constrained",
diff --git a/src/views/AutogenView.vue b/src/views/AutogenView.vue
index 38fb535..b78d3f6 100644
--- a/src/views/AutogenView.vue
+++ b/src/views/AutogenView.vue
@@ -19,7 +19,7 @@
   <div>
     <a-card class="breadcrumb-card">
       <a-row>
-        <a-col :span="14" style="padding-left: 6px">
+        <a-col :span="12" style="padding-left: 6px">
           <breadcrumb :resource="resource">
             <a-tooltip placement="bottom" slot="end">
               <template slot="title">
@@ -37,7 +37,7 @@
             </a-tooltip>
           </breadcrumb>
         </a-col>
-        <a-col :span="10">
+        <a-col :span="12">
           <span style="float: right">
             <action-button
               style="margin-bottom: 5px"
@@ -47,6 +47,17 @@
               :dataView="dataView"
               :resource="resource"
               @exec-action="execAction"/>
+            <a-select
+              v-if="filters && filters.length > 0"
+              placeholder="Filter By"
+              :value="$t(selectedFilter)"
+              style="min-width: 100px; margin-left: 10px"
+              @change="changeFilter">
+              <a-icon slot="suffixIcon" type="filter" />
+              <a-select-option v-for="filter in filters" :key="filter">
+                {{ $t(filter) }}
+              </a-select-option>
+            </a-select>
             <a-input-search
               style="width: 20vw; margin-left: 10px"
               placeholder="Search"
@@ -305,6 +316,8 @@ export default {
       showAction: false,
       dataView: false,
       treeView: false,
+      selectedFilter: '',
+      filters: [],
       actions: [],
       treeData: [],
       treeSelected: {},
@@ -337,6 +350,7 @@ export default {
         this.searchQuery = ''
         this.page = 1
         this.itemCount = 0
+        this.selectedFilter = ''
         this.fetchData()
       }
     },
@@ -347,7 +361,7 @@ export default {
     }
   },
   methods: {
-    fetchData () {
+    fetchData (params = { listall: true }) {
       if (this.routeName !== this.$route.name) {
         this.routeName = this.$route.name
         this.items = []
@@ -357,11 +371,12 @@ export default {
       }
       this.apiName = ''
       this.actions = []
+      this.filters = this.$route.meta.filters || []
       this.columns = []
       this.columnKeys = []
       this.treeData = []
       this.treeSelected = {}
-      var params = { listall: true }
+
       if (Object.keys(this.$route.query).length > 0) {
         Object.assign(params, this.$route.query)
       } else if (this.$route.meta.params) {
@@ -393,6 +408,26 @@ export default {
         return
       }
 
+      if (['listTemplates', 'listIsos'].includes(this.apiName) && !this.dataView) {
+        if (['Admin'].includes(this.$store.getters.userInfo.roletype)) {
+          this.filters = ['all', ...this.filters]
+          if (this.selectedFilter === '') {
+            this.selectedFilter = 'all'
+          }
+        }
+        if (this.selectedFilter === '') {
+          this.selectedFilter = 'self'
+        }
+      }
+
+      if (this.selectedFilter && this.filters.length > 0) {
+        if (this.$route.path.startsWith('/template')) {
+          params.templatefilter = this.selectedFilter
+        } else if (this.$route.path.startsWith('/iso')) {
+          params.isofilter = this.selectedFilter
+        }
+      }
+
       if (this.searchQuery !== '') {
         if (this.apiName === 'listRoles') {
           params.name = this.searchQuery
@@ -450,12 +485,6 @@ export default {
         delete params.treeView
       }
 
-      if (['listTemplates', 'listIsos'].includes(this.apiName) && !this.dataView) {
-        if (['Admin'].includes(this.$store.getters.userInfo.roletype)) {
-          params.templatefilter = 'all'
-        }
-      }
-
       api(this.apiName, params).then(json => {
         var responseName
         var objectName
@@ -524,6 +553,13 @@ export default {
         this.loading = false
       })
     },
+    removeStringStartSubstringIfPresent (str, searchstr) {
+      var index = str.indexOf(searchstr)
+      if (index !== 0) {
+        return str
+      }
+      return str.slice(index + searchstr.length)
+    },
     onSearch (value) {
       this.searchQuery = value
       this.page = 1
@@ -576,7 +612,7 @@ export default {
         }
       }
       this.currentAction.loading = false
-      if (action.dataView && action.icon === 'edit') {
+      if (action.dataView && ['copy', 'edit'].includes(action.icon)) {
         this.fillEditFormFieldValues()
       }
     },
@@ -585,8 +621,11 @@ export default {
         return
       }
       var paramName = param.name
+      var extractedParamName = paramName.replace('ids', '').replace('id', '').toLowerCase()
+      extractedParamName = this.removeStringStartSubstringIfPresent(extractedParamName, 'source')
+      extractedParamName = this.removeStringStartSubstringIfPresent(extractedParamName, 'dest')
       var params = { listall: true }
-      const possibleName = 'list' + paramName.replace('ids', '').replace('id', '').toLowerCase() + 's'
+      const possibleName = 'list' + extractedParamName + 's'
       var possibleApi
       if (this.currentAction.mapping && param.name in this.currentAction.mapping && this.currentAction.mapping[param.name].api) {
         possibleApi = this.currentAction.mapping[param.name].api
@@ -668,6 +707,7 @@ export default {
         let fieldName = null
         if (field.type === 'uuid' || field.type === 'list' || field.name === 'account' || (this.currentAction.mapping && field.name in this.currentAction.mapping)) {
           fieldName = field.name.replace('ids', 'name').replace('id', 'name')
+          fieldName = this.removeStringStartSubstringIfPresent(fieldName, 'source')
         } else {
           fieldName = field.name
         }
@@ -774,6 +814,10 @@ export default {
         }
       })
     },
+    changeFilter (filter) {
+      this.selectedFilter = filter
+      this.fetchData()
+    },
     changePage (page, pageSize) {
       this.page = page
       this.pageSize = pageSize
diff --git a/src/views/image/IsoZones.vue b/src/views/image/IsoZones.vue
index 34cbe1b..afff771 100644
--- a/src/views/image/IsoZones.vue
+++ b/src/views/image/IsoZones.vue
@@ -16,35 +16,100 @@
 // under the License.
 
 <template>
-  <div class="row-iso-zone">
-    <a-row :gutter="12">
-      <a-col :md="24" :lg="24">
-        <a-table
-          size="small"
-          style="overflow-y: auto"
-          :loading="loading || fetchLoading"
-          :columns="columns"
-          :dataSource="dataSource"
-          :pagination="false"
-          :rowKey="record => record.zoneid || record.id">
-          <div slot="isready" slot-scope="text, record">
-            <span v-if="record.isready">{{ $t('Yes') }}</span>
-            <span v-else>{{ $t('No') }}</span>
-          </div>
-        </a-table>
-        <a-pagination
-          class="row-element"
-          size="small"
-          :current="page"
-          :pageSize="pageSize"
-          :total="itemCount"
-          :showTotal="total => `Total ${total} items`"
-          :pageSizeOptions="['10', '20', '40', '80', '100']"
-          @change="handleChangePage"
-          @showSizeChange="handleChangePageSize"
-          showSizeChanger/>
-      </a-col>
-    </a-row>
+  <div>
+    <a-table
+      size="small"
+      style="overflow-y: auto"
+      :loading="loading || fetchLoading"
+      :columns="columns"
+      :dataSource="dataSource"
+      :pagination="false"
+      :rowKey="record => record.zoneid">
+      <div slot="isready" slot-scope="text, record">
+        <span v-if="record.isready">{{ $t('Yes') }}</span>
+        <span v-else>{{ $t('No') }}</span>
+      </div>
+      <template slot="action" slot-scope="text, record">
+        <span style="margin-right: 5px">
+          <a-button
+            :disabled="!('copyIso' in $store.getters.apis)"
+            icon="copy"
+            shape="circle"
+            :loading="copyLoading"
+            @click="showCopyIso(record)" />
+        </span>
+        <span style="margin-right: 5px">
+          <a-popconfirm
+            v-if="'deleteIso' in $store.getters.apis"
+            placement="topRight"
+            title="Delete the ISO for this zone?"
+            :ok-text="$t('Yes')"
+            :cancel-text="$t('No')"
+            :loading="deleteLoading"
+            @confirm="deleteIso(record)"
+          >
+            <a-button
+              type="danger"
+              icon="delete"
+              shape="circle" />
+          </a-popconfirm>
+        </span>
+      </template>
+    </a-table>
+    <a-pagination
+      class="row-element"
+      size="small"
+      :current="page"
+      :pageSize="pageSize"
+      :total="itemCount"
+      :showTotal="total => `Total ${total} items`"
+      :pageSizeOptions="['10', '20', '40', '80', '100']"
+      @change="handleChangePage"
+      @showSizeChange="handleChangePageSize"
+      showSizeChanger/>
+
+    <a-modal
+      v-if="'copyIso' in $store.getters.apis"
+      style="top: 20px;"
+      :title="$t('label.action.copy.ISO')"
+      :visible="showCopyActionForm"
+      :closable="true"
+      @ok="handleCopyIsoSubmit"
+      @cancel="onCloseCopyForm"
+      :confirmLoading="copyLoading"
+      centered>
+      <a-spin :spinning="copyLoading">
+        <a-form
+          :form="form"
+          @submit="handleCopyIsoSubmit"
+          layout="vertical">
+          <a-form-item :label="$t('zoneid')">
+            <a-select
+              id="zone-selection"
+              mode="multiple"
+              placeholder="Select Zones"
+              v-decorator="['zoneid', {
+                rules: [
+                  {
+                    required: true,
+                    message: 'Please select option'
+                  }
+                ]
+              }]"
+              showSearch
+              optionFilterProp="children"
+              :filterOption="(input, option) => {
+                return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
+              }"
+              :loading="zoneLoading">
+              <a-select-option v-for="zone in zones" :key="zone.id">
+                {{ zone.name }}
+              </a-select-option>
+            </a-select>
+          </a-form-item>
+        </a-form>
+      </a-spin>
+    </a-modal>
   </div>
 </template>
 
@@ -67,33 +132,49 @@ export default {
     return {
       columns: [],
       dataSource: [],
-      detailColumn: [],
-      detail: [],
       page: 1,
       pageSize: 10,
       itemCount: 0,
-      fetchLoading: false
+      fetchLoading: false,
+      showCopyActionForm: false,
+      currentRecord: {},
+      zones: [],
+      zoneLoading: false,
+      copyLoading: false,
+      deleteLoading: false
     }
   },
+  beforeCreate () {
+    this.form = this.$form.createForm(this)
+    this.apiConfigParams = (this.$store.getters.apis.copyIso && this.$store.getters.apis.copyIso.params) || []
+    this.apiParams = {}
+    this.apiConfigParams.forEach(param => {
+      this.apiParams[param.name] = param
+    })
+  },
   created () {
     this.columns = [
       {
-        title: this.$t('name'),
-        dataIndex: 'zonename',
-        scopedSlots: { customRender: 'name' }
+        title: this.$t('zonename'),
+        dataIndex: 'zonename'
       },
       {
         title: this.$t('status'),
-        dataIndex: 'status',
-        scopedSlots: { customRender: 'status' }
+        dataIndex: 'status'
       },
       {
         title: this.$t('isready'),
         dataIndex: 'isready',
         scopedSlots: { customRender: 'isready' }
+      },
+      {
+        title: '',
+        dataIndex: 'action',
+        fixed: 'right',
+        width: 100,
+        scopedSlots: { customRender: 'action' }
       }
     ]
-    this.detailColumn = ['name', 'id', 'zonename', 'zoneid']
   },
   mounted () {
     this.fetchData()
@@ -108,24 +189,18 @@ export default {
   methods: {
     fetchData () {
       const params = {}
-      params.listAll = true
       params.id = this.resource.id
-      params.isofilter = 'self'
+      params.isofilter = 'executable'
+      params.listall = true
       params.page = this.page
       params.pagesize = this.pageSize
 
       this.dataSource = []
       this.itemCount = 0
       this.fetchLoading = true
-
       api('listIsos', params).then(json => {
-        const listIsos = json.listisosresponse.iso
-        const count = json.listisosresponse.count
-
-        if (listIsos) {
-          this.dataSource = listIsos
-          this.itemCount = count
-        }
+        this.dataSource = json.listisosresponse.iso || []
+        this.itemCount = json.listisosresponse.count || 0
       }).catch(error => {
         this.$notifyError(error)
       }).finally(() => {
@@ -141,6 +216,105 @@ export default {
       this.page = currentPage
       this.pageSize = pageSize
       this.fetchData()
+    },
+    deleteIso (record) {
+      const params = {
+        id: record.id,
+        zoneid: record.zoneid
+      }
+      this.deleteLoading = true
+      api('deleteIso', params).then(json => {
+        const jobId = json.deleteisoresponse.jobid
+        this.$store.dispatch('AddAsyncJob', {
+          title: this.$t('label.action.delete.ISO'),
+          jobid: jobId,
+          description: this.resource.name,
+          status: 'progress'
+        })
+        const singleZone = (this.dataSource.length === 1)
+        this.$pollJob({
+          jobId,
+          successMethod: result => {
+            if (singleZone) {
+              this.$router.go(-1)
+            } else {
+              this.fetchData()
+            }
+          },
+          errorMethod: () => this.fetchData(),
+          loadingMessage: `Deleting ISO ${this.resource.name} in progress`,
+          catchMessage: 'Error encountered while fetching async job result'
+        })
+      }).catch(error => {
+        this.$notifyError(error)
+      }).finally(() => {
+        this.deleteLoading = false
+        this.fetchData()
+      })
+    },
+    fetchZoneData () {
+      this.zones = []
+      this.zoneLoading = true
+      api('listZones', { listall: true }).then(json => {
+        const zones = json.listzonesresponse.zone || []
+        this.zones = [...zones.filter((zone) => this.currentRecord.zoneid !== zone.id)]
+      }).finally(() => {
+        this.zoneLoading = false
+      })
+    },
+    showCopyIso (record) {
+      this.currentRecord = record
+      this.form.setFieldsValue({
+        zoneid: []
+      })
+      this.fetchZoneData()
+      this.showCopyActionForm = true
+    },
+    onCloseCopyForm () {
+      this.currentRecord = {}
+      this.showCopyActionForm = false
+    },
+    handleCopyIsoSubmit (e) {
+      e.preventDefault()
+      this.form.validateFields((err, values) => {
+        if (err) {
+          return
+        }
+        const params = {
+          id: this.currentRecord.id,
+          sourcezoneid: this.currentRecord.zoneid,
+          destzoneids: values.zoneid.join()
+        }
+        this.copyLoading = true
+        api('copyIso', params).then(json => {
+          const jobId = json.copytemplateresponse.jobid
+          this.$store.dispatch('AddAsyncJob', {
+            title: this.$t('label.action.copy.ISO'),
+            jobid: jobId,
+            description: this.resource.name,
+            status: 'progress'
+          })
+          this.$pollJob({
+            jobId,
+            successMethod: result => {
+              this.fetchData()
+            },
+            errorMethod: () => this.fetchData(),
+            loadingMessage: `Copy ISO ${this.resource.name} in progress`,
+            catchMessage: 'Error encountered while fetching async job result'
+          })
+        }).catch(error => {
+          this.$notification.error({
+            message: 'Request Failed',
+            description: (error.response && error.response.headers && error.response.headers['x-description']) || error.message
+          })
+        }).finally(() => {
+          this.copyLoading = false
+          this.$emit('refresh-data')
+          this.onCloseCopyForm()
+          this.fetchData()
+        })
+      })
     }
   }
 }
@@ -151,8 +325,4 @@ export default {
   margin-top: 15px;
   margin-bottom: 15px;
 }
-
-.action-button button {
-  margin-right: 5px;
-}
 </style>
diff --git a/src/views/image/RegisterOrUploadTemplate.vue b/src/views/image/RegisterOrUploadTemplate.vue
index abbaf49..ef4d45d 100644
--- a/src/views/image/RegisterOrUploadTemplate.vue
+++ b/src/views/image/RegisterOrUploadTemplate.vue
@@ -79,14 +79,14 @@
           <a-row :gutter="12">
             <a-col :md="24" :lg="24">
               <a-form-item
-                :label="$t('zoneids')"
+                :label="$t('zone')"
                 :validate-status="zoneError"
                 :help="zoneErrorMessage">
                 <a-select
                   v-decorator="['zoneids', {
                     rules: [
                       {
-                        required: false,
+                        required: true,
                         message: 'Please select option',
                         type: 'array'
                       }
@@ -96,7 +96,7 @@
                   mode="multiple"
                   :placeholder="apiParams.zoneids.description"
                   @change="handlerSelectZone">
-                  <a-select-option v-for="opt in zones.opts" :key="opt.name || opt.description">
+                  <a-select-option v-for="opt in zones.opts" :key="opt.id">
                     {{ opt.name || opt.description }}
                   </a-select-option>
                 </a-select>
@@ -720,6 +720,12 @@ export default {
             description: 'VHD'
           })
           break
+        case 'Simulator':
+          format.push({
+            id: 'VHD',
+            description: 'VHD'
+          })
+          break
         case 'VMware':
           this.hyperVMWShow = true
           format.push({
@@ -764,9 +770,8 @@ export default {
       this.resetSelect()
 
       const params = {}
-      const allZoneExists = value.filter(zone => zone === this.$t('label.all.zone'))
 
-      if (allZoneExists.length > 0) {
+      if (value.includes(this.$t('label.all.zone'))) {
         params.listAll = true
         this.fetchHyperVisor(params)
         return
@@ -817,15 +822,7 @@ export default {
               params.zoneids = '-1'
               continue
             }
-            const zonesSelected = []
-            for (const index in input) {
-              const name = input[index]
-              const zone = this.zones.opts.filter(zone => zone.name === name)
-              if (zone && zone[0]) {
-                zonesSelected.push(zone[0].id)
-              }
-            }
-            params[key] = zonesSelected.join(',')
+            params[key] = input.join()
           } else if (key === 'zoneid') {
             params[key] = values[key]
           } else if (key === 'ostypeid') {
diff --git a/src/views/image/TemplateZones.vue b/src/views/image/TemplateZones.vue
index 0583d8f..87e81a6 100644
--- a/src/views/image/TemplateZones.vue
+++ b/src/views/image/TemplateZones.vue
@@ -16,35 +16,100 @@
 // under the License.
 
 <template>
-  <div class="row-template-zone">
-    <a-row :gutter="12">
-      <a-col :md="24" :lg="24">
-        <a-table
-          size="small"
-          style="overflow-y: auto"
-          :loading="loading || fetchLoading"
-          :columns="columns"
-          :dataSource="dataSource"
-          :pagination="false"
-          :rowKey="record => record.zoneid">
-          <div slot="isready" slot-scope="text, record">
-            <span v-if="record.isready">{{ $t('Yes') }}</span>
-            <span v-else>{{ $t('No') }}</span>
-          </div>
-        </a-table>
-        <a-pagination
-          class="row-element"
-          size="small"
-          :current="page"
-          :pageSize="pageSize"
-          :total="itemCount"
-          :showTotal="total => `Total ${total} items`"
-          :pageSizeOptions="['10', '20', '40', '80', '100']"
-          @change="handleChangePage"
-          @showSizeChange="handleChangePageSize"
-          showSizeChanger/>
-      </a-col>
-    </a-row>
+  <div>
+    <a-table
+      size="small"
+      style="overflow-y: auto"
+      :loading="loading || fetchLoading"
+      :columns="columns"
+      :dataSource="dataSource"
+      :pagination="false"
+      :rowKey="record => record.zoneid">
+      <div slot="isready" slot-scope="text, record">
+        <span v-if="record.isready">{{ $t('Yes') }}</span>
+        <span v-else>{{ $t('No') }}</span>
+      </div>
+      <template slot="action" slot-scope="text, record">
+        <span style="margin-right: 5px">
+          <a-button
+            :disabled="!('copyTemplate' in $store.getters.apis)"
+            icon="copy"
+            shape="circle"
+            :loading="copyLoading"
+            @click="showCopyTemplate(record)" />
+        </span>
+        <span style="margin-right: 5px">
+          <a-popconfirm
+            v-if="'deleteTemplate' in $store.getters.apis"
+            placement="topRight"
+            title="Delete the template for this zone?"
+            :ok-text="$t('Yes')"
+            :cancel-text="$t('No')"
+            :loading="deleteLoading"
+            @confirm="deleteTemplate(record)"
+          >
+            <a-button
+              type="danger"
+              icon="delete"
+              shape="circle" />
+          </a-popconfirm>
+        </span>
+      </template>
+    </a-table>
+    <a-pagination
+      class="row-element"
+      size="small"
+      :current="page"
+      :pageSize="pageSize"
+      :total="itemCount"
+      :showTotal="total => `Total ${total} items`"
+      :pageSizeOptions="['10', '20', '40', '80', '100']"
+      @change="handleChangePage"
+      @showSizeChange="handleChangePageSize"
+      showSizeChanger/>
+
+    <a-modal
+      v-if="'copyTemplate' in $store.getters.apis"
+      style="top: 20px;"
+      :title="$t('label.action.copy.template')"
+      :visible="showCopyActionForm"
+      :closable="true"
+      @ok="handleCopyTemplateSubmit"
+      @cancel="onCloseCopyForm"
+      :confirmLoading="copyLoading"
+      centered>
+      <a-spin :spinning="copyLoading">
+        <a-form
+          :form="form"
+          @submit="handleCopyTemplateSubmit"
+          layout="vertical">
+          <a-form-item :label="$t('zoneid')">
+            <a-select
+              id="zone-selection"
+              mode="multiple"
+              placeholder="Select Zones"
+              v-decorator="['zoneid', {
+                rules: [
+                  {
+                    required: true,
+                    message: 'Please select option'
+                  }
+                ]
+              }]"
+              showSearch
+              optionFilterProp="children"
+              :filterOption="(input, option) => {
+                return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
+              }"
+              :loading="zoneLoading">
+              <a-select-option v-for="zone in zones" :key="zone.id">
+                {{ zone.name }}
+              </a-select-option>
+            </a-select>
+          </a-form-item>
+        </a-form>
+      </a-spin>
+    </a-modal>
   </div>
 </template>
 
@@ -70,25 +135,44 @@ export default {
       page: 1,
       pageSize: 10,
       itemCount: 0,
-      fetchLoading: false
+      fetchLoading: false,
+      showCopyActionForm: false,
+      currentRecord: {},
+      zones: [],
+      zoneLoading: false,
+      copyLoading: false,
+      deleteLoading: false
     }
   },
+  beforeCreate () {
+    this.form = this.$form.createForm(this)
+    this.apiConfigParams = (this.$store.getters.apis.copyTemplate && this.$store.getters.apis.copyTemplate.params) || []
+    this.apiParams = {}
+    this.apiConfigParams.forEach(param => {
+      this.apiParams[param.name] = param
+    })
+  },
   created () {
     this.columns = [
       {
-        title: this.$t('name'),
-        dataIndex: 'zonename',
-        scopedSlots: { customRender: 'name' }
+        title: this.$t('zonename'),
+        dataIndex: 'zonename'
       },
       {
         title: this.$t('status'),
-        dataIndex: 'status',
-        scopedSlots: { customRender: 'status' }
+        dataIndex: 'status'
       },
       {
         title: this.$t('isready'),
         dataIndex: 'isready',
         scopedSlots: { customRender: 'isready' }
+      },
+      {
+        title: '',
+        dataIndex: 'action',
+        fixed: 'right',
+        width: 100,
+        scopedSlots: { customRender: 'action' }
       }
     ]
   },
@@ -105,24 +189,18 @@ export default {
   methods: {
     fetchData () {
       const params = {}
-      params.listAll = true
       params.id = this.resource.id
-      params.templatefilter = 'self'
+      params.templatefilter = 'executable'
+      params.listall = true
       params.page = this.page
       params.pagesize = this.pageSize
 
       this.dataSource = []
       this.itemCount = 0
       this.fetchLoading = true
-
       api('listTemplates', params).then(json => {
-        const listTemplates = json.listtemplatesresponse.template
-        const count = json.listtemplatesresponse.count
-
-        if (listTemplates) {
-          this.dataSource = listTemplates
-          this.itemCount = count
-        }
+        this.dataSource = json.listtemplatesresponse.template || []
+        this.itemCount = json.listtemplatesresponse.count || 0
       }).catch(error => {
         this.$notifyError(error)
       }).finally(() => {
@@ -138,6 +216,105 @@ export default {
       this.page = currentPage
       this.pageSize = pageSize
       this.fetchData()
+    },
+    deleteTemplate (record) {
+      const params = {
+        id: record.id,
+        zoneid: record.zoneid
+      }
+      this.deleteLoading = true
+      api('deleteTemplate', params).then(json => {
+        const jobId = json.deletetemplateresponse.jobid
+        this.$store.dispatch('AddAsyncJob', {
+          title: this.$t('label.action.delete.template'),
+          jobid: jobId,
+          description: this.resource.name,
+          status: 'progress'
+        })
+        const singleZone = (this.dataSource.length === 1)
+        this.$pollJob({
+          jobId,
+          successMethod: result => {
+            if (singleZone) {
+              this.$router.go(-1)
+            } else {
+              this.fetchData()
+            }
+          },
+          errorMethod: () => this.fetchData(),
+          loadingMessage: `Deleting template ${this.resource.name} in progress`,
+          catchMessage: 'Error encountered while fetching async job result'
+        })
+      }).catch(error => {
+        this.$notifyError(error)
+      }).finally(() => {
+        this.deleteLoading = false
+        this.fetchData()
+      })
+    },
+    fetchZoneData () {
+      this.zones = []
+      this.zoneLoading = true
+      api('listZones', { listall: true }).then(json => {
+        const zones = json.listzonesresponse.zone || []
+        this.zones = [...zones.filter((zone) => this.currentRecord.zoneid !== zone.id)]
+      }).finally(() => {
+        this.zoneLoading = false
+      })
+    },
+    showCopyTemplate (record) {
+      this.currentRecord = record
+      this.form.setFieldsValue({
+        zoneid: []
+      })
+      this.fetchZoneData()
+      this.showCopyActionForm = true
+    },
+    onCloseCopyForm () {
+      this.currentRecord = {}
+      this.showCopyActionForm = false
+    },
+    handleCopyTemplateSubmit (e) {
+      e.preventDefault()
+      this.form.validateFields((err, values) => {
+        if (err) {
+          return
+        }
+        const params = {
+          id: this.currentRecord.id,
+          sourcezoneid: this.currentRecord.zoneid,
+          destzoneids: values.zoneid.join()
+        }
+        this.copyLoading = true
+        api('copyTemplate', params).then(json => {
+          const jobId = json.copytemplateresponse.jobid
+          this.$store.dispatch('AddAsyncJob', {
+            title: this.$t('label.action.copy.template'),
+            jobid: jobId,
+            description: this.resource.name,
+            status: 'progress'
+          })
+          this.$pollJob({
+            jobId,
+            successMethod: result => {
+              this.fetchData()
+            },
+            errorMethod: () => this.fetchData(),
+            loadingMessage: `Copy template ${this.resource.name} in progress`,
+            catchMessage: 'Error encountered while fetching async job result'
+          })
+        }).catch(error => {
+          this.$notification.error({
+            message: 'Request Failed',
+            description: (error.response && error.response.headers && error.response.headers['x-description']) || error.message
+          })
+        }).finally(() => {
+          this.copyLoading = false
+          this.$emit('refresh-data')
+          this.onCloseCopyForm()
+          this.fetchData()
+        })
+      })
     }
   }
 }