github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/ui/app/components/global-search/control.js (about) 1 import Component from '@ember/component'; 2 import { classNames, attributeBindings } from '@ember-decorators/component'; 3 import { task } from 'ember-concurrency'; 4 import { action, set } from '@ember/object'; 5 import { inject as service } from '@ember/service'; 6 import { debounce, next } from '@ember/runloop'; 7 8 const SLASH_KEY = '/'; 9 const MAXIMUM_RESULTS = 10; 10 11 @classNames('global-search-container') 12 @attributeBindings('data-test-search-parent') 13 export default class GlobalSearchControl extends Component { 14 @service router; 15 @service token; 16 17 searchString = null; 18 19 constructor() { 20 super(...arguments); 21 this['data-test-search-parent'] = true; 22 } 23 24 keyDownHandler(e) { 25 const targetElementName = e.target.nodeName.toLowerCase(); 26 27 if (targetElementName != 'input' && targetElementName != 'textarea') { 28 if (e.key === SLASH_KEY) { 29 e.preventDefault(); 30 this.open(); 31 } 32 } 33 } 34 35 didInsertElement() { 36 super.didInsertElement(...arguments); 37 set(this, '_keyDownHandler', this.keyDownHandler.bind(this)); 38 document.addEventListener('keydown', this._keyDownHandler); 39 } 40 41 willDestroyElement() { 42 super.willDestroyElement(...arguments); 43 document.removeEventListener('keydown', this._keyDownHandler); 44 } 45 46 @task(function* (string) { 47 const searchResponse = yield this.token.authorizedRequest( 48 '/v1/search/fuzzy', 49 { 50 method: 'POST', 51 body: JSON.stringify({ 52 Text: string, 53 Context: 'all', 54 Namespace: '*', 55 }), 56 } 57 ); 58 59 const results = yield searchResponse.json(); 60 61 const allJobResults = results.Matches.jobs || []; 62 const allNodeResults = results.Matches.nodes || []; 63 const allAllocationResults = results.Matches.allocs || []; 64 const allTaskGroupResults = results.Matches.groups || []; 65 const allCSIPluginResults = results.Matches.plugins || []; 66 67 const jobResults = allJobResults 68 .slice(0, MAXIMUM_RESULTS) 69 .map(({ ID: name, Scope: [namespace, id] }) => ({ 70 type: 'job', 71 id, 72 namespace, 73 label: `${namespace} > ${name}`, 74 })); 75 76 const nodeResults = allNodeResults 77 .slice(0, MAXIMUM_RESULTS) 78 .map(({ ID: name, Scope: [id] }) => ({ 79 type: 'node', 80 id, 81 label: name, 82 })); 83 84 const allocationResults = allAllocationResults 85 .slice(0, MAXIMUM_RESULTS) 86 .map(({ ID: name, Scope: [namespace, id] }) => ({ 87 type: 'allocation', 88 id, 89 label: `${namespace} > ${name}`, 90 })); 91 92 const taskGroupResults = allTaskGroupResults 93 .slice(0, MAXIMUM_RESULTS) 94 .map(({ ID: id, Scope: [namespace, jobId] }) => ({ 95 type: 'task-group', 96 id, 97 namespace, 98 jobId, 99 label: `${namespace} > ${jobId} > ${id}`, 100 })); 101 102 const csiPluginResults = allCSIPluginResults 103 .slice(0, MAXIMUM_RESULTS) 104 .map(({ ID: id }) => ({ 105 type: 'plugin', 106 id, 107 label: id, 108 })); 109 110 const { 111 jobs: jobsTruncated, 112 nodes: nodesTruncated, 113 allocs: allocationsTruncated, 114 groups: taskGroupsTruncated, 115 plugins: csiPluginsTruncated, 116 } = results.Truncations; 117 118 return [ 119 { 120 groupName: resultsGroupLabel( 121 'Jobs', 122 jobResults, 123 allJobResults, 124 jobsTruncated 125 ), 126 options: jobResults, 127 }, 128 { 129 groupName: resultsGroupLabel( 130 'Clients', 131 nodeResults, 132 allNodeResults, 133 nodesTruncated 134 ), 135 options: nodeResults, 136 }, 137 { 138 groupName: resultsGroupLabel( 139 'Allocations', 140 allocationResults, 141 allAllocationResults, 142 allocationsTruncated 143 ), 144 options: allocationResults, 145 }, 146 { 147 groupName: resultsGroupLabel( 148 'Task Groups', 149 taskGroupResults, 150 allTaskGroupResults, 151 taskGroupsTruncated 152 ), 153 options: taskGroupResults, 154 }, 155 { 156 groupName: resultsGroupLabel( 157 'CSI Plugins', 158 csiPluginResults, 159 allCSIPluginResults, 160 csiPluginsTruncated 161 ), 162 options: csiPluginResults, 163 }, 164 ]; 165 }) 166 search; 167 168 @action 169 open() { 170 if (this.select) { 171 this.select.actions.open(); 172 } 173 } 174 175 @action 176 ensureMinimumLength(string) { 177 return string.length > 1; 178 } 179 180 @action 181 selectOption(model) { 182 if (model.type === 'job') { 183 this.router.transitionTo('jobs.job', model.id, { 184 queryParams: { namespace: model.namespace }, 185 }); 186 } else if (model.type === 'node') { 187 this.router.transitionTo('clients.client', model.id); 188 } else if (model.type === 'task-group') { 189 this.router.transitionTo('jobs.job.task-group', model.jobId, model.id, { 190 queryParams: { namespace: model.namespace }, 191 }); 192 } else if (model.type === 'plugin') { 193 this.router.transitionTo('csi.plugins.plugin', model.id); 194 } else if (model.type === 'allocation') { 195 this.router.transitionTo('allocations.allocation', model.id); 196 } 197 } 198 199 @action 200 storeSelect(select) { 201 if (select) { 202 this.select = select; 203 } 204 } 205 206 @action 207 openOnClickOrTab(select, { target }) { 208 // Bypass having to press enter to access search after clicking/tabbing 209 const targetClassList = target.classList; 210 const targetIsTrigger = targetClassList.contains( 211 'ember-power-select-trigger' 212 ); 213 214 // Allow tabbing out of search 215 const triggerIsNotActive = !targetClassList.contains( 216 'ember-power-select-trigger--active' 217 ); 218 219 if (targetIsTrigger && triggerIsNotActive) { 220 debounce(this, this.open, 150); 221 } 222 } 223 224 @action 225 onCloseEvent(select, event) { 226 if (event.key === 'Escape') { 227 next(() => { 228 this.element.querySelector('.ember-power-select-trigger').blur(); 229 }); 230 } 231 } 232 233 calculatePosition(trigger) { 234 const { top, left, width } = trigger.getBoundingClientRect(); 235 return { 236 style: { 237 left, 238 width, 239 top, 240 }, 241 }; 242 } 243 } 244 245 function resultsGroupLabel(type, renderedResults, allResults, truncated) { 246 let countString; 247 248 if (renderedResults.length < allResults.length) { 249 countString = `showing ${renderedResults.length} of ${allResults.length}`; 250 } else { 251 countString = renderedResults.length; 252 } 253 254 const truncationIndicator = truncated ? '+' : ''; 255 256 return `${type} (${countString}${truncationIndicator})`; 257 }