Files
nomad/ui/app/controllers/evaluations/index.js
Phil Renaud a7bd071a49 Allow wildcard for Evaluations API (#13530)
* Failing test and TODO for wildcard

* Alias the namespace query parameter for Evals

* eval: fix list when using ACLs and * namespace

Apply the same verification process as in job, allocs and scaling
policy list endpoints to handle the eval list when using an ACL token
with limited namespace support but querying using the `*` wildcard
namespace.

* changelog: add entry for #13530

* ui: set namespace when querying eval

Evals have a unique UUID as ID, but when querying them the Nomad API
still expects a namespace query param, otherwise it assumes `default`.

Co-authored-by: Luiz Aoqui <luiz@hashicorp.com>
2022-07-11 16:42:17 -04:00

267 lines
7.1 KiB
JavaScript

import { getOwner } from '@ember/application';
import Controller from '@ember/controller';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { schedule } from '@ember/runloop';
import { inject as service } from '@ember/service';
import { useMachine } from 'ember-statecharts';
import { use } from 'ember-usable';
import evaluationsMachine from '../../machines/evaluations';
const ALL_NAMESPACE_WILDCARD = '*';
export default class EvaluationsController extends Controller {
@service store;
@service userSettings;
// We use statecharts here to manage complex user flows for the sidebar logic
@use
statechart = useMachine(evaluationsMachine).withConfig({
services: {
loadEvaluation: this.loadEvaluation,
},
actions: {
updateEvaluationQueryParameter: this.updateEvaluationQueryParameter,
removeCurrentEvaluationQueryParameter:
this.removeCurrentEvaluationQueryParameter,
},
guards: {
sidebarIsOpen: this._sidebarIsOpen,
},
});
queryParams = [
'nextToken',
'currentEval',
'pageSize',
'status',
{ qpNamespace: 'namespace' },
'type',
'searchTerm',
];
@tracked currentEval = null;
@action
_sidebarIsOpen() {
return !!this.currentEval;
}
@action
async loadEvaluation(context, { evaluation }) {
let evaluationId;
if (evaluation?.id) {
evaluationId = evaluation.id;
} else {
evaluationId = this.currentEval;
}
return this.store.findRecord('evaluation', evaluationId, {
reload: true,
adapterOptions: { related: true },
});
}
@action
async handleEvaluationClick(evaluation, e) {
if (
e instanceof MouseEvent ||
(e instanceof KeyboardEvent && (e.code === 'Enter' || e.code === 'Space'))
) {
this.statechart.send('LOAD_EVALUATION', { evaluation });
}
}
@action
notifyEvalChange([evaluation]) {
schedule('actions', this, () => {
this.statechart.send('CHANGE_EVAL', { evaluation });
});
}
@action
updateEvaluationQueryParameter(context, { evaluation }) {
this.currentEval = evaluation.id;
}
@action
removeCurrentEvaluationQueryParameter() {
this.currentEval = null;
}
get shouldDisableNext() {
return !this.model.meta?.nextToken;
}
get shouldDisablePrev() {
return !this.previousTokens.length;
}
get optionsEvaluationsStatus() {
return [
{ key: null, label: 'All' },
{ key: 'blocked', label: 'Blocked' },
{ key: 'pending', label: 'Pending' },
{ key: 'complete', label: 'Complete' },
{ key: 'failed', label: 'Failed' },
{ key: 'canceled', label: 'Canceled' },
];
}
get optionsTriggeredBy() {
return [
{ key: null, label: 'All' },
{ key: 'job-register', label: 'Job Register' },
{ key: 'job-deregister', label: 'Job Deregister' },
{ key: 'periodic-job', label: 'Periodic Job' },
{ key: 'node-drain', label: 'Node Drain' },
{ key: 'node-update', label: 'Node Update' },
{ key: 'alloc-stop', label: 'Allocation Stop' },
{ key: 'scheduled', label: 'Scheduled' },
{ key: 'rolling-update', label: 'Rolling Update' },
{ key: 'deployment-watcher', label: 'Deployment Watcher' },
{ key: 'failed-follow-up', label: 'Failed Follow Up' },
{ key: 'max-disconnect-timeout', label: 'Max Disconnect Timeout' },
{ key: 'max-plan-attempts', label: 'Max Plan Attempts' },
{ key: 'alloc-failure', label: 'Allocation Failure' },
{ key: 'queued-allocs', label: 'Queued Allocations' },
{ key: 'preemption', label: 'Preemption' },
{ key: 'job-scaling', label: 'Job Scalling' },
];
}
get optionsNamespaces() {
const namespaces = this.store.peekAll('namespace').map((namespace) => ({
key: namespace.name,
label: namespace.name,
}));
// Create default namespace selection
namespaces.unshift({
key: ALL_NAMESPACE_WILDCARD,
label: 'All (*)',
});
return namespaces;
}
get optionsType() {
return [
{ key: null, label: 'All' },
{ key: 'client', label: 'Client' },
{ key: 'no client', label: 'No Client' },
];
}
filters = ['status', 'qpNamespace', 'type', 'triggeredBy', 'searchTerm'];
get hasFiltersApplied() {
return this.filters.reduce((result, filter) => {
// By default we always set qpNamespace to the '*' wildcard
// We need to ensure that if namespace is the only filter, that we send the correct error message to the user
if (this[filter] && filter !== 'qpNamespace') {
result = true;
}
return result;
}, false);
}
get currentFilters() {
const result = [];
for (const filter of this.filters) {
const isNamespaceWildcard =
filter === 'qpNamespace' && this[filter] === '*';
if (this[filter] && !isNamespaceWildcard) {
result.push({ [filter]: this[filter] });
}
}
return result;
}
get noMatchText() {
let text = '';
const cleanNames = {
status: 'Status',
qpNamespace: 'Namespace',
type: 'Type',
triggeredBy: 'Triggered By',
searchTerm: 'Search Term',
};
if (this.hasFiltersApplied) {
for (let i = 0; i < this.currentFilters.length; i++) {
const filter = this.currentFilters[i];
const [name] = Object.keys(filter);
const filterName = cleanNames[name];
const filterValue = filter[name];
if (this.currentFilters.length === 1)
return `${filterName}: ${filterValue}.`;
if (i !== 0 && i !== this.currentFilters.length - 1)
text = text.concat(`, ${filterName}: ${filterValue}`);
if (i === 0) text = text.concat(`${filterName}: ${filterValue}`);
if (i === this.currentFilters.length - 1) {
return text.concat(`, ${filterName}: ${filterValue}.`);
}
}
}
return text;
}
@tracked pageSize = this.userSettings.pageSize;
@tracked nextToken = null;
@tracked previousTokens = [];
@tracked status = null;
@tracked triggeredBy = null;
@tracked qpNamespace = ALL_NAMESPACE_WILDCARD;
@tracked type = null;
@tracked searchTerm = null;
@action
onChange(newPageSize) {
this.pageSize = newPageSize;
}
@action
onNext(nextToken) {
this.previousTokens = [...this.previousTokens, this.nextToken];
this.nextToken = nextToken;
}
@action
onPrev() {
const lastToken = this.previousTokens.pop();
this.previousTokens = [...this.previousTokens];
this.nextToken = lastToken;
}
@action
refresh() {
const isDefaultParams = this.nextToken === null && this.status === null;
if (isDefaultParams) {
getOwner(this).lookup('route:evaluations.index').refresh();
return;
}
this._resetTokens();
this.status = null;
this.pageSize = this.userSettings.pageSize;
}
@action
setQueryParam(qp, selection) {
this._resetTokens();
this[qp] = selection;
}
@action
toggle() {
this._resetTokens();
this.shouldOnlyDisplayClientEvals = !this.shouldOnlyDisplayClientEvals;
}
@action
_resetTokens() {
this.nextToken = null;
this.previousTokens = [];
}
}