Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ui: Replaces Service listing filterbar with a phrase-editor search #5507

Merged
merged 2 commits into from
Mar 22, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'));
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would normally expect this component to accept an action as an argument and call it rather than requiring a searchable as an interface. It limits its reusability.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep agree, this will eventually move up into a wrapping component when we compose more functionality here and wrap it up in a final reusable component.

},
});
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, this.searchParams.service) || '').split('\n'));
}),
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 @@ -53,14 +54,14 @@ export default Controller.extend(WithEventSource, WithSearching, WithHealthFilte
remainingWidth: computed('maxWidth', function() {
return htmlSafe(`width: calc(50% - ${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
1 change: 1 addition & 0 deletions ui-v2/app/routes/dc/services/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default Route.extend({
model: function(params) {
const repo = get(this, 'repo');
return hash({
terms: typeof params.s !== 'undefined' ? params.s.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.indexOf('service:') === 0:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For what it's worth, Ember has had term.startsWith('service:') for ages and startsWith has been standard lib as of ES2015.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah, nice, thankyou! Will sort in a sec.

return find(get(item, 'Name'), term.substr(8));
case term.indexOf('tag:') === 0:
return find(get(item, 'Tags') || [], term.substr(4));
case term.indexOf('status:') === 0:
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 @@ -11,7 +11,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
5 changes: 3 additions & 2 deletions ui-v2/tests/acceptance/components/catalog-filter.feature
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Feature: components / catalog-filter
Where:
-------------------------------------------------
| Model | Page | Url |
| service | services | /dc-1/services |
# | service | services | /dc-1/services |
| node | nodes | /dc-1/nodes |
-------------------------------------------------
Scenario: Filtering [Model] in [Page]
Expand Down Expand Up @@ -123,7 +123,8 @@ Feature: components / catalog-filter
| Model | Page | Url |
| service | node | /dc-1/nodes/node-0 |
-------------------------------------------------
Scenario:
@ignore
Scenario: Freetext filtering the service listing
Given 1 datacenter model with the value "dc-1"
And 3 service models from yaml
---
Expand Down
34 changes: 34 additions & 0 deletions ui-v2/tests/integration/components/phrase-editor-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';

moduleForComponent('phrase-editor', 'Integration | Component | phrase editor', {
integration: true,
});

test('it renders', function(assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.on('myAction', function(val) { ... });

this.render(hbs`{{phrase-editor}}`);

assert.equal(
this.$()
.text()
.trim(),
'Search'
);

// Template block usage:
this.render(hbs`
{{#phrase-editor}}
template block text
{{/phrase-editor}}
`);

assert.equal(
this.$()
.text()
.trim(),
'Search'
);
});