Add massaged results of class codemod

Manual interventions:
• decorators on the same line for service and controller
  injections and most computed property macros
• preserving import order when possible, both per-line
  and intra-line
• moving new imports to the bottom
• removal of classic decorator for trivial cases
• conversion of init to constructor when appropriate
This commit is contained in:
Buck Doyle
2020-06-10 08:49:16 -05:00
parent 9ccec0afbb
commit 24eadd269c
222 changed files with 4332 additions and 2826 deletions

View File

@@ -15,6 +15,9 @@ module.exports = {
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
legacyDecorators: true,
},
},
plugins: [
'ember'

View File

@@ -2,19 +2,23 @@ import { Ability } from 'ember-can';
import { inject as service } from '@ember/service';
import { computed, get } from '@ember/object';
import { equal, not } from '@ember/object/computed';
import classic from 'ember-classic-decorator';
export default Ability.extend({
system: service(),
token: service(),
@classic
export default class Abstract extends Ability {
@service system;
@service token;
bypassAuthorization: not('token.aclEnabled'),
selfTokenIsManagement: equal('token.selfToken.type', 'management'),
@not('token.aclEnabled') bypassAuthorization;
@equal('token.selfToken.type', 'management') selfTokenIsManagement;
activeNamespace: computed('system.activeNamespace.name', function() {
@computed('system.activeNamespace.name')
get activeNamespace() {
return this.get('system.activeNamespace.name') || 'default';
}),
}
rulesForActiveNamespace: computed('activeNamespace', 'token.selfTokenPolicies.[]', function() {
@computed('activeNamespace', 'token.selfTokenPolicies.[]')
get rulesForActiveNamespace() {
let activeNamespace = this.activeNamespace;
return (this.get('token.selfTokenPolicies') || []).toArray().reduce((rules, policy) => {
@@ -28,7 +32,7 @@ export default Ability.extend({
return rules;
}, []);
}),
}
// Chooses the closest namespace as described at the bottom here:
// https://www.nomadproject.io/guides/security/acl.html#namespace-rules
@@ -67,5 +71,5 @@ export default Ability.extend({
} else if (namespaceNames.includes('default')) {
return 'default';
}
},
});
}
}

View File

@@ -2,13 +2,15 @@ import AbstractAbility from './abstract';
import { computed, get } from '@ember/object';
import { or } from '@ember/object/computed';
export default AbstractAbility.extend({
canExec: or('bypassAuthorization', 'selfTokenIsManagement', 'policiesSupportExec'),
export default class Allocation extends AbstractAbility {
@or('bypassAuthorization', 'selfTokenIsManagement', 'policiesSupportExec')
canExec;
policiesSupportExec: computed('rulesForActiveNamespace.@each.capabilities', function() {
@computed('rulesForActiveNamespace.@each.capabilities')
get policiesSupportExec() {
return this.rulesForActiveNamespace.some(rules => {
let capabilities = get(rules, 'Capabilities') || [];
return capabilities.includes('alloc-exec');
});
}),
});
}
}

View File

@@ -2,12 +2,14 @@ import AbstractAbility from './abstract';
import { computed, get } from '@ember/object';
import { or } from '@ember/object/computed';
export default AbstractAbility.extend({
export default class Client extends AbstractAbility {
// Map abilities to policy options (which are coarse for nodes)
// instead of specific behaviors.
canWrite: or('bypassAuthorization', 'selfTokenIsManagement', 'policiesIncludeNodeWrite'),
@or('bypassAuthorization', 'selfTokenIsManagement', 'policiesIncludeNodeWrite')
canWrite;
policiesIncludeNodeWrite: computed('token.selfTokenPolicies.[]', function() {
@computed('token.selfTokenPolicies.[]')
get policiesIncludeNodeWrite() {
// For each policy record, extract the Node policy
const policies = (this.get('token.selfTokenPolicies') || [])
.toArray()
@@ -16,5 +18,5 @@ export default AbstractAbility.extend({
// Node write is allowed if any policy allows it
return policies.some(policy => policy === 'write');
}),
});
}
}

View File

@@ -2,13 +2,15 @@ import AbstractAbility from './abstract';
import { computed, get } from '@ember/object';
import { or } from '@ember/object/computed';
export default AbstractAbility.extend({
canRun: or('bypassAuthorization', 'selfTokenIsManagement', 'policiesSupportRunning'),
export default class Job extends AbstractAbility {
@or('bypassAuthorization', 'selfTokenIsManagement', 'policiesSupportRunning')
canRun;
policiesSupportRunning: computed('rulesForActiveNamespace.@each.capabilities', function() {
@computed('rulesForActiveNamespace.@each.capabilities')
get policiesSupportRunning() {
return this.rulesForActiveNamespace.some(rules => {
let capabilities = get(rules, 'Capabilities') || [];
return capabilities.includes('submit-job');
});
}),
});
}
}

View File

@@ -1,9 +1,10 @@
import ApplicationAdapter from './application';
export default ApplicationAdapter.extend({
pathForType: () => 'agent/members',
export default class Agent extends ApplicationAdapter {
pathForType = () => 'agent/members';
urlForFindRecord() {
const [, ...args] = arguments;
return this.urlForFindAll(...args);
},
});
}
}

View File

@@ -4,20 +4,23 @@ import RESTAdapter from 'ember-data/adapters/rest';
import codesForError from '../utils/codes-for-error';
import removeRecord from '../utils/remove-record';
import { default as NoLeaderError, NO_LEADER } from '../utils/no-leader-error';
import classic from 'ember-classic-decorator';
export const namespace = 'v1';
export default RESTAdapter.extend({
@classic
export default class Application extends RESTAdapter {
// TODO: This can be removed once jquery-integration is turned off for
// the entire app.
useFetch: true,
useFetch = true;
namespace,
namespace = namespace;
system: service(),
token: service(),
@service system;
@service token;
headers: computed('token.secret', function() {
@computed('token.secret')
get headers() {
const token = this.get('token.secret');
if (token) {
return {
@@ -26,17 +29,17 @@ export default RESTAdapter.extend({
}
return;
}),
}
handleResponse(status, headers, payload) {
if (status === 500 && payload === NO_LEADER) {
return new NoLeaderError();
}
return this._super(...arguments);
},
return super.handleResponse(...arguments);
}
findAll() {
return this._super(...arguments).catch(error => {
return super.findAll(...arguments).catch(error => {
const errorCodes = codesForError(error);
const isNotImplemented = errorCodes.includes('501');
@@ -48,7 +51,7 @@ export default RESTAdapter.extend({
// Rethrow to be handled downstream
throw error;
});
},
}
ajaxOptions(url, type, options = {}) {
options.data || (options.data = {});
@@ -58,13 +61,13 @@ export default RESTAdapter.extend({
options.data.region = region;
}
}
return this._super(url, type, options);
},
return super.ajaxOptions(url, type, options);
}
// In order to remove stale records from the store, findHasMany has to unload
// all records related to the request in question.
findHasMany(store, snapshot, link, relationship) {
return this._super(...arguments).then(payload => {
return super.findHasMany(...arguments).then(payload => {
const relationshipType = relationship.type;
const inverse = snapshot.record.inverseFor(relationship.key);
if (inverse) {
@@ -77,7 +80,7 @@ export default RESTAdapter.extend({
}
return payload;
});
},
}
// Single record requests deviate from REST practice by using
// the singular form of the resource name.
@@ -87,9 +90,10 @@ export default RESTAdapter.extend({
//
// This is the original implementation of _buildURL
// without the pluralization of modelName
urlForFindRecord: urlForRecord,
urlForUpdateRecord: urlForRecord,
});
urlForFindRecord = urlForRecord;
urlForUpdateRecord = urlForRecord;
}
function urlForRecord(id, modelName) {
let path;

View File

@@ -1,6 +1,6 @@
import Watchable from './watchable';
export default Watchable.extend({
export default class Deployment extends Watchable {
promote(deployment) {
const id = deployment.get('id');
const url = urlForAction(this.urlForFindRecord(id, 'deployment'), '/promote');
@@ -10,8 +10,8 @@ export default Watchable.extend({
All: true,
},
});
},
});
}
}
// The deployment action API endpoints all end with the ID
// /deployment/:action/:deployment_id instead of /deployment/:deployment_id/:action

View File

@@ -1,12 +1,12 @@
import Watchable from './watchable';
export default Watchable.extend({
export default class JobSummary extends Watchable {
urlForFindRecord(id, type, hash) {
const [name, namespace] = JSON.parse(id);
let url = this._super(name, 'job', hash) + '/summary';
let url = super.urlForFindRecord(name, 'job', hash) + '/summary';
if (namespace && namespace !== 'default') {
url += `?namespace=${namespace}`;
}
return url;
},
});
}
}

View File

@@ -1,13 +1,13 @@
import ApplicationAdapter from './application';
import codesForError from '../utils/codes-for-error';
export default ApplicationAdapter.extend({
export default class Namespace extends ApplicationAdapter {
findRecord(store, modelClass, id) {
return this._super(...arguments).catch(error => {
return super.findRecord(...arguments).catch(error => {
const errorCodes = codesForError(error);
if (errorCodes.includes('501')) {
return { Name: id };
}
});
},
});
}
}

View File

@@ -1,14 +1,14 @@
import Watchable from './watchable';
import addToPath from 'nomad-ui/utils/add-to-path';
export default Watchable.extend({
export default class Node extends Watchable {
setEligible(node) {
return this.setEligibility(node, true);
},
}
setIneligible(node) {
return this.setEligibility(node, false);
},
}
setEligibility(node, isEligible) {
const url = addToPath(this.urlForFindRecord(node.id, 'node'), '/eligibility');
@@ -18,7 +18,7 @@ export default Watchable.extend({
Eligibility: isEligible ? 'eligible' : 'ineligible',
},
});
},
}
// Force: -1s deadline
// No Deadline: 0 deadline
@@ -36,7 +36,7 @@ export default Watchable.extend({
),
},
});
},
}
forceDrain(node, drainSpec) {
return this.drain(
@@ -45,7 +45,7 @@ export default Watchable.extend({
Deadline: -1,
})
);
},
}
cancelDrain(node) {
const url = addToPath(this.urlForFindRecord(node.id, 'node'), '/drain');
@@ -55,5 +55,5 @@ export default Watchable.extend({
DrainSpec: null,
},
});
},
});
}
}

View File

@@ -1,5 +1,5 @@
import { default as ApplicationAdapter, namespace } from './application';
export default ApplicationAdapter.extend({
namespace: namespace + '/acl',
});
export default class Policy extends ApplicationAdapter {
namespace = namespace + '/acl';
}

View File

@@ -1,10 +1,10 @@
import { inject as service } from '@ember/service';
import { default as ApplicationAdapter, namespace } from './application';
export default ApplicationAdapter.extend({
store: service(),
export default class Token extends ApplicationAdapter {
@service store;
namespace: namespace + '/acl',
namespace = namespace + '/acl';
findSelf() {
return this.ajax(`${this.buildURL()}/token/self`, 'GET').then(token => {
@@ -15,5 +15,5 @@ export default ApplicationAdapter.extend({
return store.peekRecord('token', store.normalize('token', token).data.id);
});
},
});
}
}

View File

@@ -6,9 +6,9 @@ import queryString from 'query-string';
import ApplicationAdapter from './application';
import removeRecord from '../utils/remove-record';
export default ApplicationAdapter.extend({
watchList: service(),
store: service(),
export default class Watchable extends ApplicationAdapter {
@service watchList;
@service store;
// Overriding ajax is not advised, but this is a minimal modification
// that sets off a series of events that results in query params being
@@ -19,7 +19,7 @@ export default ApplicationAdapter.extend({
// to ajaxOptions or overriding ajax completely.
ajax(url, type, options) {
const hasParams = hasNonBlockingQueryParams(options);
if (!hasParams || type !== 'GET') return this._super(url, type, options);
if (!hasParams || type !== 'GET') return super.ajax(url, type, options);
const params = { ...options.data };
delete params.index;
@@ -29,8 +29,8 @@ export default ApplicationAdapter.extend({
// at this point since everything else is added to the URL in advance.
options.data = options.data.index ? { index: options.data.index } : {};
return this._super(`${url}?${queryString.stringify(params)}`, type, options);
},
return super.ajax(`${url}?${queryString.stringify(params)}`, type, options);
}
findAll(store, type, sinceToken, snapshotRecordArray, additionalParams = {}) {
const params = assign(this.buildQuery(), additionalParams);
@@ -45,7 +45,7 @@ export default ApplicationAdapter.extend({
signal,
data: params,
});
},
}
findRecord(store, type, id, snapshot, additionalParams = {}) {
let [url, params] = this.buildURL(type.modelName, id, snapshot, 'findRecord').split('?');
@@ -65,7 +65,7 @@ export default ApplicationAdapter.extend({
}
throw error;
});
},
}
query(store, type, query, snapshotRecordArray, options, additionalParams = {}) {
const url = this.buildURL(type.modelName, null, null, 'query', query);
@@ -107,7 +107,7 @@ export default ApplicationAdapter.extend({
return payload;
});
},
}
reloadRelationship(model, relationshipName, options = { watch: false, abortController: null }) {
const { watch, abortController } = options;
@@ -156,7 +156,7 @@ export default ApplicationAdapter.extend({
}
);
}
},
}
handleResponse(status, headers, payload, requestData) {
// Some browsers lowercase all headers. Others keep them
@@ -166,9 +166,9 @@ export default ApplicationAdapter.extend({
this.watchList.setIndexFor(requestData.url, newIndex);
}
return this._super(...arguments);
},
});
return super.handleResponse(...arguments);
}
}
function hasNonBlockingQueryParams(options) {
if (!options || !options.data) return false;

View File

@@ -5,11 +5,11 @@ import config from './config/environment';
let App;
App = Application.extend({
modulePrefix: config.modulePrefix,
podModulePrefix: config.podModulePrefix,
Resolver,
});
App = class AppApplication extends Application {
modulePrefix = config.modulePrefix;
podModulePrefix = config.podModulePrefix;
Resolver = Resolver;
};
loadInitializers(App, config.modulePrefix);

View File

@@ -8,46 +8,48 @@ import { run } from '@ember/runloop';
import { task, timeout } from 'ember-concurrency';
import { lazyClick } from '../helpers/lazy-click';
import AllocationStatsTracker from 'nomad-ui/utils/classes/allocation-stats-tracker';
import classic from 'ember-classic-decorator';
import { classNames, tagName } from '@ember-decorators/component';
export default Component.extend({
store: service(),
token: service(),
@classic
@tagName('tr')
@classNames('allocation-row', 'is-interactive')
export default class AllocationRow extends Component {
@service store;
@service token;
tagName: 'tr',
classNames: ['allocation-row', 'is-interactive'],
allocation: null,
allocation = null;
// Used to determine whether the row should mention the node or the job
context: null,
context = null;
// Internal state
statsError: false,
statsError = false;
enablePolling: overridable(() => !Ember.testing),
@overridable(() => !Ember.testing) enablePolling;
stats: computed('allocation', 'allocation.isRunning', function() {
@computed('allocation', 'allocation.isRunning')
get stats() {
if (!this.get('allocation.isRunning')) return;
return AllocationStatsTracker.create({
fetch: url => this.token.authorizedRequest(url),
allocation: this.allocation,
});
}),
}
cpu: alias('stats.cpu.lastObject'),
memory: alias('stats.memory.lastObject'),
@alias('stats.cpu.lastObject') cpu;
@alias('stats.memory.lastObject') memory;
onClick() {},
onClick() {}
click(event) {
lazyClick([this.onClick, event]);
},
}
didReceiveAttrs() {
this.updateStatsTracker();
},
}
updateStatsTracker() {
const allocation = this.allocation;
@@ -57,9 +59,9 @@ export default Component.extend({
} else {
this.fetchStats.cancelAll();
}
},
}
fetchStats: task(function*() {
@(task(function*() {
do {
if (this.stats) {
try {
@@ -72,8 +74,9 @@ export default Component.extend({
yield timeout(500);
} while (this.enablePolling);
}).drop(),
});
}).drop())
fetchStats;
}
async function qualifyAllocation() {
const allocation = this.allocation;

View File

@@ -2,41 +2,47 @@ import Component from '@ember/component';
import { computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import { formatBytes } from 'nomad-ui/helpers/format-bytes';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: '',
@classic
@tagName('')
export default class AllocationStat extends Component {
allocation = null;
statsTracker = null;
isLoading = false;
error = null;
metric = 'memory'; // Either memory or cpu
allocation: null,
statsTracker: null,
isLoading: false,
error: null,
metric: 'memory', // Either memory or cpu
statClass: computed('metric', function() {
@computed('metric')
get statClass() {
return this.metric === 'cpu' ? 'is-info' : 'is-danger';
}),
}
cpu: alias('statsTracker.cpu.lastObject'),
memory: alias('statsTracker.memory.lastObject'),
@alias('statsTracker.cpu.lastObject') cpu;
@alias('statsTracker.memory.lastObject') memory;
stat: computed('metric', 'cpu', 'memory', function() {
@computed('metric', 'cpu', 'memory')
get stat() {
const { metric } = this;
if (metric === 'cpu' || metric === 'memory') {
return this[this.metric];
}
return;
}),
}
formattedStat: computed('metric', 'stat.used', function() {
@computed('metric', 'stat.used')
get formattedStat() {
if (!this.stat) return;
if (this.metric === 'memory') return formatBytes([this.stat.used]);
return this.stat.used;
}),
}
formattedReserved: computed('metric', 'statsTracker.{reservedMemory,reservedCPU}', function() {
@computed('metric', 'statsTracker.{reservedMemory,reservedCPU}')
get formattedReserved() {
if (this.metric === 'memory') return `${this.statsTracker.reservedMemory} MiB`;
if (this.metric === 'cpu') return `${this.statsTracker.reservedCPU} MHz`;
return;
}),
});
}
}

View File

@@ -1,14 +1,19 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
import { equal, or } from '@ember/object/computed';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
router: service(),
@classic
@tagName('')
export default class AllocationSubnav extends Component {
@service router;
tagName: '',
@equal('router.currentRouteName', 'allocations.allocation.fs')
fsIsActive;
fsIsActive: equal('router.currentRouteName', 'allocations.allocation.fs'),
fsRootIsActive: equal('router.currentRouteName', 'allocations.allocation.fs-root'),
@equal('router.currentRouteName', 'allocations.allocation.fs-root')
fsRootIsActive;
filesLinkActive: or('fsIsActive', 'fsRootIsActive'),
});
@or('fsIsActive', 'fsRootIsActive') filesLinkActive;
}

View File

@@ -1,11 +1,13 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
import { reads } from '@ember/object/computed';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
breadcrumbsService: service('breadcrumbs'),
@classic
@tagName('')
export default class AppBreadcrumbs extends Component {
@service('breadcrumbs') breadcrumbsService;
tagName: '',
breadcrumbs: reads('breadcrumbsService.breadcrumbs'),
});
@reads('breadcrumbsService.breadcrumbs') breadcrumbs;
}

View File

@@ -1,5 +1,7 @@
import Component from '@ember/component';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: '',
});
@classic
@tagName('')
export default class AttributesSection extends Component {}

View File

@@ -1,27 +1,26 @@
import { computed } from '@ember/object';
import DistributionBar from './distribution-bar';
import classic from 'ember-classic-decorator';
export default DistributionBar.extend({
layoutName: 'components/distribution-bar',
@classic
export default class ChildrenStatusBar extends DistributionBar {
layoutName = 'components/distribution-bar';
job: null,
job = null;
'data-test-children-status-bar': true,
'data-test-children-status-bar' = true;
data: computed('job.{pendingChildren,runningChildren,deadChildren}', function() {
@computed('job.{pendingChildren,runningChildren,deadChildren}')
get data() {
if (!this.job) {
return [];
}
const children = this.job.getProperties(
'pendingChildren',
'runningChildren',
'deadChildren'
);
const children = this.job.getProperties('pendingChildren', 'runningChildren', 'deadChildren');
return [
{ label: 'Pending', value: children.pendingChildren, className: 'queued' },
{ label: 'Running', value: children.runningChildren, className: 'running' },
{ label: 'Dead', value: children.deadChildren, className: 'complete' },
];
}),
});
}
}

View File

@@ -4,20 +4,22 @@ import { lazyClick } from '../helpers/lazy-click';
import { watchRelationship } from 'nomad-ui/utils/properties/watch';
import WithVisibilityDetection from 'nomad-ui/mixins/with-component-visibility-detection';
import { computed } from '@ember/object';
import { classNames, tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend(WithVisibilityDetection, {
store: service(),
@classic
@tagName('tr')
@classNames('client-node-row', 'is-interactive')
export default class ClientNodeRow extends Component.extend(WithVisibilityDetection) {
@service store;
tagName: 'tr',
classNames: ['client-node-row', 'is-interactive'],
node = null;
node: null,
onClick() {},
onClick() {}
click(event) {
lazyClick([this.onClick, event]);
},
}
didReceiveAttrs() {
// Reload the node in order to get detail information
@@ -27,7 +29,7 @@ export default Component.extend(WithVisibilityDetection, {
this.watch.perform(node, 100);
});
}
},
}
visibilityHandler() {
if (document.hidden) {
@@ -38,16 +40,17 @@ export default Component.extend(WithVisibilityDetection, {
this.watch.perform(node, 100);
}
}
},
}
willDestroy() {
this.watch.cancelAll();
this._super(...arguments);
},
super.willDestroy(...arguments);
}
watch: watchRelationship('allocations'),
@watchRelationship('allocations') watch;
compositeStatusClass: computed('node.compositeStatus', function() {
@computed('node.compositeStatus')
get compositeStatusClass() {
let compositeStatus = this.get('node.compositeStatus');
if (compositeStatus === 'draining') {
@@ -59,5 +62,5 @@ export default Component.extend(WithVisibilityDetection, {
} else {
return '';
}
}),
});
}
}

View File

@@ -1,16 +1,19 @@
import Component from '@ember/component';
import { task, timeout } from 'ember-concurrency';
import { classNames } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
classNames: ['copy-button'],
@classic
@classNames('copy-button')
export default class CopyButton extends Component {
clipboardText = null;
state = null;
clipboardText: null,
state: null,
indicateSuccess: task(function*() {
@(task(function*() {
this.set('state', 'success');
yield timeout(2000);
this.set('state', null);
}).restartable(),
});
}).restartable())
indicateSuccess;
}

View File

@@ -4,30 +4,34 @@ import { equal } from '@ember/object/computed';
import { computed as overridable } from 'ember-overridable-computed';
import { task } from 'ember-concurrency';
import Duration from 'duration-js';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: '',
@classic
@tagName('')
export default class DrainPopover extends Component {
client = null;
isDisabled = false;
client: null,
isDisabled: false,
onError() {}
onDrain() {}
onError() {},
onDrain() {},
parseError = '';
parseError: '',
deadlineEnabled = false;
forceDrain = false;
drainSystemJobs = true;
deadlineEnabled: false,
forceDrain: false,
drainSystemJobs: true,
selectedDurationQuickOption: overridable(function() {
@overridable(function() {
return this.durationQuickOptions[0];
}),
})
selectedDurationQuickOption;
durationIsCustom: equal('selectedDurationQuickOption.value', 'custom'),
customDuration: '',
@equal('selectedDurationQuickOption.value', 'custom') durationIsCustom;
customDuration = '';
durationQuickOptions: computed(function() {
@computed
get durationQuickOptions() {
return [
{ label: '1 Hour', value: '1h' },
{ label: '4 Hours', value: '4h' },
@@ -36,21 +40,21 @@ export default Component.extend({
{ label: '1 Day', value: '1d' },
{ label: 'Custom', value: 'custom' },
];
}),
}
deadline: computed(
@computed(
'deadlineEnabled',
'durationIsCustom',
'customDuration',
'selectedDurationQuickOption.value',
function() {
if (!this.deadlineEnabled) return 0;
if (this.durationIsCustom) return this.customDuration;
return this.selectedDurationQuickOption.value;
}
),
'selectedDurationQuickOption.value'
)
get deadline() {
if (!this.deadlineEnabled) return 0;
if (this.durationIsCustom) return this.customDuration;
return this.selectedDurationQuickOption.value;
}
drain: task(function*(close) {
@task(function*(close) {
if (!this.client) return;
const isUpdating = this.client.isDraining;
@@ -79,9 +83,10 @@ export default Component.extend({
} catch (err) {
this.onError(err);
}
}),
})
drain;
preventDefault(e) {
e.preventDefault();
},
});
}
}

View File

@@ -1,10 +1,12 @@
import Component from '@ember/component';
import { FitAddon } from 'xterm-addon-fit';
import WindowResizable from '../mixins/window-resizable';
import { classNames } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend(WindowResizable, {
classNames: ['terminal-container'],
@classic
@classNames('terminal-container')
export default class ExecTerminal extends Component.extend(WindowResizable) {
didInsertElement() {
let fitAddon = new FitAddon();
this.fitAddon = fitAddon;
@@ -13,12 +15,12 @@ export default Component.extend(WindowResizable, {
this.terminal.open(this.element.querySelector('.terminal'));
fitAddon.fit();
},
}
windowResizeHandler(e) {
this.fitAddon.fit();
if (this.terminal.resized) {
this.terminal.resized(e);
}
},
});
}
}

View File

@@ -1,25 +1,27 @@
import Component from '@ember/component';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import generateExecUrl from 'nomad-ui/utils/generate-exec-url';
import openExecUrl from 'nomad-ui/utils/open-exec-url';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: '',
@classic
@tagName('')
export default class OpenButton extends Component {
@service router;
router: service(),
actions: {
open() {
openExecUrl(this.generateUrl());
},
},
@action
open() {
openExecUrl(this.generateUrl());
}
generateUrl() {
return generateExecUrl(this.router, {
job: this.job,
taskGroup: this.taskGroup,
task: this.task,
allocation: this.task
allocation: this.task,
});
},
});
}
}

