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  }