open-nomad/ui/app/components/global-search/control.js
Buck Doyle 6450abfe76
Change node id search to prefix-only (#9244)
This test has been periodically failing, like here:
https://app.circleci.com/pipelines/github/hashicorp/nomad/12879/workflows/40c0445c-b244-4a04-a5a3-d9685b656c94/jobs/114751/tests

The failure was because sometimes the first node contains the
beginning of the ”otherNode” id somewhere within its id. It seems
less useful to match within the node, so this changes id search to
only match at the beginning of the id.
2020-11-04 12:12:31 -06:00

217 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.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})`;
}