/** * Copyright (c) HashiCorp, Inc. * SPDX-License-Identifier: BUSL-1.1 */ // @ts-check /* eslint-disable ember/no-incorrect-calls-with-inline-anonymous-functions */ import { alias, readOnly } from '@ember/object/computed'; import { inject as service } from '@ember/service'; import Controller, { inject as controller } from '@ember/controller'; import { action, computed } from '@ember/object'; import { scheduleOnce } from '@ember/runloop'; import intersection from 'lodash.intersection'; import SortableFactory from 'nomad-ui/mixins/sortable-factory'; import Searchable from 'nomad-ui/mixins/searchable'; import { serialize, deserializedQueryParam as selection, } from 'nomad-ui/utils/qp-serialize'; import classic from 'ember-classic-decorator'; @classic export default class IndexController extends Controller.extend( SortableFactory(['id', 'name', 'compositeStatus', 'datacenter', 'version']), Searchable ) { @service userSettings; @controller('clients') clientsController; @alias('model.nodes') nodes; queryParams = [ { currentPage: 'page', }, { searchTerm: 'search', }, { sortProperty: 'sort', }, { sortDescending: 'desc', }, { qpClass: 'class', }, { qpDatacenter: 'dc', }, { qpVersion: 'version', }, { qpVolume: 'volume', }, { qpNodePool: 'nodePool', }, ]; filterFunc = (node) => { return node.isEligible; }; clientFilterToggles = { state: [ { label: 'initializing', qp: 'state_initializing', default: true, filter: (node) => node.status === 'initializing', }, { label: 'ready', qp: 'state_ready', default: true, filter: (node) => node.status === 'ready', }, { label: 'down', qp: 'state_down', default: true, filter: (node) => node.status === 'down', }, { label: 'disconnected', qp: 'state_disconnected', default: true, filter: (node) => node.status === 'disconnected', }, ], eligibility: [ { label: 'eligible', qp: 'eligibility_eligible', default: true, filter: (node) => node.isEligible, }, { label: 'ineligible', qp: 'eligibility_ineligible', default: true, filter: (node) => !node.isEligible, }, ], drainStatus: [ { label: 'draining', qp: 'drain_status_draining', default: true, filter: (node) => node.isDraining, }, { label: 'not draining', qp: 'drain_status_not_draining', default: true, filter: (node) => !node.isDraining, }, ], }; @computed( 'state_initializing', 'state_ready', 'state_down', 'state_disconnected', 'eligibility_eligible', 'eligibility_ineligible', 'drain_status_draining', 'drain_status_not_draining', 'allToggles.[]' ) get activeToggles() { return this.allToggles.filter((t) => this[t.qp]); } get allToggles() { return Object.values(this.clientFilterToggles).reduce( (acc, filters) => acc.concat(filters), [] ); } // eslint-disable-next-line ember/classic-decorator-hooks constructor() { super(...arguments); this.addDynamicQueryParams(); } addDynamicQueryParams() { this.clientFilterToggles.state.forEach((filter) => { this.queryParams.push({ [filter.qp]: filter.qp }); this.set(filter.qp, filter.default); }); this.clientFilterToggles.eligibility.forEach((filter) => { this.queryParams.push({ [filter.qp]: filter.qp }); this.set(filter.qp, filter.default); }); this.clientFilterToggles.drainStatus.forEach((filter) => { this.queryParams.push({ [filter.qp]: filter.qp }); this.set(filter.qp, filter.default); }); } currentPage = 1; @readOnly('userSettings.pageSize') pageSize; sortProperty = 'modifyIndex'; sortDescending = true; @computed get searchProps() { return ['id', 'name', 'datacenter']; } qpClass = ''; qpDatacenter = ''; qpVersion = ''; qpVolume = ''; qpNodePool = ''; @selection('qpClass') selectionClass; @selection('qpDatacenter') selectionDatacenter; @selection('qpVersion') selectionVersion; @selection('qpVolume') selectionVolume; @selection('qpNodePool') selectionNodePool; @computed('nodes.[]', 'selectionClass') get optionsClass() { const classes = Array.from(new Set(this.nodes.mapBy('nodeClass'))) .compact() .without(''); // Remove any invalid node classes from the query param/selection scheduleOnce('actions', () => { // eslint-disable-next-line ember/no-side-effects this.set( 'qpClass', serialize(intersection(classes, this.selectionClass)) ); }); return classes.sort().map((dc) => ({ key: dc, label: dc })); } @computed('nodes.[]', 'selectionDatacenter') get optionsDatacenter() { const datacenters = Array.from( new Set(this.nodes.mapBy('datacenter')) ).compact(); // Remove any invalid datacenters from the query param/selection scheduleOnce('actions', () => { // eslint-disable-next-line ember/no-side-effects this.set( 'qpDatacenter', serialize(intersection(datacenters, this.selectionDatacenter)) ); }); return datacenters.sort().map((dc) => ({ key: dc, label: dc })); } @computed('nodes.[]', 'selectionVersion') get optionsVersion() { const versions = Array.from(new Set(this.nodes.mapBy('version'))).compact(); // Remove any invalid versions from the query param/selection scheduleOnce('actions', () => { // eslint-disable-next-line ember/no-side-effects this.set( 'qpVersion', serialize(intersection(versions, this.selectionVersion)) ); }); return versions.sort().map((v) => ({ key: v, label: v })); } @computed('nodes.[]', 'selectionVolume') get optionsVolume() { const flatten = (acc, val) => acc.concat(val.toArray()); const allVolumes = this.nodes.mapBy('hostVolumes').reduce(flatten, []); const volumes = Array.from(new Set(allVolumes.mapBy('name'))); scheduleOnce('actions', () => { // eslint-disable-next-line ember/no-side-effects this.set( 'qpVolume', serialize(intersection(volumes, this.selectionVolume)) ); }); return volumes.sort().map((volume) => ({ key: volume, label: volume })); } @computed('selectionNodePool', 'model.nodePools.[]') get optionsNodePool() { const availableNodePools = this.model.nodePools.filter( (p) => p.name !== 'all' ); scheduleOnce('actions', () => { // eslint-disable-next-line ember/no-side-effects this.set( 'qpNodePool', serialize( intersection( availableNodePools.map(({ name }) => name), this.selectionNodePool ) ) ); }); return availableNodePools.map((nodePool) => ({ key: nodePool.name, label: nodePool.name, })); } @computed( 'clientFilterToggles', 'drain_status_draining', 'drain_status_not_draining', 'eligibility_eligible', 'eligibility_ineligible', 'nodes.[]', 'selectionClass', 'selectionDatacenter', 'selectionNodePool', 'selectionVersion', 'selectionVolume', 'state_disconnected', 'state_down', 'state_initializing', 'state_ready' ) get filteredNodes() { const { selectionClass: classes, selectionDatacenter: datacenters, selectionNodePool: nodePools, selectionVersion: versions, selectionVolume: volumes, } = this; let nodes = this.nodes; // new QP style filtering for (let category in this.clientFilterToggles) { nodes = nodes.filter((node) => { let includeNode = false; for (let filter of this.clientFilterToggles[category]) { if (this[filter.qp] && filter.filter(node)) { includeNode = true; break; } } return includeNode; }); } return nodes.filter((node) => { if (classes.length && !classes.includes(node.get('nodeClass'))) return false; if (datacenters.length && !datacenters.includes(node.get('datacenter'))) return false; if (versions.length && !versions.includes(node.get('version'))) return false; if ( volumes.length && !node.hostVolumes.find((volume) => volumes.includes(volume.name)) ) return false; if (nodePools.length && !nodePools.includes(node.get('nodePool'))) { return false; } return true; }); } @alias('filteredNodes') listToSort; @alias('listSorted') listToSearch; @alias('listSearched') sortedNodes; @alias('clientsController.isForbidden') isForbidden; setFacetQueryParam(queryParam, selection) { this.set(queryParam, serialize(selection)); } @action handleFilterChange(queryParamValue, option, queryParamLabel) { if (queryParamValue.includes(option)) { queryParamValue.removeObject(option); } else { queryParamValue.addObject(option); } this.set(queryParamLabel, serialize(queryParamValue)); } @action gotoNode(node) { this.transitionToRoute('clients.client', node); } }