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