open-nomad/ui/app/components/global-search/control.js
Buck Doyle 07831ab455
Update to v4 of Ember Power Select (#10226)
This closes #10146.

Because of cibernox/ember-power-select#1203, which documents
the current impossibility of attaching test selectors to a
PowerSelect invocation, this uses test selectors on parent
containers instead, occasionally adding wrappers when needed.
I chose to leave the existing test selectors in the hopes that
we can return to using them eventually, but I could easily
remove them if it seems like extra noise now.

Presumably for the same reason, @class no longer works, so
this adjusts the scoping of global search CSS to preserve the style
of the search control.

I also included an update to the latest version of
ember-test-selectors, since we were far behind and I tried
that before finding the aforelinked issue.

Finally, this replaces ember-cli-uglify with ember-cli-terser to address
production build failures as described at ember-cli/ember-cli#9290.
2021-03-26 08:55:12 -05:00

218 lines
5.2 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;
const MAXIMUM_RESULTS = 10;
@classNames('global-search-container')
export default class GlobalSearchControl extends Component {
@service dataCaches;
@service router;
@service store;
searchString = null;
constructor() {
super(...arguments);
this['data-test-search-parent'] = true;
this.jobSearch = JobSearch.create({
dataSource: this,
});
this.nodeNameSearch = NodeNameSearch.create({
dataSource: this,
});
this.nodeIdSearch = NodeIdSearch.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.slice(0, MAXIMUM_RESULTS);
const mergedNodeListSearched = this.nodeIdSearch.listSearched.concat(this.nodeNameSearch.listSearched).uniq();
const nodeResults = mergedNodeListSearched.slice(0, MAXIMUM_RESULTS);
return [
{
groupName: resultsGroupLabel('Jobs', jobResults, this.jobSearch.listSearched),
options: jobResults,
},
{
groupName: resultsGroupLabel('Clients', nodeResults, mergedNodeListSearched),
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.plainId, {
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.element.querySelector('.ember-power-select-trigger').blur();
});
}
}
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 NodeNameSearch extends EmberObject.extend(Searchable) {
@computed
get searchProps() {
return ['name'];
}
@computed
get fuzzySearchProps() {
return ['name'];
}
@alias('dataSource.nodes') listToSearch;
@alias('dataSource.searchString') searchTerm;
fuzzySearchEnabled = true;
includeFuzzySearchMatches = true;
}
@classic
class NodeIdSearch extends EmberObject.extend(Searchable) {
@computed
get regexSearchProps() {
return ['id'];
}
@alias('dataSource.nodes') listToSearch;
@computed('dataSource.searchString')
get searchTerm() {
return `^${this.get('dataSource.searchString')}`;
}
exactMatchEnabled = false;
regexEnabled = true;
}
function resultsGroupLabel(type, renderedResults, allResults) {
let countString;
if (renderedResults.length < allResults.length) {
countString = `showing ${renderedResults.length} of ${allResults.length}`;
} else {
countString = renderedResults.length;
}
return `${type} (${countString})`;
}