From 8a825860a15c4084268fbe736a9c0dfb606e0c5a Mon Sep 17 00:00:00 2001 From: loganhz Date: Wed, 11 Sep 2019 20:32:43 +0800 Subject: [PATCH] Support search in apps and multi-cluster apps list page --- app/apps-tab/index/controller.js | 5 +- app/apps-tab/index/template.hbs | 10 +- app/components/namespace-table/component.js | 38 +---- app/components/pod-dots/component.js | 32 +--- .../multi-cluster-apps/index/controller.js | 4 +- .../multi-cluster-apps/index/template.hbs | 10 +- .../addon/components/search-text/component.js | 14 ++ .../addon/components/search-text/template.hbs | 17 +++ .../components/sortable-table/component.js | 142 ++--------------- lib/shared/addon/utils/search-text.js | 144 ++++++++++++++++++ .../app/components/search-text/component.js | 1 + lib/shared/app/utils/search-text.js | 1 + translations/en-us.yaml | 2 + 13 files changed, 218 insertions(+), 202 deletions(-) create mode 100644 lib/shared/addon/components/search-text/component.js create mode 100644 lib/shared/addon/components/search-text/template.hbs create mode 100644 lib/shared/addon/utils/search-text.js create mode 100644 lib/shared/app/components/search-text/component.js create mode 100644 lib/shared/app/utils/search-text.js diff --git a/app/apps-tab/index/controller.js b/app/apps-tab/index/controller.js index be74bf147..4cca24464 100644 --- a/app/apps-tab/index/controller.js +++ b/app/apps-tab/index/controller.js @@ -4,6 +4,7 @@ import Controller, { inject as controller } from '@ember/controller'; import C from 'ui/utils/constants'; import { computed, get, observer } from '@ember/object'; import { once } from '@ember/runloop'; +import { filter } from 'ui/utils/search-text'; export default Controller.extend({ prefs: service(), @@ -18,7 +19,7 @@ export default Controller.extend({ once(() => this.get('catalog').fetchAppTemplates(get(this, 'model.apps'))); }), - filteredApps: computed('model.apps.@each.{type,isFromCatalog,tags,state}', 'tags', function() { + filteredApps: computed('model.apps.@each.{type,isFromCatalog,tags,state}', 'tags', 'searchText', function() { var needTags = get(this, 'tags'); var apps = get(this, 'model.apps').filter((ns) => !C.REMOVEDISH_STATES.includes(get(ns, 'state'))); @@ -30,6 +31,8 @@ export default Controller.extend({ apps = apps.filterBy('isIstio', false); apps = apps.sortBy('displayName'); + apps = filter(apps, get(this, 'searchText')); + const group = []; let dataIndex = 0; diff --git a/app/apps-tab/index/template.hbs b/app/apps-tab/index/template.hbs index ef370aef3..2ac0f63c9 100644 --- a/app/apps-tab/index/template.hbs +++ b/app/apps-tab/index/template.hbs @@ -8,6 +8,10 @@
+
+ {{search-text searchText=searchText}} +
+ {{#each filteredApps as |group|}}
{{#each group as |ns|}} @@ -18,7 +22,11 @@
{{else}}
- {{t 'nav.apps.noData'}} + {{#if searchText}} + {{t "nav.apps.noMatch"}} + {{else}} + {{t "nav.apps.noData"}} + {{/if}}
{{/each}}
\ No newline at end of file diff --git a/app/components/namespace-table/component.js b/app/components/namespace-table/component.js index c78dae484..595a28a84 100644 --- a/app/components/namespace-table/component.js +++ b/app/components/namespace-table/component.js @@ -2,7 +2,7 @@ import Component from '@ember/component'; import layout from './template'; import { inject as service } from '@ember/service'; import { get, computed } from '@ember/object'; -import { matches } from 'shared/components/sortable-table/component'; +import { filter } from 'ui/utils/search-text'; const headers = [ { @@ -45,40 +45,6 @@ export default Component.extend({ ], projectsWithoutNamespace: computed('projectsWithoutNamespaces.[]', 'searchText', function() { - let searchText = (get(this, 'searchText') || '').trim().toLowerCase(); - let out = get(this, 'projectsWithoutNamespaces').slice(); - let searchFields = ['displayName']; - - if ( searchText.length ) { - let searchTokens = searchText.split(/\s*[, ]\s*/); - - for ( let i = out.length - 1 ; i >= 0 ; i-- ) { - let hits = 0; - let row = out[i]; - let mainFound = true; - - for ( let j = 0 ; j < searchTokens.length ; j++ ) { - let expect = true; - let token = searchTokens[j]; - - if ( token.substr(0, 1) === '!' ) { - expect = false; - token = token.substr(1); - } - - if ( token && matches(searchFields, token, row) !== expect ) { - mainFound = false; - - break; - } - } - - if ( !mainFound && hits === 0 ) { - out.removeAt(i); - } - } - } - - return out + return filter(get(this, 'projectsWithoutNamespaces').slice(), get(this, 'searchText'), ['displayName']); }), }); diff --git a/app/components/pod-dots/component.js b/app/components/pod-dots/component.js index 9ee144fc5..bba59f38f 100644 --- a/app/components/pod-dots/component.js +++ b/app/components/pod-dots/component.js @@ -1,8 +1,8 @@ -import { computed, observer } from '@ember/object'; +import { get, computed, observer } from '@ember/object'; import { alias } from '@ember/object/computed'; import Component from '@ember/component'; import pagedArray from 'ember-cli-pagination/computed/paged-array'; -import { matches } from 'shared/components/sortable-table/component'; +import { filter } from 'ui/utils/search-text'; import layout from './template'; export const searchFields = ['displayName', 'id:prefix', 'displayState', 'image', 'displayIp:ip']; @@ -48,33 +48,7 @@ export default Component.extend({ out.pushObject(pod); } - let searchFields = this.get('searchFields'); - let searchText = (this.get('searchText') || '').trim().toLowerCase(); - - if ( searchText.length ) { - let searchTokens = searchText.split(/\s*[, ]\s*/); - - for ( let i = out.length - 1 ; i >= 0 ; i-- ) { - let row = out[i].containers[0]; - - for ( let j = 0 ; j < searchTokens.length ; j++ ) { - let expect = true; - let token = searchTokens[j]; - - if ( token.substr(0, 1) === '!' ) { - expect = false; - token = token.substr(1); - } - - if ( token && matches(searchFields, token, row) !== expect ) { - out.removeAt(i); - break; - } - } - } - } - - return out; + return filter(out, get(this, 'searchText'), get(this, 'searchFields')); }), pagedContent: pagedArray('filtered', { diff --git a/lib/global-admin/addon/multi-cluster-apps/index/controller.js b/lib/global-admin/addon/multi-cluster-apps/index/controller.js index 0f5719b9c..3dd171a91 100644 --- a/lib/global-admin/addon/multi-cluster-apps/index/controller.js +++ b/lib/global-admin/addon/multi-cluster-apps/index/controller.js @@ -3,6 +3,7 @@ import Controller from '@ember/controller'; import C from 'ui/utils/constants'; import { computed, get, observer } from '@ember/object'; import { once } from '@ember/runloop'; +import { filter } from 'ui/utils/search-text'; export default Controller.extend({ prefs: service(), @@ -14,10 +15,11 @@ export default Controller.extend({ once(() => this.get('catalog').fetchAppTemplates(get(this, 'model.apps'))); }), - filteredApps: computed('model.apps.@each.{type,isFromCatalog,state}', function() { + filteredApps: computed('model.apps.@each.{type,isFromCatalog,state}', 'searchText', function() { let apps = get(this, 'model.apps').filter((ns) => !C.REMOVEDISH_STATES.includes(get(ns, 'state'))); apps = apps.sortBy('displayName'); + apps = filter(apps, get(this, 'searchText')); const group = []; let dataIndex = 0; diff --git a/lib/global-admin/addon/multi-cluster-apps/index/template.hbs b/lib/global-admin/addon/multi-cluster-apps/index/template.hbs index e8116747e..0202b6577 100644 --- a/lib/global-admin/addon/multi-cluster-apps/index/template.hbs +++ b/lib/global-admin/addon/multi-cluster-apps/index/template.hbs @@ -19,6 +19,10 @@
+
+ {{search-text searchText=searchText}} +
+ {{#each filteredApps as |group|}}
{{#each group as |app|}} @@ -31,7 +35,11 @@
{{else}}
- {{t 'multiClusterAppsPage.noData'}} + {{#if searchText}} + {{t "multiClusterAppsPage.noMatch"}} + {{else}} + {{t "multiClusterAppsPage.noData"}} + {{/if}}
{{/each}}
\ No newline at end of file diff --git a/lib/shared/addon/components/search-text/component.js b/lib/shared/addon/components/search-text/component.js new file mode 100644 index 000000000..b98bf8398 --- /dev/null +++ b/lib/shared/addon/components/search-text/component.js @@ -0,0 +1,14 @@ +import Component from '@ember/component'; +import { set } from '@ember/object'; +import layout from './template'; + +export default Component.extend({ + layout, + searchFields: ['displayName', 'id:prefix', 'displayState'], + + actions: { + clearSearch() { + set(this, 'searchText', ''); + }, + }, +}); diff --git a/lib/shared/addon/components/search-text/template.hbs b/lib/shared/addon/components/search-text/template.hbs new file mode 100644 index 000000000..43e52ab98 --- /dev/null +++ b/lib/shared/addon/components/search-text/template.hbs @@ -0,0 +1,17 @@ +
+ {{input + value=searchText + aria-title=(t "generic.search") + type="search" class="input-sm pull-right" + placeholder=(t "generic.search") + }} + {{#if searchText}} + + + + {{/if}} +
\ No newline at end of file diff --git a/lib/shared/addon/components/sortable-table/component.js b/lib/shared/addon/components/sortable-table/component.js index 1024fc7bf..8176cae38 100644 --- a/lib/shared/addon/components/sortable-table/component.js +++ b/lib/shared/addon/components/sortable-table/component.js @@ -11,6 +11,7 @@ import { isArray } from '@ember/array'; import { observer } from '@ember/object' import { run } from '@ember/runloop'; import { isAlternate, isMore, isRange } from 'shared/utils/platform'; +import { filter } from 'ui/utils/search-text'; function toggleInput(node, on) { let id = get(node, 'id'); @@ -34,70 +35,6 @@ function toggleInput(node, on) { } } -export function matches(fields, token, item) { - let tokenMayBeIp = /^[0-9a-f\.:]+$/i.test(token); - - for ( let i = 0 ; i < fields.length ; i++ ) { - let field = fields[i]; - - if ( field ) { - // Modifiers: - // id: The token must match id format (i.e. 1i123) - let idx = field.indexOf(':'); - let modifier = null; - - if ( idx > 0 ) { - modifier = field.substr(idx + 1); - field = field.substr(0, idx); - } - - let val = get(item, field); - - if ( val === undefined ) { - continue; - } - - val = (`${ val }`).toLowerCase(); - if ( !val ) { - continue; - } - - switch ( modifier ) { - case 'exact': - if ( val === token ) { - return true; - } - - break; - case 'ip': - if ( tokenMayBeIp ) { - let re = new RegExp(`(?:^|\.)${ token }(?:\.|$)`); - - if ( re.test(val) ) { - return true; - } - } - - break; - case 'prefix': - if ( val.indexOf(token) === 0) { - return true; - } - - break; - default: - if ( val.indexOf(token) >= 0) { - return true; - } - - break; - } - } - } - - return false; -} - export default Component.extend(Sortable, StickyHeader, { prefs: service(), intl: service(), @@ -446,78 +383,17 @@ export default Component.extend(Sortable, StickyHeader, { }), filtered: computed('arranged.[]', 'searchText', function() { - let out = get(this, 'arranged').slice(); - let searchFields = get(this, 'searchFields'); - let searchText = (get(this, 'searchText') || '').trim().toLowerCase(); - let subSearchField = get(this, 'subSearchField'); - let subFields = get(this, 'subFields'); - let subMatches = null; - - if ( searchText.length ) { - subMatches = {}; - - let searchTokens = searchText.split(/\s*[, ]\s*/); - - for ( let i = out.length - 1 ; i >= 0 ; i-- ) { - let hits = 0; - let row = out[i]; - let mainFound = true; - - for ( let j = 0 ; j < searchTokens.length ; j++ ) { - let expect = true; - let token = searchTokens[j]; - - if ( token.substr(0, 1) === '!' ) { - expect = false; - token = token.substr(1); - } - - if ( token && matches(searchFields, token, row) !== expect ) { - mainFound = false; - - break; - } - } - - if ( subFields && subSearchField) { - let subRows = (row.get(subSearchField) || []); - - for ( let k = subRows.length - 1 ; k >= 0 ; k-- ) { - let subFound = true; - - for ( let l = 0 ; l < searchTokens.length ; l++ ) { - let expect = true; - let token = searchTokens[l]; - - if ( token.substr(0, 1) === '!' ) { - expect = false; - token = token.substr(1); - } - - if ( matches(subFields, token, subRows[k]) !== expect ) { - subFound = false; - - break; - } - } - - if ( subFound ) { - hits++; - } - } - - subMatches[row.get('id')] = hits; - } - - if ( !mainFound && hits === 0 ) { - out.removeAt(i); - } - } - } + const { matches, subMatches } = filter( + get(this, 'arranged').slice(), + get(this, 'searchText'), + get(this, 'searchFields'), + get(this, 'subFields'), + get(this, 'subSearchField') + ); set(this, 'subMatches', subMatches); - return out; + return matches; }), indexFrom: computed('page', 'perPage', function() { diff --git a/lib/shared/addon/utils/search-text.js b/lib/shared/addon/utils/search-text.js new file mode 100644 index 000000000..c4829962e --- /dev/null +++ b/lib/shared/addon/utils/search-text.js @@ -0,0 +1,144 @@ +import { get } from '@ember/object'; + +const SEARCH_FIELDS = ['displayName', 'id:prefix', 'displayState']; + +export function matches(fields, token, item) { + let tokenMayBeIp = /^[0-9a-f\.:]+$/i.test(token); + + for ( let i = 0 ; i < fields.length ; i++ ) { + let field = fields[i]; + + if ( field ) { + // Modifiers: + // id: The token must match id format (i.e. 1i123) + let idx = field.indexOf(':'); + let modifier = null; + + if ( idx > 0 ) { + modifier = field.substr(idx + 1); + field = field.substr(0, idx); + } + + let val = get(item, field); + + if ( val === undefined ) { + continue; + } + + val = (`${ val }`).toLowerCase(); + if ( !val ) { + continue; + } + + switch ( modifier ) { + case 'exact': + if ( val === token ) { + return true; + } + + break; + case 'ip': + if ( tokenMayBeIp ) { + let re = new RegExp(`(?:^|\.)${ token }(?:\.|$)`); + + if ( re.test(val) ) { + return true; + } + } + + break; + case 'prefix': + if ( val.indexOf(token) === 0) { + return true; + } + + break; + default: + if ( val.indexOf(token) >= 0) { + return true; + } + + break; + } + } + } + + return false; +} + +export function filter(out, searchText, searchFields = SEARCH_FIELDS, subFields, subSearchField) { + let subMatches = null; + + searchText = (searchText || '').trim().toLowerCase(); + + if ( searchText.length ) { + subMatches = {}; + + let searchTokens = searchText.split(/\s*[, ]\s*/); + + for ( let i = out.length - 1 ; i >= 0 ; i-- ) { + let row = out[i]; + let hits = 0; + let mainFound = true; + + for ( let j = 0 ; j < searchTokens.length ; j++ ) { + let expect = true; + let token = searchTokens[j]; + + if ( token.substr(0, 1) === '!' ) { + expect = false; + token = token.substr(1); + } + + if ( token && matches(searchFields, token, row) !== expect ) { + mainFound = false; + + break; + } + } + + if ( subFields && subSearchField ) { + let subRows = (row.get(subSearchField) || []); + + for ( let k = subRows.length - 1 ; k >= 0 ; k-- ) { + let subFound = true; + + for ( let l = 0 ; l < searchTokens.length ; l++ ) { + let expect = true; + let token = searchTokens[l]; + + if ( token.substr(0, 1) === '!' ) { + expect = false; + token = token.substr(1); + } + + if ( matches(subFields, token, subRows[k]) !== expect ) { + subFound = false; + + break; + } + } + + if ( subFound ) { + hits++; + } + } + + subMatches[row.get('id')] = hits; + } + + if ( !mainFound && hits === 0 ) { + out.removeAt(i); + } + } + } + + if ( subFields && subSearchField ) { + return { + matches: out, + subMatches + }; + } else { + return out; + } +} diff --git a/lib/shared/app/components/search-text/component.js b/lib/shared/app/components/search-text/component.js new file mode 100644 index 000000000..6f891e0f3 --- /dev/null +++ b/lib/shared/app/components/search-text/component.js @@ -0,0 +1 @@ +export { default } from 'shared/components/search-text/component'; diff --git a/lib/shared/app/utils/search-text.js b/lib/shared/app/utils/search-text.js new file mode 100644 index 000000000..6a1e6ae85 --- /dev/null +++ b/lib/shared/app/utils/search-text.js @@ -0,0 +1 @@ +export { filter } from 'shared/utils/search-text'; diff --git a/translations/en-us.yaml b/translations/en-us.yaml index fc546578c..74ed46381 100644 --- a/translations/en-us.yaml +++ b/translations/en-us.yaml @@ -1573,6 +1573,7 @@ globalDnsPage: multiClusterAppsPage: header: Multi-Cluster Apps noData: There are no multi-cluster apps launched + noMatch: No multi-cluster apps match the current search error: appData: Error loading global app data @@ -7764,6 +7765,7 @@ nav: tab: Apps apps: Apps noData: There are no apps launched. + noMatch: No apps match the current search launch: Launch manage: Manage Catalogs infra: