bf7ed82def
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.
182 lines
4.1 KiB
JavaScript
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;
|
|
}
|