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: