Merge pull request #3340 from loganhz/apps

Support search in apps and multi-cluster apps list page
This commit is contained in:
Vincent Fiduccia 2019-09-11 10:21:36 -07:00 committed by GitHub
commit 51a52fd8ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 218 additions and 202 deletions

View File

@ -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;

View File

@ -8,6 +8,10 @@
</section>
<section class="pl-0 pr-0">
<div class="row mb-15">
{{search-text searchText=searchText}}
</div>
{{#each filteredApps as |group|}}
<div class="row flex">
{{#each group as |ns|}}
@ -18,7 +22,11 @@
</div>
{{else}}
<div class="text-center text-muted">
{{t 'nav.apps.noData'}}
{{#if searchText}}
{{t "nav.apps.noMatch"}}
{{else}}
{{t "nav.apps.noData"}}
{{/if}}
</div>
{{/each}}
</section>

View File

@ -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']);
}),
});

View File

@ -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', {

View File

@ -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;

View File

@ -19,6 +19,10 @@
</section>
<section class="pl-0 pr-0">
<div class="row mb-15">
{{search-text searchText=searchText}}
</div>
{{#each filteredApps as |group|}}
<div class="row flex">
{{#each group as |app|}}
@ -31,7 +35,11 @@
</div>
{{else}}
<div class="text-center text-muted">
{{t 'multiClusterAppsPage.noData'}}
{{#if searchText}}
{{t "multiClusterAppsPage.noMatch"}}
{{else}}
{{t "multiClusterAppsPage.noData"}}
{{/if}}
</div>
{{/each}}
</section>

View File

@ -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', '');
},
},
});

View File

@ -0,0 +1,17 @@
<div class="pull-right search-group input-group">
{{input
value=searchText
aria-title=(t "generic.search")
type="search" class="input-sm pull-right"
placeholder=(t "generic.search")
}}
{{#if searchText}}
<span class="input-group-btn">
<button
class="btn bg-transparent text-info pl-10 pr-10" {{action "clearSearch"}}
>
<i class="icon icon-close"/>
</button>
</span>
{{/if}}
</div>

View File

@ -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() {

View File

@ -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;
}
}

View File

@ -0,0 +1 @@
export { default } from 'shared/components/search-text/component';

View File

@ -0,0 +1 @@
export { filter } from 'shared/utils/search-text';

View File

@ -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: