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: Improve global search UX #8249

Merged
merged 11 commits into from
Jun 25, 2020
17 changes: 13 additions & 4 deletions ui/app/components/global-search/control.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import Component from '@ember/component';
import { tagName } from '@ember-decorators/component';
import { classNames } from '@ember-decorators/component';
import { task } from 'ember-concurrency';
import EmberObject, { action, computed, set } from '@ember/object';
import { alias } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import { run } from '@ember/runloop';
import { debounce, run } from '@ember/runloop';
import Searchable from 'nomad-ui/mixins/searchable';
import classic from 'ember-classic-decorator';

const SLASH_KEY = 191;

@tagName('')
@classNames('global-search-container')
export default class GlobalSearchControl extends Component {
@service dataCaches;
@service router;
Expand Down Expand Up @@ -117,8 +117,15 @@ export default class GlobalSearchControl extends Component {
const triggerIsNotActive = !targetClassList.contains('ember-power-select-trigger--active');

if (targetIsTrigger && triggerIsNotActive) {
debounce(this, this.open, 150);
}
}

@action
onCloseEvent(select, event) {
if (event.key === 'Escape') {
run.next(() => {
select.actions.open();
this.select.actions.setIsActive(false);
});
}
}
Expand Down Expand Up @@ -151,6 +158,7 @@ class JobSearch extends EmberObject.extend(Searchable) {
@alias('dataSource.searchString') searchTerm;

fuzzySearchEnabled = true;
includeFuzzySearchMatches = true;
}

@classic
Expand All @@ -169,4 +177,5 @@ class NodeSearch extends EmberObject.extend(Searchable) {
@alias('dataSource.searchString') searchTerm;

fuzzySearchEnabled = true;
includeFuzzySearchMatches = true;
}
30 changes: 30 additions & 0 deletions ui/app/components/global-search/match.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Component from '@ember/component';
import { tagName } from '@ember-decorators/component';
import { computed, get } from '@ember/object';
import { alias } from '@ember/object/computed';

@tagName('')
export default class GlobalSearchMatch extends Component {
@alias('match.fuzzySearchMatches.firstObject') firstMatch;
@alias('firstMatch.indices.firstObject') firstIndices;

@computed('match.name')
get label() {
return get(this, 'match.name') || '';
}

@computed('label', 'firstIndices.[]')
get beforeHighlighted() {
return this.label.substring(0, this.firstIndices[0]);
}

@computed('label', 'firstIndices.[]')
get highlighted() {
return this.label.substring(this.firstIndices[0], this.firstIndices[1] + 1);
}

@computed('label', 'firstIndices.[]')
get afterHighlighted() {
return this.label.substring(this.firstIndices[1] + 1);
}
}
14 changes: 13 additions & 1 deletion ui/app/mixins/searchable.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default Mixin.create({
// Three search modes
exactMatchEnabled: true,
fuzzySearchEnabled: false,
includeFuzzySearchMatches: false,
regexEnabled: true,

// Search should reset pagination. Not every instance of
Expand All @@ -59,6 +60,7 @@ export default Mixin.create({
matchAllTokens: true,
maxPatternLength: 32,
minMatchCharLength: 1,
includeMatches: this.includeFuzzySearchMatches,
keys: this.fuzzySearchProps || [],
getFn(item, key) {
return get(item, key);
Expand Down Expand Up @@ -91,7 +93,17 @@ export default Mixin.create({
}

if (this.fuzzySearchEnabled) {
results.push(...this.fuse.search(searchTerm));
let fuseSearchResults = this.fuse.search(searchTerm);

if (this.includeFuzzySearchMatches) {
fuseSearchResults = fuseSearchResults.map(result => {
const item = result.item;
item.set('fuzzySearchMatches', result.matches);
return item;
});
}

results.push(...fuseSearchResults);
}

if (this.regexEnabled) {
Expand Down
2 changes: 1 addition & 1 deletion ui/app/styles/components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
@import './components/exec-button';
@import './components/exec-window';
@import './components/fs-explorer';
@import './components/global-search-control';
@import './components/global-search-container';
@import './components/global-search-dropdown';
@import './components/gutter';
@import './components/gutter-toggle';
Expand Down
66 changes: 66 additions & 0 deletions ui/app/styles/components/global-search-container.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
.global-search-container {
position: absolute;
width: 100%;
left: 0;
top: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;

.global-search {
width: 30em;

@media #{$mq-hidden-gutter} {
width: 20em;
}

.ember-power-select-trigger {
background: $nomad-green-darker;
border: 1px solid darken($nomad-green-darker, 5%);

.icon {
margin-top: 1px;
margin-left: 2px;

fill: white;
opacity: 0.7;
}

.placeholder {
opacity: 0.7;
display: inline-block;
padding-left: 2px;
transform: translateY(-1px);
}

.shortcut {
position: absolute;
right: 5px;
top: 5px;
bottom: 5px;
width: 1.4rem;
padding-top: 0.5px;
text-align: center;
opacity: 0.7;
border: 1px solid darken($nomad-green-darker, 5%);
}

&.ember-power-select-trigger--active {
background: white;
border-color: white;

.icon {
fill: black;
opacity: 1;
}
}
}

.ember-basic-dropdown-content-wormhole-origin {
position: absolute;
top: 0;
width: 100%;
}
}
}
37 changes: 0 additions & 37 deletions ui/app/styles/components/global-search-control.scss

This file was deleted.

14 changes: 14 additions & 0 deletions ui/app/styles/components/global-search-dropdown.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@
outline: 0;
}

// Prevent Safari from disrupting styling, adapted from http://geek.michaelgrace.org/2011/06/webkit-search-input-styling/
input[type='search'] {
-webkit-appearance: textfield;
}

input::-webkit-search-decoration,
input::-webkit-search-cancel-button {
display: none;
}

.ember-power-select-options {
background: white;
padding: 0.35rem;
Expand All @@ -32,6 +42,10 @@
background: transparentize($blue, 0.8);
color: $blue;
}

.highlighted {
font-weight: $weight-semibold;
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion ui/app/templates/components/global-search/control.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
@search={{perform this.search}}
@onChange={{action 'selectOption'}}
@onFocus={{action 'openOnClickOrTab'}}
@onClose={{action 'onCloseEvent'}}
@dropdownClass="global-search-dropdown"
@calculatePosition={{this.calculatePosition}}
@searchMessageComponent="global-search/message"
@triggerComponent="global-search/trigger"
@registerAPI={{action 'storeSelect'}}
as |option|>
{{option.name}}
<GlobalSearch::Match @match={{option}} />
</PowerSelect>
5 changes: 5 additions & 0 deletions ui/app/templates/components/global-search/match.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{{#if firstIndices}}
{{beforeHighlighted}}<span class='highlighted'>{{highlighted}}</span>{{afterHighlighted}}
{{else}}
{{label}}
{{/if}}
5 changes: 4 additions & 1 deletion ui/app/templates/components/global-search/trigger.hbs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{{x-icon "search" class="is-small"}}
{{#unless select.isOpen}}
<span class='placeholder'>Search</span>
{{/unless}}
{{/unless}}
{{#if (not (or select.isActive select.isOpen))}}
<span class='shortcut' title="Type '/' to search">/</span>
{{/if}}
2 changes: 1 addition & 1 deletion ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
"ember-moment": "^7.8.1",
"ember-overridable-computed": "^1.0.0",
"ember-page-title": "^5.0.2",
"ember-power-select": "^3.0.4",
"ember-power-select": "backspace/ember-power-select#setIsActiveAPI",
"ember-qunit": "^4.4.1",
"ember-qunit-nice-errors": "^1.2.0",
"ember-resolver": "^5.0.1",
Expand Down
30 changes: 30 additions & 0 deletions ui/tests/acceptance/search-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,36 @@ module('Acceptance | search', function(hooks) {
clock.restore();
});

test('search highlights matching substrings', async function(assert) {
server.create('node', { name: 'xyz' });

server.create('job', { id: 'traefik', namespaceId: 'default' });
server.create('job', { id: 'tracking', namespace: 'default' });

await visit('/');

await selectSearch(PageLayout.navbar.search.scope, 'trae');

PageLayout.navbar.search.as(search => {
search.groups[0].as(jobs => {
assert.equal(jobs.options[0].text, 'traefik');
assert.equal(jobs.options[0].highlighted, 'trae');

assert.equal(jobs.options[1].text, 'tracking');
assert.equal(jobs.options[1].highlighted, 'tra');
});
});

await selectSearch(PageLayout.navbar.search.scope, 'ra');

PageLayout.navbar.search.as(search => {
search.groups[0].as(jobs => {
assert.equal(jobs.options[0].highlighted, 'ra');
assert.equal(jobs.options[1].highlighted, 'ra');
});
});
});

test('clicking the search field starts search immediately', async function(assert) {
await visit('/');

Expand Down
1 change: 1 addition & 0 deletions ui/tests/pages/layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default create({

options: collection('.ember-power-select-option', {
label: text(),
highlighted: text('.highlighted'),
}),
}),

Expand Down
33 changes: 33 additions & 0 deletions ui/tests/unit/mixins/searchable-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,39 @@ module('Unit | Mixin | Searchable', function(hooks) {
);
});

test('the fuzzy search can include match results', function(assert) {
const subject = this.subject();
subject.set('source', [
EmberObject.create({ id: '1', name: 'United States of America', continent: 'North America' }),
EmberObject.create({ id: '2', name: 'Canada', continent: 'North America' }),
EmberObject.create({ id: '3', name: 'Mexico', continent: 'North America' }),
]);

subject.set('fuzzySearchEnabled', true);
subject.set('includeFuzzySearchMatches', true);
subject.set('searchTerm', 'Ameerica');
assert.deepEqual(
subject
.get('listSearched')
.map(object => object.getProperties('id', 'name', 'continent', 'fuzzySearchMatches')),
[
{
id: '1',
name: 'United States of America',
continent: 'North America',
fuzzySearchMatches: [
{
indices: [[2, 2], [4, 4], [9, 9], [11, 11], [17, 23]],
value: 'United States of America',
key: 'name',
},
],
},
],
'America is matched due to fuzzy matching'
);
});

test('the exact match search mode can be disabled', function(assert) {
const subject = this.subject();
subject.set('source', [
Expand Down
Loading