View File

@@ -1,5 +1,7 @@
import Component from '@ember/component';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: '',
});
@classic
@tagName('')
export default class TaskContents extends Component {}

View File

@@ -1,18 +1,21 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { isEmpty } from '@ember/utils';
import { classNames, tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: 'nav',
classNames: ['breadcrumb'],
@classic
@tagName('nav')
@classNames('breadcrumb')
export default class Breadcrumbs extends Component {
'data-test-fs-breadcrumbs' = true;
'data-test-fs-breadcrumbs': true,
allocation = null;
taskState = null;
path = null;
allocation: null,
taskState: null,
path: null,
breadcrumbs: computed('path', function() {
@computed('path')
get breadcrumbs() {
const breadcrumbs = this.path
.split('/')
.reject(isEmpty)
@@ -39,5 +42,5 @@ export default Component.extend({
}
return breadcrumbs;
}),
});
}
}

View File

@@ -1,58 +1,59 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { filterBy } from '@ember/object/computed';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: '',
@classic
@tagName('')
export default class Browser extends Component {
model = null;
model: null,
allocation: computed('model', function() {
@computed('model')
get allocation() {
if (this.model.allocation) {
return this.model.allocation;
} else {
return this.model;
}
}),
}
taskState: computed('model', function() {
@computed('model')
get taskState() {
if (this.model.allocation) {
return this.model;
}
return;
}),
}
type: computed('taskState', function() {
@computed('taskState')
get type() {
if (this.taskState) {
return 'task';
} else {
return 'allocation';
}
}),
}
directories: filterBy('directoryEntries', 'IsDir'),
files: filterBy('directoryEntries', 'IsDir', false),
@filterBy('directoryEntries', 'IsDir') directories;
@filterBy('directoryEntries', 'IsDir', false) files;
sortedDirectoryEntries: computed(
'directoryEntries.[]',
'sortProperty',
'sortDescending',
function() {
const sortProperty = this.sortProperty;
@computed('directoryEntries.[]', 'sortProperty', 'sortDescending')
get sortedDirectoryEntries() {
const sortProperty = this.sortProperty;
const directorySortProperty = sortProperty === 'Size' ? 'Name' : sortProperty;
const directorySortProperty = sortProperty === 'Size' ? 'Name' : sortProperty;
const sortedDirectories = this.directories.sortBy(directorySortProperty);
const sortedFiles = this.files.sortBy(sortProperty);
const sortedDirectories = this.directories.sortBy(directorySortProperty);
const sortedFiles = this.files.sortBy(sortProperty);
const sortedDirectoryEntries = sortedDirectories.concat(sortedFiles);
const sortedDirectoryEntries = sortedDirectories.concat(sortedFiles);
if (this.sortDescending) {
return sortedDirectoryEntries.reverse();
} else {
return sortedDirectoryEntries;
}
if (this.sortDescending) {
return sortedDirectoryEntries.reverse();
} else {
return sortedDirectoryEntries;
}
),
});
}
}

View File

@@ -1,14 +1,17 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { isEmpty } from '@ember/utils';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: '',
@classic
@tagName('')
export default class DirectoryEntry extends Component {
allocation = null;
taskState = null;
allocation: null,
taskState: null,
pathToEntry: computed('path', 'entry.Name', function() {
@computed('path', 'entry.Name')
get pathToEntry() {
const pathWithNoLeadingSlash = this.get('path').replace(/^\//, '');
const name = encodeURIComponent(this.get('entry.Name'));
@@ -17,5 +20,5 @@ export default Component.extend({
} else {
return `${pathWithNoLeadingSlash}/${name}`;
}
}),
});
}
}

View File

@@ -1,36 +1,38 @@
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import { computed } from '@ember/object';
import { gt } from '@ember/object/computed';
import { equal } from '@ember/object/computed';
import { action, computed } from '@ember/object';
import { equal, gt } from '@ember/object/computed';
import RSVP from 'rsvp';
import Log from 'nomad-ui/utils/classes/log';
import timeout from 'nomad-ui/utils/timeout';
import { classNames } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
token: service(),
@classic
@classNames('boxed-section', 'task-log')
export default class File extends Component {
@service token;
classNames: ['boxed-section', 'task-log'],
'data-test-file-viewer' = true;
'data-test-file-viewer': true,
allocation: null,
taskState: null,
file: null,
stat: null, // { Name, IsDir, Size, FileMode, ModTime, ContentType }
allocation = null;
taskState = null;
file = null;
stat = null; // { Name, IsDir, Size, FileMode, ModTime, ContentType }
// When true, request logs from the server agent
useServer: false,
useServer = false;
// When true, logs cannot be fetched from either the client or the server
noConnection: false,
noConnection = false;
clientTimeout: 1000,
serverTimeout: 5000,
clientTimeout = 1000;
serverTimeout = 5000;
mode: 'head',
mode = 'head';
fileComponent: computed('stat.ContentType', function() {
@computed('stat.ContentType')
get fileComponent() {
const contentType = this.stat.ContentType || '';
if (contentType.startsWith('image/')) {
@@ -40,21 +42,23 @@ export default Component.extend({
} else {
return 'unknown';
}
}),
}
isLarge: gt('stat.Size', 50000),
@gt('stat.Size', 50000) isLarge;
fileTypeIsUnknown: equal('fileComponent', 'unknown'),
isStreamable: equal('fileComponent', 'stream'),
isStreaming: false,
@equal('fileComponent', 'unknown') fileTypeIsUnknown;
@equal('fileComponent', 'stream') isStreamable;
isStreaming = false;
catUrl: computed('allocation.id', 'taskState.name', 'file', function() {
@computed('allocation.id', 'taskState.name', 'file')
get catUrl() {
const taskUrlPrefix = this.taskState ? `${this.taskState.name}/` : '';
const encodedPath = encodeURIComponent(`${taskUrlPrefix}${this.file}`);
return `/v1/client/fs/cat/${this.allocation.id}?path=${encodedPath}`;
}),
}
fetchMode: computed('isLarge', 'mode', function() {
@computed('isLarge', 'mode')
get fetchMode() {
if (this.mode === 'streaming') {
return 'stream';
}
@@ -66,15 +70,17 @@ export default Component.extend({
}
return;
}),
}
fileUrl: computed('allocation.{id,node.httpAddr}', 'fetchMode', 'useServer', function() {
@computed('allocation.{id,node.httpAddr}', 'fetchMode', 'useServer')
get fileUrl() {
const address = this.get('allocation.node.httpAddr');
const url = `/v1/client/fs/${this.fetchMode}/${this.allocation.id}`;
return this.useServer ? url : `//${address}${url}`;
}),
}
fileParams: computed('taskState.name', 'file', 'mode', function() {
@computed('taskState.name', 'file', 'mode')
get fileParams() {
// The Log class handles encoding query params
const taskUrlPrefix = this.taskState ? `${this.taskState.name}/` : '';
const path = `${taskUrlPrefix}${this.file}`;
@@ -89,9 +95,10 @@ export default Component.extend({
default:
return { path };
}
}),
}
logger: computed('fileUrl', 'fileParams', 'mode', function() {
@computed('fileUrl', 'fileParams', 'mode')
get logger() {
// The cat and readat APIs are in plainText while the stream API is always encoded.
const plainText = this.mode === 'head' || this.mode === 'tail';
@@ -115,7 +122,7 @@ export default Component.extend({
params: this.fileParams,
url: this.fileUrl,
});
}),
}
nextErrorState(error) {
if (this.useServer) {
@@ -124,23 +131,28 @@ export default Component.extend({
this.send('failoverToServer');
}
throw error;
},
}
actions: {
toggleStream() {
this.set('mode', 'streaming');
this.toggleProperty('isStreaming');
},
gotoHead() {
this.set('mode', 'head');
this.set('isStreaming', false);
},
gotoTail() {
this.set('mode', 'tail');
this.set('isStreaming', false);
},
failoverToServer() {
this.set('useServer', true);
},
},
});
@action
toggleStream() {
this.set('mode', 'streaming');
this.toggleProperty('isStreaming');
}
@action
gotoHead() {
this.set('mode', 'head');
this.set('isStreaming', false);
}
@action
gotoTail() {
this.set('mode', 'tail');
this.set('isStreaming', false);
}
@action
failoverToServer() {
this.set('useServer', true);
}
}

View File

@@ -1,8 +1,10 @@
import Component from '@ember/component';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: '',
allocation: null,
taskState: null,
});
@classic
@tagName('')
export default class Link extends Component {
allocation = null;
taskState = null;
}

View File

@@ -5,19 +5,22 @@ import { guidFor } from '@ember/object/internals';
import { run } from '@ember/runloop';
import d3Shape from 'd3-shape';
import WindowResizable from 'nomad-ui/mixins/window-resizable';
import { classNames } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend(WindowResizable, {
classNames: ['chart', 'gauge-chart'],
@classic
@classNames('chart', 'gauge-chart')
export default class GaugeChart extends Component.extend(WindowResizable) {
value = null;
complement = null;
total = null;
chartClass = 'is-info';
value: null,
complement: null,
total: null,
chartClass: 'is-info',
width = 0;
height = 0;
width: 0,
height: 0,
percent: computed('value', 'complement', 'total', function() {
@computed('value', 'complement', 'total')
get percent() {
assert(
'Provide complement OR total to GaugeChart, not both.',
this.complement != null || this.total != null
@@ -28,23 +31,27 @@ export default Component.extend(WindowResizable, {
}
return this.value / this.total;
}),
}
fillId: computed(function() {
@computed
get fillId() {
return `gauge-chart-fill-${guidFor(this)}`;
}),
}
maskId: computed(function() {
@computed
get maskId() {
return `gauge-chart-mask-${guidFor(this)}`;
}),
}
radius: computed('width', function() {
@computed('width')
get radius() {
return this.width / 2;
}),
}
weight: 4,
weight = 4;
backgroundArc: computed('radius', 'weight', function() {
@computed('radius', 'weight')
get backgroundArc() {
const { radius, weight } = this;
const arc = d3Shape
.arc()
@@ -54,9 +61,10 @@ export default Component.extend(WindowResizable, {
.startAngle(-Math.PI / 2)
.endAngle(Math.PI / 2);
return arc();
}),
}
valueArc: computed('radius', 'weight', 'percent', function() {
@computed('radius', 'weight', 'percent')
get valueArc() {
const { radius, weight, percent } = this;
const arc = d3Shape
@@ -67,18 +75,18 @@ export default Component.extend(WindowResizable, {
.startAngle(-Math.PI / 2)
.endAngle(-Math.PI / 2 + Math.PI * percent);
return arc();
}),
}
didInsertElement() {
this.updateDimensions();
},
}
updateDimensions() {
const width = this.element.querySelector('svg').clientWidth;
this.setProperties({ width, height: width / 2 });
},
}
windowResizeHandler() {
run.once(this, this.updateDimensions);
},
});
}
}

View File

@@ -1,7 +1,8 @@
import Component from '@ember/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
'data-test-global-header': true,
onHamburgerClick() {},
});
@classic
export default class GlobalHeader extends Component {
'data-test-global-header' = true;
onHamburgerClick() {}
}

View File

@@ -1,12 +1,15 @@
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import { computed } from '@ember/object';
import classic from 'ember-classic-decorator';
export default Component.extend({
system: service(),
router: service(),
@classic
export default class GutterMenu extends Component {
@service system;
@service router;
sortedNamespaces: computed('system.namespaces.@each.name', function() {
@computed('system.namespaces.@each.name')
get sortedNamespaces() {
const namespaces = this.get('system.namespaces').toArray() || [];
return namespaces.sort((a, b) => {
@@ -30,9 +33,9 @@ export default Component.extend({
return 0;
});
}),
}
onHamburgerClick() {},
onHamburgerClick() {}
gotoJobsForNamespace(namespace) {
if (!namespace || !namespace.get('id')) return;
@@ -46,5 +49,5 @@ export default Component.extend({
this.router.transitionTo(destination, {
queryParams: { namespace: namespace.get('id') },
});
},
});
}
}

View File

@@ -1,23 +1,27 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { classNames, tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: 'figure',
classNames: 'image-file',
'data-test-image-file': true,
@classic
@tagName('figure')
@classNames('image-file')
export default class ImageFile extends Component {
'data-test-image-file' = true;
src: null,
alt: null,
size: null,
src = null;
alt = null;
size = null;
// Set by updateImageMeta
width: 0,
height: 0,
width = 0;
height = 0;
fileName: computed('src', function() {
@computed('src')
get fileName() {
if (!this.src) return;
return this.src.includes('/') ? this.src.match(/^.*\/(.*)$/)[1] : this.src;
}),
}
updateImageMeta(event) {
const img = event.target;
@@ -25,5 +29,5 @@ export default Component.extend({
width: img.naturalWidth,
height: img.naturalHeight,
});
},
});
}
}

View File

@@ -1,8 +1,10 @@
import Component from '@ember/component';
import { classNames } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
classNames: ['job-deployment', 'boxed-section'],
deployment: null,
isOpen: false,
});
@classic
@classNames('job-deployment', 'boxed-section')
export default class JobDeployment extends Component {
deployment = null;
isOpen = false;
}

View File

@@ -1,5 +1,7 @@
import Component from '@ember/component';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: '',
});
@classic
@tagName('')
export default class DeploymentMetrics extends Component {}

View File

@@ -2,20 +2,22 @@ import Component from '@ember/component';
import { computed } from '@ember/object';
import { computed as overridable } from 'ember-overridable-computed';
import moment from 'moment';
import { classNames, tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: 'ol',
classNames: ['timeline'],
@classic
@tagName('ol')
@classNames('timeline')
export default class JobDeploymentsStream extends Component {
@overridable(() => []) deployments;
deployments: overridable(() => []),
@computed('deployments.@each.versionSubmitTime')
get sortedDeployments() {
return this.deployments.sortBy('versionSubmitTime').reverse();
}
sortedDeployments: computed('deployments.@each.versionSubmitTime', function() {
return this.deployments
.sortBy('versionSubmitTime')
.reverse();
}),
annotatedDeployments: computed('sortedDeployments.@each.version', function() {
@computed('sortedDeployments.@each.version')
get annotatedDeployments() {
const deployments = this.sortedDeployments;
return deployments.map((deployment, index) => {
const meta = {};
@@ -39,5 +41,5 @@ export default Component.extend({
return { deployment, meta };
});
}),
});
}
}

View File

@@ -1,5 +1,7 @@
import Component from '@ember/component';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: '',
});
@classic
@tagName('')
export default class JobDiffFieldsAndObjects extends Component {}

View File

@@ -1,15 +1,17 @@
import { equal } from '@ember/object/computed';
import Component from '@ember/component';
import { classNames, classNameBindings } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
classNames: ['job-diff'],
classNameBindings: ['isEdited:is-edited', 'isAdded:is-added', 'isDeleted:is-deleted'],
@classic
@classNames('job-diff')
@classNameBindings('isEdited:is-edited', 'isAdded:is-added', 'isDeleted:is-deleted')
export default class JobDiff extends Component {
diff = null;
diff: null,
verbose = true;
verbose: true,
isEdited: equal('diff.Type', 'Edited'),
isAdded: equal('diff.Type', 'Added'),
isDeleted: equal('diff.Type', 'Deleted'),
});
@equal('diff.Type', 'Edited') isEdited;
@equal('diff.Type', 'Added') isAdded;
@equal('diff.Type', 'Deleted') isDeleted;
}

View File

@@ -5,44 +5,48 @@ import { computed } from '@ember/object';
import { task } from 'ember-concurrency';
import messageFromAdapterError from 'nomad-ui/utils/message-from-adapter-error';
import localStorageProperty from 'nomad-ui/utils/properties/local-storage';
import classic from 'ember-classic-decorator';
export default Component.extend({
store: service(),
config: service(),
@classic
export default class JobEditor extends Component {
@service store;
@service config;
'data-test-job-editor': true,
'data-test-job-editor' = true;
job: null,
onSubmit() {},
context: computed({
get() {
return this._context;
},
set(key, value) {
const allowedValues = ['new', 'edit'];
job = null;
onSubmit() {}
assert(`context must be one of: ${allowedValues.join(', ')}`, allowedValues.includes(value));
@computed
get context() {
return this._context;
}
this.set('_context', value);
return value;
},
}),
set context(value) {
const allowedValues = ['new', 'edit'];
_context: null,
parseError: null,
planError: null,
runError: null,
assert(`context must be one of: ${allowedValues.join(', ')}`, allowedValues.includes(value));
planOutput: null,
this.set('_context', value);
return value;
}
showPlanMessage: localStorageProperty('nomadMessageJobPlan', true),
showEditorMessage: localStorageProperty('nomadMessageJobEditor', true),
_context = null;
parseError = null;
planError = null;
runError = null;
stage: computed('planOutput', function() {
planOutput = null;
@localStorageProperty('nomadMessageJobPlan', true) showPlanMessage;
@localStorageProperty('nomadMessageJobEditor', true) showEditorMessage;
@computed('planOutput')
get stage() {
return this.planOutput ? 'plan' : 'editor';
}),
}
plan: task(function*() {
@(task(function*() {
this.reset();
try {
@@ -62,9 +66,10 @@ export default Component.extend({
this.set('planError', error);
this.scrollToError();
}
}).drop(),
}).drop())
plan;
submit: task(function*() {
@task(function*() {
try {
if (this.context === 'new') {
yield this.job.run();
@@ -85,18 +90,19 @@ export default Component.extend({
this.set('planOutput', null);
this.scrollToError();
}
}),
})
submit;
reset() {
this.set('planOutput', null);
this.set('planError', null);
this.set('parseError', null);
this.set('runError', null);
},
}
scrollToError() {
if (!this.get('config.isTest')) {
window.scrollTo(0, 0);
}
},
});
}
}

View File

@@ -1,28 +1,32 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import classic from 'ember-classic-decorator';
export default Component.extend({
system: service(),
@classic
export default class Abstract extends Component {
@service system;
job: null,
job = null;
// Provide a value that is bound to a query param
sortProperty: null,
sortDescending: null,
sortProperty = null;
sortDescending = null;
// Provide actions that require routing
gotoTaskGroup() {},
gotoJob() {},
gotoTaskGroup() {}
gotoJob() {}
// Set to a { title, description } to surface an error
errorMessage: null,
errorMessage = null;
actions: {
clearErrorMessage() {
this.set('errorMessage', null);
},
handleError(errorObject) {
this.set('errorMessage', errorObject);
},
},
});
@action
clearErrorMessage() {
this.set('errorMessage', null);
}
@action
handleError(errorObject) {
this.set('errorMessage', errorObject);
}
}

View File

@@ -1,3 +1,5 @@
import AbstractJobPage from './abstract';
import classic from 'ember-classic-decorator';
export default AbstractJobPage.extend();
@classic
export default class Batch extends AbstractJobPage {}

View File

@@ -1,10 +1,14 @@
import { computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import PeriodicChildJobPage from './periodic-child';
import classic from 'ember-classic-decorator';
export default PeriodicChildJobPage.extend({
payload: alias('job.decodedPayload'),
payloadJSON: computed('payload', function() {
@classic
export default class ParameterizedChild extends PeriodicChildJobPage {
@alias('job.decodedPayload') payload;
@computed('payload')
get payloadJSON() {
let json;
try {
json = JSON.parse(this.payload);
@@ -12,5 +16,5 @@ export default PeriodicChildJobPage.extend({
// Swallow error and fall back to plain text rendering
}
return json;
}),
});
}
}

View File

@@ -1,3 +1,5 @@
import AbstractJobPage from './abstract';
import classic from 'ember-classic-decorator';
export default AbstractJobPage.extend();
@classic
export default class Parameterized extends AbstractJobPage {}

View File

@@ -2,30 +2,34 @@ import Component from '@ember/component';
import { computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import Sortable from 'nomad-ui/mixins/sortable';
import { classNames } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend(Sortable, {
job: null,
classNames: ['boxed-section'],
@classic
@classNames('boxed-section')
export default class Children extends Component.extend(Sortable) {
job = null;
// Provide a value that is bound to a query param
sortProperty: null,
sortDescending: null,
currentPage: null,
sortProperty = null;
sortDescending = null;
currentPage = null;
// Provide an action with access to the router
gotoJob() {},
gotoJob() {}
pageSize: 10,
pageSize = 10;
taskGroups: computed('job.taskGroups.[]', function() {
@computed('job.taskGroups.[]')
get taskGroups() {
return this.get('job.taskGroups') || [];
}),
}
children: computed('job.children.[]', function() {
@computed('job.children.[]')
get children() {
return this.get('job.children') || [];
}),
}
listToSort: alias('children'),
sortedChildren: alias('listSorted'),
});
@alias('children') listToSort;
@alias('listSorted') sortedChildren;
}

View File

@@ -1,8 +1,10 @@
import Component from '@ember/component';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: '',
errorMessage: null,
onDismiss() {},
});
@classic
@tagName('')
export default class Error extends Component {
errorMessage = null;
onDismiss() {}
}

View File

@@ -2,16 +2,19 @@ import Component from '@ember/component';
import { task } from 'ember-concurrency';
import { ForbiddenError } from '@ember-data/adapter/error';
import messageFromAdapterError from 'nomad-ui/utils/message-from-adapter-error';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
job: null,
tagName: '',
@classic
@tagName('')
export default class LatestDeployment extends Component {
job = null;
handleError() {},
handleError() {}
isShowingDeploymentDetails: false,
isShowingDeploymentDetails = false;
promote: task(function*() {
@task(function*() {
try {
yield this.get('job.latestDeployment.content').promote();
} catch (err) {
@@ -26,5 +29,6 @@ export default Component.extend({
description: message,
});
}
}),
});
})
promote;
}

View File

@@ -1,6 +1,9 @@
import Component from '@ember/component';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
job: null,
tagName: '',
});
@classic
@tagName('')
export default class PlacementFailures extends Component {
job = null;
}

View File

@@ -1,16 +1,20 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { action, computed } from '@ember/object';
import { inject as service } from '@ember/service';
import PromiseArray from 'nomad-ui/utils/classes/promise-array';
import { classNames } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
classNames: ['boxed-section'],
@classic
@classNames('boxed-section')
export default class RecentAllocations extends Component {
@service router;
router: service(),
sortProperty = 'modifyIndex';
sortDescending = true;
sortProperty: 'modifyIndex',
sortDescending: true,
sortedAllocations: computed('job.allocations.@each.modifyIndex', function() {
@computed('job.allocations.@each.modifyIndex')
get sortedAllocations() {
return PromiseArray.create({
promise: this.get('job.allocations').then(allocations =>
allocations
@@ -19,11 +23,10 @@ export default Component.extend({
.slice(0, 5)
),
});
}),
}
actions: {
gotoAllocation(allocation) {
this.router.transitionTo('allocations.allocation', allocation.id);
},
},
});
@action
gotoAllocation(allocation) {
this.router.transitionTo('allocations.allocation', allocation.id);
}
}

View File

@@ -1,17 +1,21 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { classNames } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
job: null,
classNames: ['boxed-section'],
@classic
@classNames('boxed-section')
export default class Summary extends Component {
job = null;
isExpanded: computed(function() {
@computed
get isExpanded() {
const storageValue = window.localStorage.nomadExpandJobSummary;
return storageValue != null ? JSON.parse(storageValue) : true;
}),
}
persist(item, isOpen) {
window.localStorage.nomadExpandJobSummary = isOpen;
this.notifyPropertyChange('isExpanded');
},
});
}
}

View File

@@ -2,23 +2,26 @@ import Component from '@ember/component';
import { computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import Sortable from 'nomad-ui/mixins/sortable';
import { classNames } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend(Sortable, {
job: null,
classNames: ['boxed-section'],
@classic
@classNames('boxed-section')
export default class TaskGroups extends Component.extend(Sortable) {
job = null;
// Provide a value that is bound to a query param
sortProperty: null,
sortDescending: null,
sortProperty = null;
sortDescending = null;
// Provide an action with access to the router
gotoTaskGroup() {},
gotoTaskGroup() {}
taskGroups: computed('job.taskGroups.[]', function() {
@computed('job.taskGroups.[]')
get taskGroups() {
return this.get('job.taskGroups') || [];
}),
}
listToSort: alias('taskGroups'),
sortedTaskGroups: alias('listSorted'),
});
@alias('taskGroups') listToSort;
@alias('listSorted') sortedTaskGroups;
}

View File

@@ -2,16 +2,18 @@ import Component from '@ember/component';
import { task } from 'ember-concurrency';
import { ForbiddenError } from '@ember-data/adapter/error';
import messageFromAdapterError from 'nomad-ui/utils/message-from-adapter-error';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: '',
@classic
@tagName('')
export default class Title extends Component {
job = null;
title = null;
job: null,
title: null,
handleError() {}
handleError() {},
stopJob: task(function*() {
@task(function*() {
try {
const job = this.job;
yield job.stop();
@@ -23,9 +25,10 @@ export default Component.extend({
description: 'Your ACL token does not grant permission to stop jobs.',
});
}
}),
})
stopJob;
startJob: task(function*() {
@task(function*() {
const job = this.job;
const definition = yield job.fetchRawDefinition();
@@ -49,5 +52,6 @@ export default Component.extend({
description: message,
});
}
}),
});
})
startJob;
}

View File

@@ -1,8 +1,11 @@
import AbstractJobPage from './abstract';
import { computed } from '@ember/object';
import classic from 'ember-classic-decorator';
export default AbstractJobPage.extend({
breadcrumbs: computed('job.{name,id}', 'job.parent.{name,id}', function() {
@classic
export default class PeriodicChild extends AbstractJobPage {
@computed('job.{name,id}', 'job.parent.{name,id}')
get breadcrumbs() {
const job = this.job;
const parent = this.get('job.parent');
@@ -17,5 +20,5 @@ export default AbstractJobPage.extend({
args: ['jobs.job', job],
},
];
}),
});
}
}

View File

@@ -1,24 +1,26 @@
import AbstractJobPage from './abstract';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import classic from 'ember-classic-decorator';
export default AbstractJobPage.extend({
store: service(),
@classic
export default class Periodic extends AbstractJobPage {
@service store;
errorMessage: null,
errorMessage = null;
actions: {
forceLaunch() {
this.job
.forcePeriodic()
.catch(() => {
this.set('errorMessage', {
title: 'Could Not Force Launch',
description: 'Your ACL token does not grant permission to submit jobs.',
});
});
},
clearErrorMessage() {
this.set('errorMessage', null);
},
},
});
@action
forceLaunch() {
this.job.forcePeriodic().catch(() => {
this.set('errorMessage', {
title: 'Could Not Force Launch',
description: 'Your ACL token does not grant permission to submit jobs.',
});
});
}
@action
clearErrorMessage() {
this.set('errorMessage', null);
}
}

View File

@@ -1,3 +1,5 @@
import AbstractJobPage from './abstract';
import classic from 'ember-classic-decorator';
export default AbstractJobPage.extend();
@classic
export default class Service extends AbstractJobPage {}

View File

@@ -1,3 +1,5 @@
import AbstractJobPage from './abstract';
import classic from 'ember-classic-decorator';
export default AbstractJobPage.extend();
@classic
export default class System extends AbstractJobPage {}

View File

@@ -1,18 +1,20 @@
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import { lazyClick } from '../helpers/lazy-click';
import { classNames, tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
store: service(),
@classic
@tagName('tr')
@classNames('job-row', 'is-interactive')
export default class JobRow extends Component {
@service store;
tagName: 'tr',
classNames: ['job-row', 'is-interactive'],
job = null;
job: null,
onClick() {},
onClick() {}
click(event) {
lazyClick([this.onClick, event]);
},
});
}
}

View File

@@ -1,18 +1,21 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { action, computed } from '@ember/object';
import { classNames } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
const changeTypes = ['Added', 'Deleted', 'Edited'];
export default Component.extend({
classNames: ['job-version', 'boxed-section'],
version: null,
isOpen: false,
@classic
@classNames('job-version', 'boxed-section')
export default class JobVersion extends Component {
version = null;
isOpen = false;
// Passes through to the job-diff component
verbose: true,
verbose = true;
changeCount: computed('version.diff', function() {
@computed('version.diff')
get changeCount() {
const diff = this.get('version.diff');
const taskGroups = diff.TaskGroups || [];
@@ -25,14 +28,13 @@ export default Component.extend({
taskGroups.reduce(arrayOfFieldChanges, 0) +
(taskGroups.mapBy('Tasks') || []).reduce(flatten, []).reduce(arrayOfFieldChanges, 0)
);
}),
}
actions: {
toggleDiff() {
this.toggleProperty('isOpen');
},
},
});
@action
toggleDiff() {
this.toggleProperty('isOpen');
}
}
const flatten = (accumulator, array) => accumulator.concat(array);
const countChanges = (total, field) => (changeTypes.includes(field.Type) ? total + 1 : total);

View File

@@ -2,20 +2,21 @@ import Component from '@ember/component';
import { computed } from '@ember/object';
import { computed as overridable } from 'ember-overridable-computed';
import moment from 'moment';
import { classNames, tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: 'ol',
classNames: ['timeline'],
versions: overridable(() => []),
@classic
@tagName('ol')
@classNames('timeline')
export default class JobVersionsStream extends Component {
@overridable(() => []) versions;
// Passes through to the job-diff component
verbose: true,
verbose = true;
annotatedVersions: computed('versions.[]', function() {
const versions = this.versions
.sortBy('submitTime')
.reverse();
@computed('versions.[]')
get annotatedVersions() {
const versions = this.versions.sortBy('submitTime').reverse();
return versions.map((version, index) => {
const meta = {};
@@ -32,5 +33,5 @@ export default Component.extend({
return { version, meta };
});
}),
});
}
}

View File

@@ -1,11 +1,15 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { classNames } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
classNames: ['json-viewer'],
@classic
@classNames('json-viewer')
export default class JsonViewer extends Component {
json = null;
json: null,
jsonStr: computed('json', function() {
@computed('json')
get jsonStr() {
return JSON.stringify(this.json, null, 2);
}),
});
}
}

View File

@@ -1,22 +1,26 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: '',
activeClass: computed('taskState.state', function() {
@classic
@tagName('')
export default class LifecycleChartRow extends Component {
@computed('taskState.state')
get activeClass() {
if (this.taskState && this.taskState.state === 'running') {
return 'is-active';
}
return;
}),
}
finishedClass: computed('taskState.finishedAt', function() {
@computed('taskState.finishedAt')
get finishedClass() {
if (this.taskState && this.taskState.finishedAt) {
return 'is-finished';
}
return;
}),
});
}
}

View File

@@ -1,14 +1,17 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { sort } from '@ember/object/computed';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: '',
@classic
@tagName('')
export default class LifecycleChart extends Component {
tasks = null;
taskStates = null;
tasks: null,
taskStates: null,
lifecyclePhases: computed('tasks.@each.lifecycle', 'taskStates.@each.state', function() {
@computed('tasks.@each.lifecycle', 'taskStates.@each.state')
get lifecyclePhases() {
const tasksOrStates = this.taskStates || this.tasks;
const lifecycles = {
prestarts: [],
@@ -38,16 +41,18 @@ export default Component.extend({
}
return phases;
}),
}
sortedLifecycleTaskStates: sort('taskStates', function(a, b) {
@sort('taskStates', function(a, b) {
return getTaskSortPrefix(a.task).localeCompare(getTaskSortPrefix(b.task));
}),
})
sortedLifecycleTaskStates;
sortedLifecycleTasks: sort('tasks', function(a, b) {
@sort('tasks', function(a, b) {
return getTaskSortPrefix(a).localeCompare(getTaskSortPrefix(b));
}),
});
})
sortedLifecycleTasks;
}
const lifecycleNameSortPrefix = {
prestart: 0,

View File

@@ -1,17 +1,20 @@
import Component from '@ember/component';
import { computed, get } from '@ember/object';
import { computed as overridable } from 'ember-overridable-computed';
import { classNames } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
classNames: ['accordion'],
@classic
@classNames('accordion')
export default class ListAccordion extends Component {
key = 'id';
@overridable(() => []) source;
key: 'id',
source: overridable(() => []),
onToggle /* item, isOpen */() {}
startExpanded = false;
onToggle(/* item, isOpen */) {},
startExpanded: false,
decoratedSource: computed('source.[]', function() {
@computed('source.[]')
get decoratedSource() {
const stateCache = this.stateCache;
const key = this.key;
const deepKey = `item.${key}`;
@@ -28,9 +31,9 @@ export default Component.extend({
// eslint-disable-next-line ember/no-side-effects
this.set('stateCache', decoratedSource);
return decoratedSource;
}),
}
// When source updates come in, the state cache is used to preserve
// open/close state.
stateCache: overridable(() => []),
});
@overridable(() => []) stateCache;
}

View File

@@ -1,6 +1,9 @@
import Component from '@ember/component';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: '',
isOpen: false,
});
@classic
@tagName('')
export default class AccordionBody extends Component {
isOpen = false;
}

View File

@@ -1,16 +1,18 @@
import Component from '@ember/component';
import { classNames, classNameBindings } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
classNames: ['accordion-head'],
classNameBindings: ['isOpen::is-light', 'isExpandable::is-inactive'],
@classic
@classNames('accordion-head')
@classNameBindings('isOpen::is-light', 'isExpandable::is-inactive')
export default class AccordionHead extends Component {
'data-test-accordion-head' = true;
'data-test-accordion-head': true,
buttonLabel = 'toggle';
isOpen = false;
isExpandable = true;
item = null;
buttonLabel: 'toggle',
isOpen: false,
isExpandable: true,
item: null,
onClose() {},
onOpen() {},
});
onClose() {}
onOpen() {}
}

View File

@@ -1,26 +1,32 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { computed as overridable } from 'ember-overridable-computed';
import classic from 'ember-classic-decorator';
export default Component.extend({
source: overridable(() => []),
size: 25,
page: 1,
spread: 2,
@classic
export default class ListPagination extends Component {
@overridable(() => []) source;
size = 25;
page = 1;
spread = 2;
startsAt: computed('size', 'page', function() {
@computed('size', 'page')
get startsAt() {
return (this.page - 1) * this.size + 1;
}),
}
endsAt: computed('source.[]', 'size', 'page', function() {
@computed('source.[]', 'size', 'page')
get endsAt() {
return Math.min(this.page * this.size, this.get('source.length'));
}),
}
lastPage: computed('source.[]', 'size', function() {
@computed('source.[]', 'size')
get lastPage() {
return Math.ceil(this.get('source.length') / this.size);
}),
}
pageLinks: computed('source.[]', 'page', 'spread', function() {
@computed('source.[]', 'page', 'spread')
get pageLinks() {
const { spread, page, lastPage } = this;
// When there is only one page, don't bother with page links
@@ -36,11 +42,12 @@ export default Component.extend({
.map((_, index) => ({
pageNumber: lowerBound + index,
}));
}),
}
list: computed('source.[]', 'page', 'size', function() {
@computed('source.[]', 'page', 'size')
get list() {
const size = this.size;
const start = (this.page - 1) * size;
return this.source.slice(start, start + size);
}),
});
}
}

View File

@@ -1,5 +1,7 @@
import Component from '@ember/component';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: '',
});
@classic
@tagName('')
export default class ListPager extends Component {}

View File

@@ -1,17 +1,20 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { computed as overridable } from 'ember-overridable-computed';
import { classNames, tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: 'table',
classNames: ['table'],
source: overridable(() => []),
@classic
@tagName('table')
@classNames('table')
export default class ListTable extends Component {
@overridable(() => []) source;
// Plan for a future with metadata (e.g., isSelected)
decoratedSource: computed('source.[]', function() {
@computed('source.[]')
get decoratedSource() {
return this.source.map(row => ({
model: row,
}));
}),
});
}
}

View File

@@ -1,25 +1,32 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import {
classNames,
attributeBindings,
classNameBindings,
tagName,
} from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: 'th',
attributeBindings: ['title'],
@classic
@tagName('th')
@attributeBindings('title')
@classNames('is-selectable')
@classNameBindings('isActive:is-active', 'sortDescending:desc:asc')
export default class SortBy extends Component {
// The prop that the table is currently sorted by
currentProp: '',
currentProp = '';
// The prop this sorter controls
prop: '',
prop = '';
classNames: ['is-selectable'],
classNameBindings: ['isActive:is-active', 'sortDescending:desc:asc'],
isActive: computed('currentProp', 'prop', function() {
@computed('currentProp', 'prop')
get isActive() {
return this.currentProp === this.prop;
}),
}
shouldSortDescending: computed('sortDescending', 'isActive', function() {
@computed('sortDescending', 'isActive')
get shouldSortDescending() {
return !this.isActive || !this.sortDescending;
}),
});
}
}

View File

@@ -1,5 +1,7 @@
import Component from '@ember/component';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: 'tbody',
});
@classic
@tagName('tbody')
export default class TableBody extends Component {}

View File

@@ -1,5 +1,7 @@
import Component from '@ember/component';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: 'thead',
});
@classic
@tagName('thead')
export default class TableHead extends Component {}

View File

@@ -1,6 +1,9 @@
import Component from '@ember/component';
import { action } from '@ember/object';
import { computed as overridable } from 'ember-overridable-computed';
import { run } from '@ember/runloop';
import { classNames } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
const TAB = 9;
const ESC = 27;
@@ -8,93 +11,94 @@ const SPACE = 32;
const ARROW_UP = 38;
const ARROW_DOWN = 40;
export default Component.extend({
classNames: ['dropdown'],
@classic
@classNames('dropdown')
export default class MultiSelectDropdown extends Component {
@overridable(() => []) options;
@overridable(() => []) selection;
options: overridable(() => []),
selection: overridable(() => []),
onSelect() {}
onSelect() {},
isOpen: false,
dropdown: null,
isOpen = false;
dropdown = null;
capture(dropdown) {
// It's not a good idea to grab a dropdown reference like this, but it's necessary
// in order to invoke dropdown.actions.close in traverseList as well as
// dropdown.actions.reposition when the label or selection length changes.
this.set('dropdown', dropdown);
},
}
didReceiveAttrs() {
const dropdown = this.dropdown;
if (this.isOpen && dropdown) {
run.scheduleOnce('afterRender', this, this.repositionDropdown);
}
},
}
repositionDropdown() {
this.dropdown.actions.reposition();
},
}
actions: {
toggle({ key }) {
const newSelection = this.selection.slice();
if (newSelection.includes(key)) {
newSelection.removeObject(key);
} else {
newSelection.addObject(key);
}
this.onSelect(newSelection);
},
@action
toggle({ key }) {
const newSelection = this.selection.slice();
if (newSelection.includes(key)) {
newSelection.removeObject(key);
} else {
newSelection.addObject(key);
}
this.onSelect(newSelection);
}
openOnArrowDown(dropdown, e) {
this.capture(dropdown);
@action
openOnArrowDown(dropdown, e) {
this.capture(dropdown);
if (!this.isOpen && e.keyCode === ARROW_DOWN) {
dropdown.actions.open(e);
e.preventDefault();
} else if (this.isOpen && (e.keyCode === TAB || e.keyCode === ARROW_DOWN)) {
const optionsId = this.element.querySelector('.dropdown-trigger').getAttribute('aria-owns');
const firstElement = document.querySelector(`#${optionsId} .dropdown-option`);
if (!this.isOpen && e.keyCode === ARROW_DOWN) {
dropdown.actions.open(e);
e.preventDefault();
} else if (this.isOpen && (e.keyCode === TAB || e.keyCode === ARROW_DOWN)) {
const optionsId = this.element.querySelector('.dropdown-trigger').getAttribute('aria-owns');
const firstElement = document.querySelector(`#${optionsId} .dropdown-option`);
if (firstElement) {
firstElement.focus();
e.preventDefault();
}
}
},
traverseList(option, e) {
if (e.keyCode === ESC) {
// Close the dropdown
const dropdown = this.dropdown;
if (dropdown) {
dropdown.actions.close(e);
// Return focus to the trigger so tab works as expected
const trigger = this.element.querySelector('.dropdown-trigger');
if (trigger) trigger.focus();
e.preventDefault();
this.set('dropdown', null);
}
} else if (e.keyCode === ARROW_UP) {
// previous item
const prev = e.target.previousElementSibling;
if (prev) {
prev.focus();
e.preventDefault();
}
} else if (e.keyCode === ARROW_DOWN) {
// next item
const next = e.target.nextElementSibling;
if (next) {
next.focus();
e.preventDefault();
}
} else if (e.keyCode === SPACE) {
this.send('toggle', option);
if (firstElement) {
firstElement.focus();
e.preventDefault();
}
},
},
});
}
}
@action
traverseList(option, e) {
if (e.keyCode === ESC) {
// Close the dropdown
const dropdown = this.dropdown;
if (dropdown) {
dropdown.actions.close(e);
// Return focus to the trigger so tab works as expected
const trigger = this.element.querySelector('.dropdown-trigger');
if (trigger) trigger.focus();
e.preventDefault();
this.set('dropdown', null);
}
} else if (e.keyCode === ARROW_UP) {
// previous item
const prev = e.target.previousElementSibling;
if (prev) {
prev.focus();
e.preventDefault();
}
} else if (e.keyCode === ARROW_DOWN) {
// next item
const next = e.target.nextElementSibling;
if (next) {
next.focus();
e.preventDefault();
}
} else if (e.keyCode === SPACE) {
this.send('toggle', option);
e.preventDefault();
}
}
}

View File

@@ -1,7 +1,9 @@
import Component from '@ember/component';
import { classNames } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
classNames: ['page-layout'],
isGutterOpen: false,
});
@classic
@classNames('page-layout')
export default class PageLayout extends Component {
isGutterOpen = false;
}

View File

@@ -1,10 +1,12 @@
import Component from '@ember/component';
import { or } from '@ember/object/computed';
import classic from 'ember-classic-decorator';
export default Component.extend({
@classic
export default class PlacementFailure extends Component {
// Either provide a taskGroup or a failedTGAlloc
taskGroup: null,
failedTGAlloc: null,
taskGroup = null;
failedTGAlloc = null;
placementFailures: or('taskGroup.placementFailures', 'failedTGAlloc'),
});
@or('taskGroup.placementFailures', 'failedTGAlloc') placementFailures;
}

View File

@@ -1,14 +1,16 @@
import AllocationRow from 'nomad-ui/components/allocation-row';
import classic from 'ember-classic-decorator';
export default AllocationRow.extend({
pluginAllocation: null,
allocation: null,
@classic
export default class PluginAllocationRow extends AllocationRow {
pluginAllocation = null;
allocation = null;
didReceiveAttrs() {
// Allocation is always set through pluginAllocation
this.set('allocation', null);
this.setAllocation();
},
}
// The allocation for the plugin's controller or storage plugin needs
// to be imperatively fetched since these plugins are Fragments which
@@ -21,5 +23,5 @@ export default AllocationRow.extend({
this.updateStatsTracker();
}
}
},
});
}
}

View File

@@ -1,5 +1,8 @@
import Component from '@ember/component';
import { action } from '@ember/object';
import { run } from '@ember/runloop';
import { classNames } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
const TAB = 9;
const ARROW_DOWN = 40;
@@ -11,49 +14,48 @@ const FOCUSABLE = [
'[tabindex]:not([disabled]):not([tabindex="-1"])',
].join(', ');
export default Component.extend({
classNames: ['popover'],
@classic
@classNames('popover')
export default class PopoverMenu extends Component {
triggerClass = '';
isOpen = false;
isDisabled = false;
label = '';
triggerClass: '',
isOpen: false,
isDisabled: false,
label: '',
dropdown: null,
dropdown = null;
capture(dropdown) {
// It's not a good idea to grab a dropdown reference like this, but it's necessary
// in order to invoke dropdown.actions.close in traverseList as well as
// dropdown.actions.reposition when the label or selection length changes.
this.set('dropdown', dropdown);
},
}
didReceiveAttrs() {
const dropdown = this.dropdown;
if (this.isOpen && dropdown) {
run.scheduleOnce('afterRender', this, this.repositionDropdown);
}
},
}
repositionDropdown() {
this.dropdown.actions.reposition();
},
}
actions: {
openOnArrowDown(dropdown, e) {
if (!this.isOpen && e.keyCode === ARROW_DOWN) {
dropdown.actions.open(e);
@action
openOnArrowDown(dropdown, e) {
if (!this.isOpen && e.keyCode === ARROW_DOWN) {
dropdown.actions.open(e);
e.preventDefault();
} else if (this.isOpen && (e.keyCode === TAB || e.keyCode === ARROW_DOWN)) {
const optionsId = this.element.querySelector('.popover-trigger').getAttribute('aria-owns');
const popoverContentEl = document.querySelector(`#${optionsId}`);
const firstFocusableElement = popoverContentEl.querySelector(FOCUSABLE);
if (firstFocusableElement) {
firstFocusableElement.focus();
e.preventDefault();
} else if (this.isOpen && (e.keyCode === TAB || e.keyCode === ARROW_DOWN)) {
const optionsId = this.element.querySelector('.popover-trigger').getAttribute('aria-owns');
const popoverContentEl = document.querySelector(`#${optionsId}`);
const firstFocusableElement = popoverContentEl.querySelector(FOCUSABLE);
if (firstFocusableElement) {
firstFocusableElement.focus();
e.preventDefault();
}
}
},
},
});
}
}
}

View File

@@ -3,49 +3,54 @@ import Component from '@ember/component';
import { inject as service } from '@ember/service';
import { computed } from '@ember/object';
import { task, timeout } from 'ember-concurrency';
import { classNames } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
token: service(),
statsTrackersRegistry: service('stats-trackers-registry'),
classNames: ['primary-metric'],
@classic
@classNames('primary-metric')
export default class PrimaryMetric extends Component {
@service token;
@service('stats-trackers-registry') statsTrackersRegistry;
// One of Node, Allocation, or TaskState
resource: null,
resource = null;
// cpu or memory
metric: null,
metric = null;
'data-test-primary-metric': true,
'data-test-primary-metric' = true;
// An instance of a StatsTracker. An alternative interface to resource
tracker: computed('trackedResource', 'type', function() {
@computed('trackedResource', 'type')
get tracker() {
const resource = this.trackedResource;
return this.statsTrackersRegistry.getTracker(resource);
}),
}
type: computed('resource', function() {
@computed('resource')
get type() {
const resource = this.resource;
return resource && resource.constructor.modelName;
}),
}
trackedResource: computed('resource', 'type', function() {
@computed('resource', 'type')
get trackedResource() {
// TaskStates use the allocation stats tracker
return this.type === 'task-state'
? this.get('resource.allocation')
: this.resource;
}),
return this.type === 'task-state' ? this.get('resource.allocation') : this.resource;
}
metricLabel: computed('metric', function() {
@computed('metric')
get metricLabel() {
const metric = this.metric;
const mappings = {
cpu: 'CPU',
memory: 'Memory',
};
return mappings[metric] || metric;
}),
}
data: computed('resource', 'metric', 'type', function() {
@computed('resource', 'metric', 'type')
get data() {
if (!this.tracker) return [];
const metric = this.metric;
@@ -56,9 +61,10 @@ export default Component.extend({
}
return this.get(`tracker.${metric}`);
}),
}
reservedAmount: computed('resource', 'metric', 'type', function() {
@computed('resource', 'metric', 'type')
get reservedAmount() {
const metricProperty = this.metric === 'cpu' ? 'reservedCPU' : 'reservedMemory';
if (this.type === 'task-state') {
@@ -67,9 +73,10 @@ export default Component.extend({
}
return this.get(`tracker.${metricProperty}`);
}),
}
chartClass: computed('metric', function() {
@computed('metric')
get chartClass() {
const metric = this.metric;
const mappings = {
cpu: 'is-info',
@@ -77,23 +84,24 @@ export default Component.extend({
};
return mappings[metric] || 'is-primary';
}),
}
poller: task(function*() {
@task(function*() {
do {
this.get('tracker.poll').perform();
yield timeout(100);
} while (!Ember.testing);
}),
})
poller;
didReceiveAttrs() {
if (this.tracker) {
this.poller.perform();
}
},
}
willDestroy() {
this.poller.cancelAll();
this.get('tracker.signalPause').perform();
},
});
}
}

View File

@@ -1,5 +1,7 @@
import Component from '@ember/component';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: '',
});
@classic
@tagName('')
export default class ProxyTag extends Component {}

View File

@@ -1,21 +1,24 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { inject as service } from '@ember/service';
import classic from 'ember-classic-decorator';
export default Component.extend({
system: service(),
router: service(),
store: service(),
@classic
export default class RegionSwitcher extends Component {
@service system;
@service router;
@service store;
sortedRegions: computed('system.regions', function() {
@computed('system.regions')
get sortedRegions() {
return this.get('system.regions')
.toArray()
.sort();
}),
}
gotoRegion(region) {
this.router.transitionTo('jobs', {
queryParams: { region },
});
},
});
}
}

View File

@@ -1,20 +1,24 @@
import Component from '@ember/component';
import { computed as overridable } from 'ember-overridable-computed';
import { inject as service } from '@ember/service';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
store: service(),
tagName: '',
@classic
@tagName('')
export default class RescheduleEventRow extends Component {
@service store;
// When given a string, the component will fetch the allocation
allocationId: null,
allocationId = null;
// An allocation can also be provided directly
allocation: overridable('allocationId', function() {
@overridable('allocationId', function() {
return this.store.findRecord('allocation', this.allocationId);
}),
})
allocation;
time: null,
linkToAllocation: true,
label: '',
});
time = null;
linkToAllocation = true;
label = '';
}

View File

@@ -1,34 +1,37 @@
import { reads } from '@ember/object/computed';
import Component from '@ember/component';
import { action } from '@ember/object';
import { run } from '@ember/runloop';
import { classNames } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
@classic
@classNames('search-box', 'field', 'has-addons')
export default class SearchBox extends Component {
// Passed to the component (mutable)
searchTerm: null,
searchTerm = null;
// Used as a debounce buffer
_searchTerm: reads('searchTerm'),
@reads('searchTerm') _searchTerm;
// Used to throttle sets to searchTerm
debounce: 150,
debounce = 150;
// A hook that's called when the search value changes
onChange() {},
onChange() {}
classNames: ['search-box', 'field', 'has-addons'],
@action
setSearchTerm(e) {
this.set('_searchTerm', e.target.value);
run.debounce(this, updateSearch, this.debounce);
}
actions: {
setSearchTerm(e) {
this.set('_searchTerm', e.target.value);
run.debounce(this, updateSearch, this.debounce);
},
clear() {
this.set('_searchTerm', '');
run.debounce(this, updateSearch, this.debounce);
},
},
});
@action
clear() {
this.set('_searchTerm', '');
run.debounce(this, updateSearch, this.debounce);
}
}
function updateSearch() {
const newTerm = this._searchTerm;

View File

@@ -3,20 +3,24 @@ import { alias } from '@ember/object/computed';
import Component from '@ember/component';
import { computed } from '@ember/object';
import { lazyClick } from '../helpers/lazy-click';
import { classNames, classNameBindings, tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
@classic
@tagName('tr')
@classNames('server-agent-row', 'is-interactive')
@classNameBindings('isActive:is-active')
export default class ServerAgentRow extends Component {
// TODO Switch back to the router service once the service behaves more like Route
// https://github.com/emberjs/ember.js/issues/15801
// router: inject.service('router'),
_router: service('-routing'),
router: alias('_router.router'),
@service('-routing') _router;
@alias('_router.router') router;
tagName: 'tr',
classNames: ['server-agent-row', 'is-interactive'],
classNameBindings: ['isActive:is-active'],
agent = null;
agent: null,
isActive: computed('agent', 'router.currentURL', function() {
@computed('agent', 'router.currentURL')
get isActive() {
// TODO Switch back to the router service once the service behaves more like Route
// https://github.com/emberjs/ember.js/issues/15801
// const targetURL = this.get('router').urlFor('servers.server', this.get('agent'));
@@ -30,10 +34,10 @@ export default Component.extend({
// Account for potential URI encoding
return currentURL.replace(/%40/g, '@') === targetURL.replace(/%40/g, '@');
}),
}
click() {
const transition = () => this.router.transitionTo('servers.server', this.agent);
lazyClick([transition, event]);
},
});
}
}

View File

@@ -6,24 +6,27 @@ import d3Scale from 'd3-scale';
import d3Array from 'd3-array';
import LineChart from 'nomad-ui/components/line-chart';
import formatDuration from 'nomad-ui/utils/format-duration';
import classic from 'ember-classic-decorator';
export default LineChart.extend({
xProp: 'timestamp',
yProp: 'percent',
timeseries: true,
@classic
export default class StatsTimeSeries extends LineChart {
xProp = 'timestamp';
yProp = 'percent';
timeseries = true;
xFormat() {
return d3TimeFormat.timeFormat('%H:%M:%S');
},
}
yFormat() {
return d3Format.format('.1~%');
},
}
// Specific a11y descriptors
title: 'Stats Time Series Chart',
title = 'Stats Time Series Chart';
description: computed('data.[]', 'xProp', 'yProp', function() {
@computed('data.[]', 'xProp', 'yProp')
get description() {
const { xProp, yProp, data } = this;
const yRange = d3Array.extent(data, d => d[yProp]);
const xRange = d3Array.extent(data, d => d[xProp]);
@@ -31,10 +34,13 @@ export default LineChart.extend({
const duration = formatDuration(xRange[1] - xRange[0], 'ms', true);
return `Time series data for the last ${duration}, with values ranging from ${yFormatter(yRange[0])} to ${yFormatter(yRange[1])}`;
}),
return `Time series data for the last ${duration}, with values ranging from ${yFormatter(
yRange[0]
)} to ${yFormatter(yRange[1])}`;
}
xScale: computed('data.[]', 'xProp', 'timeseries', 'yAxisOffset', function() {
@computed('data.[]', 'xProp', 'timeseries', 'yAxisOffset')
get xScale() {
const xProp = this.xProp;
const scale = this.timeseries ? d3Scale.scaleTime() : d3Scale.scaleLinear();
const data = this.data;
@@ -48,9 +54,10 @@ export default LineChart.extend({
scale.rangeRound([10, this.yAxisOffset]).domain(extent);
return scale;
}),
}
yScale: computed('data.[]', 'yProp', 'xAxisOffset', function() {
@computed('data.[]', 'yProp', 'xAxisOffset')
get yScale() {
const yProp = this.yProp;
const yValues = (this.data || []).mapBy(yProp);
@@ -63,5 +70,5 @@ export default LineChart.extend({
.scaleLinear()
.rangeRound([this.xAxisOffset, 10])
.domain([Math.min(0, low), Math.max(1, high)]);
}),
});
}
}

View File

@@ -2,15 +2,18 @@ import Component from '@ember/component';
import { run } from '@ember/runloop';
import { task } from 'ember-concurrency';
import WindowResizable from 'nomad-ui/mixins/window-resizable';
import { classNames, tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend(WindowResizable, {
tagName: 'pre',
classNames: ['cli-window'],
'data-test-log-cli': true,
@classic
@tagName('pre')
@classNames('cli-window')
export default class StreamingFile extends Component.extend(WindowResizable) {
'data-test-log-cli' = true;
mode: 'streaming', // head, tail, streaming
isStreaming: true,
logger: null,
mode = 'streaming'; // head, tail, streaming
isStreaming = true;
logger = null;
didReceiveAttrs() {
if (!this.logger) {
@@ -18,7 +21,7 @@ export default Component.extend(WindowResizable, {
}
run.scheduleOnce('actions', this, this.performTask);
},
}
performTask() {
switch (this.mode) {
@@ -36,15 +39,15 @@ export default Component.extend(WindowResizable, {
}
break;
}
},
}
didInsertElement() {
this.fillAvailableHeight();
},
}
windowResizeHandler() {
run.once(this, this.fillAvailableHeight);
},
}
fillAvailableHeight() {
// This math is arbitrary and far from bulletproof, but the UX
@@ -52,21 +55,23 @@ export default Component.extend(WindowResizable, {
const margins = 30; // Account for padding and margin on either side of the CLI
const cliWindow = this.element;
cliWindow.style.height = `${window.innerHeight - cliWindow.offsetTop - margins}px`;
},
}
head: task(function*() {
@task(function*() {
yield this.get('logger.gotoHead').perform();
run.scheduleOnce('afterRender', this, this.scrollToTop);
}),
})
head;
scrollToTop() {
this.element.scrollTop = 0;
},
}
tail: task(function*() {
@task(function*() {
yield this.get('logger.gotoTail').perform();
run.scheduleOnce('afterRender', this, this.synchronizeScrollPosition, [true]);
}),
})
tail;
synchronizeScrollPosition(force = false) {
const cliWindow = this.element;
@@ -74,9 +79,9 @@ export default Component.extend(WindowResizable, {
// If the window is approximately scrolled to the bottom, follow the log
cliWindow.scrollTop = cliWindow.scrollHeight;
}
},
}
stream: task(function*() {
@task(function*() {
// Force the scroll position to the bottom of the window when starting streaming
this.logger.one('tick', () => {
run.scheduleOnce('afterRender', this, this.synchronizeScrollPosition, [true]);
@@ -87,13 +92,14 @@ export default Component.extend(WindowResizable, {
yield this.logger.startStreaming();
this.logger.off('tick', this, 'scheduleScrollSynchronization');
}),
})
stream;
scheduleScrollSynchronization() {
run.scheduleOnce('afterRender', this, this.synchronizeScrollPosition);
},
}
willDestroy() {
this.logger.stop();
},
});
}
}

View File

@@ -1,16 +1,17 @@
import Component from '@ember/component';
import { lazyClick } from '../helpers/lazy-click';
import { classNames, tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: 'tr',
@classic
@tagName('tr')
@classNames('task-group-row', 'is-interactive')
export default class TaskGroupRow extends Component {
taskGroup = null;
classNames: ['task-group-row', 'is-interactive'],
taskGroup: null,
onClick() {},
onClick() {}
click(event) {
lazyClick([this.onClick, event]);
},
});
}
}

View File

@@ -1,49 +1,53 @@
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import { computed } from '@ember/object';
import { action, computed } from '@ember/object';
import RSVP from 'rsvp';
import { logger } from 'nomad-ui/utils/classes/log';
import timeout from 'nomad-ui/utils/timeout';
import { AbortController } from 'fetch';
import { classNames } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
token: service(),
@classic
@classNames('boxed-section', 'task-log')
export default class TaskLog extends Component {
@service token;
classNames: ['boxed-section', 'task-log'],
allocation: null,
task: null,
allocation = null;
task = null;
// When true, request logs from the server agent
useServer: false,
useServer = false;
// When true, logs cannot be fetched from either the client or the server
noConnection: false,
noConnection = false;
clientTimeout: 1000,
serverTimeout: 5000,
clientTimeout = 1000;
serverTimeout = 5000;
isStreaming: true,
streamMode: 'streaming',
isStreaming = true;
streamMode = 'streaming';
mode: 'stdout',
mode = 'stdout';
logUrl: computed('allocation.{id,node.httpAddr}', 'useServer', function() {
@computed('allocation.{id,node.httpAddr}', 'useServer')
get logUrl() {
const address = this.get('allocation.node.httpAddr');
const allocation = this.get('allocation.id');
const url = `/v1/client/fs/logs/${allocation}`;
return this.useServer ? url : `//${address}${url}`;
}),
}
logParams: computed('task', 'mode', function() {
@computed('task', 'mode')
get logParams() {
return {
task: this.task,
type: this.mode,
};
}),
}
logger: logger('logUrl', 'logParams', function logFetch() {
@logger('logUrl', 'logParams', function logFetch() {
// If the log request can't settle in one second, the client
// must be unavailable and the server should be used instead
@@ -71,28 +75,36 @@ export default Component.extend({
throw error;
}
);
}),
})
logger;
actions: {
setMode(mode) {
if (this.mode === mode) return;
this.logger.stop();
this.set('mode', mode);
},
toggleStream() {
this.set('streamMode', 'streaming');
this.toggleProperty('isStreaming');
},
gotoHead() {
this.set('streamMode', 'head');
this.set('isStreaming', false);
},
gotoTail() {
this.set('streamMode', 'tail');
this.set('isStreaming', false);
},
failoverToServer() {
this.set('useServer', true);
},
},
});
@action
setMode(mode) {
if (this.mode === mode) return;
this.logger.stop();
this.set('mode', mode);
}
@action
toggleStream() {
this.set('streamMode', 'streaming');
this.toggleProperty('isStreaming');
}
@action
gotoHead() {
this.set('streamMode', 'head');
this.set('isStreaming', false);
}
@action
gotoTail() {
this.set('streamMode', 'tail');
this.set('isStreaming', false);
}
@action
failoverToServer() {
this.set('useServer', true);
}
}

View File

@@ -5,47 +5,52 @@ import { computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import { task, timeout } from 'ember-concurrency';
import { lazyClick } from '../helpers/lazy-click';
import { classNames, tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
store: service(),
token: service(),
statsTrackersRegistry: service('stats-trackers-registry'),
@classic
@tagName('tr')
@classNames('task-row', 'is-interactive')
export default class TaskRow extends Component {
@service store;
@service token;
@service('stats-trackers-registry') statsTrackersRegistry;
tagName: 'tr',
classNames: ['task-row', 'is-interactive'],
task: null,
task = null;
// Internal state
statsError: false,
statsError = false;
enablePolling: computed(function() {
@computed
get enablePolling() {
return !Ember.testing;
}),
}
// Since all tasks for an allocation share the same tracker, use the registry
stats: computed('task', 'task.isRunning', function() {
@computed('task', 'task.isRunning')
get stats() {
if (!this.get('task.isRunning')) return;
return this.statsTrackersRegistry.getTracker(this.get('task.allocation'));
}),
}
taskStats: computed('task.name', 'stats.tasks.[]', function() {
@computed('task.name', 'stats.tasks.[]')
get taskStats() {
if (!this.stats) return;
return this.get('stats.tasks').findBy('task', this.get('task.name'));
}),
}
cpu: alias('taskStats.cpu.lastObject'),
memory: alias('taskStats.memory.lastObject'),
@alias('taskStats.cpu.lastObject') cpu;
@alias('taskStats.memory.lastObject') memory;
onClick() {},
onClick() {}
click(event) {
lazyClick([this.onClick, event]);
},
}
fetchStats: task(function*() {
@(task(function*() {
do {
if (this.stats) {
try {
@@ -58,7 +63,8 @@ export default Component.extend({
yield timeout(500);
} while (this.enablePolling);
}).drop(),
}).drop())
fetchStats;
didReceiveAttrs() {
const allocation = this.get('task.allocation');
@@ -68,5 +74,5 @@ export default Component.extend({
} else {
this.fetchStats.cancelAll();
}
},
});
}
}

View File

@@ -1,14 +1,20 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
import { equal, or } from '@ember/object/computed';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
router: service(),
@classic
@tagName('')
export default class TaskSubnav extends Component {
@service router;
tagName: '',
@equal('router.currentRouteName', 'allocations.allocation.task.fs')
fsIsActive;
fsIsActive: equal('router.currentRouteName', 'allocations.allocation.task.fs'),
fsRootIsActive: equal('router.currentRouteName', 'allocations.allocation.task.fs-root'),
@equal('router.currentRouteName', 'allocations.allocation.task.fs-root')
fsRootIsActive;
filesLinkActive: or('fsIsActive', 'fsRootIsActive'),
});
@or('fsIsActive', 'fsRootIsActive')
filesLinkActive;
}

View File

@@ -1,13 +1,15 @@
import Component from '@ember/component';
import { classNames, classNameBindings, tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: 'label',
classNames: ['toggle'],
classNameBindings: ['isDisabled:is-disabled', 'isActive:is-active'],
@classic
@tagName('label')
@classNames('toggle')
@classNameBindings('isDisabled:is-disabled', 'isActive:is-active')
export default class Toggle extends Component {
'data-test-label' = true;
'data-test-label': true,
isActive: false,
isDisabled: false,
onToggle() {},
});
isActive = false;
isDisabled = false;
onToggle() {}
}

View File

@@ -1,51 +1,58 @@
import Component from '@ember/component';
import { action } from '@ember/object';
import { next } from '@ember/runloop';
import { equal } from '@ember/object/computed';
import { task, waitForEvent } from 'ember-concurrency';
import RSVP from 'rsvp';
import { classNames } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
classNames: ['two-step-button'],
@classic
@classNames('two-step-button')
export default class TwoStepButton extends Component {
idleText = '';
cancelText = '';
confirmText = '';
confirmationMessage = '';
awaitingConfirmation = false;
disabled = false;
alignRight = false;
isInfoAction = false;
onConfirm() {}
onCancel() {}
idleText: '',
cancelText: '',
confirmText: '',
confirmationMessage: '',
awaitingConfirmation: false,
disabled: false,
alignRight: false,
isInfoAction: false,
onConfirm() {},
onCancel() {},
state = 'idle';
@equal('state', 'idle') isIdle;
@equal('state', 'prompt') isPendingConfirmation;
state: 'idle',
isIdle: equal('state', 'idle'),
isPendingConfirmation: equal('state', 'prompt'),
cancelOnClickOutside: task(function*() {
@task(function*() {
while (true) {
let ev = yield waitForEvent(document.body, 'click');
if (!this.element.contains(ev.target) && !this.awaitingConfirmation) {
this.send('setToIdle');
}
}
}),
})
cancelOnClickOutside;
actions: {
setToIdle() {
this.set('state', 'idle');
this.cancelOnClickOutside.cancelAll();
},
promptForConfirmation() {
this.set('state', 'prompt');
next(() => {
this.cancelOnClickOutside.perform();
});
},
confirm() {
RSVP.resolve(this.onConfirm()).then(() => {
this.send('setToIdle');
});
},
},
});
@action
setToIdle() {
this.set('state', 'idle');
this.cancelOnClickOutside.cancelAll();
}
@action
promptForConfirmation() {
this.set('state', 'prompt');
next(() => {
this.cancelOnClickOutside.perform();
});
}
@action
confirm() {
RSVP.resolve(this.onConfirm()).then(() => {
this.send('setToIdle');
});
}
}

View File

@@ -1,3 +1,3 @@
import FSController from './fs';
export default FSController.extend();
export default class FsRootController extends FSController {}

View File

@@ -1,22 +1,23 @@
import Controller from '@ember/controller';
import { computed } from '@ember/object';
export default Controller.extend({
queryParams: {
export default class FsController extends Controller {
queryParams = {
sortProperty: 'sort',
sortDescending: 'desc',
},
};
sortProperty: 'Name',
sortDescending: false,
sortProperty = 'Name';
sortDescending = false;
path: null,
allocation: null,
directoryEntries: null,
isFile: null,
stat: null,
path = null;
allocation = null;
directoryEntries = null;
isFile = null;
stat = null;
pathWithLeadingSlash: computed('path', function() {
@computed('path')
get pathWithLeadingSlash() {
const path = this.path;
if (path.startsWith('/')) {
@@ -24,5 +25,5 @@ export default Controller.extend({
} else {
return `/${path}`;
}
}),
});
}
}

View File

@@ -1,3 +1,3 @@
import FSController from './fs';
export default FSController.extend();
export default class FsRootController extends FSController {}

View File

@@ -1,22 +1,23 @@
import Controller from '@ember/controller';
import { computed } from '@ember/object';
export default Controller.extend({
queryParams: {
export default class FsController extends Controller {
queryParams = {
sortProperty: 'sort',
sortDescending: 'desc',
},
};
sortProperty: 'Name',
sortDescending: false,
sortProperty = 'Name';
sortDescending = false;
path: null,
taskState: null,
directoryEntries: null,
isFile: null,
stat: null,
path = null;
taskState = null;
directoryEntries = null;
isFile = null;
stat = null;
pathWithLeadingSlash: computed('path', function() {
@computed('path')
get pathWithLeadingSlash() {
const path = this.path;
if (path.startsWith('/')) {
@@ -24,5 +25,5 @@ export default Controller.extend({
} else {
return `/${path}`;
}
}),
});
}
}

View File

@@ -3,19 +3,25 @@ import { computed } from '@ember/object';
import { computed as overridable } from 'ember-overridable-computed';
import { alias } from '@ember/object/computed';
import { task } from 'ember-concurrency';
import classic from 'ember-classic-decorator';
export default Controller.extend({
otherTaskStates: computed('model.task.taskGroup.tasks.@each.name', function() {
@classic
export default class IndexController extends Controller {
@computed('model.task.taskGroup.tasks.@each.name')
get otherTaskStates() {
const taskName = this.model.task.name;
return this.model.allocation.states.rejectBy('name', taskName);
}),
}
prestartTaskStates: computed('otherTaskStates.@each.lifecycle', function() {
@computed('otherTaskStates.@each.lifecycle')
get prestartTaskStates() {
return this.otherTaskStates.filterBy('task.lifecycle');
}),
}
network: alias('model.resources.networks.firstObject'),
ports: computed('network.{reservedPorts.[],dynamicPorts.[]}', function() {
@alias('model.resources.networks.firstObject') network;
@computed('network.{reservedPorts.[],dynamicPorts.[]}')
get ports() {
return (this.get('network.reservedPorts') || [])
.map(port => ({
name: port.Label,
@@ -30,18 +36,19 @@ export default Controller.extend({
}))
)
.sortBy('name');
}),
}
error: overridable(() => {
@overridable(() => {
// { title, description }
return null;
}),
})
error;
onDismiss() {
this.set('error', null);
},
}
restartTask: task(function*() {
@task(function*() {
try {
yield this.model.restart();
} catch (err) {
@@ -50,5 +57,6 @@ export default Controller.extend({
description: 'Your ACL token does not grant allocation lifecycle permissions.',
});
}
}),
});
})
restartTask;
}

View File

@@ -2,49 +2,59 @@
import { inject as service } from '@ember/service';
import Controller from '@ember/controller';
import { run } from '@ember/runloop';
import { observer, computed } from '@ember/object';
import { observes } from '@ember-decorators/object';
import { computed } from '@ember/object';
import Ember from 'ember';
import codesForError from '../utils/codes-for-error';
import NoLeaderError from '../utils/no-leader-error';
import classic from 'ember-classic-decorator';
export default Controller.extend({
config: service(),
system: service(),
@classic
export default class ApplicationController extends Controller {
@service config;
@service system;
queryParams: {
queryParams = {
region: 'region',
},
};
region: null,
region = null;
error: null,
error = null;
errorStr: computed('error', function() {
@computed('error')
get errorStr() {
return this.error.toString();
}),
}
errorCodes: computed('error', function() {
@computed('error')
get errorCodes() {
return codesForError(this.error);
}),
}
is403: computed('errorCodes.[]', function() {
@computed('errorCodes.[]')
get is403() {
return this.errorCodes.includes('403');
}),
}
is404: computed('errorCodes.[]', function() {
@computed('errorCodes.[]')
get is404() {
return this.errorCodes.includes('404');
}),
}
is500: computed('errorCodes.[]', function() {
@computed('errorCodes.[]')
get is500() {
return this.errorCodes.includes('500');
}),
}
isNoLeader: computed('error', function() {
@computed('error')
get isNoLeader() {
const error = this.error;
return error instanceof NoLeaderError;
}),
}
throwError: observer('error', function() {
@observes('error')
throwError() {
if (this.get('config.isDev')) {
run.next(() => {
throw this.error;
@@ -55,5 +65,5 @@ export default Controller.extend({
console.warn('UNRECOVERABLE ERROR:', this.error);
});
}
}),
});
}
}

View File

@@ -1,5 +1,5 @@
import Controller from '@ember/controller';
export default Controller.extend({
isForbidden: false,
});
export default class ClientsController extends Controller {
isForbidden = false;
}

View File

@@ -2,153 +2,158 @@
import { alias, readOnly } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import Controller, { inject as controller } from '@ember/controller';
import { computed } from '@ember/object';
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';
export default Controller.extend(
SortableFactory(['id', 'name', 'compositeStatus', 'datacenter']),
Searchable,
{
userSettings: service(),
clientsController: controller('clients'),
@classic
export default class IndexController extends Controller.extend(
SortableFactory(['id', 'name', 'compositeStatus', 'datacenter']),
Searchable
) {
@service userSettings;
@controller('clients') clientsController;
nodes: alias('model.nodes'),
agents: alias('model.agents'),
@alias('model.nodes') nodes;
@alias('model.agents') agents;
queryParams: {
currentPage: 'page',
searchTerm: 'search',
sortProperty: 'sort',
sortDescending: 'desc',
qpClass: 'class',
qpState: 'state',
qpDatacenter: 'dc',
qpVolume: 'volume',
},
queryParams = {
currentPage: 'page',
searchTerm: 'search',
sortProperty: 'sort',
sortDescending: 'desc',
qpClass: 'class',
qpState: 'state',
qpDatacenter: 'dc',
qpVolume: 'volume',
};
currentPage: 1,
pageSize: readOnly('userSettings.pageSize'),
currentPage = 1;
@readOnly('userSettings.pageSize') pageSize;
sortProperty: 'modifyIndex',
sortDescending: true,
sortProperty = 'modifyIndex';
sortDescending = true;
searchProps: computed(function() {
return ['id', 'name', 'datacenter'];
}),
qpClass: '',
qpState: '',
qpDatacenter: '',
qpVolume: '',
selectionClass: selection('qpClass'),
selectionState: selection('qpState'),
selectionDatacenter: selection('qpDatacenter'),
selectionVolume: selection('qpVolume'),
optionsClass: computed('nodes.[]', function() {
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 }));
}),
optionsState: computed(function() {
return [
{ key: 'initializing', label: 'Initializing' },
{ key: 'ready', label: 'Ready' },
{ key: 'down', label: 'Down' },
{ key: 'ineligible', label: 'Ineligible' },
{ key: 'draining', label: 'Draining' },
];
}),
optionsDatacenter: computed('nodes.[]', function() {
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 }));
}),
optionsVolume: computed('nodes.[]', function() {
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 }));
}),
filteredNodes: computed(
'nodes.[]',
'selectionClass',
'selectionState',
'selectionDatacenter',
'selectionVolume',
function() {
const {
selectionClass: classes,
selectionState: states,
selectionDatacenter: datacenters,
selectionVolume: volumes,
} = this;
const onlyIneligible = states.includes('ineligible');
const onlyDraining = states.includes('draining');
// states is a composite of node status and other node states
const statuses = states.without('ineligible').without('draining');
return this.nodes.filter(node => {
if (classes.length && !classes.includes(node.get('nodeClass'))) return false;
if (statuses.length && !statuses.includes(node.get('status'))) return false;
if (datacenters.length && !datacenters.includes(node.get('datacenter'))) return false;
if (volumes.length && !node.hostVolumes.find(volume => volumes.includes(volume.name)))
return false;
if (onlyIneligible && node.get('isEligible')) return false;
if (onlyDraining && !node.get('isDraining')) return false;
return true;
});
}
),
listToSort: alias('filteredNodes'),
listToSearch: alias('listSorted'),
sortedNodes: alias('listSearched'),
isForbidden: alias('clientsController.isForbidden'),
setFacetQueryParam(queryParam, selection) {
this.set(queryParam, serialize(selection));
},
actions: {
gotoNode(node) {
this.transitionToRoute('clients.client', node);
},
},
@computed
get searchProps() {
return ['id', 'name', 'datacenter'];
}
);
qpClass = '';
qpState = '';
qpDatacenter = '';
qpVolume = '';
@selection('qpClass') selectionClass;
@selection('qpState') selectionState;
@selection('qpDatacenter') selectionDatacenter;
@selection('qpVolume') selectionVolume;
@computed('nodes.[]')
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
get optionsState() {
return [
{ key: 'initializing', label: 'Initializing' },
{ key: 'ready', label: 'Ready' },
{ key: 'down', label: 'Down' },
{ key: 'ineligible', label: 'Ineligible' },
{ key: 'draining', label: 'Draining' },
];
}
@computed('nodes.[]')
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.[]')
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(
'nodes.[]',
'selectionClass',
'selectionState',
'selectionDatacenter',
'selectionVolume'
)
get filteredNodes() {
const {
selectionClass: classes,
selectionState: states,
selectionDatacenter: datacenters,
selectionVolume: volumes,
} = this;
const onlyIneligible = states.includes('ineligible');
const onlyDraining = states.includes('draining');
// states is a composite of node status and other node states
const statuses = states.without('ineligible').without('draining');
return this.nodes.filter(node => {
if (classes.length && !classes.includes(node.get('nodeClass'))) return false;
if (statuses.length && !statuses.includes(node.get('status'))) return false;
if (datacenters.length && !datacenters.includes(node.get('datacenter'))) return false;
if (volumes.length && !node.hostVolumes.find(volume => volumes.includes(volume.name)))
return false;
if (onlyIneligible && node.get('isEligible')) return false;
if (onlyDraining && !node.get('isDraining')) 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
gotoNode(node) {
this.transitionToRoute('clients.client', node);
}
}

Some files were not shown because too many files have changed in this diff Show More