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  }