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/07/05 06:19:41 UTC

[cloudstack-primate] branch master updated: views: custom search framework for list views (#235)

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 6b1c61c  views: custom search framework for list views (#235)
6b1c61c is described below

commit 6b1c61c8d2a6a7985ba4b6a74acee3df62d8fa84
Author: Hoang Nguyen <ho...@unitech.vn>
AuthorDate: Sun Jul 5 13:19:30 2020 +0700

    views: custom search framework for list views (#235)
    
    This adds a new search view component that will allow users to do
    custom search using a popover component for vm, storage, network,
    image, event, project and routers.
    
    Signed-off-by: Rohit Yadav <ro...@shapeblue.com>
    Co-authored-by: Rohit Yadav <ro...@shapeblue.com>
---
 src/components/view/SearchView.vue  | 481 ++++++++++++++++++++++++++++++++++++
 src/config/router.js                |   5 +-
 src/config/section/compute.js       |   1 +
 src/config/section/event.js         |   1 +
 src/config/section/image.js         |   2 +
 src/config/section/infra/routers.js |   1 +
 src/config/section/network.js       |   3 +
 src/config/section/project.js       |   1 +
 src/config/section/storage.js       |   3 +
 src/locales/en.json                 |   5 +-
 src/utils/plugins.js                |   2 -
 src/views/AutogenView.vue           |  46 +++-
 12 files changed, 539 insertions(+), 12 deletions(-)

diff --git a/src/components/view/SearchView.vue b/src/components/view/SearchView.vue
new file mode 100644
index 0000000..6e5cfb3
--- /dev/null
+++ b/src/components/view/SearchView.vue
@@ -0,0 +1,481 @@
+// 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>
+  <span :style="styleSearch">
+    <span v-if="!searchFilters || searchFilters.length === 0" style="display: flex;">
+      <a-input-search
+        style="width: 100%; display: table-cell"
+        :placeholder="$t('label.search')"
+        v-model="searchQuery"
+        allowClear
+        @search="onSearch" />
+    </span>
+
+    <span
+      v-else
+      class="filter-group">
+      <a-input-search
+        allowClear
+        class="input-search"
+        placeholder="Search"
+        v-model="searchQuery"
+        @search="onSearch">
+        <a-popover
+          placement="bottomRight"
+          slot="addonBefore"
+          trigger="click"
+          v-model="visibleFilter">
+          <template slot="content">
+            <a-form
+              style="min-width: 170px"
+              :form="form"
+              layout="vertical"
+              @submit="handleSubmit">
+              <a-form-item
+                v-for="(field, index) in fields"
+                :key="index"
+                :label="field.name==='keyword' ? $t('label.name') : $t('label.' + field.name)">
+                <a-select
+                  allowClear
+                  v-if="field.type==='list'"
+                  v-decorator="[field.name]"
+                  :loading="field.loading">
+                  <a-select-option
+                    v-for="(opt, idx) in field.opts"
+                    :key="idx"
+                    :value="opt.id">{{ $t(opt.name) }}</a-select-option>
+                </a-select>
+                <a-input
+                  v-else-if="field.type==='input'"
+                  v-decorator="[field.name]" />
+                <div v-else-if="field.type==='tag'">
+                  <div>
+                    <a-input-group
+                      type="text"
+                      size="small"
+                      compact>
+                      <a-input ref="input" :value="inputKey" @change="e => inputKey = e.target.value" style="width: 50px; text-align: center" :placeholder="$t('label.key')" />
+                      <a-input style=" width: 20px; border-left: 0; pointer-events: none; backgroundColor: #fff" placeholder="=" disabled />
+                      <a-input :value="inputValue" @change="handleValueChange" style="width: 50px; text-align: center; border-left: 0" :placeholder="$t('label.value')" />
+                      <a-button shape="circle" size="small" @click="inputKey = inputValue = ''">
+                        <a-icon type="close"/>
+                      </a-button>
+                    </a-input-group>
+                  </div>
+                </div>
+              </a-form-item>
+              <div class="filter-group-button">
+                <a-button
+                  class="filter-group-button-clear"
+                  type="default"
+                  size="small"
+                  icon="stop"
+                  @click="onClear">{{ $t('label.reset') }}</a-button>
+                <a-button
+                  class="filter-group-button-search"
+                  type="primary"
+                  size="small"
+                  icon="search"
+                  @click="handleSubmit">{{ $t('label.search') }}</a-button>
+              </div>
+            </a-form>
+          </template>
+          <a-button
+            class="filter-button"
+            size="small"
+            @click="() => { searchQuery = null }">
+            <a-icon type="filter" :theme="Object.keys(selectedFilters).length > 0 ? 'twoTone' : 'outlined'" />
+          </a-button>
+        </a-popover>
+      </a-input-search>
+    </span>
+  </span>
+</template>
+
+<script>
+import { api } from '@/api'
+
+export default {
+  name: 'SearchView',
+  props: {
+    searchFilters: {
+      type: Array,
+      default: () => []
+    },
+    apiName: {
+      type: String,
+      default: () => ''
+    },
+    selectedFilters: {
+      type: Object,
+      default: () => {}
+    }
+  },
+  inject: ['parentSearch', 'parentFilter', 'parentChangeFilter'],
+  data () {
+    return {
+      searchQuery: null,
+      paramsFilter: {},
+      visibleFilter: false,
+      fields: [],
+      inputKey: null,
+      inputValue: null
+    }
+  },
+  beforeCreate () {
+    this.form = this.$form.createForm(this)
+  },
+  watch: {
+    visibleFilter (newValue, oldValue) {
+      if (newValue) {
+        this.initFormFieldData()
+      }
+    }
+  },
+  computed: {
+    styleSearch () {
+      if (!this.searchFilters || this.searchFilters.length === 0) {
+        return {
+          width: '100%',
+          display: 'table-cell'
+        }
+      }
+
+      return {
+        width: '100%',
+        display: 'table-cell',
+        lineHeight: '31px'
+      }
+    }
+  },
+  methods: {
+    async initFormFieldData () {
+      const arrayField = []
+      this.fields = []
+      this.searchFilters.forEach(item => {
+        let type = 'input'
+
+        if (item === 'domainid' && !('listDomains' in this.$store.getters.apis)) {
+          return true
+        }
+        if (item === 'account' && !('addAccountToProject' in this.$store.getters.apis || 'createAccount' in this.$store.getters.apis)) {
+          return true
+        }
+        if (item === 'podid' && !('listPods' in this.$store.getters.apis)) {
+          return true
+        }
+        if (item === 'clusterid' && !('listClusters' in this.$store.getters.apis)) {
+          return true
+        }
+        if (['zoneid', 'domainid', 'state', 'level', 'clusterid', 'podid'].includes(item)) {
+          type = 'list'
+        } else if (item === 'tags') {
+          type = 'tag'
+        }
+
+        this.fields.push({
+          type: type,
+          name: item,
+          opts: [],
+          loading: false
+        })
+        arrayField.push(item)
+      })
+
+      const promises = []
+      let zoneIndex = -1
+      let domainIndex = -1
+      let podIndex = -1
+      let clusterIndex = -1
+
+      if (arrayField.includes('state')) {
+        const stateIndex = this.fields.findIndex(item => item.name === 'state')
+        this.fields[stateIndex].loading = true
+        this.fields[stateIndex].opts = this.fetchState()
+        this.fields[stateIndex].loading = false
+      }
+
+      if (arrayField.includes('level')) {
+        const levelIndex = this.fields.findIndex(item => item.name === 'level')
+        this.fields[levelIndex].loading = true
+        this.fields[levelIndex].opts = this.fetchLevel()
+        this.fields[levelIndex].loading = false
+      }
+
+      if (arrayField.includes('zoneid')) {
+        zoneIndex = this.fields.findIndex(item => item.name === 'zoneid')
+        this.fields[zoneIndex].loading = true
+        promises.push(await this.fetchZones())
+      }
+
+      if (arrayField.includes('domainid')) {
+        domainIndex = this.fields.findIndex(item => item.name === 'domainid')
+        this.fields[domainIndex].loading = true
+        promises.push(await this.fetchDomains())
+      }
+
+      if (arrayField.includes('podid')) {
+        podIndex = this.fields.findIndex(item => item.name === 'podid')
+        this.fields[podIndex].loading = true
+        promises.push(await this.fetchPods())
+      }
+
+      if (arrayField.includes('clusterid')) {
+        clusterIndex = this.fields.findIndex(item => item.name === 'clusterid')
+        this.fields[clusterIndex].loading = true
+        promises.push(await this.fetchClusters())
+      }
+
+      Promise.all(promises).then(response => {
+        if (zoneIndex > -1) {
+          const zones = response.filter(item => item.type === 'zoneid')
+          if (zones && zones.length > 0) {
+            this.fields[zoneIndex].opts = zones[0].data
+          }
+        }
+        if (domainIndex > -1) {
+          const domain = response.filter(item => item.type === 'domainid')
+          if (domain && domain.length > 0) {
+            this.fields[domainIndex].opts = domain[0].data
+          }
+        }
+        if (podIndex > -1) {
+          const pod = response.filter(item => item.type === 'podid')
+          if (pod && pod.length > 0) {
+            this.fields[podIndex].opts = pod[0].data
+          }
+        }
+        if (clusterIndex > -1) {
+          const cluster = response.filter(item => item.type === 'clusterid')
+          console.log(cluster)
+          if (cluster && cluster.length > 0) {
+            this.fields[clusterIndex].opts = cluster[0].data
+          }
+        }
+        this.$forceUpdate()
+      }).finally(() => {
+        if (zoneIndex > -1) {
+          this.fields[zoneIndex].loading = false
+        }
+        if (domainIndex > -1) {
+          this.fields[domainIndex].loading = false
+        }
+        if (podIndex > -1) {
+          this.fields[podIndex].loading = false
+        }
+        if (clusterIndex > -1) {
+          this.fields[clusterIndex].loading = false
+        }
+      })
+    },
+    fetchZones () {
+      return new Promise((resolve, reject) => {
+        api('listZones', { listAll: true }).then(json => {
+          const zones = json.listzonesresponse.zone
+          resolve({
+            type: 'zoneid',
+            data: zones
+          })
+        }).catch(error => {
+          reject(error.response.headers['x-description'])
+        })
+      })
+    },
+    fetchDomains () {
+      return new Promise((resolve, reject) => {
+        api('listDomains', { listAll: true }).then(json => {
+          const domain = json.listdomainsresponse.domain
+          resolve({
+            type: 'domainid',
+            data: domain
+          })
+        }).catch(error => {
+          reject(error.response.headers['x-description'])
+        })
+      })
+    },
+    fetchPods () {
+      return new Promise((resolve, reject) => {
+        api('listPods', { listAll: true }).then(json => {
+          const pods = json.listpodsresponse.pod
+          resolve({
+            type: 'podid',
+            data: pods
+          })
+        }).catch(error => {
+          reject(error.response.headers['x-description'])
+        })
+      })
+    },
+    fetchClusters () {
+      return new Promise((resolve, reject) => {
+        api('listClusters', { listAll: true }).then(json => {
+          const clusters = json.listclustersresponse.cluster
+          resolve({
+            type: 'clusterid',
+            data: clusters
+          })
+        }).catch(error => {
+          reject(error.response.headers['x-description'])
+        })
+      })
+    },
+    fetchState () {
+      const state = []
+      if (this.apiName.indexOf('listVolumes') > -1) {
+        state.push({
+          id: 'Allocated',
+          name: 'label.allocated'
+        })
+        state.push({
+          id: 'Ready',
+          name: 'label.isready'
+        })
+        state.push({
+          id: 'Destroy',
+          name: 'label.destroy'
+        })
+        state.push({
+          id: 'Expunging',
+          name: 'label.expunging'
+        })
+        state.push({
+          id: 'Expunged',
+          name: 'label.expunged'
+        })
+      }
+      return state
+    },
+    fetchLevel () {
+      const levels = []
+      levels.push({
+        id: 'INFO',
+        name: 'label.info.upper'
+      })
+      levels.push({
+        id: 'WARN',
+        name: 'label.warn.upper'
+      })
+      levels.push({
+        id: 'ERROR',
+        name: 'label.error.upper'
+      })
+      return levels
+    },
+    onSearch (value) {
+      this.paramsFilter = {}
+      this.searchQuery = value
+      this.parentSearch(this.searchQuery)
+    },
+    onClear () {
+      this.searchFilters.map(item => {
+        const field = {}
+        field[item] = undefined
+        this.form.setFieldsValue(field)
+      })
+      this.inputKey = null
+      this.inputValue = null
+      this.searchQuery = null
+      this.paramsFilter = {}
+      this.parentFilter(this.paramsFilter)
+    },
+    handleSubmit (e) {
+      e.preventDefault()
+      this.paramsFilter = {}
+      this.form.validateFields((err, values) => {
+        if (err) {
+          return
+        }
+        for (const key in values) {
+          const input = values[key]
+          if (input === '' || input === null || input === undefined) {
+            continue
+          }
+          this.paramsFilter[key] = input
+        }
+        if (this.searchFilters.includes('tags')) {
+          if (this.inputKey) {
+            this.paramsFilter['tags[0].key'] = this.inputKey
+            this.paramsFilter['tags[0].value'] = this.inputValue
+          }
+        }
+        this.parentFilter(this.paramsFilter)
+      })
+    },
+    handleKeyChange (e) {
+      this.inputKey = e.target.value
+    },
+    handleValueChange (e) {
+      this.inputValue = e.target.value
+    },
+    changeFilter (filter) {
+      this.parentChangeFilter(filter)
+    }
+  }
+}
+</script>
+
+<style scoped lang="less">
+.input-search {
+  margin-left: 10px;
+}
+
+.filter-group {
+  /deep/.ant-input-group-addon {
+    padding: 0 5px;
+  }
+
+  &-button {
+    background: inherit;
+    border: 0;
+    padding: 0;
+  }
+
+  &-button {
+    position: relative;
+    display: block;
+    min-height: 25px;
+
+    &-clear {
+      position: absolute;
+      left: 0;
+    }
+
+    &-search {
+      position: absolute;
+      right: 0;
+    }
+  }
+
+  /deep/.ant-input-group {
+    .ant-input-affix-wrapper {
+      width: calc(100% - 10px);
+    }
+  }
+}
+
+.filter-button {
+  background: inherit;
+  border: 0;
+  padding: 0;
+  position: relative;
+  display: block;
+  min-height: 25px;
+  width: 20px;
+}
+</style>
diff --git a/src/config/router.js b/src/config/router.js
index cad2aee..0c0699b 100644
--- a/src/config/router.js
+++ b/src/config/router.js
@@ -42,7 +42,7 @@ function generateRouterMap (section) {
     name: section.name,
     path: '/' + section.name,
     hidden: section.hidden,
-    meta: { title: section.title, icon: section.icon, docHelp: section.docHelp },
+    meta: { title: section.title, icon: section.icon, docHelp: section.docHelp, searchFilters: section.searchFilters },
     component: RouteView
   }
 
@@ -67,6 +67,7 @@ function generateRouterMap (section) {
           params: child.params ? child.params : {},
           columns: child.columns,
           details: child.details,
+          searchFilters: child.searchFilters,
           related: child.related,
           actions: child.actions,
           tabs: child.tabs
@@ -86,6 +87,7 @@ function generateRouterMap (section) {
               resourceType: child.resourceType,
               params: child.params ? child.params : {},
               details: child.details,
+              searchFilters: child.searchFilters,
               related: child.related,
               tabs: child.tabs,
               actions: child.actions ? child.actions : []
@@ -142,6 +144,7 @@ function generateRouterMap (section) {
         params: section.params ? section.params : {},
         details: section.details,
         related: section.related,
+        searchFilters: section.searchFilters,
         tabs: section.tabs,
         actions: section.actions ? section.actions : []
       },
diff --git a/src/config/section/compute.js b/src/config/section/compute.js
index c2d0df2..72bcdcf 100644
--- a/src/config/section/compute.js
+++ b/src/config/section/compute.js
@@ -61,6 +61,7 @@ export default {
         }
         return fields
       },
+      searchFilters: ['name', 'zoneid', 'domainid', 'account', 'tags'],
       details: ['displayname', 'name', 'id', 'state', 'ipaddress', 'templatename', 'ostypename', 'serviceofferingname', 'isdynamicallyscalable', 'haenable', 'hypervisor', 'boottype', 'bootmode', 'account', 'domain', 'zonename'],
       related: [{
         name: 'vmsnapshot',
diff --git a/src/config/section/event.js b/src/config/section/event.js
index 7924286..eee70fa 100644
--- a/src/config/section/event.js
+++ b/src/config/section/event.js
@@ -23,6 +23,7 @@ export default {
   permission: ['listEvents'],
   columns: ['username', 'description', 'state', 'level', 'type', 'account', 'domain', 'created'],
   details: ['username', 'id', 'description', 'state', 'level', 'type', 'account', 'domain', 'created'],
+  searchFilters: ['level', 'domainid', 'account', 'keyword'],
   related: [{
     name: 'event',
     title: 'label.event.timeline',
diff --git a/src/config/section/image.js b/src/config/section/image.js
index 6c2fcec..41a1970 100644
--- a/src/config/section/image.js
+++ b/src/config/section/image.js
@@ -40,6 +40,7 @@ export default {
         return fields
       },
       details: ['name', 'id', 'displaytext', 'checksum', 'hypervisor', 'format', 'ostypename', 'size', 'isready', 'passwordenabled', 'directdownload', 'isextractable', 'isdynamicallyscalable', 'ispublic', 'isfeatured', 'crosszones', 'type', 'account', 'domain', 'created'],
+      searchFilters: ['name', 'zoneid', 'tags'],
       related: [{
         name: 'vm',
         title: 'label.instances',
@@ -130,6 +131,7 @@ export default {
       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'],
+      searchFilters: ['name', 'zoneid', 'tags'],
       related: [{
         name: 'vm',
         title: 'label.instances',
diff --git a/src/config/section/infra/routers.js b/src/config/section/infra/routers.js
index 0be025d..43679aa 100644
--- a/src/config/section/infra/routers.js
+++ b/src/config/section/infra/routers.js
@@ -23,6 +23,7 @@ export default {
   permission: ['listRouters'],
   params: { projectid: '-1' },
   columns: ['name', 'state', 'publicip', 'guestnetworkname', 'vpcname', 'redundantstate', 'version', 'hostname', 'account', 'zonename', 'requiresupgrade'],
+  searchFilters: ['name', 'zoneid', 'podid', 'clusterid'],
   details: ['name', 'id', 'version', 'requiresupgrade', 'guestnetworkname', 'vpcname', 'publicip', 'guestipaddress', 'linklocalip', 'serviceofferingname', 'networkdomain', 'isredundantrouter', 'redundantstate', 'hostname', 'account', 'zonename', 'created'],
   tabs: [{
     name: 'details',
diff --git a/src/config/section/network.js b/src/config/section/network.js
index 95f9db0..3cf924d 100644
--- a/src/config/section/network.js
+++ b/src/config/section/network.js
@@ -31,6 +31,7 @@ export default {
       resourceType: 'Network',
       columns: ['name', 'state', 'type', 'cidr', 'ip6cidr', 'broadcasturi', 'account', 'zonename'],
       details: ['name', 'id', 'description', 'type', 'traffictype', 'vpcid', 'vlan', 'broadcasturi', 'cidr', 'ip6cidr', 'netmask', 'gateway', 'ispersistent', 'restartrequired', 'reservediprange', 'redundantrouter', 'networkdomain', 'zonename', 'account', 'domain'],
+      searchFilters: ['keyword', 'zoneid', 'domainid', 'account', 'tags'],
       related: [{
         name: 'vm',
         title: 'label.instances',
@@ -113,6 +114,7 @@ export default {
       resourceType: 'Vpc',
       columns: ['name', 'state', 'displaytext', 'cidr', 'account', 'zonename'],
       details: ['name', 'id', 'displaytext', 'cidr', 'networkdomain', 'ispersistent', 'redundantvpcrouter', 'restartrequired', 'zonename', 'account', 'domain'],
+      searchFilters: ['name', 'zoneid', 'domainid', 'account', 'tags'],
       related: [{
         name: 'vm',
         title: 'label.instances',
@@ -554,6 +556,7 @@ export default {
       permission: ['listVpnCustomerGateways'],
       columns: ['name', 'gateway', 'cidrlist', 'ipsecpsk', 'account', 'domain'],
       details: ['name', 'id', 'gateway', 'cidrlist', 'ipsecpsk', 'ikepolicy', 'ikelifetime', 'esppolicy', 'esplifetime', 'dpd', 'forceencap', 'account', 'domain'],
+      searchFilters: ['keyword', 'domainid', 'account'],
       actions: [
         {
           api: 'createVpnCustomerGateway',
diff --git a/src/config/section/project.js b/src/config/section/project.js
index e5fdb3f..56e0d46 100644
--- a/src/config/section/project.js
+++ b/src/config/section/project.js
@@ -23,6 +23,7 @@ export default {
   permission: ['listProjects'],
   resourceType: 'Project',
   columns: ['name', 'state', 'displaytext', 'account', 'domain'],
+  searchFilters: ['name', 'displaytext', 'domainid', 'account'],
   details: ['name', 'id', 'displaytext', 'projectaccountname', 'account', 'domain'],
   tabs: [
     {
diff --git a/src/config/section/storage.js b/src/config/section/storage.js
index ac41970..970d979 100644
--- a/src/config/section/storage.js
+++ b/src/config/section/storage.js
@@ -64,6 +64,7 @@ export default {
         title: 'label.snapshots',
         param: 'volumeid'
       }],
+      searchFilters: ['name', 'zoneid', 'domainid', 'account', 'state', 'tags'],
       actions: [
         {
           api: 'createVolume',
@@ -231,6 +232,7 @@ export default {
       resourceType: 'Snapshot',
       columns: ['name', 'state', 'volumename', 'intervaltype', 'created', 'account'],
       details: ['name', 'id', 'volumename', 'intervaltype', 'account', 'domain', 'created'],
+      searchFilters: ['name', 'domainid', 'account', 'tags'],
       actions: [
         {
           api: 'createTemplate',
@@ -283,6 +285,7 @@ export default {
       resourceType: 'VMSnapshot',
       columns: ['displayname', 'state', 'type', 'current', 'parentName', 'created', 'account'],
       details: ['name', 'id', 'displayname', 'description', 'type', 'current', 'parentName', 'virtualmachineid', 'account', 'domain', 'created'],
+      searchFilters: ['name', 'domainid', 'account', 'tags'],
       actions: [
         {
           api: 'revertToVMSnapshot',
diff --git a/src/locales/en.json b/src/locales/en.json
index 24f0ede..96d1229 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -514,6 +514,7 @@
 "label.ciscovnmc.resource.details": "CiscoVNMC resource details",
 "label.cks.cluster.size": "Cluster size (Worker nodes)",
 "label.cleanup": "Clean up",
+"label.clear": "Clear",
 "label.clear.list": "Clear list",
 "label.close": "Close",
 "label.cloud.console": "Cloud Management Console",
@@ -723,7 +724,7 @@
 "label.domain.name": "Domain Name",
 "label.domain.router": "Domain router",
 "label.domain.suffix": "DNS Domain Suffix (i.e., xyz.com)",
-"label.domainid": "Domain ID",
+"label.domainid": "Domain",
 "label.domainname": "Domain",
 "label.domainpath": "Domain",
 "label.domains": "Domains",
@@ -801,6 +802,8 @@
 "label.example": "Example",
 "label.existingnetworks": "Existing networks",
 "label.expunge": "Expunge",
+"label.expunged": "Expunged",
+"label.expunging": "Expunging",
 "label.external.link": "External link",
 "label.externalid": "External Id",
 "label.externalloadbalanceripaddress": "External load balancer IP address",
diff --git a/src/utils/plugins.js b/src/utils/plugins.js
index 0e3e7f9..5b9328d 100644
--- a/src/utils/plugins.js
+++ b/src/utils/plugins.js
@@ -21,7 +21,6 @@ import { api } from '@/api'
 import { message, notification } from 'ant-design-vue'
 
 export const pollJobPlugin = {
-
   install (Vue) {
     Vue.prototype.$pollJob = function (options) {
       /**
@@ -111,7 +110,6 @@ export const pollJobPlugin = {
 }
 
 export const notifierPlugin = {
-
   install (Vue) {
     Vue.prototype.$notifyError = function (error) {
       console.log(error)
diff --git a/src/views/AutogenView.vue b/src/views/AutogenView.vue
index f1b5f22..c2815a6 100644
--- a/src/views/AutogenView.vue
+++ b/src/views/AutogenView.vue
@@ -68,13 +68,11 @@
             :dataView="dataView"
             :resource="resource"
             @exec-action="execAction"/>
-          <a-input-search
+          <search-view
             v-if="!dataView"
-            style="width: 100%; display: table-cell"
-            :placeholder="$t('label.search')"
-            v-model="searchQuery"
-            allowClear
-            @search="onSearch" />
+            :searchFilters="searchFilters"
+            :selectedFilters="paramsFilters"
+            :apiName="apiName"/>
         </a-col>
       </a-row>
     </a-card>
@@ -304,7 +302,7 @@
         :current="page"
         :pageSize="pageSize"
         :total="itemCount"
-        :showTotal="total => `Showing ${1+((page-1)*pageSize)}-${Math.min(page*pageSize, total)} of ${total} items`"
+        :showTotal="total => `Showing ${Math.min(total, 1+((page-1)*pageSize))}-${Math.min(page*pageSize, total)} of ${total} items`"
         :pageSizeOptions="device === 'desktop' ? ['20', '50', '100', '500'] : ['10', '20', '50', '100', '500']"
         @change="changePage"
         @showSizeChange="changePageSize"
@@ -326,6 +324,7 @@ import Status from '@/components/widgets/Status'
 import ListView from '@/components/view/ListView'
 import ResourceView from '@/components/view/ResourceView'
 import ActionButton from '@/components/view/ActionButton'
+import SearchView from '@/components/view/SearchView'
 
 export default {
   name: 'Resource',
@@ -335,7 +334,8 @@ export default {
     ResourceView,
     ListView,
     Status,
-    ActionButton
+    ActionButton,
+    SearchView
   },
   mixins: [mixinDevice],
   provide: function () {
@@ -344,6 +344,9 @@ export default {
       parentToggleLoading: this.toggleLoading,
       parentStartLoading: this.startLoading,
       parentFinishLoading: this.finishLoading,
+      parentSearch: this.onSearch,
+      parentChangeFilter: this.changeFilter,
+      parentFilter: this.onFilter,
       parentChangeResource: this.changeResource,
       parentPollActionCompletion: this.pollActionCompletion,
       parentEditTariffAction: () => {}
@@ -367,6 +370,8 @@ export default {
       dataView: false,
       selectedFilter: '',
       filters: [],
+      searchFilters: [],
+      paramsFilters: {},
       actions: [],
       formModel: {},
       confirmDirty: false
@@ -397,6 +402,7 @@ export default {
     '$route' (to, from) {
       if (to.fullPath !== from.fullPath && !to.fullPath.includes('action/')) {
         this.searchQuery = ''
+        this.paramsFilters = {}
         this.page = 1
         this.itemCount = 0
         this.selectedFilter = ''
@@ -449,6 +455,11 @@ export default {
       } else if (this.$route.meta.params) {
         Object.assign(params, this.$route.meta.params)
       }
+      if (Object.keys(this.paramsFilters).length > 0) {
+        Object.assign(params, this.paramsFilters)
+      }
+
+      this.searchFilters = this.$route && this.$route.meta && this.$route.meta.searchFilters
 
       if (this.$route && this.$route.params && this.$route.params.id) {
         this.dataView = true
@@ -616,6 +627,16 @@ export default {
           this.$emit('change-resource', this.resource)
         }
       }).catch(error => {
+        if (Object.keys(this.paramsFilters).length > 0) {
+          this.itemCount = 0
+          this.items = []
+          this.$message.error({
+            content: error.response.headers['x-description'],
+            duration: 5
+          })
+          return
+        }
+
         this.$notifyError(error)
 
         if ([401, 405].includes(error.response.status)) {
@@ -634,10 +655,19 @@ export default {
       })
     },
     onSearch (value) {
+      this.paramsFilters = {}
       this.searchQuery = value
       this.page = 1
       this.fetchData()
     },
+    onFilter (filters) {
+      this.paramsFilters = {}
+      if (filters && Object.keys(filters).length > 0) {
+        this.paramsFilters = filters
+      }
+      this.page = 1
+      this.fetchData()
+    },
     closeAction () {
       this.actionLoading = false
       this.showAction = false