Skip to content

Commit

Permalink
ui: Search improvements (#5540)
Browse files Browse the repository at this point in the history
* ui: Replaces Service listing filterbar with a phrase-editor search (#5507)

1. New phrase-editor restricting search to whole phrases (acts on
enter key). Allows removal of previously entered phrases
2. Searching now allows arrays of terms, multiple terms work via AND
  • Loading branch information
johncowen authored Apr 30, 2019
1 parent 1437472 commit bc22ab5
Show file tree
Hide file tree
Showing 18 changed files with 289 additions and 45 deletions.
44 changes: 44 additions & 0 deletions ui-v2/app/components/phrase-editor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Component from '@ember/component';
import { get, set } from '@ember/object';

export default Component.extend({
classNames: ['phrase-editor'],
item: '',
remove: function(index, e) {
this.items.removeAt(index, 1);
this.onchange(e);
},
add: function(e) {
const value = get(this, 'item').trim();
if (value !== '') {
set(this, 'item', '');
const currentItems = get(this, 'items') || [];
const items = new Set(currentItems).add(value);
if (items.size > currentItems.length) {
set(this, 'items', [...items]);
this.onchange(e);
}
}
},
onkeydown: function(e) {
switch (e.keyCode) {
case 8:
if (e.target.value == '' && this.items.length > 0) {
this.remove(this.items.length - 1);
}
break;
}
},
oninput: function(e) {
set(this, 'item', e.target.value);
},
onchange: function(e) {
let searchable = get(this, 'searchable');
if (!Array.isArray(searchable)) {
searchable = [searchable];
}
searchable.forEach(item => {
item.search(get(this, 'items'));
});
},
});
29 changes: 15 additions & 14 deletions ui-v2/app/controllers/dc/services/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import Controller from '@ember/controller';
import { get, computed } from '@ember/object';
import { htmlSafe } from '@ember/string';
import WithEventSource from 'consul-ui/mixins/with-event-source';
import WithHealthFiltering from 'consul-ui/mixins/with-health-filtering';
import WithSearching from 'consul-ui/mixins/with-searching';
const max = function(arr, prop) {
return arr.reduce(function(prev, item) {
Expand All @@ -26,21 +25,23 @@ const width = function(num) {
const widthDeclaration = function(num) {
return htmlSafe(`width: ${num}px`);
};
export default Controller.extend(WithEventSource, WithSearching, WithHealthFiltering, {
export default Controller.extend(WithEventSource, WithSearching, {
queryParams: {
s: {
as: 'filter',
},
},
init: function() {
this.searchParams = {
service: 's',
};
this._super(...arguments);
},
searchable: computed('filtered', function() {
searchable: computed('items.[]', function() {
return get(this, 'searchables.service')
.add(get(this, 'filtered'))
.search(get(this, this.searchParams.service));
.add(get(this, 'items'))
.search(get(this, 'terms'));
}),
filter: function(item, { s = '', status = '' }) {
return item.hasStatus(status);
},
maxWidth: computed('{maxPassing,maxWarning,maxCritical}', function() {
const PADDING = 32 * 3 + 13;
return ['maxPassing', 'maxWarning', 'maxCritical'].reduce((prev, item) => {
Expand All @@ -58,14 +59,14 @@ export default Controller.extend(WithEventSource, WithSearching, WithHealthFilte
// so again divide that by 2 and take it off each fluid column
return htmlSafe(`width: calc(50% - 50px - ${Math.round(get(this, 'maxWidth') / 2)}px)`);
}),
maxPassing: computed('filtered', function() {
return max(get(this, 'filtered'), 'ChecksPassing');
maxPassing: computed('items.[]', function() {
return max(get(this, 'items'), 'ChecksPassing');
}),
maxWarning: computed('filtered', function() {
return max(get(this, 'filtered'), 'ChecksWarning');
maxWarning: computed('items.[]', function() {
return max(get(this, 'items'), 'ChecksWarning');
}),
maxCritical: computed('filtered', function() {
return max(get(this, 'filtered'), 'ChecksCritical');
maxCritical: computed('items.[]', function() {
return max(get(this, 'items'), 'ChecksCritical');
}),
passingWidth: computed('maxPassing', function() {
return widthDeclaration(width(get(this, 'maxPassing')));
Expand Down
19 changes: 19 additions & 0 deletions ui-v2/app/routes/dc/services/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,29 @@ export default Route.extend({
as: 'filter',
replace: true,
},
// temporary support of old style status
status: {
as: 'status',
},
},
model: function(params) {
const repo = get(this, 'repo');
let terms = params.s || '';
// we check for the old style `status` variable here
// and convert it to the new style filter=status:critical
let status = params.status;
if (status) {
status = `status:${status}`;
if (terms.indexOf(status) === -1) {
terms = terms
.split('\n')
.concat(status)
.join('\n')
.trim();
}
}
return hash({
terms: terms !== '' ? terms.split('\n') : [],
items: repo.findAllByDatacenter(this.modelFor('dc').dc.Name),
});
},
Expand Down
36 changes: 28 additions & 8 deletions ui-v2/app/search/filters/service.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,34 @@
import { get } from '@ember/object';
import ucfirst from 'consul-ui/utils/ucfirst';
const find = function(obj, term) {
if (Array.isArray(obj)) {
return obj.some(function(item) {
return find(item, term);
});
}
return obj.toLowerCase().indexOf(term) !== -1;
};
export default function(filterable) {
return filterable(function(item, { s = '' }) {
const term = s.toLowerCase();
return (
get(item, 'Name')
.toLowerCase()
.indexOf(term) !== -1 ||
(get(item, 'Tags') || []).some(function(item) {
return item.toLowerCase().indexOf(term) !== -1;
})
);
let status;
switch (true) {
case term.startsWith('service:'):
return find(get(item, 'Name'), term.substr(8));
case term.startsWith('tag:'):
return find(get(item, 'Tags') || [], term.substr(4));
case term.startsWith('status:'):
status = term.substr(7);
switch (term.substr(7)) {
case 'warning':
case 'critical':
case 'passing':
return get(item, `Checks${ucfirst(status)}`) > 0;
default:
return false;
}
default:
return find(get(item, 'Name'), term) || find(get(item, 'Tags') || [], term);
}
});
}
14 changes: 14 additions & 0 deletions ui-v2/app/styles/base/icons/base-placeholders.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
%with-icon {
background-repeat: no-repeat;
background-position: center;
}
%as-pseudo {
display: inline-block;
content: '';
visibility: visible;
background-size: contain;
}
%with-cancel-plain-icon {
@extend %with-icon;
background-image: $cancel-plain-svg;
}
1 change: 1 addition & 0 deletions ui-v2/app/styles/base/icons/index.scss
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
@import './base-variables';
@import './base-placeholders';
1 change: 1 addition & 0 deletions ui-v2/app/styles/components/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
@import './healthcheck-info';
@import './healthchecked-resource';
@import './freetext-filter';
@import './phrase-editor';
@import './filter-bar';
@import './tomography-graph';
@import './action-group';
Expand Down
4 changes: 4 additions & 0 deletions ui-v2/app/styles/components/phrase-editor.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@import './phrase-editor/index';
.phrase-editor {
@extend %phrase-editor;
}
2 changes: 2 additions & 0 deletions ui-v2/app/styles/components/phrase-editor/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@import './skin';
@import './layout';
46 changes: 46 additions & 0 deletions ui-v2/app/styles/components/phrase-editor/layout.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
%phrase-editor {
display: flex;
margin-top: 14px;
margin-bottom: 5px;
}
%phrase-editor ul {
overflow: hidden;
}
%phrase-editor li {
@extend %pill;
float: left;
margin-right: 4px;
}
%phrase-editor span {
display: none;
}
%phrase-editor label {
flex-grow: 1;
}
%phrase-editor input {
width: 100%;
height: 33px;
padding: 8px 10px;
box-sizing: border-box;
}
@media #{$--horizontal-selects} {
%phrase-editor {
margin-top: 14px;
}
%phrase-editor ul {
padding-top: 5px;
padding-left: 5px;
}
%phrase-editor input {
padding-left: 3px;
}
}
@media #{$--lt-horizontal-selects} {
%phrase-editor {
margin-top: 9px;
}
%phrase-editor label {
display: block;
margin-top: 5px;
}
}
18 changes: 18 additions & 0 deletions ui-v2/app/styles/components/phrase-editor/skin.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@media #{$--horizontal-selects} {
%phrase-editor {
border: 1px solid $gray-300;
border-radius: 2px;
}
%phrase-editor input:focus {
outline: 0;
}
}
@media #{$--lt-horizontal-selects} {
%phrase-editor label {
border: 1px solid $gray-300;
border-radius: 2px;
}
}
%phrase-editor input {
-webkit-appearance: none;
}
4 changes: 4 additions & 0 deletions ui-v2/app/styles/components/pill/layout.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@
display: inline-block;
padding: 1px 5px;
}
%pill button {
padding: 0;
margin-right: 3px;
}
10 changes: 10 additions & 0 deletions ui-v2/app/styles/components/pill/skin.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,13 @@
@extend %frame-gray-900;
border-radius: $radius-small;
}
%pill button {
background-color: transparent;
font-size: 0;
cursor: pointer;
}
%pill button::before {
@extend %with-cancel-plain-icon, %as-pseudo;
width: 10px;
height: 10px;
}
11 changes: 11 additions & 0 deletions ui-v2/app/templates/components/phrase-editor.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<ul>
{{#each items as |item index|}}
<li>
<button type="button" onclick={{action remove index}}>Remove</button>{{item}}
</li>
{{/each}}
</ul>
<label class="type-search">
<span>Search</span>
<input onchange={{action add}} onsearch={{action add}} oninput={{action oninput}} onkeydown={{action onkeydown}} placeholder="{{placeholder}}" value="{{item}}" type="search" name="s" autofocus="autofocus" />
</label>
2 changes: 1 addition & 1 deletion ui-v2/app/templates/dc/services/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
{{/block-slot}}
{{#block-slot 'toolbar'}}
{{#if (gt items.length 0) }}
{{catalog-filter searchable=searchable search=filters.s status=filters.status onchange=(action 'filter')}}
{{#phrase-editor placeholder=(if (eq terms.length 0) 'service:name tag:name status:critical search-term' '') items=terms searchable=searchable}}{{/phrase-editor}}
{{/if}}
{{/block-slot}}
{{#block-slot 'content'}}
Expand Down
29 changes: 19 additions & 10 deletions ui-v2/app/utils/search/filterable.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,31 @@ export default function(EventTarget = RSVP.EventTarget, P = Promise) {
this.data = data;
return this;
},
search: function(term = '') {
this.value = term === null ? '' : term.trim();
find: function(terms = []) {
this.value = terms
.filter(function(item) {
return typeof item === 'string' && item !== '';
})
.map(function(term) {
return term.trim();
});
return P.resolve(
this.value.reduce(function(prev, term) {
return prev.filter(item => {
return filter(item, { s: term });
});
}, this.data)
);
},
search: function(terms = []) {
// specifically no return here we return `this` instead
// right now filtering is sync but we introduce an async
// flow now for later on
P.resolve(
this.value !== ''
? this.data.filter(item => {
return filter(item, { s: term });
})
: this.data
).then(data => {
this.find(Array.isArray(terms) ? terms : [terms]).then(data => {
// TODO: For the moment, lets just fake a target
this.trigger('change', {
target: {
value: this.value,
value: this.value.join('\n'),
// TODO: selectedOptions is what <select> uses, consider that
data: data,
},
Expand Down
Loading

0 comments on commit bc22ab5

Please sign in to comment.