open-nomad/ui/app/components/global-search/control.js
Buck Doyle bf7ed82def
Improve global search UX (#8249)
This updates the look of the search control, adds a hint about the slash
shortcut, adds highlighting of fuzzy search results, and addresses a few
edge case UX failures. It moves to using a fork of Ember Power Select
to handle an edge case where pressing escape would put the control
in an undesirable active-but-not-open state.
2020-06-25 08:51:52 -05:00

182 lines
4.1 KiB
JavaScript

import Component from '@ember/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 { debounce, run } from '@ember/runloop';
import Searchable from 'nomad-ui/mixins/searchable';
import classic from 'ember-classic-decorator';
const SLASH_KEY = 191;
@classNames('global-search-container')
export default class GlobalSearchControl extends Component {
@service dataCaches;
@service router;
@service store;
searchString = null;
constructor() {
super(...arguments);
this.jobSearch = JobSearch.create({
dataSource: this,
});
this.nodeSearch = NodeSearch.create({
dataSource: this,
});
}
keyDownHandler(e) {
const targetElementName = e.target.nodeName.toLowerCase();
if (targetElementName != 'input' && targetElementName != 'textarea') {
if (e.keyCode === SLASH_KEY) {
e.preventDefault();
this.open();
}
}
}
didInsertElement() {
set(this, '_keyDownHandler', this.keyDownHandler.bind(this));
document.addEventListener('keydown', this._keyDownHandler);
}
willDestroyElement() {
document.removeEventListener('keydown', this._keyDownHandler);
}
@task(function*(string) {
try {
set(this, 'searchString', string);
const jobs = yield this.dataCaches.fetch('job');
const nodes = yield this.dataCaches.fetch('node');
set(this, 'jobs', jobs.toArray());
set(this, 'nodes', nodes.toArray());
const jobResults = this.jobSearch.listSearched;
const nodeResults = this.nodeSearch.listSearched;
return [
{
groupName: `Jobs (${jobResults.length})`,
options: jobResults,
},
{
groupName: `Clients (${nodeResults.length})`,
options: nodeResults,
},
];
} catch (e) {
// eslint-disable-next-line
console.log('exception searching', e);
}
})
search;
@action
open() {
if (this.select) {
this.select.actions.open();
}
}
@action
selectOption(model) {
const itemModelName = model.constructor.modelName;
if (itemModelName === 'job') {
this.router.transitionTo('jobs.job', model.name, {
queryParams: { namespace: model.get('namespace.name') },
});
} else if (itemModelName === 'node') {
this.router.transitionTo('clients.client', model.id);
}
}
@action
storeSelect(select) {
if (select) {
this.select = select;
}
}
@action
openOnClickOrTab(select, { target }) {
// Bypass having to press enter to access search after clicking/tabbing
const targetClassList = target.classList;
const targetIsTrigger = targetClassList.contains('ember-power-select-trigger');
// Allow tabbing out of search
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(() => {
this.select.actions.setIsActive(false);
});
}
}
calculatePosition(trigger) {
const { top, left, width } = trigger.getBoundingClientRect();
return {
style: {
left,
width,
top,
},
};
}
}
@classic
class JobSearch extends EmberObject.extend(Searchable) {
@computed
get searchProps() {
return ['id', 'name'];
}
@computed
get fuzzySearchProps() {
return ['name'];
}
@alias('dataSource.jobs') listToSearch;
@alias('dataSource.searchString') searchTerm;
fuzzySearchEnabled = true;
includeFuzzySearchMatches = true;
}
@classic
class NodeSearch extends EmberObject.extend(Searchable) {
@computed
get searchProps() {
return ['id', 'name'];
}
@computed
get fuzzySearchProps() {
return ['name'];
}
@alias('dataSource.nodes') listToSearch;
@alias('dataSource.searchString') searchTerm;
fuzzySearchEnabled = true;
includeFuzzySearchMatches = true;
}