6450abfe76
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.
217 lines
5.2 KiB
JavaScript
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})`;
|
|
}
|