cbd4deedf8
* 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
269 lines
7.1 KiB
JavaScript
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 = [];
|
|
}
|
|
}
|