open-nomad/ui/app/controllers/evaluations/index.js
Phil Renaud cbd4deedf8
[ui] general keyboard navigation: 1.3.4 release (#14138)
* Initialized keyboard service

Neat but funky: dynamic subnav traversal

👻

generalized traverseSubnav method

Shift as special modifier key

Nice little demo panel

Keyboard shortcuts keycard

Some animation styles on keyboard shortcuts

Handle situations where a link is deeply nested from its parent menu item

Keyboard service cleanup

helper-based initializer and teardown for new contextual commands

Keyboard shortcuts modal component added and demo-ghost removed

Removed j and k from subnav traversal

Register and unregister methods for subnav plus new subnavs for volumes and volume

register main nav method

Generalizing the register nav method

12762 table keynav (#12975)

* Experimental feature: shortcut visual hints

* Long way around to a custom modifier for keyboard shortcuts

* dynamic table and list iterative shortcuts

* Progress with regular old tether

* Delogging

* Table Keynav tether fix, server and client navs, and fix to shiftless on modified arrow keys

Go to Optimize keyboard link and storage key changed to g r

parameterized jobs keyboard nav

Dynamic numeric keynav for multiple tables (#13482)

* Multiple tables init

* URL-bind enumerable keyboard commands and add to more taskRow and allocationRows

* Type safety and lint fixes

* Consolidated push to keyCommands

* Default value when removing keyCommands

* Remove the URL-based removal method and perform a recompute on any add

Get tests passing in Keynav: remove math helpers and a few other defensive moves (#13761)

* Remove ember math helpers

* Test fixes for jobparts/body

* Kill an unneeded integration helper test

* delog

* Trying if disabling percy lets this finish

* Okay so its not percy; try parallelism in circle

* Percyless yet again

* Trying a different angle to not have percy

* Upgrade percy to 1.6.1

[ui] Keyboard nav: "u" key to go up a level (#13754)

* U to go up a level

* Mislabelled my conditional

* Custom lint ignore rule

* Custom lint ignore rule, this time with commas

* Since we're getting rid of ember math helpers elsewhere, do the math ourselves here

Replace ArrowLeft etc. with an ascii arrow (#13776)

* Replace ArrowLeft etc. with an ascii arrow

* non-mutative helper cleanup

Keyboard Nav: let users rebind their shortcuts (#13781)

* click-outside and shortcuts enabled/disabled toggle

* Trap focus when modal open

* Enabled/disabled saved to localStorage

* Autofocus edit button on variable index

* Modal overflow styles

* Functional rebind

* Saving rebinds to localStorage for all majors

* Started on defaultCommandBindings

* Modal header style and cancel rebind on escape

* keyboardable keybindings w buttons instead of spans

* recording and defaultvalues

* Enter short-circuits rebind

* Only some commands are rebindable, and dont show dupes

* No unused get import

* More visually distinct header on modal

* Disallowed keys for rebind, showing buffer as you type, and moving dedupe to modal logic

willDestroy hook to prevent tests from doubling/tripling up addEventListener on kb events

remove unused tests

Keyboard Navigation acceptance tests (#13893)

* Acceptance tests for keyboard modal

* a11y audit fix and localStorage clear

* Bind/rebind/localStorage tests

* Keyboard tests for dynamic nav and tables

* Rebinder and assert expectation

* Second percy snapshot showing hints no longer relevant

Weird issue where linktos with query props specifically from the task-groups page would fail to route / hit undefined.shouldSuperCede errors

Adds the concept of exclusivity to a keycommand, removing peers that also share its label

Lintfix

Changelog and PR feedback

Changelog and PR feedback

Fix to rebinding in firefox by blurring the now-disabled button on rebind (#14053)

* Secure Variables shortcuts removed

* Variable index route autofocus removed

* Updated changelog entry

* Updated changelog entry

* Keynav docs (#14148)

* Section added to the API Docs UI page

* Added a note about disabling

* Prev and Next order

* Remove dev log and unneeded comments
2022-08-17 12:59:33 -04:00

269 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')) ||
!e
) {
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 = [];
}
}