github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/ui/app/components/global-search/control.js (about) 1 import Component from '@ember/component'; 2 import { classNames } from '@ember-decorators/component'; 3 import { task } from 'ember-concurrency'; 4 import EmberObject, { action, computed, set } from '@ember/object'; 5 import { alias } from '@ember/object/computed'; 6 import { inject as service } from '@ember/service'; 7 import { debounce, run } from '@ember/runloop'; 8 import Searchable from 'nomad-ui/mixins/searchable'; 9 import classic from 'ember-classic-decorator'; 10 11 const SLASH_KEY = 191; 12 const MAXIMUM_RESULTS = 10; 13 14 @classNames('global-search-container') 15 export default class GlobalSearchControl extends Component { 16 @service dataCaches; 17 @service router; 18 @service store; 19 20 searchString = null; 21 22 constructor() { 23 super(...arguments); 24 25 this.jobSearch = JobSearch.create({ 26 dataSource: this, 27 }); 28 29 this.nodeNameSearch = NodeNameSearch.create({ 30 dataSource: this, 31 }); 32 33 this.nodeIdSearch = NodeIdSearch.create({ 34 dataSource: this, 35 }); 36 } 37 38 keyDownHandler(e) { 39 const targetElementName = e.target.nodeName.toLowerCase(); 40 41 if (targetElementName != 'input' && targetElementName != 'textarea') { 42 if (e.keyCode === SLASH_KEY) { 43 e.preventDefault(); 44 this.open(); 45 } 46 } 47 } 48 49 didInsertElement() { 50 set(this, '_keyDownHandler', this.keyDownHandler.bind(this)); 51 document.addEventListener('keydown', this._keyDownHandler); 52 } 53 54 willDestroyElement() { 55 document.removeEventListener('keydown', this._keyDownHandler); 56 } 57 58 @task(function*(string) { 59 try { 60 set(this, 'searchString', string); 61 62 const jobs = yield this.dataCaches.fetch('job'); 63 const nodes = yield this.dataCaches.fetch('node'); 64 65 set(this, 'jobs', jobs.toArray()); 66 set(this, 'nodes', nodes.toArray()); 67 68 const jobResults = this.jobSearch.listSearched.slice(0, MAXIMUM_RESULTS); 69 70 const mergedNodeListSearched = this.nodeIdSearch.listSearched.concat(this.nodeNameSearch.listSearched).uniq(); 71 const nodeResults = mergedNodeListSearched.slice(0, MAXIMUM_RESULTS); 72 73 return [ 74 { 75 groupName: resultsGroupLabel('Jobs', jobResults, this.jobSearch.listSearched), 76 options: jobResults, 77 }, 78 { 79 groupName: resultsGroupLabel('Clients', nodeResults, mergedNodeListSearched), 80 options: nodeResults, 81 }, 82 ]; 83 } catch (e) { 84 // eslint-disable-next-line 85 console.log('exception searching', e); 86 } 87 }) 88 search; 89 90 @action 91 open() { 92 if (this.select) { 93 this.select.actions.open(); 94 } 95 } 96 97 @action 98 selectOption(model) { 99 const itemModelName = model.constructor.modelName; 100 101 if (itemModelName === 'job') { 102 this.router.transitionTo('jobs.job', model.plainId, { 103 queryParams: { namespace: model.get('namespace.name') }, 104 }); 105 } else if (itemModelName === 'node') { 106 this.router.transitionTo('clients.client', model.id); 107 } 108 } 109 110 @action 111 storeSelect(select) { 112 if (select) { 113 this.select = select; 114 } 115 } 116 117 @action 118 openOnClickOrTab(select, { target }) { 119 // Bypass having to press enter to access search after clicking/tabbing 120 const targetClassList = target.classList; 121 const targetIsTrigger = targetClassList.contains('ember-power-select-trigger'); 122 123 // Allow tabbing out of search 124 const triggerIsNotActive = !targetClassList.contains('ember-power-select-trigger--active'); 125 126 if (targetIsTrigger && triggerIsNotActive) { 127 debounce(this, this.open, 150); 128 } 129 } 130 131 @action 132 onCloseEvent(select, event) { 133 if (event.key === 'Escape') { 134 run.next(() => { 135 this.element.querySelector('.ember-power-select-trigger').blur(); 136 }); 137 } 138 } 139 140 calculatePosition(trigger) { 141 const { top, left, width } = trigger.getBoundingClientRect(); 142 return { 143 style: { 144 left, 145 width, 146 top, 147 }, 148 }; 149 } 150 } 151 152 @classic 153 class JobSearch extends EmberObject.extend(Searchable) { 154 @computed 155 get searchProps() { 156 return ['id', 'name']; 157 } 158 159 @computed 160 get fuzzySearchProps() { 161 return ['name']; 162 } 163 164 @alias('dataSource.jobs') listToSearch; 165 @alias('dataSource.searchString') searchTerm; 166 167 fuzzySearchEnabled = true; 168 includeFuzzySearchMatches = true; 169 } 170 @classic 171 class NodeNameSearch extends EmberObject.extend(Searchable) { 172 @computed 173 get searchProps() { 174 return ['name']; 175 } 176 177 @computed 178 get fuzzySearchProps() { 179 return ['name']; 180 } 181 182 @alias('dataSource.nodes') listToSearch; 183 @alias('dataSource.searchString') searchTerm; 184 185 fuzzySearchEnabled = true; 186 includeFuzzySearchMatches = true; 187 } 188 189 @classic 190 class NodeIdSearch extends EmberObject.extend(Searchable) { 191 @computed 192 get regexSearchProps() { 193 return ['id']; 194 } 195 196 @alias('dataSource.nodes') listToSearch; 197 @computed('dataSource.searchString') 198 get searchTerm() { 199 return `^${this.get('dataSource.searchString')}`; 200 } 201 202 exactMatchEnabled = false; 203 regexEnabled = true; 204 } 205 206 function resultsGroupLabel(type, renderedResults, allResults) { 207 let countString; 208 209 if (renderedResults.length < allResults.length) { 210 countString = `showing ${renderedResults.length} of ${allResults.length}`; 211 } else { 212 countString = renderedResults.length; 213 } 214 215 return `${type} (${countString})`; 216 }