diff --git a/ui/.eslintrc.js b/ui/.eslintrc.js index 51f829062..5a0e65e68 100644 --- a/ui/.eslintrc.js +++ b/ui/.eslintrc.js @@ -15,6 +15,9 @@ module.exports = { parserOptions: { ecmaVersion: 2018, sourceType: 'module', + ecmaFeatures: { + legacyDecorators: true, + }, }, plugins: [ 'ember' diff --git a/ui/app/abilities/abstract.js b/ui/app/abilities/abstract.js index 6f45ae795..d8af277d4 100644 --- a/ui/app/abilities/abstract.js +++ b/ui/app/abilities/abstract.js @@ -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'; } - }, -}); + } +} diff --git a/ui/app/abilities/allocation.js b/ui/app/abilities/allocation.js index e5ce8c325..0ac627049 100644 --- a/ui/app/abilities/allocation.js +++ b/ui/app/abilities/allocation.js @@ -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'); }); - }), -}); + } +} diff --git a/ui/app/abilities/client.js b/ui/app/abilities/client.js index 58380081c..e8a94df34 100644 --- a/ui/app/abilities/client.js +++ b/ui/app/abilities/client.js @@ -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'); - }), -}); + } +} diff --git a/ui/app/abilities/job.js b/ui/app/abilities/job.js index 07b4edc73..1f8418c15 100644 --- a/ui/app/abilities/job.js +++ b/ui/app/abilities/job.js @@ -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'); }); - }), -}); + } +} diff --git a/ui/app/adapters/agent.js b/ui/app/adapters/agent.js index 3e68a1d35..8b2d24fa8 100644 --- a/ui/app/adapters/agent.js +++ b/ui/app/adapters/agent.js @@ -1,9 +1,10 @@ import ApplicationAdapter from './application'; -export default ApplicationAdapter.extend({ - pathForType: () => 'agent/members', +export default class AgentAdapter extends ApplicationAdapter { + pathForType = () => 'agent/members'; + urlForFindRecord() { const [, ...args] = arguments; return this.urlForFindAll(...args); - }, -}); + } +} diff --git a/ui/app/adapters/allocation.js b/ui/app/adapters/allocation.js index 93981bc57..20cca3c29 100644 --- a/ui/app/adapters/allocation.js +++ b/ui/app/adapters/allocation.js @@ -1,8 +1,8 @@ import Watchable from './watchable'; import addToPath from 'nomad-ui/utils/add-to-path'; -export default Watchable.extend({ - stop: adapterAction('/stop'), +export default class AllocationAdapter extends Watchable { + stop = adapterAction('/stop'); restart(allocation, taskName) { const prefix = `${this.host || '/'}${this.urlPrefix()}`; @@ -10,22 +10,20 @@ export default Watchable.extend({ return this.ajax(url, 'PUT', { data: taskName && { TaskName: taskName }, }); - }, + } ls(model, path) { return this.token .authorizedRequest(`/v1/client/fs/ls/${model.id}?path=${encodeURIComponent(path)}`) .then(handleFSResponse); - }, + } stat(model, path) { return this.token - .authorizedRequest( - `/v1/client/fs/stat/${model.id}?path=${encodeURIComponent(path)}` - ) + .authorizedRequest(`/v1/client/fs/stat/${model.id}?path=${encodeURIComponent(path)}`) .then(handleFSResponse); - }, -}); + } +} async function handleFSResponse(response) { if (response.ok) { diff --git a/ui/app/adapters/application.js b/ui/app/adapters/application.js index 7bbf51b02..e03cdffe3 100644 --- a/ui/app/adapters/application.js +++ b/ui/app/adapters/application.js @@ -4,20 +4,19 @@ 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({ - // TODO: This can be removed once jquery-integration is turned off for - // the entire app. - useFetch: true, +@classic +export default class ApplicationAdapter extends RESTAdapter { + namespace = namespace; - namespace, + @service system; + @service token; - system: service(), - token: service(), - - headers: computed('token.secret', function() { + @computed('token.secret') + get headers() { const token = this.get('token.secret'); if (token) { return { @@ -25,18 +24,18 @@ export default RESTAdapter.extend({ }; } - return; - }), + return undefined; + } 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 +47,7 @@ export default RESTAdapter.extend({ // Rethrow to be handled downstream throw error; }); - }, + } ajaxOptions(url, type, options = {}) { options.data || (options.data = {}); @@ -58,13 +57,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 +76,7 @@ export default RESTAdapter.extend({ } return payload; }); - }, + } // Single record requests deviate from REST practice by using // the singular form of the resource name. @@ -87,35 +86,36 @@ export default RESTAdapter.extend({ // // This is the original implementation of _buildURL // without the pluralization of modelName - urlForFindRecord: urlForRecord, - urlForUpdateRecord: urlForRecord, -}); + urlForFindRecord(id, modelName) { + let path; + let url = []; + let host = get(this, 'host'); + let prefix = this.urlPrefix(); -function urlForRecord(id, modelName) { - let path; - let url = []; - let host = get(this, 'host'); - let prefix = this.urlPrefix(); - - if (modelName) { - path = modelName.camelize(); - if (path) { - url.push(path); + if (modelName) { + path = modelName.camelize(); + if (path) { + url.push(path); + } } + + if (id) { + url.push(encodeURIComponent(id)); + } + + if (prefix) { + url.unshift(prefix); + } + + url = url.join('/'); + if (!host && url && url.charAt(0) !== '/') { + url = '/' + url; + } + + return url; } - if (id) { - url.push(encodeURIComponent(id)); + urlForUpdateRecord() { + return this.urlForFindRecord(...arguments); } - - if (prefix) { - url.unshift(prefix); - } - - url = url.join('/'); - if (!host && url && url.charAt(0) !== '/') { - url = '/' + url; - } - - return url; } diff --git a/ui/app/adapters/deployment.js b/ui/app/adapters/deployment.js index 6dbc20dbd..5530166d6 100644 --- a/ui/app/adapters/deployment.js +++ b/ui/app/adapters/deployment.js @@ -1,6 +1,6 @@ import Watchable from './watchable'; -export default Watchable.extend({ +export default class DeploymentAdapter 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 diff --git a/ui/app/adapters/job-summary.js b/ui/app/adapters/job-summary.js index e21a54068..569311c66 100644 --- a/ui/app/adapters/job-summary.js +++ b/ui/app/adapters/job-summary.js @@ -1,12 +1,12 @@ import Watchable from './watchable'; -export default Watchable.extend({ +export default class JobSummaryAdapter 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; - }, -}); + } +} diff --git a/ui/app/adapters/job.js b/ui/app/adapters/job.js index 2bc323838..f6992cb13 100644 --- a/ui/app/adapters/job.js +++ b/ui/app/adapters/job.js @@ -1,28 +1,27 @@ -import Watchable from './watchable'; +import WatchableNamespaceIDs from './watchable-namespace-ids'; import addToPath from 'nomad-ui/utils/add-to-path'; -import WithNamespaceIDs from 'nomad-ui/mixins/with-namespace-ids'; -export default Watchable.extend(WithNamespaceIDs, { - relationshipFallbackLinks: Object.freeze({ +export default class JobAdapter extends WatchableNamespaceIDs { + relationshipFallbackLinks = { summary: '/summary', - }), + }; fetchRawDefinition(job) { const url = this.urlForFindRecord(job.get('id'), 'job'); return this.ajax(url, 'GET'); - }, + } forcePeriodic(job) { if (job.get('periodic')) { const url = addToPath(this.urlForFindRecord(job.get('id'), 'job'), '/periodic/force'); return this.ajax(url, 'POST'); } - }, + } stop(job) { const url = this.urlForFindRecord(job.get('id'), 'job'); return this.ajax(url, 'DELETE'); - }, + } parse(spec) { const url = addToPath(this.urlForFindAll('job'), '/parse'); @@ -32,7 +31,7 @@ export default Watchable.extend(WithNamespaceIDs, { Canonicalize: true, }, }); - }, + } plan(job) { const jobId = job.get('id') || job.get('_idBeforeSaving'); @@ -49,7 +48,7 @@ export default Watchable.extend(WithNamespaceIDs, { store.pushPayload('job-plan', { jobPlans: [json] }); return store.peekRecord('job-plan', jobId); }); - }, + } // Running a job doesn't follow REST create semantics so it's easier to // treat it as an action. @@ -59,7 +58,7 @@ export default Watchable.extend(WithNamespaceIDs, { Job: job.get('_newDefinitionJSON'), }, }); - }, + } update(job) { const jobId = job.get('id') || job.get('_idBeforeSaving'); @@ -68,5 +67,5 @@ export default Watchable.extend(WithNamespaceIDs, { Job: job.get('_newDefinitionJSON'), }, }); - }, -}); + } +} diff --git a/ui/app/adapters/namespace.js b/ui/app/adapters/namespace.js index 9b4b04df8..365c0933d 100644 --- a/ui/app/adapters/namespace.js +++ b/ui/app/adapters/namespace.js @@ -1,13 +1,13 @@ import ApplicationAdapter from './application'; import codesForError from '../utils/codes-for-error'; -export default ApplicationAdapter.extend({ +export default class NamespaceAdapter 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 }; } }); - }, -}); + } +} diff --git a/ui/app/adapters/node.js b/ui/app/adapters/node.js index d541d70ad..746f51f6e 100644 --- a/ui/app/adapters/node.js +++ b/ui/app/adapters/node.js @@ -1,14 +1,14 @@ import Watchable from './watchable'; import addToPath from 'nomad-ui/utils/add-to-path'; -export default Watchable.extend({ +export default class NodeAdapter 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, }, }); - }, -}); + } +} diff --git a/ui/app/adapters/plugin.js b/ui/app/adapters/plugin.js index 836488ad1..80096f31b 100644 --- a/ui/app/adapters/plugin.js +++ b/ui/app/adapters/plugin.js @@ -1,7 +1,7 @@ import Watchable from './watchable'; -export default Watchable.extend({ - queryParamsToAttrs: Object.freeze({ +export default class PluginAdapter extends Watchable { + queryParamsToAttrs = { type: 'type', - }), -}); + }; +} diff --git a/ui/app/adapters/policy.js b/ui/app/adapters/policy.js index 66f6c0b30..79e68bcc4 100644 --- a/ui/app/adapters/policy.js +++ b/ui/app/adapters/policy.js @@ -1,5 +1,5 @@ import { default as ApplicationAdapter, namespace } from './application'; -export default ApplicationAdapter.extend({ - namespace: namespace + '/acl', -}); +export default class PolicyAdapter extends ApplicationAdapter { + namespace = namespace + '/acl'; +} diff --git a/ui/app/adapters/token.js b/ui/app/adapters/token.js index 7db388248..771d101d9 100644 --- a/ui/app/adapters/token.js +++ b/ui/app/adapters/token.js @@ -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 TokenAdapter 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); }); - }, -}); + } +} diff --git a/ui/app/adapters/volume.js b/ui/app/adapters/volume.js index 8f2d8532a..594f929a1 100644 --- a/ui/app/adapters/volume.js +++ b/ui/app/adapters/volume.js @@ -1,9 +1,8 @@ -import Watchable from './watchable'; -import WithNamespaceIDs from 'nomad-ui/mixins/with-namespace-ids'; +import WatchableNamespaceIDs from './watchable-namespace-ids'; -export default Watchable.extend(WithNamespaceIDs, { - queryParamsToAttrs: Object.freeze({ +export default class VolumeAdapter extends WatchableNamespaceIDs { + queryParamsToAttrs = { type: 'type', plugin_id: 'plugin.id', - }), -}); + }; +} diff --git a/ui/app/mixins/with-namespace-ids.js b/ui/app/adapters/watchable-namespace-ids.js similarity index 63% rename from ui/app/mixins/with-namespace-ids.js rename to ui/app/adapters/watchable-namespace-ids.js index bdbdb9969..24eeffe15 100644 --- a/ui/app/mixins/with-namespace-ids.js +++ b/ui/app/adapters/watchable-namespace-ids.js @@ -1,57 +1,50 @@ import { inject as service } from '@ember/service'; -import Mixin from '@ember/object/mixin'; +import Watchable from './watchable'; -// eslint-disable-next-line ember/no-new-mixins -export default Mixin.create({ - system: service(), +export default class WatchableNamespaceIDs extends Watchable { + @service system; findAll() { const namespace = this.get('system.activeNamespace'); - return this._super(...arguments).then(data => { + return super.findAll(...arguments).then(data => { data.forEach(record => { record.Namespace = namespace ? namespace.get('id') : 'default'; }); return data; }); - }, + } findRecord(store, type, id, snapshot) { const [, namespace] = JSON.parse(id); const namespaceQuery = namespace && namespace !== 'default' ? { namespace } : {}; - return this._super(store, type, id, snapshot, namespaceQuery); - }, + return super.findRecord(store, type, id, snapshot, namespaceQuery); + } urlForFindAll() { - const url = this._super(...arguments); + const url = super.urlForFindAll(...arguments); const namespace = this.get('system.activeNamespace.id'); return associateNamespace(url, namespace); - }, + } urlForQuery() { - const url = this._super(...arguments); + const url = super.urlForQuery(...arguments); const namespace = this.get('system.activeNamespace.id'); return associateNamespace(url, namespace); - }, + } urlForFindRecord(id, type, hash) { const [name, namespace] = JSON.parse(id); - let url = this._super(name, type, hash); + let url = super.urlForFindRecord(name, type, hash); return associateNamespace(url, namespace); - }, - - urlForUpdateRecord(id, type, hash) { - const [name, namespace] = JSON.parse(id); - let url = this._super(name, type, hash); - return associateNamespace(url, namespace); - }, + } xhrKey(url, method, options = {}) { - const plainKey = this._super(...arguments); + const plainKey = super.xhrKey(...arguments); const namespace = options.data && options.data.namespace; return associateNamespace(plainKey, namespace); - }, -}); + } +} function associateNamespace(url, namespace) { if (namespace && namespace !== 'default') { diff --git a/ui/app/adapters/watchable.js b/ui/app/adapters/watchable.js index 983c87b8b..feb07b316 100644 --- a/ui/app/adapters/watchable.js +++ b/ui/app/adapters/watchable.js @@ -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; diff --git a/ui/app/app.js b/ui/app/app.js index 7d6bae3ce..5e9a038ba 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -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); diff --git a/ui/app/components/allocation-row.js b/ui/app/components/allocation-row.js index c2e0d46a5..0a9fb1c0a 100644 --- a/ui/app/components/allocation-row.js +++ b/ui/app/components/allocation-row.js @@ -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() { - if (!this.get('allocation.isRunning')) return; + @computed('allocation', 'allocation.isRunning') + get stats() { + if (!this.get('allocation.isRunning')) return undefined; 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; diff --git a/ui/app/components/allocation-stat.js b/ui/app/components/allocation-stat.js index b1d702a13..814d43e52 100644 --- a/ui/app/components/allocation-stat.js +++ b/ui/app/components/allocation-stat.js @@ -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; - }), + return undefined; + } - formattedStat: computed('metric', 'stat.used', function() { - if (!this.stat) return; + @computed('metric', 'stat.used') + get formattedStat() { + if (!this.stat) return undefined; 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; - }), -}); + return undefined; + } +} diff --git a/ui/app/components/allocation-status-bar.js b/ui/app/components/allocation-status-bar.js index 6e3046570..803884b6d 100644 --- a/ui/app/components/allocation-status-bar.js +++ b/ui/app/components/allocation-status-bar.js @@ -1,45 +1,45 @@ import { computed } from '@ember/object'; import DistributionBar from './distribution-bar'; -export default DistributionBar.extend({ - layoutName: 'components/distribution-bar', +export default class AllocationStatusBar extends DistributionBar { + layoutName = 'components/distribution-bar'; - allocationContainer: null, + allocationContainer = null; - 'data-test-allocation-status-bar': true, + 'data-test-allocation-status-bar' = true; - data: computed( - 'allocationContainer.{queuedAllocs,completeAllocs,failedAllocs,runningAllocs,startingAllocs}', - function() { - if (!this.allocationContainer) { - return []; - } - - const allocs = this.allocationContainer.getProperties( - 'queuedAllocs', - 'completeAllocs', - 'failedAllocs', - 'runningAllocs', - 'startingAllocs', - 'lostAllocs' - ); - return [ - { label: 'Queued', value: allocs.queuedAllocs, className: 'queued' }, - { - label: 'Starting', - value: allocs.startingAllocs, - className: 'starting', - layers: 2, - }, - { label: 'Running', value: allocs.runningAllocs, className: 'running' }, - { - label: 'Complete', - value: allocs.completeAllocs, - className: 'complete', - }, - { label: 'Failed', value: allocs.failedAllocs, className: 'failed' }, - { label: 'Lost', value: allocs.lostAllocs, className: 'lost' }, - ]; + @computed( + 'allocationContainer.{queuedAllocs,completeAllocs,failedAllocs,runningAllocs,startingAllocs}' + ) + get data() { + if (!this.allocationContainer) { + return []; } - ), -}); + + const allocs = this.allocationContainer.getProperties( + 'queuedAllocs', + 'completeAllocs', + 'failedAllocs', + 'runningAllocs', + 'startingAllocs', + 'lostAllocs' + ); + return [ + { label: 'Queued', value: allocs.queuedAllocs, className: 'queued' }, + { + label: 'Starting', + value: allocs.startingAllocs, + className: 'starting', + layers: 2, + }, + { label: 'Running', value: allocs.runningAllocs, className: 'running' }, + { + label: 'Complete', + value: allocs.completeAllocs, + className: 'complete', + }, + { label: 'Failed', value: allocs.failedAllocs, className: 'failed' }, + { label: 'Lost', value: allocs.lostAllocs, className: 'lost' }, + ]; + } +} diff --git a/ui/app/components/allocation-subnav.js b/ui/app/components/allocation-subnav.js index 133d18a27..594440223 100644 --- a/ui/app/components/allocation-subnav.js +++ b/ui/app/components/allocation-subnav.js @@ -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; +} diff --git a/ui/app/components/app-breadcrumbs.js b/ui/app/components/app-breadcrumbs.js index 28a08a85d..1537febbd 100644 --- a/ui/app/components/app-breadcrumbs.js +++ b/ui/app/components/app-breadcrumbs.js @@ -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; +} diff --git a/ui/app/components/attributes-section.js b/ui/app/components/attributes-section.js index 479865264..6e1c763e5 100644 --- a/ui/app/components/attributes-section.js +++ b/ui/app/components/attributes-section.js @@ -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 {} diff --git a/ui/app/components/children-status-bar.js b/ui/app/components/children-status-bar.js index 63c62e481..5ea7d6eb7 100644 --- a/ui/app/components/children-status-bar.js +++ b/ui/app/components/children-status-bar.js @@ -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' }, ]; - }), -}); + } +} diff --git a/ui/app/components/client-node-row.js b/ui/app/components/client-node-row.js index 4eaec00f8..2127b5930 100644 --- a/ui/app/components/client-node-row.js +++ b/ui/app/components/client-node-row.js @@ -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 ''; } - }), -}); + } +} diff --git a/ui/app/components/copy-button.js b/ui/app/components/copy-button.js index d8a891d85..107786d73 100644 --- a/ui/app/components/copy-button.js +++ b/ui/app/components/copy-button.js @@ -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; +} diff --git a/ui/app/components/distribution-bar.js b/ui/app/components/distribution-bar.js index 743aead63..ee9af8456 100644 --- a/ui/app/components/distribution-bar.js +++ b/ui/app/components/distribution-bar.js @@ -1,30 +1,35 @@ /* eslint-disable ember/no-observers */ import Component from '@ember/component'; -import { computed, observer, set } from '@ember/object'; +import { computed, set } from '@ember/object'; +import { observes } from '@ember-decorators/object'; import { run } from '@ember/runloop'; import { assign } from '@ember/polyfills'; import { guidFor } from '@ember/object/internals'; import { copy } from 'ember-copy'; +import { computed as overridable } from 'ember-overridable-computed'; import d3 from 'd3-selection'; import 'd3-transition'; import WindowResizable from '../mixins/window-resizable'; import styleStringProperty from '../utils/properties/style-string'; +import { classNames, classNameBindings } from '@ember-decorators/component'; +import classic from 'ember-classic-decorator'; const sumAggregate = (total, val) => total + val; -export default Component.extend(WindowResizable, { - classNames: ['chart', 'distribution-bar'], - classNameBindings: ['isNarrow:is-narrow'], +@classic +@classNames('chart', 'distribution-bar') +@classNameBindings('isNarrow:is-narrow') +export default class DistributionBar extends Component.extend(WindowResizable) { + chart = null; + @overridable(() => null) data; + activeDatum = null; + isNarrow = false; - chart: null, - data: null, - activeDatum: null, - isNarrow: false, + @styleStringProperty('tooltipPosition') tooltipStyle; + maskId = null; - tooltipStyle: styleStringProperty('tooltipPosition'), - maskId: null, - - _data: computed('data', function() { + @computed('data') + get _data() { const data = copy(this.data, true); const sum = data.mapBy('value').reduce(sumAggregate, 0); @@ -41,7 +46,7 @@ export default Component.extend(WindowResizable, { .mapBy('value') .reduce(sumAggregate, 0) / sum, })); - }), + } didInsertElement() { const svg = this.element.querySelector('svg'); @@ -63,15 +68,16 @@ export default Component.extend(WindowResizable, { }); this.renderChart(); - }, + } didUpdateAttrs() { this.renderChart(); - }, + } - updateChart: observer('_data.@each.{value,label,className}', function() { + @observes('_data.@each.{value,label,className}') + updateChart() { this.renderChart(); - }), + } // prettier-ignore /* eslint-disable */ @@ -166,10 +172,10 @@ export default Component.extend(WindowResizable, { .attr('height', '6px') .attr('y', '50%'); } - }, + } /* eslint-enable */ windowResizeHandler() { run.once(this, this.renderChart); - }, -}); + } +} diff --git a/ui/app/components/drain-popover.js b/ui/app/components/drain-popover.js index f964a8081..6690e9d72 100644 --- a/ui/app/components/drain-popover.js +++ b/ui/app/components/drain-popover.js @@ -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(); - }, -}); + } +} diff --git a/ui/app/components/exec-terminal.js b/ui/app/components/exec-terminal.js index 255b8f588..e396677a7 100644 --- a/ui/app/components/exec-terminal.js +++ b/ui/app/components/exec-terminal.js @@ -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); } - }, -}); + } +} diff --git a/ui/app/components/exec/open-button.js b/ui/app/components/exec/open-button.js index 0525cc022..84b552318 100644 --- a/ui/app/components/exec/open-button.js +++ b/ui/app/components/exec/open-button.js @@ -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, }); - }, -}); + } +} diff --git a/ui/app/components/exec/task-contents.js b/ui/app/components/exec/task-contents.js index 479865264..e4ccbb5e3 100644 --- a/ui/app/components/exec/task-contents.js +++ b/ui/app/components/exec/task-contents.js @@ -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 {} diff --git a/ui/app/components/exec/task-group-parent.js b/ui/app/components/exec/task-group-parent.js index 2eced1676..8537d9f01 100644 --- a/ui/app/components/exec/task-group-parent.js +++ b/ui/app/components/exec/task-group-parent.js @@ -1,16 +1,19 @@ import Component from '@ember/component'; import { inject as service } from '@ember/service'; -import { computed } from '@ember/object'; +import { action, computed } from '@ember/object'; import { filterBy, mapBy, or, sort } from '@ember/object/computed'; import generateExecUrl from 'nomad-ui/utils/generate-exec-url'; import openExecUrl from 'nomad-ui/utils/open-exec-url'; +import classic from 'ember-classic-decorator'; -export default Component.extend({ - router: service(), +@classic +export default class TaskGroupParent extends Component { + @service router; - isOpen: or('clickedOpen', 'currentRouteIsThisTaskGroup'), + @or('clickedOpen', 'currentRouteIsThisTaskGroup') isOpen; - currentRouteIsThisTaskGroup: computed('router.currentRoute', function() { + @computed('router.currentRoute') + get currentRouteIsThisTaskGroup() { const route = this.router.currentRoute; if (route.name.includes('task-group')) { @@ -24,58 +27,60 @@ export default Component.extend({ } else { return false; } - }), + } - hasPendingAllocations: computed('taskGroup.allocations.@each.clientStatus', function() { + @computed('taskGroup.allocations.@each.clientStatus') + get hasPendingAllocations() { return this.taskGroup.allocations.any(allocation => allocation.clientStatus === 'pending'); - }), + } - allocationTaskStatesRecordArrays: mapBy('taskGroup.allocations', 'states'), - allocationTaskStates: computed('allocationTaskStatesRecordArrays.[]', function() { + @mapBy('taskGroup.allocations', 'states') allocationTaskStatesRecordArrays; + @computed('allocationTaskStatesRecordArrays.[]') + get allocationTaskStates() { const flattenRecordArrays = (accumulator, recordArray) => accumulator.concat(recordArray.toArray()); return this.allocationTaskStatesRecordArrays.reduce(flattenRecordArrays, []); - }), + } - activeTaskStates: filterBy('allocationTaskStates', 'isActive'), + @filterBy('allocationTaskStates', 'isActive') activeTaskStates; - activeTasks: mapBy('activeTaskStates', 'task'), - activeTaskGroups: mapBy('activeTasks', 'taskGroup'), + @mapBy('activeTaskStates', 'task') activeTasks; + @mapBy('activeTasks', 'taskGroup') activeTaskGroups; - tasksWithRunningStates: computed( + @computed( 'taskGroup.name', 'activeTaskStates.@each.name', 'activeTasks.@each.name', - 'activeTaskGroups.@each.name', - function() { - const activeTaskStateNames = this.activeTaskStates - .filter(taskState => { - return taskState.task && taskState.task.taskGroup.name === this.taskGroup.name; - }) - .mapBy('name'); + 'activeTaskGroups.@each.name' + ) + get tasksWithRunningStates() { + const activeTaskStateNames = this.activeTaskStates + .filter(taskState => { + return taskState.task && taskState.task.taskGroup.name === this.taskGroup.name; + }) + .mapBy('name'); - return this.taskGroup.tasks.filter(task => activeTaskStateNames.includes(task.name)); - } - ), + return this.taskGroup.tasks.filter(task => activeTaskStateNames.includes(task.name)); + } - taskSorting: Object.freeze(['name']), - sortedTasks: sort('tasksWithRunningStates', 'taskSorting'), + taskSorting = ['name']; + @sort('tasksWithRunningStates', 'taskSorting') sortedTasks; - clickedOpen: false, + clickedOpen = false; - actions: { - toggleOpen() { - this.toggleProperty('clickedOpen'); - }, + @action + toggleOpen() { + this.toggleProperty('clickedOpen'); + } - openInNewWindow(job, taskGroup, task) { - let url = generateExecUrl(this.router, { - job, - taskGroup, - task, - }); + @action + openInNewWindow(job, taskGroup, task) { + let url = generateExecUrl(this.router, { + job, + taskGroup, + task, + }); - openExecUrl(url); - }, - }, -}); + openExecUrl(url); + } +} diff --git a/ui/app/components/fs/breadcrumbs.js b/ui/app/components/fs/breadcrumbs.js index 784de27ca..819e822b6 100644 --- a/ui/app/components/fs/breadcrumbs.js +++ b/ui/app/components/fs/breadcrumbs.js @@ -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; - }), -}); + } +} diff --git a/ui/app/components/fs/browser.js b/ui/app/components/fs/browser.js index bf6b30731..7dbfd7183 100644 --- a/ui/app/components/fs/browser.js +++ b/ui/app/components/fs/browser.js @@ -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; - }), + return undefined; + } - 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; } - ), -}); + } +} diff --git a/ui/app/components/fs/directory-entry.js b/ui/app/components/fs/directory-entry.js index 3b703dd90..69fe41c3a 100644 --- a/ui/app/components/fs/directory-entry.js +++ b/ui/app/components/fs/directory-entry.js @@ -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}`; } - }), -}); + } +} diff --git a/ui/app/components/fs/file.js b/ui/app/components/fs/file.js index c8e6a06ec..e977c7519 100644 --- a/ui/app/components/fs/file.js +++ b/ui/app/components/fs/file.js @@ -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'; } @@ -65,16 +69,18 @@ export default Component.extend({ return 'readat'; } - return; - }), + return undefined; + } - 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); + } +} diff --git a/ui/app/components/fs/link.js b/ui/app/components/fs/link.js index c6a4aee26..59aeb1452 100644 --- a/ui/app/components/fs/link.js +++ b/ui/app/components/fs/link.js @@ -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; +} diff --git a/ui/app/components/gauge-chart.js b/ui/app/components/gauge-chart.js index 3df1f5e4d..9625dbff2 100644 --- a/ui/app/components/gauge-chart.js +++ b/ui/app/components/gauge-chart.js @@ -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); - }, -}); + } +} diff --git a/ui/app/components/global-header.js b/ui/app/components/global-header.js index c90e6457d..a7fbc9d88 100644 --- a/ui/app/components/global-header.js +++ b/ui/app/components/global-header.js @@ -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() {} +} diff --git a/ui/app/components/gutter-menu.js b/ui/app/components/gutter-menu.js index 46c02d137..469ac039b 100644 --- a/ui/app/components/gutter-menu.js +++ b/ui/app/components/gutter-menu.js @@ -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') }, }); - }, -}); + } +} diff --git a/ui/app/components/image-file.js b/ui/app/components/image-file.js index fe28201ba..16ee6733c 100644 --- a/ui/app/components/image-file.js +++ b/ui/app/components/image-file.js @@ -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() { - if (!this.src) return; + @computed('src') + get fileName() { + if (!this.src) return undefined; 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, }); - }, -}); + } +} diff --git a/ui/app/components/job-deployment.js b/ui/app/components/job-deployment.js index feb13f17d..f9b31ecf7 100644 --- a/ui/app/components/job-deployment.js +++ b/ui/app/components/job-deployment.js @@ -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; +} diff --git a/ui/app/components/job-deployment/deployment-metrics.js b/ui/app/components/job-deployment/deployment-metrics.js index 479865264..213414561 100644 --- a/ui/app/components/job-deployment/deployment-metrics.js +++ b/ui/app/components/job-deployment/deployment-metrics.js @@ -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 {} diff --git a/ui/app/components/job-deployments-stream.js b/ui/app/components/job-deployments-stream.js index 7a5e3d5d5..49b534d3e 100644 --- a/ui/app/components/job-deployments-stream.js +++ b/ui/app/components/job-deployments-stream.js @@ -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 }; }); - }), -}); + } +} diff --git a/ui/app/components/job-diff-fields-and-objects.js b/ui/app/components/job-diff-fields-and-objects.js index 479865264..20812480b 100644 --- a/ui/app/components/job-diff-fields-and-objects.js +++ b/ui/app/components/job-diff-fields-and-objects.js @@ -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 {} diff --git a/ui/app/components/job-diff.js b/ui/app/components/job-diff.js index 79371f333..2f7a6921c 100644 --- a/ui/app/components/job-diff.js +++ b/ui/app/components/job-diff.js @@ -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; +} diff --git a/ui/app/components/job-editor.js b/ui/app/components/job-editor.js index fa4bf4e8f..27c0998b9 100644 --- a/ui/app/components/job-editor.js +++ b/ui/app/components/job-editor.js @@ -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); } - }, -}); + } +} diff --git a/ui/app/components/job-page/abstract.js b/ui/app/components/job-page/abstract.js index 931c5c6db..29beaa6aa 100644 --- a/ui/app/components/job-page/abstract.js +++ b/ui/app/components/job-page/abstract.js @@ -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); + } +} diff --git a/ui/app/components/job-page/batch.js b/ui/app/components/job-page/batch.js index 559b3c8b8..3aa31786c 100644 --- a/ui/app/components/job-page/batch.js +++ b/ui/app/components/job-page/batch.js @@ -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 {} diff --git a/ui/app/components/job-page/parameterized-child.js b/ui/app/components/job-page/parameterized-child.js index c7b167bef..3f941067c 100644 --- a/ui/app/components/job-page/parameterized-child.js +++ b/ui/app/components/job-page/parameterized-child.js @@ -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; - }), -}); + } +} diff --git a/ui/app/components/job-page/parameterized.js b/ui/app/components/job-page/parameterized.js index 559b3c8b8..cf69268ae 100644 --- a/ui/app/components/job-page/parameterized.js +++ b/ui/app/components/job-page/parameterized.js @@ -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 {} diff --git a/ui/app/components/job-page/parts/children.js b/ui/app/components/job-page/parts/children.js index 30772cd0a..f3b4b865a 100644 --- a/ui/app/components/job-page/parts/children.js +++ b/ui/app/components/job-page/parts/children.js @@ -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; +} diff --git a/ui/app/components/job-page/parts/error.js b/ui/app/components/job-page/parts/error.js index 241bb1662..a9259cfaf 100644 --- a/ui/app/components/job-page/parts/error.js +++ b/ui/app/components/job-page/parts/error.js @@ -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() {} +} diff --git a/ui/app/components/job-page/parts/latest-deployment.js b/ui/app/components/job-page/parts/latest-deployment.js index dacfd7497..dc78a235e 100644 --- a/ui/app/components/job-page/parts/latest-deployment.js +++ b/ui/app/components/job-page/parts/latest-deployment.js @@ -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; +} diff --git a/ui/app/components/job-page/parts/placement-failures.js b/ui/app/components/job-page/parts/placement-failures.js index 7df4236d8..12a4f9186 100644 --- a/ui/app/components/job-page/parts/placement-failures.js +++ b/ui/app/components/job-page/parts/placement-failures.js @@ -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; +} diff --git a/ui/app/components/job-page/parts/recent-allocations.js b/ui/app/components/job-page/parts/recent-allocations.js index 005c5a4ba..507a0fc0f 100644 --- a/ui/app/components/job-page/parts/recent-allocations.js +++ b/ui/app/components/job-page/parts/recent-allocations.js @@ -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); + } +} diff --git a/ui/app/components/job-page/parts/summary.js b/ui/app/components/job-page/parts/summary.js index ba6211659..87ca0a4c3 100644 --- a/ui/app/components/job-page/parts/summary.js +++ b/ui/app/components/job-page/parts/summary.js @@ -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'); - }, -}); + } +} diff --git a/ui/app/components/job-page/parts/task-groups.js b/ui/app/components/job-page/parts/task-groups.js index f5ce33757..129bb5f5a 100644 --- a/ui/app/components/job-page/parts/task-groups.js +++ b/ui/app/components/job-page/parts/task-groups.js @@ -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; +} diff --git a/ui/app/components/job-page/parts/title.js b/ui/app/components/job-page/parts/title.js index b13702d64..97dba1927 100644 --- a/ui/app/components/job-page/parts/title.js +++ b/ui/app/components/job-page/parts/title.js @@ -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; +} diff --git a/ui/app/components/job-page/periodic-child.js b/ui/app/components/job-page/periodic-child.js index 4eda0ed4d..dfe42225d 100644 --- a/ui/app/components/job-page/periodic-child.js +++ b/ui/app/components/job-page/periodic-child.js @@ -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], }, ]; - }), -}); + } +} diff --git a/ui/app/components/job-page/periodic.js b/ui/app/components/job-page/periodic.js index c108fc2ab..f36818581 100644 --- a/ui/app/components/job-page/periodic.js +++ b/ui/app/components/job-page/periodic.js @@ -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); + } +} diff --git a/ui/app/components/job-page/service.js b/ui/app/components/job-page/service.js index 559b3c8b8..0cb58e90c 100644 --- a/ui/app/components/job-page/service.js +++ b/ui/app/components/job-page/service.js @@ -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 {} diff --git a/ui/app/components/job-page/system.js b/ui/app/components/job-page/system.js index 559b3c8b8..bf2c04442 100644 --- a/ui/app/components/job-page/system.js +++ b/ui/app/components/job-page/system.js @@ -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 {} diff --git a/ui/app/components/job-row.js b/ui/app/components/job-row.js index 4d55fd71a..954332b09 100644 --- a/ui/app/components/job-row.js +++ b/ui/app/components/job-row.js @@ -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]); - }, -}); + } +} diff --git a/ui/app/components/job-version.js b/ui/app/components/job-version.js index 978111a93..254996488 100644 --- a/ui/app/components/job-version.js +++ b/ui/app/components/job-version.js @@ -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); diff --git a/ui/app/components/job-versions-stream.js b/ui/app/components/job-versions-stream.js index 3fc938ea0..5497565e5 100644 --- a/ui/app/components/job-versions-stream.js +++ b/ui/app/components/job-versions-stream.js @@ -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 }; }); - }), -}); + } +} diff --git a/ui/app/components/json-viewer.js b/ui/app/components/json-viewer.js index 63185cfc8..bcb803099 100644 --- a/ui/app/components/json-viewer.js +++ b/ui/app/components/json-viewer.js @@ -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); - }), -}); + } +} diff --git a/ui/app/components/lifecycle-chart-row.js b/ui/app/components/lifecycle-chart-row.js index 8ef057556..44985d747 100644 --- a/ui/app/components/lifecycle-chart-row.js +++ b/ui/app/components/lifecycle-chart-row.js @@ -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; - }), + return undefined; + } - finishedClass: computed('taskState.finishedAt', function() { + @computed('taskState.finishedAt') + get finishedClass() { if (this.taskState && this.taskState.finishedAt) { return 'is-finished'; } - return; - }), -}); + return undefined; + } +} diff --git a/ui/app/components/lifecycle-chart.js b/ui/app/components/lifecycle-chart.js index f15b281d9..d2e1735ea 100644 --- a/ui/app/components/lifecycle-chart.js +++ b/ui/app/components/lifecycle-chart.js @@ -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, diff --git a/ui/app/components/line-chart.js b/ui/app/components/line-chart.js index d0b282db6..5b47a0049 100644 --- a/ui/app/components/line-chart.js +++ b/ui/app/components/line-chart.js @@ -1,6 +1,7 @@ /* eslint-disable ember/no-observers */ import Component from '@ember/component'; -import { computed, observer } from '@ember/object'; +import { computed } from '@ember/object'; +import { observes } from '@ember-decorators/object'; import { computed as overridable } from 'ember-overridable-computed'; import { guidFor } from '@ember/object/internals'; import { run } from '@ember/runloop'; @@ -13,6 +14,8 @@ import d3Format from 'd3-format'; import d3TimeFormat from 'd3-time-format'; import WindowResizable from 'nomad-ui/mixins/window-resizable'; import styleStringProperty from 'nomad-ui/utils/properties/style-string'; +import { classNames } from '@ember-decorators/component'; +import classic from 'ember-classic-decorator'; // Returns a new array with the specified number of points linearly // distributed across the bounds @@ -28,68 +31,77 @@ const lerp = ([low, high], numPoints) => { // Round a number or an array of numbers const nice = val => (val instanceof Array ? val.map(nice) : Math.round(val)); -export default Component.extend(WindowResizable, { - classNames: ['chart', 'line-chart'], - +@classic +@classNames('chart', 'line-chart') +export default class LineChart extends Component.extend(WindowResizable) { // Public API - data: null, - xProp: null, - yProp: null, - timeseries: false, - chartClass: 'is-primary', + data = null; + xProp = null; + yProp = null; + timeseries = false; + chartClass = 'is-primary'; - title: 'Line Chart', - description: null, + title = 'Line Chart'; + + @overridable(function() { + return null; + }) + description; // Private Properties - width: 0, - height: 0, + width = 0; + height = 0; - isActive: false, + isActive = false; - fillId: computed(function() { + @computed() + get fillId() { return `line-chart-fill-${guidFor(this)}`; - }), + } - maskId: computed(function() { + @computed() + get maskId() { return `line-chart-mask-${guidFor(this)}`; - }), + } - activeDatum: null, + activeDatum = null; - activeDatumLabel: computed('activeDatum', function() { + @computed('activeDatum') + get activeDatumLabel() { const datum = this.activeDatum; - if (!datum) return; + if (!datum) return undefined; const x = datum[this.xProp]; return this.xFormat(this.timeseries)(x); - }), + } - activeDatumValue: computed('activeDatum', function() { + @computed('activeDatum') + get activeDatumValue() { const datum = this.activeDatum; - if (!datum) return; + if (!datum) return undefined; const y = datum[this.yProp]; return this.yFormat()(y); - }), + } // Overridable functions that retrurn formatter functions xFormat(timeseries) { return timeseries ? d3TimeFormat.timeFormat('%b') : d3Format.format(','); - }, + } yFormat() { return d3Format.format(',.2~r'); - }, + } - tooltipPosition: null, - tooltipStyle: styleStringProperty('tooltipPosition'), + tooltipPosition = null; + @styleStringProperty('tooltipPosition') tooltipStyle; - 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; @@ -99,25 +111,28 @@ export default Component.extend(WindowResizable, { scale.rangeRound([10, this.yAxisOffset]).domain(domain); return scale; - }), + } - xRange: computed('data.[]', 'xFormat', 'xProp', 'timeseries', function() { + @computed('data.[]', 'xFormat', 'xProp', 'timeseries') + get xRange() { const { xProp, timeseries, data } = this; const range = d3Array.extent(data, d => d[xProp]); const formatter = this.xFormat(timeseries); return range.map(formatter); - }), + } - yRange: computed('data.[]', 'yFormat', 'yProp', function() { + @computed('data.[]', 'yFormat', 'yProp') + get yRange() { const yProp = this.yProp; const range = d3Array.extent(this.data, d => d[yProp]); const formatter = this.yFormat(); return range.map(formatter); - }), + } - yScale: computed('data.[]', 'yProp', 'xAxisOffset', function() { + @computed('data.[]', 'yProp', 'xAxisOffset') + get yScale() { const yProp = this.yProp; let max = d3Array.max(this.data, d => d[yProp]) || 1; if (max > 1) { @@ -128,9 +143,10 @@ export default Component.extend(WindowResizable, { .scaleLinear() .rangeRound([this.xAxisOffset, 10]) .domain([0, max]); - }), + } - xAxis: computed('xScale', function() { + @computed('xScale') + get xAxis() { const formatter = this.xFormat(this.timeseries); return d3Axis @@ -138,17 +154,19 @@ export default Component.extend(WindowResizable, { .scale(this.xScale) .ticks(5) .tickFormat(formatter); - }), + } - yTicks: computed('xAxisOffset', function() { + @computed('xAxisOffset') + get yTicks() { const height = this.xAxisOffset; const tickCount = Math.ceil(height / 120) * 2 + 1; const domain = this.yScale.domain(); const ticks = lerp(domain, tickCount); return domain[1] - domain[0] > 1 ? nice(ticks) : ticks; - }), + } - yAxis: computed('yScale', function() { + @computed('yScale') + get yAxis() { const formatter = this.yFormat(); return d3Axis @@ -156,9 +174,10 @@ export default Component.extend(WindowResizable, { .scale(this.yScale) .tickValues(this.yTicks) .tickFormat(formatter); - }), + } - yGridlines: computed('yScale', function() { + @computed('yScale') + get yGridlines() { // The first gridline overlaps the x-axis, so remove it const [, ...ticks] = this.yTicks; @@ -168,33 +187,38 @@ export default Component.extend(WindowResizable, { .tickValues(ticks) .tickSize(-this.yAxisOffset) .tickFormat(''); - }), + } - xAxisHeight: computed(function() { + @computed() + get xAxisHeight() { // Avoid divide by zero errors by always having a height if (!this.element) return 1; const axis = this.element.querySelector('.x-axis'); return axis && axis.getBBox().height; - }), + } - yAxisWidth: computed(function() { + @computed() + get yAxisWidth() { // Avoid divide by zero errors by always having a width if (!this.element) return 1; const axis = this.element.querySelector('.y-axis'); return axis && axis.getBBox().width; - }), + } - xAxisOffset: overridable('height', 'xAxisHeight', function() { + @overridable('height', 'xAxisHeight', function() { return this.height - this.xAxisHeight; - }), + }) + xAxisOffset; - yAxisOffset: computed('width', 'yAxisWidth', function() { + @computed('width', 'yAxisWidth') + get yAxisOffset() { return this.width - this.yAxisWidth; - }), + } - line: computed('data.[]', 'xScale', 'yScale', function() { + @computed('data.[]', 'xScale', 'yScale') + get line() { const { xScale, yScale, xProp, yProp } = this; const line = d3Shape @@ -204,9 +228,10 @@ export default Component.extend(WindowResizable, { .y(d => yScale(d[yProp])); return line(this.data); - }), + } - area: computed('data.[]', 'xScale', 'yScale', function() { + @computed('data.[]', 'xScale', 'yScale') + get area() { const { xScale, yScale, xProp, yProp } = this; const area = d3Shape @@ -217,7 +242,7 @@ export default Component.extend(WindowResizable, { .y1(d => yScale(d[yProp])); return area(this.data); - }), + } didInsertElement() { this.updateDimensions(); @@ -243,11 +268,11 @@ export default Component.extend(WindowResizable, { run.schedule('afterRender', this, () => this.set('isActive', false)); this.set('activeDatum', null); }); - }, + } didUpdateAttrs() { this.renderChart(); - }, + } updateActiveDatum(mouseX) { const { xScale, xProp, yScale, yProp, data } = this; @@ -278,11 +303,12 @@ export default Component.extend(WindowResizable, { left: xScale(datum[xProp]), top: yScale(datum[yProp]) - 10, }); - }, + } - updateChart: observer('data.[]', function() { + @observes('data.[]') + updateChart() { this.renderChart(); - }), + } // The renderChart method should only ever be responsible for runtime calculations // and appending d3 created elements to the DOM (such as axes). @@ -308,7 +334,7 @@ export default Component.extend(WindowResizable, { this.updateActiveDatum(this.latestMouseX); } }); - }, + } mountD3Elements() { if (!this.isDestroyed && !this.isDestroying) { @@ -316,11 +342,11 @@ export default Component.extend(WindowResizable, { d3.select(this.element.querySelector('.y-axis')).call(this.yAxis); d3.select(this.element.querySelector('.y-gridlines')).call(this.yGridlines); } - }, + } windowResizeHandler() { run.once(this, this.updateDimensions); - }, + } updateDimensions() { const $svg = this.element.querySelector('svg'); @@ -329,5 +355,5 @@ export default Component.extend(WindowResizable, { this.setProperties({ width, height }); this.renderChart(); - }, -}); + } +} diff --git a/ui/app/components/list-accordion.js b/ui/app/components/list-accordion.js index 105288f1b..373c86f8b 100644 --- a/ui/app/components/list-accordion.js +++ b/ui/app/components/list-accordion.js @@ -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; +} diff --git a/ui/app/components/list-accordion/accordion-body.js b/ui/app/components/list-accordion/accordion-body.js index 32397fce6..492ee8de5 100644 --- a/ui/app/components/list-accordion/accordion-body.js +++ b/ui/app/components/list-accordion/accordion-body.js @@ -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; +} diff --git a/ui/app/components/list-accordion/accordion-head.js b/ui/app/components/list-accordion/accordion-head.js index 9cff6e92b..320092634 100644 --- a/ui/app/components/list-accordion/accordion-head.js +++ b/ui/app/components/list-accordion/accordion-head.js @@ -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() {} +} diff --git a/ui/app/components/list-pagination.js b/ui/app/components/list-pagination.js index 322a7db27..729d70684 100644 --- a/ui/app/components/list-pagination.js +++ b/ui/app/components/list-pagination.js @@ -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); - }), -}); + } +} diff --git a/ui/app/components/list-pagination/list-pager.js b/ui/app/components/list-pagination/list-pager.js index 479865264..64dae05b7 100644 --- a/ui/app/components/list-pagination/list-pager.js +++ b/ui/app/components/list-pagination/list-pager.js @@ -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 {} diff --git a/ui/app/components/list-table.js b/ui/app/components/list-table.js index 439f1d6a4..0c06f542e 100644 --- a/ui/app/components/list-table.js +++ b/ui/app/components/list-table.js @@ -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, })); - }), -}); + } +} diff --git a/ui/app/components/list-table/sort-by.js b/ui/app/components/list-table/sort-by.js index 2186eb2e7..bdc16efa6 100644 --- a/ui/app/components/list-table/sort-by.js +++ b/ui/app/components/list-table/sort-by.js @@ -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; - }), -}); + } +} diff --git a/ui/app/components/list-table/table-body.js b/ui/app/components/list-table/table-body.js index 782917851..599a80a20 100644 --- a/ui/app/components/list-table/table-body.js +++ b/ui/app/components/list-table/table-body.js @@ -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 {} diff --git a/ui/app/components/list-table/table-head.js b/ui/app/components/list-table/table-head.js index 92a17d670..e33044d1b 100644 --- a/ui/app/components/list-table/table-head.js +++ b/ui/app/components/list-table/table-head.js @@ -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 {} diff --git a/ui/app/components/multi-select-dropdown.js b/ui/app/components/multi-select-dropdown.js index 53e438d95..a2783f736 100644 --- a/ui/app/components/multi-select-dropdown.js +++ b/ui/app/components/multi-select-dropdown.js @@ -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(); + } + } +} diff --git a/ui/app/components/page-layout.js b/ui/app/components/page-layout.js index 722b5f9eb..8c011d70f 100644 --- a/ui/app/components/page-layout.js +++ b/ui/app/components/page-layout.js @@ -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; +} diff --git a/ui/app/components/page-size-select.js b/ui/app/components/page-size-select.js index 0776d7084..2151c4c58 100644 --- a/ui/app/components/page-size-select.js +++ b/ui/app/components/page-size-select.js @@ -1,11 +1,11 @@ import Component from '@ember/component'; import { inject as service } from '@ember/service'; -export default Component.extend({ - userSettings: service(), +export default class PageSizeSelect extends Component { + @service userSettings; - tagName: '', - pageSizeOptions: Object.freeze([10, 25, 50]), + tagName = ''; + pageSizeOptions = [10, 25, 50]; - onChange() {}, -}); + onChange() {} +} diff --git a/ui/app/components/placement-failure.js b/ui/app/components/placement-failure.js index b8052235f..0b2e698ce 100644 --- a/ui/app/components/placement-failure.js +++ b/ui/app/components/placement-failure.js @@ -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; +} diff --git a/ui/app/components/plugin-allocation-row.js b/ui/app/components/plugin-allocation-row.js index 6c2a65d82..d27bf09fc 100644 --- a/ui/app/components/plugin-allocation-row.js +++ b/ui/app/components/plugin-allocation-row.js @@ -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(); } } - }, -}); + } +} diff --git a/ui/app/components/popover-menu.js b/ui/app/components/popover-menu.js index 20e5fc638..939e940b7 100644 --- a/ui/app/components/popover-menu.js +++ b/ui/app/components/popover-menu.js @@ -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(); - } } - }, - }, -}); + } + } +} diff --git a/ui/app/components/primary-metric.js b/ui/app/components/primary-metric.js index 3e017d57b..c62141ae9 100644 --- a/ui/app/components/primary-metric.js +++ b/ui/app/components/primary-metric.js @@ -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(); - }, -}); + } +} diff --git a/ui/app/components/proxy-tag.js b/ui/app/components/proxy-tag.js index 479865264..ca856391f 100644 --- a/ui/app/components/proxy-tag.js +++ b/ui/app/components/proxy-tag.js @@ -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 {} diff --git a/ui/app/components/region-switcher.js b/ui/app/components/region-switcher.js index b9fe868b7..34fc3c98e 100644 --- a/ui/app/components/region-switcher.js +++ b/ui/app/components/region-switcher.js @@ -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 }, }); - }, -}); + } +} diff --git a/ui/app/components/reschedule-event-row.js b/ui/app/components/reschedule-event-row.js index 6fc566051..218241d1b 100644 --- a/ui/app/components/reschedule-event-row.js +++ b/ui/app/components/reschedule-event-row.js @@ -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 = ''; +} diff --git a/ui/app/components/search-box.js b/ui/app/components/search-box.js index 25cf7715a..94de9724b 100644 --- a/ui/app/components/search-box.js +++ b/ui/app/components/search-box.js @@ -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; diff --git a/ui/app/components/server-agent-row.js b/ui/app/components/server-agent-row.js index 3bc395395..7118a51f5 100644 --- a/ui/app/components/server-agent-row.js +++ b/ui/app/components/server-agent-row.js @@ -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]); - }, -}); + } +} diff --git a/ui/app/components/stats-time-series.js b/ui/app/components/stats-time-series.js index 932d10f84..0bce65616 100644 --- a/ui/app/components/stats-time-series.js +++ b/ui/app/components/stats-time-series.js @@ -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)]); - }), -}); + } +} diff --git a/ui/app/components/streaming-file.js b/ui/app/components/streaming-file.js index b35963574..1386dc4c0 100644 --- a/ui/app/components/streaming-file.js +++ b/ui/app/components/streaming-file.js @@ -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(); - }, -}); + } +} diff --git a/ui/app/components/task-group-row.js b/ui/app/components/task-group-row.js index 9ee2cbe5f..399d90167 100644 --- a/ui/app/components/task-group-row.js +++ b/ui/app/components/task-group-row.js @@ -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]); - }, -}); + } +} diff --git a/ui/app/components/task-log.js b/ui/app/components/task-log.js index deaef2c89..61e74b26a 100644 --- a/ui/app/components/task-log.js +++ b/ui/app/components/task-log.js @@ -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); + } +} diff --git a/ui/app/components/task-row.js b/ui/app/components/task-row.js index 4901d1742..28094acd1 100644 --- a/ui/app/components/task-row.js +++ b/ui/app/components/task-row.js @@ -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() { - if (!this.get('task.isRunning')) return; + @computed('task', 'task.isRunning') + get stats() { + if (!this.get('task.isRunning')) return undefined; return this.statsTrackersRegistry.getTracker(this.get('task.allocation')); - }), + } - taskStats: computed('task.name', 'stats.tasks.[]', function() { - if (!this.stats) return; + @computed('task.name', 'stats.tasks.[]') + get taskStats() { + if (!this.stats) return undefined; 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(); } - }, -}); + } +} diff --git a/ui/app/components/task-subnav.js b/ui/app/components/task-subnav.js index 0aab1e6f4..702b19caa 100644 --- a/ui/app/components/task-subnav.js +++ b/ui/app/components/task-subnav.js @@ -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; +} diff --git a/ui/app/components/toggle.js b/ui/app/components/toggle.js index e4becf153..6701f64f9 100644 --- a/ui/app/components/toggle.js +++ b/ui/app/components/toggle.js @@ -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() {} +} diff --git a/ui/app/components/two-step-button.js b/ui/app/components/two-step-button.js index 15fe6aad6..a3bb4f5f0 100644 --- a/ui/app/components/two-step-button.js +++ b/ui/app/components/two-step-button.js @@ -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'); + }); + } +} diff --git a/ui/app/controllers/allocations/allocation/fs-root.js b/ui/app/controllers/allocations/allocation/fs-root.js index 2297a800e..5f732bf9c 100644 --- a/ui/app/controllers/allocations/allocation/fs-root.js +++ b/ui/app/controllers/allocations/allocation/fs-root.js @@ -1,3 +1,3 @@ import FSController from './fs'; -export default FSController.extend(); +export default class FsRootController extends FSController {} diff --git a/ui/app/controllers/allocations/allocation/fs.js b/ui/app/controllers/allocations/allocation/fs.js index 745eeb9d0..4aa267505 100644 --- a/ui/app/controllers/allocations/allocation/fs.js +++ b/ui/app/controllers/allocations/allocation/fs.js @@ -1,22 +1,27 @@ import Controller from '@ember/controller'; import { computed } from '@ember/object'; -export default Controller.extend({ - queryParams: { - sortProperty: 'sort', - sortDescending: 'desc', - }, +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 +29,5 @@ export default Controller.extend({ } else { return `/${path}`; } - }), -}); + } +} diff --git a/ui/app/controllers/allocations/allocation/index.js b/ui/app/controllers/allocations/allocation/index.js index ebccdfa75..e167cdbe6 100644 --- a/ui/app/controllers/allocations/allocation/index.js +++ b/ui/app/controllers/allocations/allocation/index.js @@ -1,58 +1,68 @@ /* eslint-disable ember/no-observers */ import Controller from '@ember/controller'; import { inject as service } from '@ember/service'; -import { computed, observer } from '@ember/object'; +import { action, computed } from '@ember/object'; +import { observes } from '@ember-decorators/object'; import { computed as overridable } from 'ember-overridable-computed'; import { alias } from '@ember/object/computed'; import { task } from 'ember-concurrency'; import Sortable from 'nomad-ui/mixins/sortable'; import { lazyClick } from 'nomad-ui/helpers/lazy-click'; import { watchRecord } from 'nomad-ui/utils/properties/watch'; +import classic from 'ember-classic-decorator'; -export default Controller.extend(Sortable, { - token: service(), +@classic +export default class IndexController extends Controller.extend(Sortable) { + @service token; - queryParams: { - sortProperty: 'sort', - sortDescending: 'desc', - }, + queryParams = [ + { + sortProperty: 'sort', + }, + { + sortDescending: 'desc', + }, + ]; - sortProperty: 'name', - sortDescending: false, + sortProperty = 'name'; + sortDescending = false; - listToSort: alias('model.states'), - sortedStates: alias('listSorted'), + @alias('model.states') listToSort; + @alias('listSorted') sortedStates; // Set in the route - preempter: null, + preempter = null; - error: overridable(() => { + @overridable(function() { // { title, description } return null; - }), + }) + error; - network: alias('model.allocatedResources.networks.firstObject'), + @alias('model.allocatedResources.networks.firstObject') network; - services: computed('model.taskGroup.services.@each.name', function() { + @computed('model.taskGroup.services.@each.name') + get services() { return this.get('model.taskGroup.services').sortBy('name'); - }), + } onDismiss() { this.set('error', null); - }, + } - watchNext: watchRecord('allocation'), + @watchRecord('allocation') watchNext; - observeWatchNext: observer('model.nextAllocation.clientStatus', function() { + @observes('model.nextAllocation.clientStatus') + observeWatchNext() { const nextAllocation = this.model.nextAllocation; if (nextAllocation && nextAllocation.content) { this.watchNext.perform(nextAllocation); } else { this.watchNext.cancelAll(); } - }), + } - stopAllocation: task(function*() { + @task(function*() { try { yield this.model.stop(); // Eagerly update the allocation clientStatus to avoid flickering @@ -63,9 +73,10 @@ export default Controller.extend(Sortable, { description: 'Your ACL token does not grant allocation lifecycle permissions.', }); } - }), + }) + stopAllocation; - restartAllocation: task(function*() { + @task(function*() { try { yield this.model.restart(); } catch (err) { @@ -74,15 +85,16 @@ export default Controller.extend(Sortable, { description: 'Your ACL token does not grant allocation lifecycle permissions.', }); } - }), + }) + restartAllocation; - actions: { - gotoTask(allocation, task) { - this.transitionToRoute('allocations.allocation.task', task); - }, + @action + gotoTask(allocation, task) { + this.transitionToRoute('allocations.allocation.task', task); + } - taskClick(allocation, task, event) { - lazyClick([() => this.send('gotoTask', allocation, task), event]); - }, - }, -}); + @action + taskClick(allocation, task, event) { + lazyClick([() => this.send('gotoTask', allocation, task), event]); + } +} diff --git a/ui/app/controllers/allocations/allocation/task/fs-root.js b/ui/app/controllers/allocations/allocation/task/fs-root.js index 2297a800e..5f732bf9c 100644 --- a/ui/app/controllers/allocations/allocation/task/fs-root.js +++ b/ui/app/controllers/allocations/allocation/task/fs-root.js @@ -1,3 +1,3 @@ import FSController from './fs'; -export default FSController.extend(); +export default class FsRootController extends FSController {} diff --git a/ui/app/controllers/allocations/allocation/task/fs.js b/ui/app/controllers/allocations/allocation/task/fs.js index a0b0720cc..ee708fe28 100644 --- a/ui/app/controllers/allocations/allocation/task/fs.js +++ b/ui/app/controllers/allocations/allocation/task/fs.js @@ -1,22 +1,27 @@ import Controller from '@ember/controller'; import { computed } from '@ember/object'; -export default Controller.extend({ - queryParams: { - sortProperty: 'sort', - sortDescending: 'desc', - }, +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 +29,5 @@ export default Controller.extend({ } else { return `/${path}`; } - }), -}); + } +} diff --git a/ui/app/controllers/allocations/allocation/task/index.js b/ui/app/controllers/allocations/allocation/task/index.js index 84a62c675..994f0319f 100644 --- a/ui/app/controllers/allocations/allocation/task/index.js +++ b/ui/app/controllers/allocations/allocation/task/index.js @@ -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; +} diff --git a/ui/app/controllers/application.js b/ui/app/controllers/application.js index 0a1597503..a6b8756de 100644 --- a/ui/app/controllers/application.js +++ b/ui/app/controllers/application.js @@ -2,49 +2,61 @@ 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: { - region: 'region', - }, + 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 +67,5 @@ export default Controller.extend({ console.warn('UNRECOVERABLE ERROR:', this.error); }); } - }), -}); + } +} diff --git a/ui/app/controllers/clients.js b/ui/app/controllers/clients.js index f4d0631dc..673868f9b 100644 --- a/ui/app/controllers/clients.js +++ b/ui/app/controllers/clients.js @@ -1,5 +1,5 @@ import Controller from '@ember/controller'; -export default Controller.extend({ - isForbidden: false, -}); +export default class ClientsController extends Controller { + isForbidden = false; +} diff --git a/ui/app/controllers/clients/client.js b/ui/app/controllers/clients/client.js index 260fdc4fa..9b0121e08 100644 --- a/ui/app/controllers/clients/client.js +++ b/ui/app/controllers/clients/client.js @@ -1,84 +1,99 @@ /* eslint-disable ember/no-observers */ import { alias } from '@ember/object/computed'; import Controller from '@ember/controller'; -import { computed, observer } from '@ember/object'; +import { action, computed } from '@ember/object'; +import { observes } from '@ember-decorators/object'; import { task } from 'ember-concurrency'; import Sortable from 'nomad-ui/mixins/sortable'; import Searchable from 'nomad-ui/mixins/searchable'; import messageFromAdapterError from 'nomad-ui/utils/message-from-adapter-error'; +import classic from 'ember-classic-decorator'; -export default Controller.extend(Sortable, Searchable, { - queryParams: { - currentPage: 'page', - searchTerm: 'search', - sortProperty: 'sort', - sortDescending: 'desc', - onlyPreemptions: 'preemptions', - }, +@classic +export default class ClientController extends Controller.extend(Sortable, Searchable) { + queryParams = [ + { + currentPage: 'page', + }, + { + searchTerm: 'search', + }, + { + sortProperty: 'sort', + }, + { + sortDescending: 'desc', + }, + { + onlyPreemptions: 'preemptions', + }, + ]; // Set in the route - flagAsDraining: false, + flagAsDraining = false; - currentPage: 1, - pageSize: 8, + currentPage = 1; + pageSize = 8; - sortProperty: 'modifyIndex', - sortDescending: true, + sortProperty = 'modifyIndex'; + sortDescending = true; - searchProps: computed(function() { + @computed() + get searchProps() { return ['shortId', 'name']; - }), + } - onlyPreemptions: false, + onlyPreemptions = false; - visibleAllocations: computed( - 'model.allocations.[]', - 'preemptions.[]', - 'onlyPreemptions', - function() { - return this.onlyPreemptions ? this.preemptions : this.model.allocations; - } - ), + @computed('model.allocations.[]', 'preemptions.[]', 'onlyPreemptions') + get visibleAllocations() { + return this.onlyPreemptions ? this.preemptions : this.model.allocations; + } - listToSort: alias('visibleAllocations'), - listToSearch: alias('listSorted'), - sortedAllocations: alias('listSearched'), + @alias('visibleAllocations') listToSort; + @alias('listSorted') listToSearch; + @alias('listSearched') sortedAllocations; - eligibilityError: null, - stopDrainError: null, - drainError: null, - showDrainNotification: false, - showDrainUpdateNotification: false, - showDrainStoppedNotification: false, + eligibilityError = null; + stopDrainError = null; + drainError = null; + showDrainNotification = false; + showDrainUpdateNotification = false; + showDrainStoppedNotification = false; - preemptions: computed('model.allocations.@each.wasPreempted', function() { + @computed('model.allocations.@each.wasPreempted') + get preemptions() { return this.model.allocations.filterBy('wasPreempted'); - }), + } - sortedEvents: computed('model.events.@each.time', function() { + @computed('model.events.@each.time') + get sortedEvents() { return this.get('model.events') .sortBy('time') .reverse(); - }), + } - sortedDrivers: computed('model.drivers.@each.name', function() { + @computed('model.drivers.@each.name') + get sortedDrivers() { return this.get('model.drivers').sortBy('name'); - }), + } - sortedHostVolumes: computed('model.hostVolumes.@each.name', function() { + @computed('model.hostVolumes.@each.name') + get sortedHostVolumes() { return this.model.hostVolumes.sortBy('name'); - }), + } - setEligibility: task(function*(value) { + @(task(function*(value) { try { yield value ? this.model.setEligible() : this.model.setIneligible(); } catch (err) { const error = messageFromAdapterError(err) || 'Could not set eligibility'; this.set('eligibilityError', error); } - }).drop(), + }).drop()) + setEligibility; - stopDrain: task(function*() { + @(task(function*() { try { this.set('flagAsDraining', false); yield this.model.cancelDrain(); @@ -88,9 +103,10 @@ export default Controller.extend(Sortable, Searchable, { const error = messageFromAdapterError(err) || 'Could not stop drain'; this.set('stopDrainError', error); } - }).drop(), + }).drop()) + stopDrain; - forceDrain: task(function*() { + @(task(function*() { try { yield this.model.forceDrain({ IgnoreSystemJobs: this.model.drainStrategy.ignoreSystemJobs, @@ -99,32 +115,36 @@ export default Controller.extend(Sortable, Searchable, { const error = messageFromAdapterError(err) || 'Could not force drain'; this.set('drainError', error); } - }).drop(), + }).drop()) + forceDrain; - triggerDrainNotification: observer('model.isDraining', function() { + @observes('model.isDraining') + triggerDrainNotification() { if (!this.model.isDraining && this.flagAsDraining) { this.set('showDrainNotification', true); } this.set('flagAsDraining', this.model.isDraining); - }), + } - actions: { - gotoAllocation(allocation) { - this.transitionToRoute('allocations.allocation', allocation); - }, + @action + gotoAllocation(allocation) { + this.transitionToRoute('allocations.allocation', allocation); + } - setPreemptionFilter(value) { - this.set('onlyPreemptions', value); - }, + @action + setPreemptionFilter(value) { + this.set('onlyPreemptions', value); + } - drainNotify(isUpdating) { - this.set('showDrainUpdateNotification', isUpdating); - }, + @action + drainNotify(isUpdating) { + this.set('showDrainUpdateNotification', isUpdating); + } - drainError(err) { - const error = messageFromAdapterError(err) || 'Could not run drain'; - this.set('drainError', error); - }, - }, -}); + @action + setDrainError(err) { + const error = messageFromAdapterError(err) || 'Could not run drain'; + this.set('drainError', error); + } +} diff --git a/ui/app/controllers/clients/index.js b/ui/app/controllers/clients/index.js index 78de1aa35..31398ab85 100644 --- a/ui/app/controllers/clients/index.js +++ b/ui/app/controllers/clients/index.js @@ -2,153 +2,174 @@ 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: { + 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); + } +} diff --git a/ui/app/controllers/csi/plugins.js b/ui/app/controllers/csi/plugins.js index f4d0631dc..b9a8c5a54 100644 --- a/ui/app/controllers/csi/plugins.js +++ b/ui/app/controllers/csi/plugins.js @@ -1,5 +1,5 @@ import Controller from '@ember/controller'; -export default Controller.extend({ - isForbidden: false, -}); +export default class PluginsController extends Controller { + isForbidden = false; +} diff --git a/ui/app/controllers/csi/plugins/index.js b/ui/app/controllers/csi/plugins/index.js index 66a888416..bd9b3d2e5 100644 --- a/ui/app/controllers/csi/plugins/index.js +++ b/ui/app/controllers/csi/plugins/index.js @@ -1,53 +1,64 @@ import { inject as service } from '@ember/service'; -import { computed } from '@ember/object'; +import { action, computed } from '@ember/object'; import { alias, readOnly } from '@ember/object/computed'; import Controller, { inject as controller } from '@ember/controller'; import SortableFactory from 'nomad-ui/mixins/sortable-factory'; import Searchable from 'nomad-ui/mixins/searchable'; import { lazyClick } from 'nomad-ui/helpers/lazy-click'; +import classic from 'ember-classic-decorator'; -export default Controller.extend( - SortableFactory([ - 'plainId', - 'controllersHealthyProportion', - 'nodesHealthyProportion', - 'provider', - ]), - Searchable, - { - userSettings: service(), - pluginsController: controller('csi/plugins'), +@classic +export default class IndexController extends Controller.extend( + SortableFactory([ + 'plainId', + 'controllersHealthyProportion', + 'nodesHealthyProportion', + 'provider', + ]), + Searchable + ) { + @service userSettings; + @controller('csi/plugins') pluginsController; - isForbidden: alias('pluginsController.isForbidden'), + @alias('pluginsController.isForbidden') isForbidden; - queryParams: { + queryParams = [ + { currentPage: 'page', + }, + { searchTerm: 'search', + }, + { sortProperty: 'sort', + }, + { sortDescending: 'desc', }, + ]; - currentPage: 1, - pageSize: readOnly('userSettings.pageSize'), + currentPage = 1; + @readOnly('userSettings.pageSize') pageSize; - searchProps: computed(function() { - return ['id']; - }), - fuzzySearchProps: computed(function() { - return ['id']; - }), - - sortProperty: 'id', - sortDescending: false, - - listToSort: alias('model'), - listToSearch: alias('listSorted'), - sortedPlugins: alias('listSearched'), - - actions: { - gotoPlugin(plugin, event) { - lazyClick([() => this.transitionToRoute('csi.plugins.plugin', plugin.plainId), event]); - }, - }, + @computed + get searchProps() { + return ['id']; } -); + + @computed + get fuzzySearchProps() { + return ['id']; + } + + sortProperty = 'id'; + sortDescending = false; + + @alias('model') listToSort; + @alias('listSorted') listToSearch; + @alias('listSearched') sortedPlugins; + + @action + gotoPlugin(plugin, event) { + lazyClick([() => this.transitionToRoute('csi.plugins.plugin', plugin.plainId), event]); + } +} diff --git a/ui/app/controllers/csi/plugins/plugin/allocations.js b/ui/app/controllers/csi/plugins/plugin/allocations.js index 941235693..53f7712a4 100644 --- a/ui/app/controllers/csi/plugins/plugin/allocations.js +++ b/ui/app/controllers/csi/plugins/plugin/allocations.js @@ -1,86 +1,102 @@ import Controller from '@ember/controller'; import { inject as service } from '@ember/service'; -import { computed } from '@ember/object'; +import { action, computed } from '@ember/object'; import { alias, readOnly } from '@ember/object/computed'; import SortableFactory from 'nomad-ui/mixins/sortable-factory'; import { lazyClick } from 'nomad-ui/helpers/lazy-click'; import { serialize, deserializedQueryParam as selection } from 'nomad-ui/utils/qp-serialize'; +import classic from 'ember-classic-decorator'; -export default Controller.extend(SortableFactory(['updateTime', 'healthy']), { - userSettings: service(), +@classic +export default class AllocationsController extends Controller.extend( + SortableFactory(['updateTime', 'healthy']) + ) { + @service userSettings; - queryParams: { - currentPage: 'page', - sortProperty: 'sort', - sortDescending: 'desc', - qpHealth: 'healthy', - qpType: 'type', - }, + queryParams = [ + { + currentPage: 'page', + }, + { + sortProperty: 'sort', + }, + { + sortDescending: 'desc', + }, + { + qpHealth: 'healthy', + }, + { + qpType: 'type', + }, + ]; - currentPage: 1, - pageSize: readOnly('userSettings.pageSize'), + currentPage = 1; + @readOnly('userSettings.pageSize') pageSize; - sortProperty: 'updateTime', - sortDescending: false, + sortProperty = 'updateTime'; + sortDescending = false; - qpType: '', - qpHealth: '', + qpType = ''; + qpHealth = ''; - selectionType: selection('qpType'), - selectionHealth: selection('qpHealth'), + @selection('qpType') selectionType; + @selection('qpHealth') selectionHealth; - optionsType: computed(function() { + @computed + get optionsType() { return [{ key: 'controller', label: 'Controller' }, { key: 'node', label: 'Node' }]; - }), + } - optionsHealth: computed(function() { + @computed + get optionsHealth() { return [{ key: 'true', label: 'Healthy' }, { key: 'false', label: 'Unhealthy' }]; - }), + } - combinedAllocations: computed('model.{controllers.[],nodes.[]}', function() { + @computed('model.{controllers.[],nodes.[]}') + get combinedAllocations() { return this.model.controllers.toArray().concat(this.model.nodes.toArray()); - }), + } - filteredAllocations: computed( + @computed( 'combinedAllocations.[]', 'model.{controllers.[],nodes.[]}', 'selectionType', - 'selectionHealth', - function() { - const { selectionType: types, selectionHealth: healths } = this; + 'selectionHealth' + ) + get filteredAllocations() { + const { selectionType: types, selectionHealth: healths } = this; - // Instead of filtering the combined list, revert back to one of the two - // pre-existing lists. - let listToFilter = this.combinedAllocations; - if (types.length === 1 && types[0] === 'controller') { - listToFilter = this.model.controllers; - } else if (types.length === 1 && types[0] === 'node') { - listToFilter = this.model.nodes; - } - - if (healths.length === 1 && healths[0] === 'true') return listToFilter.filterBy('healthy'); - if (healths.length === 1 && healths[0] === 'false') - return listToFilter.filterBy('healthy', false); - return listToFilter; + // Instead of filtering the combined list, revert back to one of the two + // pre-existing lists. + let listToFilter = this.combinedAllocations; + if (types.length === 1 && types[0] === 'controller') { + listToFilter = this.model.controllers; + } else if (types.length === 1 && types[0] === 'node') { + listToFilter = this.model.nodes; } - ), - listToSort: alias('filteredAllocations'), - sortedAllocations: alias('listSorted'), + if (healths.length === 1 && healths[0] === 'true') return listToFilter.filterBy('healthy'); + if (healths.length === 1 && healths[0] === 'false') + return listToFilter.filterBy('healthy', false); + return listToFilter; + } + + @alias('filteredAllocations') listToSort; + @alias('listSorted') sortedAllocations; resetPagination() { if (this.currentPage != null) { this.set('currentPage', 1); } - }, + } setFacetQueryParam(queryParam, selection) { this.set(queryParam, serialize(selection)); - }, + } - actions: { - gotoAllocation(allocation, event) { - lazyClick([() => this.transitionToRoute('allocations.allocation', allocation), event]); - }, - }, -}); + @action + gotoAllocation(allocation, event) { + lazyClick([() => this.transitionToRoute('allocations.allocation', allocation), event]); + } +} diff --git a/ui/app/controllers/csi/plugins/plugin/index.js b/ui/app/controllers/csi/plugins/plugin/index.js index fcd6977e6..863eaff63 100644 --- a/ui/app/controllers/csi/plugins/plugin/index.js +++ b/ui/app/controllers/csi/plugins/plugin/index.js @@ -1,18 +1,19 @@ import Controller from '@ember/controller'; -import { computed } from '@ember/object'; +import { action, computed } from '@ember/object'; -export default Controller.extend({ - sortedControllers: computed('model.controllers.@each.updateTime', function() { +export default class IndexController extends Controller { + @computed('model.controllers.@each.updateTime') + get sortedControllers() { return this.model.controllers.sortBy('updateTime').reverse(); - }), + } - sortedNodes: computed('model.nodes.@each.updateTime', function() { + @computed('model.nodes.@each.updateTime') + get sortedNodes() { return this.model.nodes.sortBy('updateTime').reverse(); - }), + } - actions: { - gotoAllocation(allocation) { - this.transitionToRoute('allocations.allocation', allocation); - }, - }, -}); + @action + gotoAllocation(allocation) { + this.transitionToRoute('allocations.allocation', allocation); + } +} diff --git a/ui/app/controllers/csi/volumes.js b/ui/app/controllers/csi/volumes.js index bd4bec291..94772e3bc 100644 --- a/ui/app/controllers/csi/volumes.js +++ b/ui/app/controllers/csi/volumes.js @@ -1,11 +1,13 @@ import Controller from '@ember/controller'; -export default Controller.extend({ - queryParams: { - volumeNamespace: 'namespace', - }, +export default class VolumesController extends Controller { + queryParams = [ + { + volumeNamespace: 'namespace', + }, + ]; - isForbidden: false, + isForbidden = false; - volumeNamespace: 'default', -}); + volumeNamespace = 'default'; +} diff --git a/ui/app/controllers/csi/volumes/index.js b/ui/app/controllers/csi/volumes/index.js index 37a17234a..26771aa85 100644 --- a/ui/app/controllers/csi/volumes/index.js +++ b/ui/app/controllers/csi/volumes/index.js @@ -1,75 +1,86 @@ import { inject as service } from '@ember/service'; -import { computed } from '@ember/object'; +import { action, computed } from '@ember/object'; import { alias, readOnly } from '@ember/object/computed'; import Controller, { inject as controller } from '@ember/controller'; import SortableFactory from 'nomad-ui/mixins/sortable-factory'; import Searchable from 'nomad-ui/mixins/searchable'; import { lazyClick } from 'nomad-ui/helpers/lazy-click'; +import classic from 'ember-classic-decorator'; -export default Controller.extend( - SortableFactory([ - 'id', - 'schedulable', - 'controllersHealthyProportion', - 'nodesHealthyProportion', - 'provider', - ]), - Searchable, - { - system: service(), - userSettings: service(), - volumesController: controller('csi/volumes'), +@classic +export default class IndexController extends Controller.extend( + SortableFactory([ + 'id', + 'schedulable', + 'controllersHealthyProportion', + 'nodesHealthyProportion', + 'provider', + ]), + Searchable + ) { + @service system; + @service userSettings; + @controller('csi/volumes') volumesController; - isForbidden: alias('volumesController.isForbidden'), + @alias('volumesController.isForbidden') + isForbidden; - queryParams: { + queryParams = [ + { currentPage: 'page', + }, + { searchTerm: 'search', + }, + { sortProperty: 'sort', + }, + { sortDescending: 'desc', }, + ]; - currentPage: 1, - pageSize: readOnly('userSettings.pageSize'), + currentPage = 1; + @readOnly('userSettings.pageSize') pageSize; - sortProperty: 'id', - sortDescending: false, + sortProperty = 'id'; + sortDescending = false; - searchProps: computed(function() { - return ['name']; - }), - fuzzySearchProps: computed(function() { - return ['name']; - }), - fuzzySearchEnabled: true, - - /** - Visible volumes are those that match the selected namespace - */ - visibleVolumes: computed('model.{[],@each.parent}', function() { - if (!this.model) return []; - - // Namespace related properties are ommitted from the dependent keys - // due to a prop invalidation bug caused by region switching. - const hasNamespaces = this.get('system.namespaces.length'); - const activeNamespace = this.get('system.activeNamespace.id') || 'default'; - - return this.model - .compact() - .filter(volume => !hasNamespaces || volume.get('namespace.id') === activeNamespace); - }), - - listToSort: alias('visibleVolumes'), - listToSearch: alias('listSorted'), - sortedVolumes: alias('listSearched'), - - actions: { - gotoVolume(volume, event) { - lazyClick([ - () => this.transitionToRoute('csi.volumes.volume', volume.get('plainId')), - event, - ]); - }, - }, + @computed + get searchProps() { + return ['name']; } -); + + @computed + get fuzzySearchProps() { + return ['name']; + } + + fuzzySearchEnabled = true; + + /** + Visible volumes are those that match the selected namespace + */ + @computed('model.{[],@each.parent}') + get visibleVolumes() { + if (!this.model) return []; + + // Namespace related properties are ommitted from the dependent keys + // due to a prop invalidation bug caused by region switching. + const hasNamespaces = this.get('system.namespaces.length'); + const activeNamespace = this.get('system.activeNamespace.id') || 'default'; + + return this.model + .compact() + .filter(volume => !hasNamespaces || volume.get('namespace.id') === activeNamespace); + } + + @alias('visibleVolumes') listToSort; + @alias('listSorted') listToSearch; + @alias('listSearched') sortedVolumes; + + @action + gotoVolume(volume, event) { + lazyClick([() => this.transitionToRoute('csi.volumes.volume', volume.get('plainId')), event]); + } +} diff --git a/ui/app/controllers/csi/volumes/volume.js b/ui/app/controllers/csi/volumes/volume.js index 5e43260b4..d0f6ce12d 100644 --- a/ui/app/controllers/csi/volumes/volume.js +++ b/ui/app/controllers/csi/volumes/volume.js @@ -1,22 +1,23 @@ import Controller from '@ember/controller'; import { inject as service } from '@ember/service'; -import { computed } from '@ember/object'; +import { action, computed } from '@ember/object'; -export default Controller.extend({ +export default class VolumeController extends Controller { // Used in the template - system: service(), + @service system; - sortedReadAllocations: computed('model.readAllocations.@each.modifyIndex', function() { + @computed('model.readAllocations.@each.modifyIndex') + get sortedReadAllocations() { return this.model.readAllocations.sortBy('modifyIndex').reverse(); - }), + } - sortedWriteAllocations: computed('model.writeAllocations.@each.modifyIndex', function() { + @computed('model.writeAllocations.@each.modifyIndex') + get sortedWriteAllocations() { return this.model.writeAllocations.sortBy('modifyIndex').reverse(); - }), + } - actions: { - gotoAllocation(allocation) { - this.transitionToRoute('allocations.allocation', allocation); - }, - }, -}); + @action + gotoAllocation(allocation) { + this.transitionToRoute('allocations.allocation', allocation); + } +} diff --git a/ui/app/controllers/exec.js b/ui/app/controllers/exec.js index d6ea565fe..9e2691527 100644 --- a/ui/app/controllers/exec.js +++ b/ui/app/controllers/exec.js @@ -1,36 +1,39 @@ import { inject as service } from '@ember/service'; import Controller from '@ember/controller'; -import { computed } from '@ember/object'; +import { action, computed } from '@ember/object'; import { alias, mapBy, sort, uniq } from '@ember/object/computed'; import escapeTaskName from 'nomad-ui/utils/escape-task-name'; import ExecCommandEditorXtermAdapter from 'nomad-ui/utils/classes/exec-command-editor-xterm-adapter'; import ExecSocketXtermAdapter from 'nomad-ui/utils/classes/exec-socket-xterm-adapter'; import localStorageProperty from 'nomad-ui/utils/properties/local-storage'; +import classic from 'ember-classic-decorator'; const ANSI_UI_GRAY_400 = '\x1b[38;2;142;150;163m'; const ANSI_WHITE = '\x1b[0m'; -export default Controller.extend({ - sockets: service(), - system: service(), - token: service(), +@classic +export default class ExecController extends Controller { + @service sockets; + @service system; + @service token; - queryParams: ['allocation'], + queryParams = ['allocation']; - command: localStorageProperty('nomadExecCommand', '/bin/bash'), - socketOpen: false, + @localStorageProperty('nomadExecCommand', '/bin/bash') command; + socketOpen = false; - pendingAndRunningAllocations: computed('model.allocations.@each.clientStatus', function() { + @computed('model.allocations.@each.clientStatus') + get pendingAndRunningAllocations() { return this.model.allocations.filter( allocation => allocation.clientStatus === 'pending' || allocation.clientStatus === 'running' ); - }), + } - pendingAndRunningTaskGroups: mapBy('pendingAndRunningAllocations', 'taskGroup'), - uniquePendingAndRunningTaskGroups: uniq('pendingAndRunningTaskGroups'), + @mapBy('pendingAndRunningAllocations', 'taskGroup') pendingAndRunningTaskGroups; + @uniq('pendingAndRunningTaskGroups') uniquePendingAndRunningTaskGroups; - taskGroupSorting: Object.freeze(['name']), - sortedTaskGroups: sort('uniquePendingAndRunningTaskGroups', 'taskGroupSorting'), + taskGroupSorting = ['name']; + @sort('uniquePendingAndRunningTaskGroups', 'taskGroupSorting') sortedTaskGroups; setUpTerminal(Terminal) { this.terminal = new Terminal({ fontFamily: 'monospace', fontWeight: '400' }); @@ -38,86 +41,85 @@ export default Controller.extend({ this.terminal.write(ANSI_UI_GRAY_400); this.terminal.writeln('Select a task to start your session.'); - }, + } - allocations: alias('model.allocations'), + @alias('model.allocations') allocations; - taskState: computed( + @computed( 'allocations.{[],@each.isActive}', 'allocationShortId', 'taskName', 'taskGroupName', 'allocation', - 'allocation.states.@each.{name,isRunning}', - function() { - if (!this.allocations) { - return false; - } - - let allocation; - - if (this.allocationShortId) { - allocation = this.allocations.findBy('shortId', this.allocationShortId); - } else { - allocation = this.allocations.find(allocation => - allocation.states - .filterBy('isActive') - .mapBy('name') - .includes(this.taskName) - ); - } - - if (allocation) { - return allocation.states.find(state => state.name === this.taskName); - } - - return; + 'allocation.states.@each.{name,isRunning}' + ) + get taskState() { + if (!this.allocations) { + return false; } - ), - actions: { - setTaskProperties({ allocationShortId, taskName, taskGroupName }) { - this.setProperties({ - allocationShortId, - taskName, - taskGroupName, - }); + let allocation; - if (this.taskState) { - this.terminal.write(ANSI_UI_GRAY_400); - this.terminal.writeln(''); + if (this.allocationShortId) { + allocation = this.allocations.findBy('shortId', this.allocationShortId); + } else { + allocation = this.allocations.find(allocation => + allocation.states + .filterBy('isActive') + .mapBy('name') + .includes(this.taskName) + ); + } - if (!allocationShortId) { - this.terminal.writeln( - 'Multiple instances of this task are running. The allocation below was selected by random draw.' - ); - this.terminal.writeln(''); - } + if (allocation) { + return allocation.states.find(state => state.name === this.taskName); + } - this.terminal.writeln('Customize your command, then hit ‘return’ to run.'); - this.terminal.writeln(''); - this.terminal.write( - `$ nomad alloc exec -i -t -task ${escapeTaskName(taskName)} ${ - this.taskState.allocation.shortId - } ` - ); - - this.terminal.write(ANSI_WHITE); - - this.terminal.write(this.command); - - if (this.commandEditorAdapter) { - this.commandEditorAdapter.destroy(); - } - - this.commandEditorAdapter = new ExecCommandEditorXtermAdapter( - this.terminal, - this.openAndConnectSocket.bind(this), - this.command + return undefined; + } + + @action + setTaskProperties({ allocationShortId, taskName, taskGroupName }) { + this.setProperties({ + allocationShortId, + taskName, + taskGroupName, + }); + + if (this.taskState) { + this.terminal.write(ANSI_UI_GRAY_400); + this.terminal.writeln(''); + + if (!allocationShortId) { + this.terminal.writeln( + 'Multiple instances of this task are running. The allocation below was selected by random draw.' ); + this.terminal.writeln(''); } - }, - }, + + this.terminal.writeln('Customize your command, then hit ‘return’ to run.'); + this.terminal.writeln(''); + this.terminal.write( + `$ nomad alloc exec -i -t -task ${escapeTaskName(taskName)} ${ + this.taskState.allocation.shortId + } ` + ); + + this.terminal.write(ANSI_WHITE); + + this.terminal.write(this.command); + + if (this.commandEditorAdapter) { + this.commandEditorAdapter.destroy(); + } + + this.commandEditorAdapter = new ExecCommandEditorXtermAdapter( + this.terminal, + this.openAndConnectSocket.bind(this), + this.command + ); + } + } openAndConnectSocket(command) { if (this.taskState) { @@ -129,5 +131,5 @@ export default Controller.extend({ } else { this.terminal.writeln(`Failed to open a socket because task ${this.taskName} is not active.`); } - }, -}); + } +} diff --git a/ui/app/controllers/jobs.js b/ui/app/controllers/jobs.js index b38d2b11b..53b270e7f 100644 --- a/ui/app/controllers/jobs.js +++ b/ui/app/controllers/jobs.js @@ -1,14 +1,16 @@ import { inject as service } from '@ember/service'; import Controller from '@ember/controller'; -export default Controller.extend({ - system: service(), +export default class JobsController extends Controller { + @service system; - queryParams: { - jobNamespace: 'namespace', - }, + queryParams = [ + { + jobNamespace: 'namespace', + }, + ]; - isForbidden: false, + isForbidden = false; - jobNamespace: 'default', -}); + jobNamespace = 'default'; +} diff --git a/ui/app/controllers/jobs/index.js b/ui/app/controllers/jobs/index.js index a3c1bbad7..c6312472f 100644 --- a/ui/app/controllers/jobs/index.js +++ b/ui/app/controllers/jobs/index.js @@ -2,56 +2,79 @@ import { inject as service } from '@ember/service'; import { alias, readOnly } from '@ember/object/computed'; 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 Sortable from 'nomad-ui/mixins/sortable'; 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(Sortable, Searchable, { - system: service(), - userSettings: service(), - jobsController: controller('jobs'), +@classic +export default class IndexController extends Controller.extend(Sortable, Searchable) { + @service system; + @service userSettings; + @controller('jobs') jobsController; - isForbidden: alias('jobsController.isForbidden'), + @alias('jobsController.isForbidden') isForbidden; - queryParams: { - currentPage: 'page', - searchTerm: 'search', - sortProperty: 'sort', - sortDescending: 'desc', - qpType: 'type', - qpStatus: 'status', - qpDatacenter: 'dc', - qpPrefix: 'prefix', - }, + queryParams = [ + { + currentPage: 'page', + }, + { + searchTerm: 'search', + }, + { + sortProperty: 'sort', + }, + { + sortDescending: 'desc', + }, + { + qpType: 'type', + }, + { + qpStatus: 'status', + }, + { + qpDatacenter: 'dc', + }, + { + qpPrefix: 'prefix', + }, + ]; - currentPage: 1, - pageSize: readOnly('userSettings.pageSize'), + currentPage = 1; + @readOnly('userSettings.pageSize') pageSize; - sortProperty: 'modifyIndex', - sortDescending: true, + sortProperty = 'modifyIndex'; + sortDescending = true; - searchProps: computed(function() { + @computed + get searchProps() { return ['id', 'name']; - }), - fuzzySearchProps: computed(function() { + } + + @computed + get fuzzySearchProps() { return ['name']; - }), - fuzzySearchEnabled: true, + } - qpType: '', - qpStatus: '', - qpDatacenter: '', - qpPrefix: '', + fuzzySearchEnabled = true; - selectionType: selection('qpType'), - selectionStatus: selection('qpStatus'), - selectionDatacenter: selection('qpDatacenter'), - selectionPrefix: selection('qpPrefix'), + qpType = ''; + qpStatus = ''; + qpDatacenter = ''; + qpPrefix = ''; - optionsType: computed(function() { + @selection('qpType') selectionType; + @selection('qpStatus') selectionStatus; + @selection('qpDatacenter') selectionDatacenter; + @selection('qpPrefix') selectionPrefix; + + @computed + get optionsType() { return [ { key: 'batch', label: 'Batch' }, { key: 'parameterized', label: 'Parameterized' }, @@ -59,17 +82,19 @@ export default Controller.extend(Sortable, Searchable, { { key: 'service', label: 'Service' }, { key: 'system', label: 'System' }, ]; - }), + } - optionsStatus: computed(function() { + @computed + get optionsStatus() { return [ { key: 'pending', label: 'Pending' }, { key: 'running', label: 'Running' }, { key: 'dead', label: 'Dead' }, ]; - }), + } - optionsDatacenter: computed('visibleJobs.[]', function() { + @computed('visibleJobs.[]') + get optionsDatacenter() { const flatten = (acc, val) => acc.concat(val); const allDatacenters = new Set(this.visibleJobs.mapBy('datacenters').reduce(flatten, [])); @@ -84,9 +109,10 @@ export default Controller.extend(Sortable, Searchable, { }); return availableDatacenters.sort().map(dc => ({ key: dc, label: dc })); - }), + } - optionsPrefix: computed('visibleJobs.[]', function() { + @computed('visibleJobs.[]') + get optionsPrefix() { // A prefix is defined as the start of a job name up to the first - or . // ex: mktg-analytics -> mktg, ds.supermodel.classifier -> ds const hasPrefix = /.[-._]/; @@ -122,13 +148,14 @@ export default Controller.extend(Sortable, Searchable, { key: name.prefix, label: `${name.prefix} (${name.count})`, })); - }), + } /** Visible jobs are those that match the selected namespace and aren't children of periodic or parameterized jobs. */ - visibleJobs: computed('model.{[],@each.parent}', function() { + @computed('model.{[],@each.parent}') + get visibleJobs() { // Namespace related properties are ommitted from the dependent keys // due to a prop invalidation bug caused by region switching. const hasNamespaces = this.get('system.namespaces.length'); @@ -138,60 +165,59 @@ export default Controller.extend(Sortable, Searchable, { .compact() .filter(job => !hasNamespaces || job.get('namespace.id') === activeNamespace) .filter(job => !job.get('parent.content')); - }), + } - filteredJobs: computed( + @computed( 'visibleJobs.[]', 'selectionType', 'selectionStatus', 'selectionDatacenter', - 'selectionPrefix', - function() { - const { - selectionType: types, - selectionStatus: statuses, - selectionDatacenter: datacenters, - selectionPrefix: prefixes, - } = this; + 'selectionPrefix' + ) + get filteredJobs() { + const { + selectionType: types, + selectionStatus: statuses, + selectionDatacenter: datacenters, + selectionPrefix: prefixes, + } = this; - // A job must match ALL filter facets, but it can match ANY selection within a facet - // Always return early to prevent unnecessary facet predicates. - return this.visibleJobs.filter(job => { - if (types.length && !types.includes(job.get('displayType'))) { - return false; - } + // A job must match ALL filter facets, but it can match ANY selection within a facet + // Always return early to prevent unnecessary facet predicates. + return this.visibleJobs.filter(job => { + if (types.length && !types.includes(job.get('displayType'))) { + return false; + } - if (statuses.length && !statuses.includes(job.get('status'))) { - return false; - } + if (statuses.length && !statuses.includes(job.get('status'))) { + return false; + } - if (datacenters.length && !job.get('datacenters').find(dc => datacenters.includes(dc))) { - return false; - } + if (datacenters.length && !job.get('datacenters').find(dc => datacenters.includes(dc))) { + return false; + } - const name = job.get('name'); - if (prefixes.length && !prefixes.find(prefix => name.startsWith(prefix))) { - return false; - } + const name = job.get('name'); + if (prefixes.length && !prefixes.find(prefix => name.startsWith(prefix))) { + return false; + } - return true; - }); - } - ), + return true; + }); + } - listToSort: alias('filteredJobs'), - listToSearch: alias('listSorted'), - sortedJobs: alias('listSearched'), + @alias('filteredJobs') listToSort; + @alias('listSorted') listToSearch; + @alias('listSearched') sortedJobs; - isShowingDeploymentDetails: false, + isShowingDeploymentDetails = false; setFacetQueryParam(queryParam, selection) { this.set(queryParam, serialize(selection)); - }, + } - actions: { - gotoJob(job) { - this.transitionToRoute('jobs.job', job.get('plainId')); - }, - }, -}); + @action + gotoJob(job) { + this.transitionToRoute('jobs.job', job.get('plainId')); + } +} diff --git a/ui/app/controllers/jobs/job/allocations.js b/ui/app/controllers/jobs/job/allocations.js index 0de958725..a01c19951 100644 --- a/ui/app/controllers/jobs/job/allocations.js +++ b/ui/app/controllers/jobs/job/allocations.js @@ -1,41 +1,56 @@ import { alias } from '@ember/object/computed'; import Controller from '@ember/controller'; -import { computed } from '@ember/object'; +import { action, computed } from '@ember/object'; import Sortable from 'nomad-ui/mixins/sortable'; import Searchable from 'nomad-ui/mixins/searchable'; import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting'; +import classic from 'ember-classic-decorator'; -export default Controller.extend(Sortable, Searchable, WithNamespaceResetting, { - queryParams: { - currentPage: 'page', - searchTerm: 'search', - sortProperty: 'sort', - sortDescending: 'desc', - }, - - currentPage: 1, - pageSize: 25, - - sortProperty: 'modifyIndex', - sortDescending: true, - - job: alias('model'), - - searchProps: computed(function() { - return ['shortId', 'name', 'taskGroupName']; - }), - - allocations: computed('model.allocations.[]', function() { - return this.get('model.allocations') || []; - }), - - listToSort: alias('allocations'), - listToSearch: alias('listSorted'), - sortedAllocations: alias('listSearched'), - - actions: { - gotoAllocation(allocation) { - this.transitionToRoute('allocations.allocation', allocation); +@classic +export default class AllocationsController extends Controller.extend( + Sortable, + Searchable, + WithNamespaceResetting + ) { + queryParams = [ + { + currentPage: 'page', }, - }, -}); + { + searchTerm: 'search', + }, + { + sortProperty: 'sort', + }, + { + sortDescending: 'desc', + }, + ]; + + currentPage = 1; + pageSize = 25; + + sortProperty = 'modifyIndex'; + sortDescending = true; + + @alias('model') job; + + @computed + get searchProps() { + return ['shortId', 'name', 'taskGroupName']; + } + + @computed('model.allocations.[]') + get allocations() { + return this.get('model.allocations') || []; + } + + @alias('allocations') listToSort; + @alias('listSorted') listToSearch; + @alias('listSearched') sortedAllocations; + + @action + gotoAllocation(allocation) { + this.transitionToRoute('allocations.allocation', allocation); + } +} diff --git a/ui/app/controllers/jobs/job/definition.js b/ui/app/controllers/jobs/job/definition.js index 910255c2d..16c3390f5 100644 --- a/ui/app/controllers/jobs/job/definition.js +++ b/ui/app/controllers/jobs/job/definition.js @@ -1,25 +1,27 @@ import Controller from '@ember/controller'; import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting'; import { alias } from '@ember/object/computed'; +import classic from 'ember-classic-decorator'; -export default Controller.extend(WithNamespaceResetting, { - job: alias('model.job'), - definition: alias('model.definition'), +@classic +export default class DefinitionController extends Controller.extend(WithNamespaceResetting) { + @alias('model.job') job; + @alias('model.definition') definition; - isEditing: false, + isEditing = false; edit() { this.job.set('_newDefinition', JSON.stringify(this.definition, null, 2)); this.set('isEditing', true); - }, + } onCancel() { this.set('isEditing', false); - }, + } onSubmit(id, namespace) { this.transitionToRoute('jobs.job', id, { queryParams: { jobNamespace: namespace }, }); - }, -}); + } +} diff --git a/ui/app/controllers/jobs/job/deployments.js b/ui/app/controllers/jobs/job/deployments.js index 1c50d92e9..cbce847e2 100644 --- a/ui/app/controllers/jobs/job/deployments.js +++ b/ui/app/controllers/jobs/job/deployments.js @@ -1,7 +1,9 @@ import Controller from '@ember/controller'; import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting'; import { alias } from '@ember/object/computed'; +import classic from 'ember-classic-decorator'; -export default Controller.extend(WithNamespaceResetting, { - job: alias('model'), -}); +@classic +export default class DeploymentsController extends Controller.extend(WithNamespaceResetting) { + @alias('model') job; +} diff --git a/ui/app/controllers/jobs/job/evaluations.js b/ui/app/controllers/jobs/job/evaluations.js index a6da8940f..831cb3f7c 100644 --- a/ui/app/controllers/jobs/job/evaluations.js +++ b/ui/app/controllers/jobs/job/evaluations.js @@ -2,19 +2,28 @@ import { alias } from '@ember/object/computed'; import Controller from '@ember/controller'; import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting'; import Sortable from 'nomad-ui/mixins/sortable'; +import classic from 'ember-classic-decorator'; -export default Controller.extend(WithNamespaceResetting, Sortable, { - queryParams: { - sortProperty: 'sort', - sortDescending: 'desc', - }, +@classic +export default class EvaluationsController extends Controller.extend( + WithNamespaceResetting, + Sortable + ) { + queryParams = [ + { + sortProperty: 'sort', + }, + { + sortDescending: 'desc', + }, + ]; - sortProperty: 'modifyIndex', - sortDescending: true, + sortProperty = 'modifyIndex'; + sortDescending = true; - job: alias('model'), - evaluations: alias('model.evaluations'), + @alias('model') job; + @alias('model.evaluations') evaluations; - listToSort: alias('evaluations'), - sortedEvaluations: alias('listSorted'), -}); + @alias('evaluations') listToSort; + @alias('listSorted') sortedEvaluations; +} diff --git a/ui/app/controllers/jobs/job/index.js b/ui/app/controllers/jobs/job/index.js index 20fc91127..eae91858f 100644 --- a/ui/app/controllers/jobs/job/index.js +++ b/ui/app/controllers/jobs/job/index.js @@ -2,32 +2,41 @@ import { inject as service } from '@ember/service'; import { alias } from '@ember/object/computed'; import Controller from '@ember/controller'; import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting'; +import { action } from '@ember/object'; +import classic from 'ember-classic-decorator'; -export default Controller.extend(WithNamespaceResetting, { - system: service(), +@classic +export default class IndexController extends Controller.extend(WithNamespaceResetting) { + @service system; - queryParams: { - currentPage: 'page', - sortProperty: 'sort', - sortDescending: 'desc', - }, - - currentPage: 1, - - job: alias('model'), - - sortProperty: 'name', - sortDescending: false, - - actions: { - gotoTaskGroup(taskGroup) { - this.transitionToRoute('jobs.job.task-group', taskGroup.get('job'), taskGroup); + queryParams = [ + { + currentPage: 'page', }, - - gotoJob(job) { - this.transitionToRoute('jobs.job', job, { - queryParams: { jobNamespace: job.get('namespace.name') }, - }); + { + sortProperty: 'sort', }, - }, -}); + { + sortDescending: 'desc', + }, + ]; + + currentPage = 1; + + @alias('model') job; + + sortProperty = 'name'; + sortDescending = false; + + @action + gotoTaskGroup(taskGroup) { + this.transitionToRoute('jobs.job.task-group', taskGroup.get('job'), taskGroup); + } + + @action + gotoJob(job) { + this.transitionToRoute('jobs.job', job, { + queryParams: { jobNamespace: job.get('namespace.name') }, + }); + } +} diff --git a/ui/app/controllers/jobs/job/task-group.js b/ui/app/controllers/jobs/job/task-group.js index 95127c4ed..a20fdc52f 100644 --- a/ui/app/controllers/jobs/job/task-group.js +++ b/ui/app/controllers/jobs/job/task-group.js @@ -1,42 +1,57 @@ -import { alias, readOnly } from '@ember/object/computed'; import { inject as service } from '@ember/service'; +import { alias, readOnly } from '@ember/object/computed'; import Controller from '@ember/controller'; -import { computed } from '@ember/object'; +import { action, computed } from '@ember/object'; import Sortable from 'nomad-ui/mixins/sortable'; import Searchable from 'nomad-ui/mixins/searchable'; import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting'; +import classic from 'ember-classic-decorator'; -export default Controller.extend(Sortable, Searchable, WithNamespaceResetting, { - userSettings: service(), +@classic +export default class TaskGroupController extends Controller.extend( + Sortable, + Searchable, + WithNamespaceResetting + ) { + @service userSettings; - queryParams: { - currentPage: 'page', - searchTerm: 'search', - sortProperty: 'sort', - sortDescending: 'desc', - }, - - currentPage: 1, - pageSize: readOnly('userSettings.pageSize'), - - sortProperty: 'modifyIndex', - sortDescending: true, - - searchProps: computed(function() { - return ['shortId', 'name']; - }), - - allocations: computed('model.allocations.[]', function() { - return this.get('model.allocations') || []; - }), - - listToSort: alias('allocations'), - listToSearch: alias('listSorted'), - sortedAllocations: alias('listSearched'), - - actions: { - gotoAllocation(allocation) { - this.transitionToRoute('allocations.allocation', allocation); + queryParams = [ + { + currentPage: 'page', }, - }, -}); + { + searchTerm: 'search', + }, + { + sortProperty: 'sort', + }, + { + sortDescending: 'desc', + }, + ]; + + currentPage = 1; + @readOnly('userSettings.pageSize') pageSize; + + sortProperty = 'modifyIndex'; + sortDescending = true; + + @computed + get searchProps() { + return ['shortId', 'name']; + } + + @computed('model.allocations.[]') + get allocations() { + return this.get('model.allocations') || []; + } + + @alias('allocations') listToSort; + @alias('listSorted') listToSearch; + @alias('listSearched') sortedAllocations; + + @action + gotoAllocation(allocation) { + this.transitionToRoute('allocations.allocation', allocation); + } +} diff --git a/ui/app/controllers/jobs/job/versions.js b/ui/app/controllers/jobs/job/versions.js index 1c50d92e9..ca1989177 100644 --- a/ui/app/controllers/jobs/job/versions.js +++ b/ui/app/controllers/jobs/job/versions.js @@ -1,7 +1,9 @@ import Controller from '@ember/controller'; import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting'; import { alias } from '@ember/object/computed'; +import classic from 'ember-classic-decorator'; -export default Controller.extend(WithNamespaceResetting, { - job: alias('model'), -}); +@classic +export default class VersionsController extends Controller.extend(WithNamespaceResetting) { + @alias('model') job; +} diff --git a/ui/app/controllers/jobs/run.js b/ui/app/controllers/jobs/run.js index baaf84183..f57d43023 100644 --- a/ui/app/controllers/jobs/run.js +++ b/ui/app/controllers/jobs/run.js @@ -1,9 +1,9 @@ import Controller from '@ember/controller'; -export default Controller.extend({ +export default class RunController extends Controller { onSubmit(id, namespace) { this.transitionToRoute('jobs.job', id, { queryParams: { jobNamespace: namespace }, }); - }, -}); + } +} diff --git a/ui/app/controllers/servers.js b/ui/app/controllers/servers.js index 5e6f1bea4..b82e2bf58 100644 --- a/ui/app/controllers/servers.js +++ b/ui/app/controllers/servers.js @@ -2,24 +2,30 @@ import { alias } from '@ember/object/computed'; import Controller from '@ember/controller'; import Sortable from 'nomad-ui/mixins/sortable'; -export default Controller.extend(Sortable, { - nodes: alias('model.nodes'), - agents: alias('model.agents'), +export default class ServersController extends Controller.extend(Sortable) { + @alias('model.nodes') nodes; + @alias('model.agents') agents; - queryParams: { - currentPage: 'page', - sortProperty: 'sort', - sortDescending: 'desc', - }, + queryParams = [ + { + currentPage: 'page', + }, + { + sortProperty: 'sort', + }, + { + sortDescending: 'desc', + }, + ]; - currentPage: 1, - pageSize: 8, + currentPage = 1; + pageSize = 8; - sortProperty: 'isLeader', - sortDescending: true, + sortProperty = 'isLeader'; + sortDescending = true; - isForbidden: false, + isForbidden = false; - listToSort: alias('agents'), - sortedAgents: alias('listSorted'), -}); + @alias('agents') listToSort; + @alias('listSorted') sortedAgents; +} diff --git a/ui/app/controllers/servers/index.js b/ui/app/controllers/servers/index.js index ee49e8f72..67d427df7 100644 --- a/ui/app/controllers/servers/index.js +++ b/ui/app/controllers/servers/index.js @@ -1,7 +1,7 @@ import { alias } from '@ember/object/computed'; import Controller, { inject as controller } from '@ember/controller'; -export default Controller.extend({ - serversController: controller('servers'), - isForbidden: alias('serversController.isForbidden'), -}); +export default class IndexController extends Controller { + @controller('servers') serversController; + @alias('serversController.isForbidden') isForbidden; +} diff --git a/ui/app/controllers/servers/server.js b/ui/app/controllers/servers/server.js index dd455b3ad..1b511c22f 100644 --- a/ui/app/controllers/servers/server.js +++ b/ui/app/controllers/servers/server.js @@ -1,10 +1,13 @@ import Controller from '@ember/controller'; -import { computed } from '@ember/object'; +import { action, computed } from '@ember/object'; +import classic from 'ember-classic-decorator'; -export default Controller.extend({ - activeTab: 'tags', +@classic +export default class ServerController extends Controller { + activeTab = 'tags'; - sortedTags: computed('model.tags', function() { + @computed('model.tags') + get sortedTags() { const tags = this.get('model.tags') || {}; return Object.keys(tags) .map(name => ({ @@ -12,11 +15,10 @@ export default Controller.extend({ value: tags[name], })) .sortBy('name'); - }), + } - actions: { - setTab(tab) { - this.set('activeTab', tab); - }, - }, -}); + @action + setTab(tab) { + this.set('activeTab', tab); + } +} diff --git a/ui/app/controllers/settings/tokens.js b/ui/app/controllers/settings/tokens.js index 8caab695f..956268374 100644 --- a/ui/app/controllers/settings/tokens.js +++ b/ui/app/controllers/settings/tokens.js @@ -3,66 +3,69 @@ import { reads } from '@ember/object/computed'; import Controller from '@ember/controller'; import { getOwner } from '@ember/application'; import { alias } from '@ember/object/computed'; +import { action } from '@ember/object'; +import classic from 'ember-classic-decorator'; -export default Controller.extend({ - token: service(), - system: service(), - store: service(), +@classic +export default class Tokens extends Controller { + @service token; + @service system; + @service store; - secret: reads('token.secret'), + @reads('token.secret') secret; - tokenIsValid: false, - tokenIsInvalid: false, - tokenRecord: alias('token.selfToken'), + tokenIsValid = false; + tokenIsInvalid = false; + @alias('token.selfToken') tokenRecord; resetStore() { this.store.unloadAll(); - }, + } - actions: { - clearTokenProperties() { - this.token.setProperties({ - secret: undefined, - }); - this.setProperties({ - tokenIsValid: false, - tokenIsInvalid: false, - }); - this.resetStore(); - this.token.reset(); - }, + @action + clearTokenProperties() { + this.token.setProperties({ + secret: undefined, + }); + this.setProperties({ + tokenIsValid: false, + tokenIsInvalid: false, + }); + this.resetStore(); + this.token.reset(); + } - verifyToken() { - const { secret } = this; - const TokenAdapter = getOwner(this).lookup('adapter:token'); + @action + verifyToken() { + const { secret } = this; + const TokenAdapter = getOwner(this).lookup('adapter:token'); - this.set('token.secret', secret); + this.set('token.secret', secret); - TokenAdapter.findSelf().then( - () => { - // Clear out all data to ensure only data the new token is privileged to - // see is shown - this.system.reset(); - this.resetStore(); + TokenAdapter.findSelf().then( + () => { + // Clear out all data to ensure only data the new token is privileged to + // see is shown + this.system.reset(); + this.resetStore(); - // Refetch the token and associated policies - this.get('token.fetchSelfTokenAndPolicies') - .perform() - .catch(); + // Refetch the token and associated policies + this.get('token.fetchSelfTokenAndPolicies') + .perform() + .catch(); - this.setProperties({ - tokenIsValid: true, - tokenIsInvalid: false, - }); - }, - () => { - this.set('token.secret', undefined); - this.setProperties({ - tokenIsValid: false, - tokenIsInvalid: true, - }); - } - ); - }, - }, -}); + this.setProperties({ + tokenIsValid: true, + tokenIsInvalid: false, + }); + }, + () => { + this.set('token.secret', undefined); + this.setProperties({ + tokenIsValid: false, + tokenIsInvalid: true, + }); + } + ); + } +} diff --git a/ui/app/models/agent.js b/ui/app/models/agent.js index 0c210e69e..7eefb3c6b 100644 --- a/ui/app/models/agent.js +++ b/ui/app/models/agent.js @@ -2,25 +2,29 @@ import { inject as service } from '@ember/service'; import { computed } from '@ember/object'; import Model from 'ember-data/model'; import attr from 'ember-data/attr'; +import classic from 'ember-classic-decorator'; -export default Model.extend({ - system: service(), +@classic +export default class Agent extends Model { + @service system; - name: attr('string'), - address: attr('string'), - serfPort: attr('string'), - rpcPort: attr('string'), - tags: attr({ defaultValue: () => ({}) }), - status: attr('string'), - datacenter: attr('string'), - region: attr('string'), + @attr('string') name; + @attr('string') address; + @attr('string') serfPort; + @attr('string') rpcPort; + @attr({ defaultValue: () => ({}) }) tags; + @attr('string') status; + @attr('string') datacenter; + @attr('string') region; - rpcAddr: computed('address', 'port', function() { + @computed('address', 'port') + get rpcAddr() { const { address, rpcPort } = this; return address && rpcPort && `${address}:${rpcPort}`; - }), + } - isLeader: computed('system.leader.rpcAddr', function() { + @computed('system.leader.rpcAddr') + get isLeader() { return this.get('system.leader.rpcAddr') === this.rpcAddr; - }), -}); + } +} diff --git a/ui/app/models/allocation.js b/ui/app/models/allocation.js index 44a852428..a9a60d351 100644 --- a/ui/app/models/allocation.js +++ b/ui/app/models/allocation.js @@ -7,6 +7,7 @@ import { belongsTo, hasMany } from 'ember-data/relationships'; import { fragment, fragmentArray } from 'ember-data-model-fragments/attributes'; import intersection from 'lodash.intersection'; import shortUUIDProperty from '../utils/properties/short-uuid'; +import classic from 'ember-classic-decorator'; const STATUS_ORDER = { pending: 1, @@ -16,50 +17,54 @@ const STATUS_ORDER = { lost: 5, }; -export default Model.extend({ - token: service(), +@classic +export default class Allocation extends Model { + @service token; - shortId: shortUUIDProperty('id'), - job: belongsTo('job'), - node: belongsTo('node'), - name: attr('string'), - taskGroupName: attr('string'), - resources: fragment('resources'), - allocatedResources: fragment('resources'), - jobVersion: attr('number'), + @shortUUIDProperty('id') shortId; + @belongsTo('job') job; + @belongsTo('node') node; + @attr('string') name; + @attr('string') taskGroupName; + @fragment('resources') resources; + @fragment('resources') allocatedResources; + @attr('number') jobVersion; - modifyIndex: attr('number'), - modifyTime: attr('date'), + @attr('number') modifyIndex; + @attr('date') modifyTime; - createIndex: attr('number'), - createTime: attr('date'), + @attr('number') createIndex; + @attr('date') createTime; - clientStatus: attr('string'), - desiredStatus: attr('string'), - statusIndex: computed('clientStatus', function() { + @attr('string') clientStatus; + @attr('string') desiredStatus; + + @computed('clientStatus') + get statusIndex() { return STATUS_ORDER[this.clientStatus] || 100; - }), + } - isRunning: equal('clientStatus', 'running'), - isMigrating: attr('boolean'), + @equal('clientStatus', 'running') isRunning; + @attr('boolean') isMigrating; // An allocation model created from any allocation list response will be lacking // many properties (some of which can always be null). This is an indicator that // the allocation needs to be reloaded to get the complete allocation state. - isPartial: none('allocationTaskGroup'), + @none('allocationTaskGroup') isPartial; // When allocations are server-side rescheduled, a paper trail // is left linking all reschedule attempts. - previousAllocation: belongsTo('allocation', { inverse: 'nextAllocation' }), - nextAllocation: belongsTo('allocation', { inverse: 'previousAllocation' }), + @belongsTo('allocation', { inverse: 'nextAllocation' }) previousAllocation; + @belongsTo('allocation', { inverse: 'previousAllocation' }) nextAllocation; - preemptedAllocations: hasMany('allocation', { inverse: 'preemptedByAllocation' }), - preemptedByAllocation: belongsTo('allocation', { inverse: 'preemptedAllocations' }), - wasPreempted: attr('boolean'), + @hasMany('allocation', { inverse: 'preemptedByAllocation' }) preemptedAllocations; + @belongsTo('allocation', { inverse: 'preemptedAllocations' }) preemptedByAllocation; + @attr('boolean') wasPreempted; - followUpEvaluation: belongsTo('evaluation'), + @belongsTo('evaluation') followUpEvaluation; - statusClass: computed('clientStatus', function() { + @computed('clientStatus') + get statusClass() { const classMap = { pending: 'is-pending', running: 'is-primary', @@ -69,25 +74,29 @@ export default Model.extend({ }; return classMap[this.clientStatus] || 'is-dark'; - }), + } - isOld: computed('jobVersion', 'job.version', function() { + @computed('jobVersion', 'job.version') + get isOld() { return this.jobVersion !== this.get('job.version'); - }), + } - taskGroup: computed('isOld', 'jobTaskGroup', 'allocationTaskGroup', function() { + @computed('isOld', 'jobTaskGroup', 'allocationTaskGroup') + get taskGroup() { if (!this.isOld) return this.jobTaskGroup; return this.allocationTaskGroup; - }), + } - jobTaskGroup: computed('taskGroupName', 'job.taskGroups.[]', function() { + @computed('taskGroupName', 'job.taskGroups.[]') + get jobTaskGroup() { const taskGroups = this.get('job.taskGroups'); return taskGroups && taskGroups.findBy('name', this.taskGroupName); - }), + } - allocationTaskGroup: fragment('task-group', { defaultValue: null }), + @fragment('task-group', { defaultValue: null }) allocationTaskGroup; - unhealthyDrivers: computed('taskGroup.drivers.[]', 'node.unhealthyDriverNames.[]', function() { + @computed('taskGroup.drivers.[]', 'node.unhealthyDriverNames.[]') + get unhealthyDrivers() { const taskGroupUnhealthyDrivers = this.get('taskGroup.drivers'); const nodeUnhealthyDrivers = this.get('node.unhealthyDriverNames'); @@ -96,41 +105,38 @@ export default Model.extend({ } return []; - }), + } - states: fragmentArray('task-state'), - rescheduleEvents: fragmentArray('reschedule-event'), + @fragmentArray('task-state') states; + @fragmentArray('reschedule-event') rescheduleEvents; - hasRescheduleEvents: computed('rescheduleEvents.length', 'nextAllocation', function() { + @computed('rescheduleEvents.length', 'nextAllocation') + get hasRescheduleEvents() { return this.get('rescheduleEvents.length') > 0 || this.nextAllocation; - }), + } - hasStoppedRescheduling: computed( - 'nextAllocation', - 'clientStatus', - 'followUpEvaluation.content', - function() { - return ( - !this.get('nextAllocation.content') && - !this.get('followUpEvaluation.content') && - this.clientStatus === 'failed' - ); - } - ), + @computed('nextAllocation', 'clientStatus', 'followUpEvaluation.content') + get hasStoppedRescheduling() { + return ( + !this.get('nextAllocation.content') && + !this.get('followUpEvaluation.content') && + this.clientStatus === 'failed' + ); + } stop() { return this.store.adapterFor('allocation').stop(this); - }, + } restart(taskName) { return this.store.adapterFor('allocation').restart(this, taskName); - }, + } ls(path) { return this.store.adapterFor('allocation').ls(this, path); - }, + } stat(path) { return this.store.adapterFor('allocation').stat(this, path); - }, -}); + } +} diff --git a/ui/app/models/consul-connect.js b/ui/app/models/consul-connect.js index 3b617e126..168809401 100644 --- a/ui/app/models/consul-connect.js +++ b/ui/app/models/consul-connect.js @@ -1,6 +1,6 @@ import Fragment from 'ember-data-model-fragments/fragment'; import { fragment } from 'ember-data-model-fragments/attributes'; -export default Fragment.extend({ - sidecarService: fragment('sidecar-service'), -}); +export default class ConsulConnect extends Fragment { + @fragment('sidecar-service') sidecarService; +} diff --git a/ui/app/models/deployment.js b/ui/app/models/deployment.js index df7f767bd..66d6cb297 100644 --- a/ui/app/models/deployment.js +++ b/ui/app/models/deployment.js @@ -7,46 +7,53 @@ import { belongsTo, hasMany } from 'ember-data/relationships'; import { fragmentArray } from 'ember-data-model-fragments/attributes'; import shortUUIDProperty from '../utils/properties/short-uuid'; import sumAggregation from '../utils/properties/sum-aggregation'; +import classic from 'ember-classic-decorator'; -export default Model.extend({ - shortId: shortUUIDProperty('id'), +@classic +export default class Deployment extends Model { + @shortUUIDProperty('id') shortId; - job: belongsTo('job', { inverse: 'deployments' }), - jobForLatest: belongsTo('job', { inverse: 'latestDeployment' }), - versionNumber: attr('number'), + @belongsTo('job', { inverse: 'deployments' }) job; + @belongsTo('job', { inverse: 'latestDeployment' }) jobForLatest; + @attr('number') versionNumber; // If any task group is not promoted yet requires promotion and the deployment // is still running, the deployment needs promotion. - requiresPromotion: computed('taskGroupSummaries.@each.promoted', function() { - return this.status === 'running' && - this.taskGroupSummaries - .toArray() - .some(summary => summary.get('requiresPromotion') && !summary.get('promoted')); - }), + @computed('taskGroupSummaries.@each.promoted') + get requiresPromotion() { + return ( + this.status === 'running' && + this.taskGroupSummaries + .toArray() + .some(summary => summary.get('requiresPromotion') && !summary.get('promoted')) + ); + } - status: attr('string'), - statusDescription: attr('string'), + @attr('string') status; + @attr('string') statusDescription; - isRunning: equal('status', 'running'), + @equal('status', 'running') isRunning; - taskGroupSummaries: fragmentArray('task-group-deployment-summary'), - allocations: hasMany('allocations'), + @fragmentArray('task-group-deployment-summary') taskGroupSummaries; + @hasMany('allocations') allocations; - version: computed('versionNumber', 'job.versions.content.@each.number', function() { + @computed('versionNumber', 'job.versions.content.@each.number') + get version() { return (this.get('job.versions') || []).findBy('number', this.versionNumber); - }), + } // Dependent keys can only go one level past an @each so an alias is needed - versionSubmitTime: alias('version.submitTime'), + @alias('version.submitTime') versionSubmitTime; - placedCanaries: sumAggregation('taskGroupSummaries', 'placedCanaries'), - desiredCanaries: sumAggregation('taskGroupSummaries', 'desiredCanaries'), - desiredTotal: sumAggregation('taskGroupSummaries', 'desiredTotal'), - placedAllocs: sumAggregation('taskGroupSummaries', 'placedAllocs'), - healthyAllocs: sumAggregation('taskGroupSummaries', 'healthyAllocs'), - unhealthyAllocs: sumAggregation('taskGroupSummaries', 'unhealthyAllocs'), + @sumAggregation('taskGroupSummaries', 'placedCanaries') placedCanaries; + @sumAggregation('taskGroupSummaries', 'desiredCanaries') desiredCanaries; + @sumAggregation('taskGroupSummaries', 'desiredTotal') desiredTotal; + @sumAggregation('taskGroupSummaries', 'placedAllocs') placedAllocs; + @sumAggregation('taskGroupSummaries', 'healthyAllocs') healthyAllocs; + @sumAggregation('taskGroupSummaries', 'unhealthyAllocs') unhealthyAllocs; - statusClass: computed('status', function() { + @computed('status') + get statusClass() { const classMap = { running: 'is-running', successful: 'is-primary', @@ -56,10 +63,10 @@ export default Model.extend({ }; return classMap[this.status] || 'is-dark'; - }), + } promote() { assert('A deployment needs to requirePromotion to be promoted', this.requiresPromotion); return this.store.adapterFor('deployment').promote(this); - }, -}); + } +} diff --git a/ui/app/models/drain-strategy.js b/ui/app/models/drain-strategy.js index 4d2738961..4b3aa67da 100644 --- a/ui/app/models/drain-strategy.js +++ b/ui/app/models/drain-strategy.js @@ -1,12 +1,14 @@ import { lt, equal } from '@ember/object/computed'; import attr from 'ember-data/attr'; import Fragment from 'ember-data-model-fragments/fragment'; +import classic from 'ember-classic-decorator'; -export default Fragment.extend({ - deadline: attr('number'), - forceDeadline: attr('date'), - ignoreSystemJobs: attr('boolean'), +@classic +export default class DrainStrategy extends Fragment { + @attr('number') deadline; + @attr('date') forceDeadline; + @attr('boolean') ignoreSystemJobs; - isForced: lt('deadline', 0), - hasNoDeadline: equal('deadline', 0), -}); + @lt('deadline', 0) isForced; + @equal('deadline', 0) hasNoDeadline; +} diff --git a/ui/app/models/evaluation.js b/ui/app/models/evaluation.js index 2b7b0f2e0..fb8fb98b3 100644 --- a/ui/app/models/evaluation.js +++ b/ui/app/models/evaluation.js @@ -5,25 +5,25 @@ import { belongsTo } from 'ember-data/relationships'; import { fragmentArray } from 'ember-data-model-fragments/attributes'; import shortUUIDProperty from '../utils/properties/short-uuid'; -export default Model.extend({ - shortId: shortUUIDProperty('id'), - priority: attr('number'), - type: attr('string'), - triggeredBy: attr('string'), - status: attr('string'), - statusDescription: attr('string'), - failedTGAllocs: fragmentArray('placement-failure', { defaultValue: () => [] }), +export default class Evaluation extends Model { + @shortUUIDProperty('id') shortId; + @attr('number') priority; + @attr('string') type; + @attr('string') triggeredBy; + @attr('string') status; + @attr('string') statusDescription; + @fragmentArray('placement-failure', { defaultValue: () => [] }) failedTGAllocs; - hasPlacementFailures: bool('failedTGAllocs.length'), - isBlocked: equal('status', 'blocked'), + @bool('failedTGAllocs.length') hasPlacementFailures; + @equal('status', 'blocked') isBlocked; - job: belongsTo('job'), + @belongsTo('job') job; - modifyIndex: attr('number'), - modifyTime: attr('date'), + @attr('number') modifyIndex; + @attr('date') modifyTime; - createIndex: attr('number'), - createTime: attr('date'), + @attr('number') createIndex; + @attr('date') createTime; - waitUntil: attr('date'), -}); + @attr('date') waitUntil; +} diff --git a/ui/app/models/host-volume.js b/ui/app/models/host-volume.js index ff5d3e628..e6d3b7e31 100644 --- a/ui/app/models/host-volume.js +++ b/ui/app/models/host-volume.js @@ -2,10 +2,10 @@ import Fragment from 'ember-data-model-fragments/fragment'; import attr from 'ember-data/attr'; import { fragmentOwner } from 'ember-data-model-fragments/attributes'; -export default Fragment.extend({ - node: fragmentOwner(), +export default class HostVolume extends Fragment { + @fragmentOwner() node; - name: attr('string'), - path: attr('string'), - readOnly: attr('boolean'), -}); + @attr('string') name; + @attr('string') path; + @attr('boolean') readOnly; +} diff --git a/ui/app/models/job-plan.js b/ui/app/models/job-plan.js index 1ddc3d4b8..714cf186c 100644 --- a/ui/app/models/job-plan.js +++ b/ui/app/models/job-plan.js @@ -3,8 +3,8 @@ import attr from 'ember-data/attr'; import { fragmentArray } from 'ember-data-model-fragments/attributes'; import { hasMany } from 'ember-data/relationships'; -export default Model.extend({ - diff: attr(), - failedTGAllocs: fragmentArray('placement-failure', { defaultValue: () => [] }), - preemptions: hasMany('allocation'), -}); +export default class JobPlan extends Model { + @attr() diff; + @fragmentArray('placement-failure', { defaultValue: () => [] }) failedTGAllocs; + @hasMany('allocation') preemptions; +} diff --git a/ui/app/models/job-summary.js b/ui/app/models/job-summary.js index a9584aced..b32d09a52 100644 --- a/ui/app/models/job-summary.js +++ b/ui/app/models/job-summary.js @@ -4,36 +4,39 @@ import attr from 'ember-data/attr'; import { belongsTo } from 'ember-data/relationships'; import { fragmentArray } from 'ember-data-model-fragments/attributes'; import sumAggregation from '../utils/properties/sum-aggregation'; +import classic from 'ember-classic-decorator'; -export default Model.extend({ - job: belongsTo('job'), +@classic +export default class JobSummary extends Model { + @belongsTo('job') job; - taskGroupSummaries: fragmentArray('task-group-summary'), + @fragmentArray('task-group-summary') taskGroupSummaries; // Aggregate allocation counts across all summaries - queuedAllocs: sumAggregation('taskGroupSummaries', 'queuedAllocs'), - startingAllocs: sumAggregation('taskGroupSummaries', 'startingAllocs'), - runningAllocs: sumAggregation('taskGroupSummaries', 'runningAllocs'), - completeAllocs: sumAggregation('taskGroupSummaries', 'completeAllocs'), - failedAllocs: sumAggregation('taskGroupSummaries', 'failedAllocs'), - lostAllocs: sumAggregation('taskGroupSummaries', 'lostAllocs'), + @sumAggregation('taskGroupSummaries', 'queuedAllocs') queuedAllocs; + @sumAggregation('taskGroupSummaries', 'startingAllocs') startingAllocs; + @sumAggregation('taskGroupSummaries', 'runningAllocs') runningAllocs; + @sumAggregation('taskGroupSummaries', 'completeAllocs') completeAllocs; + @sumAggregation('taskGroupSummaries', 'failedAllocs') failedAllocs; + @sumAggregation('taskGroupSummaries', 'lostAllocs') lostAllocs; - allocsList: collect( + @collect( 'queuedAllocs', 'startingAllocs', 'runningAllocs', 'completeAllocs', 'failedAllocs', 'lostAllocs' - ), + ) + allocsList; - totalAllocs: sum('allocsList'), + @sum('allocsList') totalAllocs; - pendingChildren: attr('number'), - runningChildren: attr('number'), - deadChildren: attr('number'), + @attr('number') pendingChildren; + @attr('number') runningChildren; + @attr('number') deadChildren; - childrenList: collect('pendingChildren', 'runningChildren', 'deadChildren'), + @collect('pendingChildren', 'runningChildren', 'deadChildren') childrenList; - totalChildren: sum('childrenList'), -}); + @sum('childrenList') totalChildren; +} diff --git a/ui/app/models/job-version.js b/ui/app/models/job-version.js index 6204c1198..005f7657b 100644 --- a/ui/app/models/job-version.js +++ b/ui/app/models/job-version.js @@ -2,10 +2,10 @@ import Model from 'ember-data/model'; import attr from 'ember-data/attr'; import { belongsTo } from 'ember-data/relationships'; -export default Model.extend({ - job: belongsTo('job'), - stable: attr('boolean'), - submitTime: attr('date'), - number: attr('number'), - diff: attr(), -}); +export default class JobVersion extends Model { + @belongsTo('job') job; + @attr('boolean') stable; + @attr('date') submitTime; + @attr('number') number; + @attr() diff; +} diff --git a/ui/app/models/job.js b/ui/app/models/job.js index 5226b6bf9..e179cd4b4 100644 --- a/ui/app/models/job.js +++ b/ui/app/models/job.js @@ -6,121 +6,122 @@ import { belongsTo, hasMany } from 'ember-data/relationships'; import { fragmentArray } from 'ember-data-model-fragments/attributes'; import RSVP from 'rsvp'; import { assert } from '@ember/debug'; +import classic from 'ember-classic-decorator'; const JOB_TYPES = ['service', 'batch', 'system']; -export default Model.extend({ - region: attr('string'), - name: attr('string'), - plainId: attr('string'), - type: attr('string'), - priority: attr('number'), - allAtOnce: attr('boolean'), +@classic +export default class Job extends Model { + @attr('string') region; + @attr('string') name; + @attr('string') plainId; + @attr('string') type; + @attr('number') priority; + @attr('boolean') allAtOnce; - status: attr('string'), - statusDescription: attr('string'), - createIndex: attr('number'), - modifyIndex: attr('number'), + @attr('string') status; + @attr('string') statusDescription; + @attr('number') createIndex; + @attr('number') modifyIndex; // True when the job is the parent periodic or parameterized jobs // Instances of periodic or parameterized jobs are false for both properties - periodic: attr('boolean'), - parameterized: attr('boolean'), - dispatched: attr('boolean'), + @attr('boolean') periodic; + @attr('boolean') parameterized; + @attr('boolean') dispatched; - periodicDetails: attr(), - parameterizedDetails: attr(), + @attr() periodicDetails; + @attr() parameterizedDetails; - hasChildren: computed('periodic', 'parameterized', 'dispatched', function() { + @computed('periodic', 'parameterized', 'dispatched') + get hasChildren() { return this.periodic || (this.parameterized && !this.dispatched); - }), + } - parent: belongsTo('job', { inverse: 'children' }), - children: hasMany('job', { inverse: 'parent' }), + @belongsTo('job', { inverse: 'children' }) parent; + @hasMany('job', { inverse: 'parent' }) children; // The parent job name is prepended to child launch job names - trimmedName: computed('name', 'parent', function() { + @computed('name', 'parent') + get trimmedName() { return this.get('parent.content') ? this.name.replace(/.+?\//, '') : this.name; - }), + } // A composite of type and other job attributes to determine // a better type descriptor for human interpretation rather // than for scheduling. - displayType: computed('type', 'periodic', 'parameterized', function() { + @computed('type', 'periodic', 'parameterized') + get displayType() { if (this.periodic) { return 'periodic'; } else if (this.parameterized) { return 'parameterized'; } return this.type; - }), + } // A composite of type and other job attributes to determine // type for templating rather than scheduling - templateType: computed( - 'type', - 'periodic', - 'parameterized', - 'parent.{periodic,parameterized}', - function() { - const type = this.type; + @computed('type', 'periodic', 'parameterized', 'parent.{periodic,parameterized}') + get templateType() { + const type = this.type; - if (this.get('parent.periodic')) { - return 'periodic-child'; - } else if (this.get('parent.parameterized')) { - return 'parameterized-child'; - } else if (this.periodic) { - return 'periodic'; - } else if (this.parameterized) { - return 'parameterized'; - } else if (JOB_TYPES.includes(type)) { - // Guard against the API introducing a new type before the UI - // is prepared to handle it. - return this.type; - } - - // A fail-safe in the event the API introduces a new type. - return 'service'; + if (this.get('parent.periodic')) { + return 'periodic-child'; + } else if (this.get('parent.parameterized')) { + return 'parameterized-child'; + } else if (this.periodic) { + return 'periodic'; + } else if (this.parameterized) { + return 'parameterized'; + } else if (JOB_TYPES.includes(type)) { + // Guard against the API introducing a new type before the UI + // is prepared to handle it. + return this.type; } - ), - datacenters: attr(), - taskGroups: fragmentArray('task-group', { defaultValue: () => [] }), - summary: belongsTo('job-summary'), + // A fail-safe in the event the API introduces a new type. + return 'service'; + } + + @attr() datacenters; + @fragmentArray('task-group', { defaultValue: () => [] }) taskGroups; + @belongsTo('job-summary') summary; // A job model created from the jobs list response will be lacking // task groups. This is an indicator that it needs to be reloaded // if task group information is important. - isPartial: equal('taskGroups.length', 0), + @equal('taskGroups.length', 0) isPartial; // If a job has only been loaded through the list request, the task groups // are still unknown. However, the count of task groups is available through // the job-summary model which is embedded in the jobs list response. - taskGroupCount: or('taskGroups.length', 'taskGroupSummaries.length'), + @or('taskGroups.length', 'taskGroupSummaries.length') taskGroupCount; // Alias through to the summary, as if there was no relationship - taskGroupSummaries: alias('summary.taskGroupSummaries'), - queuedAllocs: alias('summary.queuedAllocs'), - startingAllocs: alias('summary.startingAllocs'), - runningAllocs: alias('summary.runningAllocs'), - completeAllocs: alias('summary.completeAllocs'), - failedAllocs: alias('summary.failedAllocs'), - lostAllocs: alias('summary.lostAllocs'), - totalAllocs: alias('summary.totalAllocs'), - pendingChildren: alias('summary.pendingChildren'), - runningChildren: alias('summary.runningChildren'), - deadChildren: alias('summary.deadChildren'), - totalChildren: alias('summary.totalChildren'), + @alias('summary.taskGroupSummaries') taskGroupSummaries; + @alias('summary.queuedAllocs') queuedAllocs; + @alias('summary.startingAllocs') startingAllocs; + @alias('summary.runningAllocs') runningAllocs; + @alias('summary.completeAllocs') completeAllocs; + @alias('summary.failedAllocs') failedAllocs; + @alias('summary.lostAllocs') lostAllocs; + @alias('summary.totalAllocs') totalAllocs; + @alias('summary.pendingChildren') pendingChildren; + @alias('summary.runningChildren') runningChildren; + @alias('summary.deadChildren') deadChildren; + @alias('summary.totalChildren') totalChildren; - version: attr('number'), + @attr('number') version; - versions: hasMany('job-versions'), - allocations: hasMany('allocations'), - deployments: hasMany('deployments'), - evaluations: hasMany('evaluations'), - namespace: belongsTo('namespace'), + @hasMany('job-versions') versions; + @hasMany('allocations') allocations; + @hasMany('deployments') deployments; + @hasMany('evaluations') evaluations; + @belongsTo('namespace') namespace; - drivers: computed('taskGroups.@each.drivers', function() { + @computed('taskGroups.@each.drivers') + get drivers() { return this.taskGroups .mapBy('drivers') .reduce((all, drivers) => { @@ -128,13 +129,14 @@ export default Model.extend({ return all; }, []) .uniq(); - }), + } - allocationsUnhealthyDrivers: mapBy('allocations', 'unhealthyDrivers'), + @mapBy('allocations', 'unhealthyDrivers') allocationsUnhealthyDrivers; // Getting all unhealthy drivers for a job can be incredibly expensive if the job // has many allocations. This can lead to making an API request for many nodes. - unhealthyDrivers: computed('allocationsUnhealthyDrivers.[]', function() { + @computed('allocationsUnhealthyDrivers.[]') + get unhealthyDrivers() { return this.allocations .mapBy('unhealthyDrivers') .reduce((all, drivers) => { @@ -142,23 +144,26 @@ export default Model.extend({ return all; }, []) .uniq(); - }), + } - hasBlockedEvaluation: computed('evaluations.@each.isBlocked', function() { + @computed('evaluations.@each.isBlocked') + get hasBlockedEvaluation() { return this.evaluations.toArray().some(evaluation => evaluation.get('isBlocked')); - }), + } - hasPlacementFailures: and('latestFailureEvaluation', 'hasBlockedEvaluation'), + @and('latestFailureEvaluation', 'hasBlockedEvaluation') hasPlacementFailures; - latestEvaluation: computed('evaluations.{@each.modifyIndex,isPending}', function() { + @computed('evaluations.{@each.modifyIndex,isPending}') + get latestEvaluation() { const evaluations = this.evaluations; if (!evaluations || evaluations.get('isPending')) { return null; } return evaluations.sortBy('modifyIndex').get('lastObject'); - }), + } - latestFailureEvaluation: computed('evaluations.{@each.modifyIndex,isPending}', function() { + @computed('evaluations.{@each.modifyIndex,isPending}') + get latestFailureEvaluation() { const evaluations = this.evaluations; if (!evaluations || evaluations.get('isPending')) { return null; @@ -169,45 +174,46 @@ export default Model.extend({ return failureEvaluations.sortBy('modifyIndex').get('lastObject'); } - return; - }), + return undefined; + } - supportsDeployments: equal('type', 'service'), + @equal('type', 'service') supportsDeployments; - latestDeployment: belongsTo('deployment', { inverse: 'jobForLatest' }), + @belongsTo('deployment', { inverse: 'jobForLatest' }) latestDeployment; - runningDeployment: computed('latestDeployment', 'latestDeployment.isRunning', function() { + @computed('latestDeployment', 'latestDeployment.isRunning') + get runningDeployment() { const latest = this.latestDeployment; if (latest.get('isRunning')) return latest; - return; - }), + return undefined; + } fetchRawDefinition() { return this.store.adapterFor('job').fetchRawDefinition(this); - }, + } forcePeriodic() { return this.store.adapterFor('job').forcePeriodic(this); - }, + } stop() { return this.store.adapterFor('job').stop(this); - }, + } plan() { assert('A job must be parsed before planned', this._newDefinitionJSON); return this.store.adapterFor('job').plan(this); - }, + } run() { assert('A job must be parsed before ran', this._newDefinitionJSON); return this.store.adapterFor('job').run(this); - }, + } update() { assert('A job must be parsed before updated', this._newDefinitionJSON); return this.store.adapterFor('job').update(this); - }, + } parse() { const definition = this._newDefinition; @@ -237,7 +243,7 @@ export default Model.extend({ } return promise; - }, + } setIdByPayload(payload) { const namespace = payload.Namespace || 'default'; @@ -250,13 +256,14 @@ export default Model.extend({ if (namespaceRecord) { this.set('namespace', namespaceRecord); } - }, + } resetId() { this.set('id', JSON.stringify([this.plainId, this.get('namespace.name') || 'default'])); - }, + } - statusClass: computed('status', function() { + @computed('status') + get statusClass() { const classMap = { pending: 'is-pending', running: 'is-primary', @@ -264,20 +271,22 @@ export default Model.extend({ }; return classMap[this.status] || 'is-dark'; - }), + } - payload: attr('string'), - decodedPayload: computed('payload', function() { + @attr('string') payload; + + @computed('payload') + get decodedPayload() { // Lazily decode the base64 encoded payload return window.atob(this.payload || ''); - }), + } // An arbitrary HCL or JSON string that is used by the serializer to plan // and run this job. Used for both new job models and saved job models. - _newDefinition: attr('string'), + @attr('string') _newDefinition; // The new definition may be HCL, in which case the API will need to parse the // spec first. In order to preserve both the original HCL and the parsed response // that will be submitted to the create job endpoint, another prop is necessary. - _newDefinitionJSON: attr('string'), -}); + @attr('string') _newDefinitionJSON; +} diff --git a/ui/app/models/lifecycle.js b/ui/app/models/lifecycle.js index 0381be2f1..a112d4a60 100644 --- a/ui/app/models/lifecycle.js +++ b/ui/app/models/lifecycle.js @@ -2,9 +2,9 @@ import attr from 'ember-data/attr'; import Fragment from 'ember-data-model-fragments/fragment'; import { fragmentOwner } from 'ember-data-model-fragments/attributes'; -export default Fragment.extend({ - task: fragmentOwner(), +export default class Lifecycle extends Fragment { + @fragmentOwner() task; - hook: attr('string'), - sidecar: attr('boolean'), -}); + @attr('string') hook; + @attr('boolean') sidecar; +} diff --git a/ui/app/models/namespace.js b/ui/app/models/namespace.js index b5b5f5750..253ca808e 100644 --- a/ui/app/models/namespace.js +++ b/ui/app/models/namespace.js @@ -2,8 +2,8 @@ import { readOnly } from '@ember/object/computed'; import Model from 'ember-data/model'; import attr from 'ember-data/attr'; -export default Model.extend({ - name: readOnly('id'), - hash: attr('string'), - description: attr('string'), -}); +export default class Namespace extends Model { + @readOnly('id') name; + @attr('string') hash; + @attr('string') description; +} diff --git a/ui/app/models/network.js b/ui/app/models/network.js index 4f6502683..2d0f637d3 100644 --- a/ui/app/models/network.js +++ b/ui/app/models/network.js @@ -2,11 +2,11 @@ import attr from 'ember-data/attr'; import Fragment from 'ember-data-model-fragments/fragment'; import { array } from 'ember-data-model-fragments/attributes'; -export default Fragment.extend({ - device: attr('string'), - cidr: attr('string'), - ip: attr('string'), - mode: attr('string'), - mbits: attr('number'), - ports: array(), -}); +export default class Network extends Fragment { + @attr('string') device; + @attr('string') cidr; + @attr('string') ip; + @attr('string') mode; + @attr('number') mbits; + @array() ports; +} diff --git a/ui/app/models/node-attributes.js b/ui/app/models/node-attributes.js index 11c199c65..810fab2c6 100644 --- a/ui/app/models/node-attributes.js +++ b/ui/app/models/node-attributes.js @@ -5,14 +5,15 @@ import flat from 'flat'; const { unflatten } = flat; -export default Fragment.extend({ - nodeAttributes: attr(), +export default class NodeAttributes extends Fragment { + @attr() nodeAttributes; - attributesStructured: computed('nodeAttributes', function() { + @computed('nodeAttributes') + get attributesStructured() { const original = this.nodeAttributes; if (!original) { - return; + return undefined; } // `unflatten` doesn't sort keys before unflattening, so manual preprocessing is necessary. @@ -23,7 +24,7 @@ export default Fragment.extend({ return obj; }, {}); return unflatten(attrs, { overwrite: true }); - }), + } unknownProperty(key) { // Returns the exact value in index 0 and the subtree in index 1 @@ -33,5 +34,5 @@ export default Fragment.extend({ if (this.attributesStructured) { return get(this.attributesStructured, key); } - }, -}); + } +} diff --git a/ui/app/models/node-driver.js b/ui/app/models/node-driver.js index b235f864f..4431985d5 100644 --- a/ui/app/models/node-driver.js +++ b/ui/app/models/node-driver.js @@ -1,26 +1,30 @@ +import classic from 'ember-classic-decorator'; import Fragment from 'ember-data-model-fragments/fragment'; -import { computed, get } from '@ember/object'; +import { get, computed } from '@ember/object'; import attr from 'ember-data/attr'; import { fragmentOwner } from 'ember-data-model-fragments/attributes'; import { fragment } from 'ember-data-model-fragments/attributes'; -export default Fragment.extend({ - node: fragmentOwner(), +@classic +export default class NodeDriver extends Fragment { + @fragmentOwner() node; - attributes: fragment('node-attributes'), + @fragment('node-attributes') attributes; - attributesShort: computed('name', 'attributes.attributesStructured', function() { + @computed('name', 'attributes.attributesStructured') + get attributesShort() { const attributes = this.get('attributes.attributesStructured'); return get(attributes, `driver.${this.name}`); - }), + } - name: attr('string'), - detected: attr('boolean', { defaultValue: false }), - healthy: attr('boolean', { defaultValue: false }), - healthDescription: attr('string'), - updateTime: attr('date'), + @attr('string') name; + @attr('boolean', { defaultValue: false }) detected; + @attr('boolean', { defaultValue: false }) healthy; + @attr('string') healthDescription; + @attr('date') updateTime; - healthClass: computed('healthy', function() { + @computed('healthy') + get healthClass() { return this.healthy ? 'running' : 'failed'; - }), -}); + } +} diff --git a/ui/app/models/node-event.js b/ui/app/models/node-event.js index 8fcf3cfad..254250f16 100644 --- a/ui/app/models/node-event.js +++ b/ui/app/models/node-event.js @@ -3,13 +3,13 @@ import Fragment from 'ember-data-model-fragments/fragment'; import attr from 'ember-data/attr'; import { fragmentOwner } from 'ember-data-model-fragments/attributes'; -export default Fragment.extend({ - node: fragmentOwner(), +export default class NodeEvent extends Fragment { + @fragmentOwner() node; - message: attr('string'), - subsystem: attr('string'), - details: attr(), - time: attr('date'), + @attr('string') message; + @attr('string') subsystem; + @attr() details; + @attr('date') time; - driver: alias('details.driver'), -}); + @alias('details.driver') driver; +} diff --git a/ui/app/models/node.js b/ui/app/models/node.js index d272c7629..2e69d56aa 100644 --- a/ui/app/models/node.js +++ b/ui/app/models/node.js @@ -8,52 +8,63 @@ import RSVP from 'rsvp'; import shortUUIDProperty from '../utils/properties/short-uuid'; import ipParts from '../utils/ip-parts'; -export default Model.extend({ +export default class Node extends Model { // Available from list response - name: attr('string'), - datacenter: attr('string'), - nodeClass: attr('string'), - isDraining: attr('boolean'), - schedulingEligibility: attr('string'), - status: attr('string'), - statusDescription: attr('string'), - shortId: shortUUIDProperty('id'), - modifyIndex: attr('number'), + @attr('string') name; + @attr('string') datacenter; + @attr('string') nodeClass; + @attr('boolean') isDraining; + @attr('string') schedulingEligibility; + @attr('string') status; + @attr('string') statusDescription; + @shortUUIDProperty('id') shortId; + @attr('number') modifyIndex; // Available from single response - httpAddr: attr('string'), - tlsEnabled: attr('boolean'), - attributes: fragment('node-attributes'), - meta: fragment('node-attributes'), - resources: fragment('resources'), - reserved: fragment('resources'), - drainStrategy: fragment('drain-strategy'), + @attr('string') httpAddr; + @attr('boolean') tlsEnabled; + @fragment('node-attributes') attributes; + @fragment('node-attributes') meta; + @fragment('resources') resources; + @fragment('resources') reserved; + @fragment('drain-strategy') drainStrategy; - isEligible: equal('schedulingEligibility', 'eligible'), + @equal('schedulingEligibility', 'eligible') isEligible; - address: computed('httpAddr', function() { + @computed('httpAddr') + get address() { return ipParts(this.httpAddr).address; - }), + } - port: computed('httpAddr', function() { + @computed('httpAddr') + get port() { return ipParts(this.httpAddr).port; - }), + } - isPartial: computed('httpAddr', function() { + @computed('httpAddr') + get isPartial() { return this.httpAddr == null; - }), + } - allocations: hasMany('allocations', { inverse: 'node' }), - completeAllocations: computed('allocations.@each.clientStatus', function() { + @hasMany('allocations', { inverse: 'node' }) allocations; + + @computed('allocations.@each.clientStatus') + get completeAllocations() { return this.allocations.filterBy('clientStatus', 'complete'); - }), - runningAllocations: computed('allocations.@each.isRunning', function() { + } + + @computed('allocations.@each.isRunning') + get runningAllocations() { return this.allocations.filterBy('isRunning'); - }), - migratingAllocations: computed('allocations.@each.{isMigrating,isRunning}', function() { + } + + @computed('allocations.@each.{isMigrating,isRunning}') + get migratingAllocations() { return this.allocations.filter(alloc => alloc.isRunning && alloc.isMigrating); - }), - lastMigrateTime: computed('allocations.@each.{isMigrating,isRunning,modifyTime}', function() { + } + + @computed('allocations.@each.{isMigrating,isRunning,modifyTime}') + get lastMigrateTime() { const allocation = this.allocations .filterBy('isRunning', false) .filterBy('isMigrating') @@ -63,28 +74,32 @@ export default Model.extend({ return allocation.modifyTime; } - return; - }), + return undefined; + } - drivers: fragmentArray('node-driver'), - events: fragmentArray('node-event'), - hostVolumes: fragmentArray('host-volume'), + @fragmentArray('node-driver') drivers; + @fragmentArray('node-event') events; + @fragmentArray('host-volume') hostVolumes; - detectedDrivers: computed('drivers.@each.detected', function() { + @computed('drivers.@each.detected') + get detectedDrivers() { return this.drivers.filterBy('detected'); - }), + } - unhealthyDrivers: computed('detectedDrivers.@each.healthy', function() { + @computed('detectedDrivers.@each.healthy') + get unhealthyDrivers() { return this.detectedDrivers.filterBy('healthy', false); - }), + } - unhealthyDriverNames: computed('unhealthyDrivers.@each.name', function() { + @computed('unhealthyDrivers.@each.name') + get unhealthyDriverNames() { return this.unhealthyDrivers.mapBy('name'); - }), + } // A status attribute that includes states not included in node status. // Useful for coloring and sorting nodes - compositeStatus: computed('isDraining', 'isEligible', 'status', function() { + @computed('isDraining', 'isEligible', 'status') + get compositeStatus() { if (this.isDraining) { return 'draining'; } else if (!this.isEligible) { @@ -92,9 +107,10 @@ export default Model.extend({ } else { return this.status; } - }), + } - compositeStatusIcon: computed('isDraining', 'isEligible', 'status', function() { + @computed('isDraining', 'isEligible', 'status') + get compositeStatusIcon() { if (this.isDraining || !this.isEligible) { return 'alert-circle-fill'; } else if (this.status === 'down') { @@ -103,31 +119,31 @@ export default Model.extend({ return 'node-init-circle-fill'; } return 'check-circle-fill'; - }), + } setEligible() { if (this.isEligible) return RSVP.resolve(); // Optimistically update schedulingEligibility for immediate feedback this.set('schedulingEligibility', 'eligible'); return this.store.adapterFor('node').setEligible(this); - }, + } setIneligible() { if (!this.isEligible) return RSVP.resolve(); // Optimistically update schedulingEligibility for immediate feedback this.set('schedulingEligibility', 'ineligible'); return this.store.adapterFor('node').setIneligible(this); - }, + } drain(drainSpec) { return this.store.adapterFor('node').drain(this, drainSpec); - }, + } forceDrain(drainSpec) { return this.store.adapterFor('node').forceDrain(this, drainSpec); - }, + } cancelDrain() { return this.store.adapterFor('node').cancelDrain(this); - }, -}); + } +} diff --git a/ui/app/models/placement-failure.js b/ui/app/models/placement-failure.js index d711166b1..6a1f328ac 100644 --- a/ui/app/models/placement-failure.js +++ b/ui/app/models/placement-failure.js @@ -1,20 +1,20 @@ import attr from 'ember-data/attr'; import Fragment from 'ember-data-model-fragments/fragment'; -export default Fragment.extend({ - name: attr('string'), +export default class PlacementFailure extends Fragment { + @attr('string') name; - coalescedFailures: attr('number'), + @attr('number') coalescedFailures; - nodesEvaluated: attr('number'), - nodesExhausted: attr('number'), + @attr('number') nodesEvaluated; + @attr('number') nodesExhausted; - // Maps keyed by relevant dimension (dc, class, constraint, etc) with count values - nodesAvailable: attr(), - classFiltered: attr(), - constraintFiltered: attr(), - classExhausted: attr(), - dimensionExhausted: attr(), - quotaExhausted: attr(), - scores: attr(), -}); + // Maps keyed by relevant dimension (dc, class, constraint, etc)ith count values + @attr() nodesAvailable; + @attr() classFiltered; + @attr() constraintFiltered; + @attr() classExhausted; + @attr() dimensionExhausted; + @attr() quotaExhausted; + @attr() scores; +} diff --git a/ui/app/models/plugin.js b/ui/app/models/plugin.js index 6042b2f68..4bbb29ba0 100644 --- a/ui/app/models/plugin.js +++ b/ui/app/models/plugin.js @@ -3,28 +3,30 @@ import Model from 'ember-data/model'; import attr from 'ember-data/attr'; import { fragmentArray } from 'ember-data-model-fragments/attributes'; -export default Model.extend({ - plainId: attr('string'), +export default class Plugin extends Model { + @attr('string') plainId; - topologies: attr(), - provider: attr('string'), - version: attr('string'), + @attr() topologies; + @attr('string') provider; + @attr('string') version; - controllers: fragmentArray('storage-controller', { defaultValue: () => [] }), - nodes: fragmentArray('storage-node', { defaultValue: () => [] }), + @fragmentArray('storage-controller', { defaultValue: () => [] }) controllers; + @fragmentArray('storage-node', { defaultValue: () => [] }) nodes; - controllerRequired: attr('boolean'), - controllersHealthy: attr('number'), - controllersExpected: attr('number'), + @attr('boolean') controllerRequired; + @attr('number') controllersHealthy; + @attr('number') controllersExpected; - controllersHealthyProportion: computed('controllersHealthy', 'controllersExpected', function() { + @computed('controllersHealthy', 'controllersExpected') + get controllersHealthyProportion() { return this.controllersHealthy / this.controllersExpected; - }), + } - nodesHealthy: attr('number'), - nodesExpected: attr('number'), + @attr('number') nodesHealthy; + @attr('number') nodesExpected; - nodesHealthyProportion: computed('nodesHealthy', 'nodesExpected', function() { + @computed('nodesHealthy', 'nodesExpected') + get nodesHealthyProportion() { return this.nodesHealthy / this.nodesExpected; - }), -}); + } +} diff --git a/ui/app/models/policy.js b/ui/app/models/policy.js index 8b333617b..444ac5ea9 100644 --- a/ui/app/models/policy.js +++ b/ui/app/models/policy.js @@ -1,9 +1,9 @@ import Model from 'ember-data/model'; import attr from 'ember-data/attr'; -export default Model.extend({ - name: attr('string'), - description: attr('string'), - rules: attr('string'), - rulesJSON: attr(), -}); +export default class Policy extends Model { + @attr('string') name; + @attr('string') description; + @attr('string') rules; + @attr() rulesJSON; +} diff --git a/ui/app/models/reschedule-event.js b/ui/app/models/reschedule-event.js index 911fe7898..6ad1d46b7 100644 --- a/ui/app/models/reschedule-event.js +++ b/ui/app/models/reschedule-event.js @@ -3,14 +3,14 @@ import attr from 'ember-data/attr'; import { fragmentOwner } from 'ember-data-model-fragments/attributes'; import shortUUIDProperty from '../utils/properties/short-uuid'; -export default Fragment.extend({ - allocation: fragmentOwner(), +export default class RescheduleEvent extends Fragment { + @fragmentOwner() allocation; - previousAllocationId: attr('string'), - previousNodeId: attr('string'), - time: attr('date'), - delay: attr('string'), + @attr('string') previousAllocationId; + @attr('string') previousNodeId; + @attr('date') time; + @attr('string') delay; - previousAllocationShortId: shortUUIDProperty('previousAllocationId'), - previousNodeShortId: shortUUIDProperty('previousNodeShortId'), -}); + @shortUUIDProperty('previousAllocationId') previousAllocationShortId; + @shortUUIDProperty('previousNodeShortId') previousNodeShortId; +} diff --git a/ui/app/models/resources.js b/ui/app/models/resources.js index 5bb900ee4..9d6082009 100644 --- a/ui/app/models/resources.js +++ b/ui/app/models/resources.js @@ -2,10 +2,10 @@ import attr from 'ember-data/attr'; import Fragment from 'ember-data-model-fragments/fragment'; import { fragmentArray } from 'ember-data-model-fragments/attributes'; -export default Fragment.extend({ - cpu: attr('number'), - memory: attr('number'), - disk: attr('number'), - iops: attr('number'), - networks: fragmentArray('network', { defaultValue: () => [] }), -}); +export default class Resources extends Fragment { + @attr('number') cpu; + @attr('number') memory; + @attr('number') disk; + @attr('number') iops; + @fragmentArray('network', { defaultValue: () => [] }) networks; +} diff --git a/ui/app/models/service.js b/ui/app/models/service.js index 1743c8180..e6f811a4a 100644 --- a/ui/app/models/service.js +++ b/ui/app/models/service.js @@ -2,9 +2,9 @@ import attr from 'ember-data/attr'; import Fragment from 'ember-data-model-fragments/fragment'; import { fragment } from 'ember-data-model-fragments/attributes'; -export default Fragment.extend({ - name: attr('string'), - portLabel: attr('string'), - tags: attr(), - connect: fragment('consul-connect'), -}); +export default class Service extends Fragment { + @attr('string') name; + @attr('string') portLabel; + @attr() tags; + @fragment('consul-connect') connect; +} diff --git a/ui/app/models/sidecar-proxy-upstream.js b/ui/app/models/sidecar-proxy-upstream.js index 76d0b0d4f..0205497f5 100644 --- a/ui/app/models/sidecar-proxy-upstream.js +++ b/ui/app/models/sidecar-proxy-upstream.js @@ -1,7 +1,7 @@ import Fragment from 'ember-data-model-fragments/fragment'; import attr from 'ember-data/attr'; -export default Fragment.extend({ - destinationName: attr('string'), - localBindPort: attr('string'), -}); +export default class SidecarProxyUpstream extends Fragment { + @attr('string') destinationName; + @attr('string') localBindPort; +} diff --git a/ui/app/models/sidecar-proxy.js b/ui/app/models/sidecar-proxy.js index 5370fdd66..e080f3123 100644 --- a/ui/app/models/sidecar-proxy.js +++ b/ui/app/models/sidecar-proxy.js @@ -1,6 +1,6 @@ import Fragment from 'ember-data-model-fragments/fragment'; import { fragmentArray } from 'ember-data-model-fragments/attributes'; -export default Fragment.extend({ - upstreams: fragmentArray('sidecar-proxy-upstream'), -}); +export default class SidecarProxy extends Fragment { + @fragmentArray('sidecar-proxy-upstream') upstreams; +} diff --git a/ui/app/models/sidecar-service.js b/ui/app/models/sidecar-service.js index 5e271d01e..3350f77ac 100644 --- a/ui/app/models/sidecar-service.js +++ b/ui/app/models/sidecar-service.js @@ -1,6 +1,6 @@ import Fragment from 'ember-data-model-fragments/fragment'; import { fragment } from 'ember-data-model-fragments/attributes'; -export default Fragment.extend({ - proxy: fragment('sidecar-proxy'), -}); +export default class SidecarService extends Fragment { + @fragment('sidecar-proxy') proxy; +} diff --git a/ui/app/models/storage-controller.js b/ui/app/models/storage-controller.js index 4d7c44114..9406d0ec2 100644 --- a/ui/app/models/storage-controller.js +++ b/ui/app/models/storage-controller.js @@ -3,24 +3,24 @@ import { belongsTo } from 'ember-data/relationships'; import Fragment from 'ember-data-model-fragments/fragment'; import { fragmentOwner } from 'ember-data-model-fragments/attributes'; -export default Fragment.extend({ - plugin: fragmentOwner(), +export default class StorageController extends Fragment { + @fragmentOwner() plugin; - node: belongsTo('node'), - allocID: attr('string'), + @belongsTo('node') node; + @attr('string') allocID; - provider: attr('string'), - version: attr('string'), - healthy: attr('boolean'), - healthDescription: attr('string'), - updateTime: attr('date'), - requiresControllerPlugin: attr('boolean'), - requiresTopologies: attr('boolean'), + @attr('string') provider; + @attr('string') version; + @attr('boolean') healthy; + @attr('string') healthDescription; + @attr('date') updateTime; + @attr('boolean') requiresControllerPlugin; + @attr('boolean') requiresTopologies; - controllerInfo: attr(), + @attr() controllerInfo; // Fragments can't have relationships, so provider a manual getter instead. async getAllocation() { return this.store.findRecord('allocation', this.allocID); - }, -}); + } +} diff --git a/ui/app/models/storage-node.js b/ui/app/models/storage-node.js index 65e43050e..1c3637c49 100644 --- a/ui/app/models/storage-node.js +++ b/ui/app/models/storage-node.js @@ -3,24 +3,24 @@ import { belongsTo } from 'ember-data/relationships'; import Fragment from 'ember-data-model-fragments/fragment'; import { fragmentOwner } from 'ember-data-model-fragments/attributes'; -export default Fragment.extend({ - plugin: fragmentOwner(), +export default class StorageNode extends Fragment { + @fragmentOwner() plugin; - node: belongsTo('node'), - allocID: attr('string'), + @belongsTo('node') node; + @attr('string') allocID; - provider: attr('string'), - version: attr('string'), - healthy: attr('boolean'), - healthDescription: attr('string'), - updateTime: attr('date'), - requiresControllerPlugin: attr('boolean'), - requiresTopologies: attr('boolean'), + @attr('string') provider; + @attr('string') version; + @attr('boolean') healthy; + @attr('string') healthDescription; + @attr('date') updateTime; + @attr('boolean') requiresControllerPlugin; + @attr('boolean') requiresTopologies; - nodeInfo: attr(), + @attr() nodeInfo; // Fragments can't have relationships, so provider a manual getter instead. async getAllocation() { return this.store.findRecord('allocation', this.allocID); - }, -}); + } +} diff --git a/ui/app/models/task-event.js b/ui/app/models/task-event.js index 124d70ebc..794c490b6 100644 --- a/ui/app/models/task-event.js +++ b/ui/app/models/task-event.js @@ -2,15 +2,15 @@ import Fragment from 'ember-data-model-fragments/fragment'; import attr from 'ember-data/attr'; import { fragmentOwner } from 'ember-data-model-fragments/attributes'; -export default Fragment.extend({ - state: fragmentOwner(), +export default class TaskEvent extends Fragment { + @fragmentOwner() state; - type: attr('string'), - signal: attr('number'), - exitCode: attr('number'), + @attr('string') type; + @attr('number') signal; + @attr('number') exitCode; - time: attr('date'), - timeNanos: attr('number'), + @attr('date') time; + @attr('number') timeNanos; - message: attr('string'), -}); + @attr('string') message; +} diff --git a/ui/app/models/task-group-deployment-summary.js b/ui/app/models/task-group-deployment-summary.js index 21f795f87..b245e7127 100644 --- a/ui/app/models/task-group-deployment-summary.js +++ b/ui/app/models/task-group-deployment-summary.js @@ -3,25 +3,25 @@ import Fragment from 'ember-data-model-fragments/fragment'; import attr from 'ember-data/attr'; import { fragmentOwner } from 'ember-data-model-fragments/attributes'; -export default Fragment.extend({ - deployment: fragmentOwner(), +export default class TaskGroupDeploymentSummary extends Fragment { + @fragmentOwner() deployment; - name: attr('string'), + @attr('string') name; - autoRevert: attr('boolean'), - promoted: attr('boolean'), - requiresPromotion: gt('desiredCanaries', 0), + @attr('boolean') autoRevert; + @attr('boolean') promoted; + @gt('desiredCanaries', 0) requiresPromotion; // The list of canary allocation IDs // hasMany is not supported in fragments - placedCanaryAllocations: attr({ defaultValue: () => [] }), + @attr({ defaultValue: () => [] }) placedCanaryAllocations; - placedCanaries: alias('placedCanaryAllocations.length'), - desiredCanaries: attr('number'), - desiredTotal: attr('number'), - placedAllocs: attr('number'), - healthyAllocs: attr('number'), - unhealthyAllocs: attr('number'), + @alias('placedCanaryAllocations.length') placedCanaries; + @attr('number') desiredCanaries; + @attr('number') desiredTotal; + @attr('number') placedAllocs; + @attr('number') healthyAllocs; + @attr('number') unhealthyAllocs; - requireProgressBy: attr('date'), -}); + @attr('date') requireProgressBy; +} diff --git a/ui/app/models/task-group-summary.js b/ui/app/models/task-group-summary.js index 9e713b8ab..882e842bd 100644 --- a/ui/app/models/task-group-summary.js +++ b/ui/app/models/task-group-summary.js @@ -1,27 +1,28 @@ -import { collect, sum } from '@ember/object/computed'; +import { sum, collect } from '@ember/object/computed'; import Fragment from 'ember-data-model-fragments/fragment'; import attr from 'ember-data/attr'; import { fragmentOwner } from 'ember-data-model-fragments/attributes'; -export default Fragment.extend({ - job: fragmentOwner(), - name: attr('string'), +export default class TaskGroupSummary extends Fragment { + @fragmentOwner() job; + @attr('string') name; - queuedAllocs: attr('number'), - startingAllocs: attr('number'), - runningAllocs: attr('number'), - completeAllocs: attr('number'), - failedAllocs: attr('number'), - lostAllocs: attr('number'), + @attr('number') queuedAllocs; + @attr('number') startingAllocs; + @attr('number') runningAllocs; + @attr('number') completeAllocs; + @attr('number') failedAllocs; + @attr('number') lostAllocs; - allocsList: collect( + @collect( 'queuedAllocs', 'startingAllocs', 'runningAllocs', 'completeAllocs', 'failedAllocs', 'lostAllocs' - ), + ) + allocsList; - totalAllocs: sum('allocsList'), -}); + @sum('allocsList') totalAllocs; +} diff --git a/ui/app/models/task-group.js b/ui/app/models/task-group.js index 361e45f7e..1e60913f2 100644 --- a/ui/app/models/task-group.js +++ b/ui/app/models/task-group.js @@ -3,45 +3,52 @@ import Fragment from 'ember-data-model-fragments/fragment'; import attr from 'ember-data/attr'; import { fragmentOwner, fragmentArray } from 'ember-data-model-fragments/attributes'; import sumAggregation from '../utils/properties/sum-aggregation'; +import classic from 'ember-classic-decorator'; const maybe = arr => arr || []; -export default Fragment.extend({ - job: fragmentOwner(), +@classic +export default class TaskGroup extends Fragment { + @fragmentOwner() job; - name: attr('string'), - count: attr('number'), + @attr('string') name; + @attr('number') count; - tasks: fragmentArray('task'), + @fragmentArray('task') tasks; - services: fragmentArray('service'), + @fragmentArray('service') services; - volumes: fragmentArray('volume-definition'), + @fragmentArray('volume-definition') volumes; - drivers: computed('tasks.@each.driver', function() { + @computed('tasks.@each.driver') + get drivers() { return this.tasks.mapBy('driver').uniq(); - }), + } - allocations: computed('job.allocations.@each.taskGroup', function() { + @computed('job.allocations.@each.taskGroup') + get allocations() { return maybe(this.get('job.allocations')).filterBy('taskGroupName', this.name); - }), + } - reservedCPU: sumAggregation('tasks', 'reservedCPU'), - reservedMemory: sumAggregation('tasks', 'reservedMemory'), - reservedDisk: sumAggregation('tasks', 'reservedDisk'), + @sumAggregation('tasks', 'reservedCPU') reservedCPU; + @sumAggregation('tasks', 'reservedMemory') reservedMemory; + @sumAggregation('tasks', 'reservedDisk') reservedDisk; - reservedEphemeralDisk: attr('number'), + @attr('number') reservedEphemeralDisk; - placementFailures: computed('job.latestFailureEvaluation.failedTGAllocs.[]', function() { + @computed('job.latestFailureEvaluation.failedTGAllocs.[]') + get placementFailures() { const placementFailures = this.get('job.latestFailureEvaluation.failedTGAllocs'); return placementFailures && placementFailures.findBy('name', this.name); - }), + } - queuedOrStartingAllocs: computed('summary.{queuedAllocs,startingAllocs}', function() { + @computed('summary.{queuedAllocs,startingAllocs}') + get queuedOrStartingAllocs() { return this.get('summary.queuedAllocs') + this.get('summary.startingAllocs'); - }), + } - summary: computed('job.taskGroupSummaries.[]', function() { + @computed('job.taskGroupSummaries.[]') + get summary() { return maybe(this.get('job.taskGroupSummaries')).findBy('name', this.name); - }), -}); + } +} diff --git a/ui/app/models/task-state.js b/ui/app/models/task-state.js index fb9736ab7..f23985463 100644 --- a/ui/app/models/task-state.js +++ b/ui/app/models/task-state.js @@ -3,41 +3,47 @@ import { alias, none, and } from '@ember/object/computed'; import Fragment from 'ember-data-model-fragments/fragment'; import attr from 'ember-data/attr'; import { fragment, fragmentOwner, fragmentArray } from 'ember-data-model-fragments/attributes'; +import classic from 'ember-classic-decorator'; -export default Fragment.extend({ - allocation: fragmentOwner(), +@classic +export default class TaskState extends Fragment { + @fragmentOwner() allocation; - name: attr('string'), - state: attr('string'), - startedAt: attr('date'), - finishedAt: attr('date'), - failed: attr('boolean'), + @attr('string') name; + @attr('string') state; + @attr('date') startedAt; + @attr('date') finishedAt; + @attr('boolean') failed; - isActive: none('finishedAt'), - isRunning: and('isActive', 'allocation.isRunning'), + @none('finishedAt') isActive; + @and('isActive', 'allocation.isRunning') isRunning; - isConnectProxy: computed('task.kind', function() { + @computed('task.kind') + get isConnectProxy() { return (this.get('task.kind') || '').startsWith('connect-proxy:'); - }), + } - task: computed('name', 'allocation.taskGroup.tasks.[]', function() { + @computed('name', 'allocation.taskGroup.tasks.[]') + get task() { const tasks = this.get('allocation.taskGroup.tasks'); return tasks && tasks.findBy('name', this.name); - }), + } - driver: alias('task.driver'), + @alias('task.driver') driver; // TaskState represents a task running on a node, so in addition to knowing the // driver via the task, the health of the driver is also known via the node - driverStatus: computed('task.driver', 'allocation.node.drivers.[]', function() { + @computed('task.driver', 'allocation.node.drivers.[]') + get driverStatus() { const nodeDrivers = this.get('allocation.node.drivers') || []; return nodeDrivers.findBy('name', this.get('task.driver')); - }), + } - resources: fragment('resources'), - events: fragmentArray('task-event'), + @fragment('resources') resources; + @fragmentArray('task-event') events; - stateClass: computed('state', function() { + @computed('state') + get stateClass() { const classMap = { pending: 'is-pending', running: 'is-primary', @@ -46,9 +52,9 @@ export default Fragment.extend({ }; return classMap[this.state] || 'is-dark'; - }), + } restart() { return this.allocation.restart(this.name); - }, -}); + } +} diff --git a/ui/app/models/task.js b/ui/app/models/task.js index 7d5061272..7c2b95a6f 100644 --- a/ui/app/models/task.js +++ b/ui/app/models/task.js @@ -3,25 +3,26 @@ import Fragment from 'ember-data-model-fragments/fragment'; import { fragment, fragmentArray, fragmentOwner } from 'ember-data-model-fragments/attributes'; import { computed } from '@ember/object'; -export default Fragment.extend({ - taskGroup: fragmentOwner(), +export default class Task extends Fragment { + @fragmentOwner() taskGroup; - name: attr('string'), - driver: attr('string'), - kind: attr('string'), + @attr('string') name; + @attr('string') driver; + @attr('string') kind; - lifecycle: fragment('lifecycle'), + @fragment('lifecycle') lifecycle; - lifecycleName: computed('lifecycle', 'lifecycle.sidecar', function() { + @computed('lifecycle', 'lifecycle.sidecar') + get lifecycleName() { if (this.lifecycle && this.lifecycle.sidecar) return 'sidecar'; if (this.lifecycle && this.lifecycle.hook === 'prestart') return 'prestart'; return 'main'; - }), + } - reservedMemory: attr('number'), - reservedCPU: attr('number'), - reservedDisk: attr('number'), - reservedEphemeralDisk: attr('number'), + @attr('number') reservedMemory; + @attr('number') reservedCPU; + @attr('number') reservedDisk; + @attr('number') reservedEphemeralDisk; - volumeMounts: fragmentArray('volume-mount', { defaultValue: () => [] }), -}); + @fragmentArray('volume-mount', { defaultValue: () => [] }) volumeMounts; +} diff --git a/ui/app/models/token.js b/ui/app/models/token.js index 37db199ae..2302dd8d6 100644 --- a/ui/app/models/token.js +++ b/ui/app/models/token.js @@ -3,14 +3,14 @@ import Model from 'ember-data/model'; import attr from 'ember-data/attr'; import { hasMany } from 'ember-data/relationships'; -export default Model.extend({ - secret: attr('string'), - name: attr('string'), - global: attr('boolean'), - createTime: attr('date'), - type: attr('string'), - policies: hasMany('policy'), - policyNames: attr(), +export default class Token extends Model { + @attr('string') secret; + @attr('string') name; + @attr('boolean') global; + @attr('date') createTime; + @attr('string') type; + @hasMany('policy') policies; + @attr() policyNames; - accessor: alias('id'), -}); + @alias('id') accessor; +} diff --git a/ui/app/models/volume-definition.js b/ui/app/models/volume-definition.js index b682b4c3a..ce94e541e 100644 --- a/ui/app/models/volume-definition.js +++ b/ui/app/models/volume-definition.js @@ -3,15 +3,15 @@ import attr from 'ember-data/attr'; import Fragment from 'ember-data-model-fragments/fragment'; import { fragmentOwner } from 'ember-data-model-fragments/attributes'; -export default Fragment.extend({ - taskGroup: fragmentOwner(), +export default class VolumeDefinition extends Fragment { + @fragmentOwner() taskGroup; - name: attr('string'), + @attr('string') name; - source: attr('string'), - type: attr('string'), - readOnly: attr('boolean'), + @attr('string') source; + @attr('string') type; + @attr('boolean') readOnly; - isCSI: equal('type', 'csi'), - namespace: alias('taskGroup.job.namespace'), -}); + @equal('type', 'csi') isCSI; + @alias('taskGroup.job.namespace') namespace; +} diff --git a/ui/app/models/volume-mount.js b/ui/app/models/volume-mount.js index 107c093f5..7fa78d465 100644 --- a/ui/app/models/volume-mount.js +++ b/ui/app/models/volume-mount.js @@ -4,23 +4,24 @@ import attr from 'ember-data/attr'; import Fragment from 'ember-data-model-fragments/fragment'; import { fragmentOwner } from 'ember-data-model-fragments/attributes'; -export default Fragment.extend({ - task: fragmentOwner(), +export default class VolumeMount extends Fragment { + @fragmentOwner() task; - volume: attr('string'), + @attr('string') volume; - volumeDeclaration: computed('task.taskGroup.volumes.@each.name', function() { + @computed('task.taskGroup.volumes.@each.name') + get volumeDeclaration() { return this.task.taskGroup.volumes.findBy('name', this.volume); - }), + } - isCSI: equal('volumeDeclaration.type', 'csi'), - source: alias('volumeDeclaration.source'), + @equal('volumeDeclaration.type', 'csi') isCSI; + @alias('volumeDeclaration.source') source; // Since CSI volumes are namespaced, the link intent of a volume mount will // be to the CSI volume with a namespace that matches this task's job's namespace. - namespace: alias('task.taskGroup.job.namespace'), + @alias('task.taskGroup.job.namespace') namespace; - destination: attr('string'), - propagationMode: attr('string'), - readOnly: attr('boolean'), -}); + @attr('string') destination; + @attr('string') propagationMode; + @attr('boolean') readOnly; +} diff --git a/ui/app/models/volume.js b/ui/app/models/volume.js index 167fc6502..ed2b285c3 100644 --- a/ui/app/models/volume.js +++ b/ui/app/models/volume.js @@ -3,44 +3,47 @@ import Model from 'ember-data/model'; import attr from 'ember-data/attr'; import { belongsTo, hasMany } from 'ember-data/relationships'; -export default Model.extend({ - plainId: attr('string'), - name: attr('string'), +export default class Volume extends Model { + @attr('string') plainId; + @attr('string') name; - namespace: belongsTo('namespace'), - plugin: belongsTo('plugin'), + @belongsTo('namespace') namespace; + @belongsTo('plugin') plugin; - writeAllocations: hasMany('allocation'), - readAllocations: hasMany('allocation'), + @hasMany('allocation') writeAllocations; + @hasMany('allocation') readAllocations; - allocations: computed('writeAllocations.[]', 'readAllocations.[]', function() { + @computed('writeAllocations.[]', 'readAllocations.[]') + get allocations() { return [...this.writeAllocations.toArray(), ...this.readAllocations.toArray()]; - }), + } - externalId: attr('string'), - topologies: attr(), - accessMode: attr('string'), - attachmentMode: attr('string'), - schedulable: attr('boolean'), - provider: attr('string'), - version: attr('string'), + @attr('string') externalId; + @attr() topologies; + @attr('string') accessMode; + @attr('string') attachmentMode; + @attr('boolean') schedulable; + @attr('string') provider; + @attr('string') version; - controllerRequired: attr('boolean'), - controllersHealthy: attr('number'), - controllersExpected: attr('number'), + @attr('boolean') controllerRequired; + @attr('number') controllersHealthy; + @attr('number') controllersExpected; - controllersHealthyProportion: computed('controllersHealthy', 'controllersExpected', function() { + @computed('controllersHealthy', 'controllersExpected') + get controllersHealthyProportion() { return this.controllersHealthy / this.controllersExpected; - }), + } - nodesHealthy: attr('number'), - nodesExpected: attr('number'), + @attr('number') nodesHealthy; + @attr('number') nodesExpected; - nodesHealthyProportion: computed('nodesHealthy', 'nodesExpected', function() { + @computed('nodesHealthy', 'nodesExpected') + get nodesHealthyProportion() { return this.nodesHealthy / this.nodesExpected; - }), + } - resourceExhausted: attr('number'), - createIndex: attr('number'), - modifyIndex: attr('number'), -}); + @attr('number') resourceExhausted; + @attr('number') createIndex; + @attr('number') modifyIndex; +} diff --git a/ui/app/router.js b/ui/app/router.js index 9645a7b70..51e162268 100644 --- a/ui/app/router.js +++ b/ui/app/router.js @@ -1,10 +1,10 @@ import EmberRouter from '@ember/routing/router'; import config from './config/environment'; -const Router = EmberRouter.extend({ - location: config.locationType, - rootURL: config.rootURL, -}); +class Router extends EmberRouter { + location = config.locationType; + rootURL = config.rootURL; +} Router.map(function() { this.route('exec', { path: '/exec/:job_name' }, function() { diff --git a/ui/app/routes/allocations/allocation.js b/ui/app/routes/allocations/allocation.js index c884af592..251f00726 100644 --- a/ui/app/routes/allocations/allocation.js +++ b/ui/app/routes/allocations/allocation.js @@ -6,12 +6,12 @@ import notifyError from 'nomad-ui/utils/notify-error'; import { qpBuilder } from 'nomad-ui/utils/classes/query-params'; import { jobCrumbs } from 'nomad-ui/utils/breadcrumb-utils'; -export default Route.extend(WithWatchers, { +export default class AllocationRoute extends Route.extend(WithWatchers) { startWatchers(controller, model) { if (model) { controller.set('watcher', this.watch.perform(model)); } - }, + } // Allocation breadcrumbs extend from job / task group breadcrumbs // even though the route structure does not. @@ -37,16 +37,17 @@ export default Route.extend(WithWatchers, { args: ['allocations.allocation', model], }, ]; - }, + } model() { // Preload the job for the allocation since it's required for the breadcrumb trail - return this._super(...arguments) + return super + .model(...arguments) .then(allocation => allocation.get('job').then(() => allocation)) .catch(notifyError(this)); - }, + } - watch: watchRecord('allocation'), + @watchRecord('allocation') watch; - watchers: collect('watch'), -}); + @collect('watch') watchers; +} diff --git a/ui/app/routes/allocations/allocation/fs-root.js b/ui/app/routes/allocations/allocation/fs-root.js index eb85dc7c3..cc21ceb44 100644 --- a/ui/app/routes/allocations/allocation/fs-root.js +++ b/ui/app/routes/allocations/allocation/fs-root.js @@ -1,5 +1,5 @@ import FSRoute from './fs'; -export default FSRoute.extend({ - templateName: 'allocations/allocation/fs', -}); +export default class FsRootRoute extends FSRoute { + templateName = 'allocations/allocation/fs'; +} diff --git a/ui/app/routes/allocations/allocation/fs.js b/ui/app/routes/allocations/allocation/fs.js index f3580621a..ae1c67fe7 100644 --- a/ui/app/routes/allocations/allocation/fs.js +++ b/ui/app/routes/allocations/allocation/fs.js @@ -2,7 +2,7 @@ import Route from '@ember/routing/route'; import RSVP from 'rsvp'; import notifyError from 'nomad-ui/utils/notify-error'; -export default Route.extend({ +export default class FsRoute extends Route { model({ path = '/' }) { const decodedPath = decodeURIComponent(path); const allocation = this.modelFor('allocations.allocation'); @@ -33,10 +33,10 @@ export default Route.extend({ } }) .catch(notifyError(this)); - }, + } setupController(controller, { path, allocation, directoryEntries, isFile, stat } = {}) { - this._super(...arguments); + super.setupController(...arguments); controller.setProperties({ path, allocation, directoryEntries, isFile, stat }); - }, -}); + } +} diff --git a/ui/app/routes/allocations/allocation/index.js b/ui/app/routes/allocations/allocation/index.js index 323af0e82..e4de01cdb 100644 --- a/ui/app/routes/allocations/allocation/index.js +++ b/ui/app/routes/allocations/allocation/index.js @@ -1,6 +1,6 @@ import Route from '@ember/routing/route'; -export default Route.extend({ +export default class IndexRoute extends Route { setupController(controller, model) { // Suppress the preemptedByAllocation fetch error in the event it's a 404 if (model) { @@ -8,12 +8,12 @@ export default Route.extend({ model.preemptedByAllocation.then(setPreempter, setPreempter); } - return this._super(...arguments); - }, + return super.setupController(...arguments); + } resetController(controller, isExiting) { if (isExiting) { controller.watchNext.cancelAll(); } - }, -}); + } +} diff --git a/ui/app/routes/allocations/allocation/task.js b/ui/app/routes/allocations/allocation/task.js index d06a7b978..7f3eec93d 100644 --- a/ui/app/routes/allocations/allocation/task.js +++ b/ui/app/routes/allocations/allocation/task.js @@ -2,8 +2,8 @@ import { inject as service } from '@ember/service'; import Route from '@ember/routing/route'; import EmberError from '@ember/error'; -export default Route.extend({ - store: service(), +export default class TaskRoute extends Route { + @service store; breadcrumbs(model) { if (!model) return []; @@ -13,7 +13,7 @@ export default Route.extend({ args: ['allocations.allocation.task', model.get('allocation'), model], }, ]; - }, + } model({ name }) { const allocation = this.modelFor('allocations.allocation'); @@ -31,5 +31,5 @@ export default Route.extend({ } return task; - }, -}); + } +} diff --git a/ui/app/routes/allocations/allocation/task/fs-root.js b/ui/app/routes/allocations/allocation/task/fs-root.js index 81f1c1ee6..38e49355f 100644 --- a/ui/app/routes/allocations/allocation/task/fs-root.js +++ b/ui/app/routes/allocations/allocation/task/fs-root.js @@ -1,5 +1,5 @@ import FSRoute from './fs'; -export default FSRoute.extend({ - templateName: 'allocations/allocation/task/fs', -}); +export default class FsRootRoute extends FSRoute { + templateName = 'allocations/allocation/task/fs'; +} diff --git a/ui/app/routes/allocations/allocation/task/fs.js b/ui/app/routes/allocations/allocation/task/fs.js index 4827d5276..7fcbc8734 100644 --- a/ui/app/routes/allocations/allocation/task/fs.js +++ b/ui/app/routes/allocations/allocation/task/fs.js @@ -2,13 +2,15 @@ import Route from '@ember/routing/route'; import RSVP from 'rsvp'; import notifyError from 'nomad-ui/utils/notify-error'; -export default Route.extend({ +export default class FsRoute extends Route { model({ path = '/' }) { const decodedPath = decodeURIComponent(path); const taskState = this.modelFor('allocations.allocation.task'); const allocation = taskState.allocation; - const pathWithTaskName = `${taskState.name}${decodedPath.startsWith('/') ? '' : '/'}${decodedPath}`; + const pathWithTaskName = `${taskState.name}${ + decodedPath.startsWith('/') ? '' : '/' + }${decodedPath}`; if (!taskState.isRunning) { return { @@ -36,10 +38,10 @@ export default Route.extend({ } }) .catch(notifyError(this)); - }, + } setupController(controller, { path, taskState, directoryEntries, isFile, stat } = {}) { - this._super(...arguments); + super.setupController(...arguments); controller.setProperties({ path, taskState, directoryEntries, isFile, stat }); - }, -}); + } +} diff --git a/ui/app/routes/allocations/allocation/task/logs.js b/ui/app/routes/allocations/allocation/task/logs.js index 3b5ad2312..dde16232f 100644 --- a/ui/app/routes/allocations/allocation/task/logs.js +++ b/ui/app/routes/allocations/allocation/task/logs.js @@ -1,8 +1,8 @@ import Route from '@ember/routing/route'; -export default Route.extend({ +export default class LogsRoute extends Route { model() { - const task = this._super(...arguments); + const task = super.model(...arguments); return task && task.get('allocation.node').then(() => task); - }, -}); + } +} diff --git a/ui/app/routes/application.js b/ui/app/routes/application.js index d97eb853c..1e7500924 100644 --- a/ui/app/routes/application.js +++ b/ui/app/routes/application.js @@ -3,24 +3,27 @@ import { next } from '@ember/runloop'; import Route from '@ember/routing/route'; import { AbortError } from '@ember-data/adapter/error'; import RSVP from 'rsvp'; +import { action } from '@ember/object'; +import classic from 'ember-classic-decorator'; -export default Route.extend({ - config: service(), - system: service(), - store: service(), - token: service(), +@classic +export default class ApplicationRoute extends Route { + @service config; + @service system; + @service store; + @service token; - queryParams: { + queryParams = { region: { refreshModel: true, }, - }, + }; resetController(controller, isExiting) { if (isExiting) { controller.set('error', null); } - }, + } beforeModel(transition) { const fetchSelfTokenAndPolicies = this.get('token.fetchSelfTokenAndPolicies') @@ -51,13 +54,13 @@ export default Route.extend({ return promises; }); - }, + } // Model is being used as a way to transfer the provided region // query param to update the controller state. model(params) { return params.region; - }, + } setupController(controller, model) { const queryParam = model; @@ -68,24 +71,25 @@ export default Route.extend({ }); } - return this._super(...arguments); - }, + return super.setupController(...arguments); + } - actions: { - didTransition() { - if (!this.get('config.isTest')) { - window.scrollTo(0, 0); - } - }, + @action + didTransition() { + if (!this.get('config.isTest')) { + window.scrollTo(0, 0); + } + } - willTransition() { - this.controllerFor('application').set('error', null); - }, + @action + willTransition() { + this.controllerFor('application').set('error', null); + } - error(error) { - if (!(error instanceof AbortError)) { - this.controllerFor('application').set('error', error); - } - }, - }, -}); + @action + error(error) { + if (!(error instanceof AbortError)) { + this.controllerFor('application').set('error', error); + } + } +} diff --git a/ui/app/routes/clients.js b/ui/app/routes/clients.js index 23c387284..c0c85460e 100644 --- a/ui/app/routes/clients.js +++ b/ui/app/routes/clients.js @@ -3,26 +3,28 @@ import Route from '@ember/routing/route'; import RSVP from 'rsvp'; import WithForbiddenState from 'nomad-ui/mixins/with-forbidden-state'; import notifyForbidden from 'nomad-ui/utils/notify-forbidden'; +import classic from 'ember-classic-decorator'; -export default Route.extend(WithForbiddenState, { - store: service(), - system: service(), +@classic +export default class ClientsRoute extends Route.extend(WithForbiddenState) { + @service store; + @service system; - breadcrumbs: Object.freeze([ + breadcrumbs = [ { label: 'Clients', args: ['clients.index'], }, - ]), + ]; beforeModel() { return this.get('system.leader'); - }, + } model() { return RSVP.hash({ nodes: this.store.findAll('node'), agents: this.store.findAll('agent'), }).catch(notifyForbidden(this)); - }, -}); + } +} diff --git a/ui/app/routes/clients/client.js b/ui/app/routes/clients/client.js index c248c944d..64cf7d087 100644 --- a/ui/app/routes/clients/client.js +++ b/ui/app/routes/clients/client.js @@ -5,12 +5,12 @@ import notifyError from 'nomad-ui/utils/notify-error'; import { watchRecord, watchRelationship } from 'nomad-ui/utils/properties/watch'; import WithWatchers from 'nomad-ui/mixins/with-watchers'; -export default Route.extend(WithWatchers, { - store: service(), +export default class ClientRoute extends Route.extend(WithWatchers) { + @service store; model() { - return this._super(...arguments).catch(notifyError(this)); - }, + return super.model(...arguments).catch(notifyError(this)); + } breadcrumbs(model) { if (!model) return []; @@ -20,20 +20,20 @@ export default Route.extend(WithWatchers, { args: ['clients.client', model.get('id')], }, ]; - }, + } afterModel(model) { if (model && model.get('isPartial')) { return model.reload().then(node => node.get('allocations')); } return model && model.get('allocations'); - }, + } setupController(controller, model) { controller.set('flagAsDraining', model && model.isDraining); - return this._super(...arguments); - }, + return super.setupController(...arguments); + } resetController(controller) { controller.setProperties({ @@ -45,17 +45,17 @@ export default Route.extend(WithWatchers, { showDrainUpdateNotification: false, showDrainStoppedNotification: false, }); - }, + } startWatchers(controller, model) { if (model) { controller.set('watchModel', this.watch.perform(model)); controller.set('watchAllocations', this.watchAllocations.perform(model)); } - }, + } - watch: watchRecord('node'), - watchAllocations: watchRelationship('allocations'), + @watchRecord('node') watch; + @watchRelationship('allocations') watchAllocations; - watchers: collect('watch', 'watchAllocations'), -}); + @collect('watch', 'watchAllocations') watchers; +} diff --git a/ui/app/routes/clients/index.js b/ui/app/routes/clients/index.js index 374c8d007..862c16b75 100644 --- a/ui/app/routes/clients/index.js +++ b/ui/app/routes/clients/index.js @@ -3,11 +3,11 @@ import { collect } from '@ember/object/computed'; import { watchAll } from 'nomad-ui/utils/properties/watch'; import WithWatchers from 'nomad-ui/mixins/with-watchers'; -export default Route.extend(WithWatchers, { +export default class IndexRoute extends Route.extend(WithWatchers) { startWatchers(controller) { controller.set('watcher', this.watch.perform()); - }, + } - watch: watchAll('node'), - watchers: collect('watch'), -}); + @watchAll('node') watch; + @collect('watch') watchers; +} diff --git a/ui/app/routes/csi/index.js b/ui/app/routes/csi/index.js index f66008d19..3b77d0513 100644 --- a/ui/app/routes/csi/index.js +++ b/ui/app/routes/csi/index.js @@ -1,7 +1,7 @@ import Route from '@ember/routing/route'; -export default Route.extend({ +export default class IndexRoute extends Route { redirect() { this.transitionTo('csi.volumes'); - }, -}); + } +} diff --git a/ui/app/routes/csi/plugins.js b/ui/app/routes/csi/plugins.js index 30b181220..0ddd99f36 100644 --- a/ui/app/routes/csi/plugins.js +++ b/ui/app/routes/csi/plugins.js @@ -3,17 +3,17 @@ import Route from '@ember/routing/route'; import WithForbiddenState from 'nomad-ui/mixins/with-forbidden-state'; import notifyForbidden from 'nomad-ui/utils/notify-forbidden'; -export default Route.extend(WithForbiddenState, { - store: service(), +export default class PluginsRoute extends Route.extend(WithForbiddenState) { + @service store; - breadcrumbs: Object.freeze([ + breadcrumbs = [ { label: 'Storage', args: ['csi.index'], }, - ]), + ]; model() { return this.store.query('plugin', { type: 'csi' }).catch(notifyForbidden(this)); - }, -}); + } +} diff --git a/ui/app/routes/csi/plugins/index.js b/ui/app/routes/csi/plugins/index.js index 2adff9d30..1155e7b80 100644 --- a/ui/app/routes/csi/plugins/index.js +++ b/ui/app/routes/csi/plugins/index.js @@ -3,11 +3,11 @@ import { collect } from '@ember/object/computed'; import { watchQuery } from 'nomad-ui/utils/properties/watch'; import WithWatchers from 'nomad-ui/mixins/with-watchers'; -export default Route.extend(WithWatchers, { +export default class IndexRoute extends Route.extend(WithWatchers) { startWatchers(controller) { controller.set('modelWatch', this.watch.perform({ type: 'csi' })); - }, + } - watch: watchQuery('plugin'), - watchers: collect('watch'), -}); + @watchQuery('plugin') watch; + @collect('watch') watchers; +} diff --git a/ui/app/routes/csi/plugins/plugin.js b/ui/app/routes/csi/plugins/plugin.js index 3cdc3157d..5643b2fb7 100644 --- a/ui/app/routes/csi/plugins/plugin.js +++ b/ui/app/routes/csi/plugins/plugin.js @@ -2,11 +2,11 @@ import { inject as service } from '@ember/service'; import Route from '@ember/routing/route'; import notifyError from 'nomad-ui/utils/notify-error'; -export default Route.extend({ - store: service(), - system: service(), +export default class PluginRoute extends Route { + @service store; + @service system; - breadcrumbs: plugin => [ + breadcrumbs = plugin => [ { label: 'Plugins', args: ['csi.plugins'], @@ -15,13 +15,13 @@ export default Route.extend({ label: plugin.plainId, args: ['csi.plugins.plugin', plugin.plainId], }, - ], + ]; serialize(model) { return { plugin_name: model.get('plainId') }; - }, + } model(params) { return this.store.findRecord('plugin', `csi/${params.plugin_name}`).catch(notifyError(this)); - }, -}); + } +} diff --git a/ui/app/routes/csi/plugins/plugin/allocations.js b/ui/app/routes/csi/plugins/plugin/allocations.js index b46d2b5a5..f25dd1ec2 100644 --- a/ui/app/routes/csi/plugins/plugin/allocations.js +++ b/ui/app/routes/csi/plugins/plugin/allocations.js @@ -3,14 +3,14 @@ import { collect } from '@ember/object/computed'; import { watchRecord } from 'nomad-ui/utils/properties/watch'; import WithWatchers from 'nomad-ui/mixins/with-watchers'; -export default Route.extend(WithWatchers, { +export default class AllocationsRoute extends Route.extend(WithWatchers) { startWatchers(controller, model) { if (!model) return; controller.set('watchers', { model: this.watch.perform(model), }); - }, + } resetController(controller, isExiting) { if (isExiting) { @@ -20,8 +20,8 @@ export default Route.extend(WithWatchers, { qpHealth: '', }); } - }, + } - watch: watchRecord('plugin'), - watchers: collect('watch'), -}); + @watchRecord('plugin') watch; + @collect('watch') watchers; +} diff --git a/ui/app/routes/csi/plugins/plugin/index.js b/ui/app/routes/csi/plugins/plugin/index.js index 350c6ac20..f80460f42 100644 --- a/ui/app/routes/csi/plugins/plugin/index.js +++ b/ui/app/routes/csi/plugins/plugin/index.js @@ -3,15 +3,15 @@ import { collect } from '@ember/object/computed'; import { watchRecord } from 'nomad-ui/utils/properties/watch'; import WithWatchers from 'nomad-ui/mixins/with-watchers'; -export default Route.extend(WithWatchers, { +export default class IndexRoute extends Route.extend(WithWatchers) { startWatchers(controller, model) { if (!model) return; controller.set('watchers', { model: this.watch.perform(model), }); - }, + } - watch: watchRecord('plugin'), - watchers: collect('watch'), -}); + @watchRecord('plugin') watch; + @collect('watch') watchers; +} diff --git a/ui/app/routes/csi/volumes.js b/ui/app/routes/csi/volumes.js index fe49f7639..14d0ada01 100644 --- a/ui/app/routes/csi/volumes.js +++ b/ui/app/routes/csi/volumes.js @@ -2,23 +2,25 @@ import { inject as service } from '@ember/service'; import Route from '@ember/routing/route'; import WithForbiddenState from 'nomad-ui/mixins/with-forbidden-state'; import notifyForbidden from 'nomad-ui/utils/notify-forbidden'; +import classic from 'ember-classic-decorator'; -export default Route.extend(WithForbiddenState, { - system: service(), - store: service(), +@classic +export default class VolumesRoute extends Route.extend(WithForbiddenState) { + @service system; + @service store; - breadcrumbs: Object.freeze([ + breadcrumbs = [ { label: 'Storage', args: ['csi.index'], }, - ]), + ]; - queryParams: { + queryParams = { volumeNamespace: { refreshModel: true, }, - }, + }; beforeModel(transition) { return this.get('system.namespaces').then(namespaces => { @@ -27,7 +29,7 @@ export default Route.extend(WithForbiddenState, { return namespaces; }); - }, + } model() { return this.store @@ -37,5 +39,5 @@ export default Route.extend(WithForbiddenState, { return volumes; }) .catch(notifyForbidden(this)); - }, -}); + } +} diff --git a/ui/app/routes/csi/volumes/index.js b/ui/app/routes/csi/volumes/index.js index 40aa58f81..e405bb5bd 100644 --- a/ui/app/routes/csi/volumes/index.js +++ b/ui/app/routes/csi/volumes/index.js @@ -3,11 +3,11 @@ import { collect } from '@ember/object/computed'; import { watchQuery } from 'nomad-ui/utils/properties/watch'; import WithWatchers from 'nomad-ui/mixins/with-watchers'; -export default Route.extend(WithWatchers, { +export default class IndexRoute extends Route.extend(WithWatchers) { startWatchers(controller) { controller.set('modelWatch', this.watch.perform({ type: 'csi' })); - }, + } - watch: watchQuery('volume'), - watchers: collect('watch'), -}); + @watchQuery('volume') watch; + @collect('watch') watchers; +} diff --git a/ui/app/routes/csi/volumes/volume.js b/ui/app/routes/csi/volumes/volume.js index c4d324fe2..77c2d2d99 100644 --- a/ui/app/routes/csi/volumes/volume.js +++ b/ui/app/routes/csi/volumes/volume.js @@ -6,11 +6,11 @@ import { qpBuilder } from 'nomad-ui/utils/classes/query-params'; import { watchRecord } from 'nomad-ui/utils/properties/watch'; import WithWatchers from 'nomad-ui/mixins/with-watchers'; -export default Route.extend(WithWatchers, { - store: service(), - system: service(), +export default class VolumeRoute extends Route.extend(WithWatchers) { + @service store; + @service system; - breadcrumbs: volume => [ + breadcrumbs = volume => [ { label: 'Volumes', args: [ @@ -26,7 +26,7 @@ export default Route.extend(WithWatchers, { qpBuilder({ volumeNamespace: volume.get('namespace.name') || 'default' }), ], }, - ], + ]; startWatchers(controller, model) { if (!model) return; @@ -34,22 +34,22 @@ export default Route.extend(WithWatchers, { controller.set('watchers', { model: this.watch.perform(model), }); - }, + } serialize(model) { return { volume_name: model.get('plainId') }; - }, + } model(params, transition) { const namespace = transition.to.queryParams.namespace || this.get('system.activeNamespace.id'); const name = params.volume_name; const fullId = JSON.stringify([`csi/${name}`, namespace || 'default']); return this.store.findRecord('volume', fullId, { reload: true }).catch(notifyError(this)); - }, + } // Since volume includes embedded records for allocations, // it's possible that allocations that are server-side deleted may // not be removed from the UI while sitting on the volume detail page. - watch: watchRecord('volume'), - watchers: collect('watch'), -}); + @watchRecord('volume') watch; + @collect('watch') watchers; +} diff --git a/ui/app/routes/exec.js b/ui/app/routes/exec.js index 883c0f44d..4ad7fbea9 100644 --- a/ui/app/routes/exec.js +++ b/ui/app/routes/exec.js @@ -4,14 +4,16 @@ import notifyError from 'nomad-ui/utils/notify-error'; import { collect } from '@ember/object/computed'; import WithWatchers from 'nomad-ui/mixins/with-watchers'; import { watchRecord, watchRelationship } from 'nomad-ui/utils/properties/watch'; +import classic from 'ember-classic-decorator'; -export default Route.extend(WithWatchers, { - store: service(), - token: service(), +@classic +export default class ExecRoute extends Route.extend(WithWatchers) { + @service store; + @service token; serialize(model) { return { job_name: model.get('plainId') }; - }, + } model(params, transition) { const namespace = transition.to.queryParams.namespace || this.get('system.activeNamespace.id'); @@ -28,22 +30,22 @@ export default Route.extend(WithWatchers, { const xtermImport = import('xterm').then(module => module.Terminal); return Promise.all([jobPromise, xtermImport]); - }, + } setupController(controller, [job, Terminal]) { - this._super(controller, job); + super.setupController(controller, job); controller.setUpTerminal(Terminal); - }, + } startWatchers(controller, model) { if (model) { controller.set('watcher', this.watch.perform(model)); controller.set('watchAllocations', this.watchAllocations.perform(model)); } - }, + } - watch: watchRecord('job'), - watchAllocations: watchRelationship('allocations'), + @watchRecord('job') watch; + @watchRelationship('allocations') watchAllocations; - watchers: collect('watch', 'watchAllocations'), -}); + @collect('watch', 'watchAllocations') watchers; +} diff --git a/ui/app/routes/exec/task-group/task.js b/ui/app/routes/exec/task-group/task.js index 984bf99d3..39ce0a899 100644 --- a/ui/app/routes/exec/task-group/task.js +++ b/ui/app/routes/exec/task-group/task.js @@ -1,8 +1,8 @@ import { inject as service } from '@ember/service'; import Route from '@ember/routing/route'; -export default Route.extend({ - store: service(), +export default class TaskRoute extends Route { + @service store; model({ task_name }) { const allocationQueryParam = this.paramsFor('exec').allocation; @@ -13,7 +13,7 @@ export default Route.extend({ taskName: task_name, taskGroupName, }; - }, + } setupController(controller, { allocationShortId, taskGroupName, taskName }) { this.controllerFor('exec').send('setTaskProperties', { @@ -22,6 +22,6 @@ export default Route.extend({ taskGroupName, }); - this._super(...arguments); - }, -}); + super.setupController(...arguments); + } +} diff --git a/ui/app/routes/index.js b/ui/app/routes/index.js index 7de6fff38..72fc1086e 100644 --- a/ui/app/routes/index.js +++ b/ui/app/routes/index.js @@ -1,7 +1,7 @@ import Route from '@ember/routing/route'; -export default Route.extend({ +export default class IndexRoute extends Route { redirect() { this.transitionTo('jobs'); - }, -}); + } +} diff --git a/ui/app/routes/jobs.js b/ui/app/routes/jobs.js index 42b2e1cb5..8ba64a9e2 100644 --- a/ui/app/routes/jobs.js +++ b/ui/app/routes/jobs.js @@ -2,23 +2,26 @@ import { inject as service } from '@ember/service'; import Route from '@ember/routing/route'; import WithForbiddenState from 'nomad-ui/mixins/with-forbidden-state'; import notifyForbidden from 'nomad-ui/utils/notify-forbidden'; +import { action } from '@ember/object'; +import classic from 'ember-classic-decorator'; -export default Route.extend(WithForbiddenState, { - system: service(), - store: service(), +@classic +export default class JobsRoute extends Route.extend(WithForbiddenState) { + @service store; + @service system; - breadcrumbs: Object.freeze([ + breadcrumbs = [ { label: 'Jobs', args: ['jobs.index'], }, - ]), + ]; - queryParams: { + queryParams = { jobNamespace: { refreshModel: true, }, - }, + }; beforeModel(transition) { return this.get('system.namespaces').then(namespaces => { @@ -27,15 +30,14 @@ export default Route.extend(WithForbiddenState, { return namespaces; }); - }, + } model() { return this.store.findAll('job', { reload: true }).catch(notifyForbidden(this)); - }, + } - actions: { - refreshRoute() { - this.refresh(); - }, - }, -}); + @action + refreshRoute() { + this.refresh(); + } +} diff --git a/ui/app/routes/jobs/index.js b/ui/app/routes/jobs/index.js index 38b9ebe18..c8369e806 100644 --- a/ui/app/routes/jobs/index.js +++ b/ui/app/routes/jobs/index.js @@ -3,11 +3,11 @@ import { collect } from '@ember/object/computed'; import { watchAll } from 'nomad-ui/utils/properties/watch'; import WithWatchers from 'nomad-ui/mixins/with-watchers'; -export default Route.extend(WithWatchers, { +export default class IndexRoute extends Route.extend(WithWatchers) { startWatchers(controller) { controller.set('modelWatch', this.watch.perform()); - }, + } - watch: watchAll('job'), - watchers: collect('watch'), -}); + @watchAll('job') watch; + @collect('watch') watchers; +} diff --git a/ui/app/routes/jobs/job.js b/ui/app/routes/jobs/job.js index 70f323049..1cf7d438e 100644 --- a/ui/app/routes/jobs/job.js +++ b/ui/app/routes/jobs/job.js @@ -4,15 +4,15 @@ import RSVP from 'rsvp'; import notifyError from 'nomad-ui/utils/notify-error'; import { jobCrumbs } from 'nomad-ui/utils/breadcrumb-utils'; -export default Route.extend({ - store: service(), - token: service(), +export default class JobRoute extends Route { + @service store; + @service token; - breadcrumbs: jobCrumbs, + breadcrumbs = jobCrumbs; serialize(model) { return { job_name: model.get('plainId') }; - }, + } model(params, transition) { const namespace = transition.to.queryParams.namespace || this.get('system.activeNamespace.id'); @@ -24,5 +24,5 @@ export default Route.extend({ return RSVP.all([job.get('allocations'), job.get('evaluations')]).then(() => job); }) .catch(notifyError(this)); - }, -}); + } +} diff --git a/ui/app/routes/jobs/job/allocations.js b/ui/app/routes/jobs/job/allocations.js index f446ef86a..d5829719c 100644 --- a/ui/app/routes/jobs/job/allocations.js +++ b/ui/app/routes/jobs/job/allocations.js @@ -3,19 +3,19 @@ import { collect } from '@ember/object/computed'; import { watchRelationship } from 'nomad-ui/utils/properties/watch'; import WithWatchers from 'nomad-ui/mixins/with-watchers'; -export default Route.extend(WithWatchers, { +export default class AllocationsRoute extends Route.extend(WithWatchers) { model() { const job = this.modelFor('jobs.job'); return job && job.get('allocations').then(() => job); - }, + } startWatchers(controller, model) { if (model) { controller.set('watchAllocations', this.watchAllocations.perform(model)); } - }, + } - watchAllocations: watchRelationship('allocations'), + @watchRelationship('allocations') watchAllocations; - watchers: collect('watchAllocations'), -}); + @collect('watchAllocations') watchers; +} diff --git a/ui/app/routes/jobs/job/definition.js b/ui/app/routes/jobs/job/definition.js index b551bd9b1..4b089e871 100644 --- a/ui/app/routes/jobs/job/definition.js +++ b/ui/app/routes/jobs/job/definition.js @@ -1,6 +1,6 @@ import Route from '@ember/routing/route'; -export default Route.extend({ +export default class DefinitionRoute extends Route { model() { const job = this.modelFor('jobs.job'); if (!job) return; @@ -9,7 +9,7 @@ export default Route.extend({ job, definition, })); - }, + } resetController(controller, isExiting) { if (isExiting) { @@ -18,5 +18,5 @@ export default Route.extend({ job.resetId(); controller.set('isEditing', false); } - }, -}); + } +} diff --git a/ui/app/routes/jobs/job/deployments.js b/ui/app/routes/jobs/job/deployments.js index 699cecf82..e1e9d6b49 100644 --- a/ui/app/routes/jobs/job/deployments.js +++ b/ui/app/routes/jobs/job/deployments.js @@ -4,21 +4,21 @@ import { collect } from '@ember/object/computed'; import { watchRelationship } from 'nomad-ui/utils/properties/watch'; import WithWatchers from 'nomad-ui/mixins/with-watchers'; -export default Route.extend(WithWatchers, { +export default class DeploymentsRoute extends Route.extend(WithWatchers) { model() { const job = this.modelFor('jobs.job'); return job && RSVP.all([job.get('deployments'), job.get('versions')]).then(() => job); - }, + } startWatchers(controller, model) { if (model) { controller.set('watchDeployments', this.watchDeployments.perform(model)); controller.set('watchVersions', this.watchVersions.perform(model)); } - }, + } - watchDeployments: watchRelationship('deployments'), - watchVersions: watchRelationship('versions'), + @watchRelationship('deployments') watchDeployments; + @watchRelationship('versions') watchVersions; - watchers: collect('watchDeployments', 'watchVersions'), -}); + @collect('watchDeployments', 'watchVersions') watchers; +} diff --git a/ui/app/routes/jobs/job/evaluations.js b/ui/app/routes/jobs/job/evaluations.js index de811fcc3..cfb7fe1c6 100644 --- a/ui/app/routes/jobs/job/evaluations.js +++ b/ui/app/routes/jobs/job/evaluations.js @@ -3,19 +3,19 @@ import { collect } from '@ember/object/computed'; import { watchRelationship } from 'nomad-ui/utils/properties/watch'; import WithWatchers from 'nomad-ui/mixins/with-watchers'; -export default Route.extend(WithWatchers, { +export default class EvaluationsRoute extends Route.extend(WithWatchers) { model() { const job = this.modelFor('jobs.job'); return job && job.get('evaluations').then(() => job); - }, + } startWatchers(controller, model) { if (model) { controller.set('watchEvaluations', this.watchEvaluations.perform(model)); } - }, + } - watchEvaluations: watchRelationship('evaluations'), + @watchRelationship('evaluations') watchEvaluations; - watchers: collect('watchEvaluations'), -}); + @collect('watchEvaluations') watchers; +} diff --git a/ui/app/routes/jobs/job/index.js b/ui/app/routes/jobs/job/index.js index 8e67de5ce..abd8b1d06 100644 --- a/ui/app/routes/jobs/job/index.js +++ b/ui/app/routes/jobs/job/index.js @@ -3,7 +3,7 @@ import { collect } from '@ember/object/computed'; import { watchRecord, watchRelationship, watchAll } from 'nomad-ui/utils/properties/watch'; import WithWatchers from 'nomad-ui/mixins/with-watchers'; -export default Route.extend(WithWatchers, { +export default class IndexRoute extends Route.extend(WithWatchers) { startWatchers(controller, model) { if (!model) { return; @@ -17,21 +17,22 @@ export default Route.extend(WithWatchers, { model.get('supportsDeployments') && this.watchLatestDeployment.perform(model), list: model.get('hasChildren') && this.watchAll.perform(), }); - }, + } - watch: watchRecord('job'), - watchAll: watchAll('job'), - watchSummary: watchRecord('job-summary'), - watchAllocations: watchRelationship('allocations'), - watchEvaluations: watchRelationship('evaluations'), - watchLatestDeployment: watchRelationship('latestDeployment'), + @watchRecord('job') watch; + @watchAll('job') watchAll; + @watchRecord('job-summary') watchSummary; + @watchRelationship('allocations') watchAllocations; + @watchRelationship('evaluations') watchEvaluations; + @watchRelationship('latestDeployment') watchLatestDeployment; - watchers: collect( + @collect( 'watch', 'watchAll', 'watchSummary', 'watchAllocations', 'watchEvaluations', 'watchLatestDeployment' - ), -}); + ) + watchers; +} diff --git a/ui/app/routes/jobs/job/task-group.js b/ui/app/routes/jobs/job/task-group.js index 134d7e829..6aad25aa0 100644 --- a/ui/app/routes/jobs/job/task-group.js +++ b/ui/app/routes/jobs/job/task-group.js @@ -7,7 +7,7 @@ import WithWatchers from 'nomad-ui/mixins/with-watchers'; import { qpBuilder } from 'nomad-ui/utils/classes/query-params'; import notifyError from 'nomad-ui/utils/notify-error'; -export default Route.extend(WithWatchers, { +export default class TaskGroupRoute extends Route.extend(WithWatchers) { breadcrumbs(model) { if (!model) return []; return [ @@ -21,7 +21,7 @@ export default Route.extend(WithWatchers, { ], }, ]; - }, + } model({ name }) { const job = this.modelFor('jobs.job'); @@ -49,7 +49,7 @@ export default Route.extend(WithWatchers, { .then(() => taskGroup); }) .catch(notifyError(this)); - }, + } startWatchers(controller, model) { if (model) { @@ -60,11 +60,11 @@ export default Route.extend(WithWatchers, { allocations: this.watchAllocations.perform(job), }); } - }, + } - watchJob: watchRecord('job'), - watchSummary: watchRecord('job-summary'), - watchAllocations: watchRelationship('allocations'), + @watchRecord('job') watchJob; + @watchRecord('job-summary') watchSummary; + @watchRelationship('allocations') watchAllocations; - watchers: collect('watchJob', 'watchSummary', 'watchAllocations'), -}); + @collect('watchJob', 'watchSummary', 'watchAllocations') watchers; +} diff --git a/ui/app/routes/jobs/job/versions.js b/ui/app/routes/jobs/job/versions.js index 38c850eaf..c6ab5684b 100644 --- a/ui/app/routes/jobs/job/versions.js +++ b/ui/app/routes/jobs/job/versions.js @@ -3,18 +3,18 @@ import { collect } from '@ember/object/computed'; import { watchRelationship } from 'nomad-ui/utils/properties/watch'; import WithWatchers from 'nomad-ui/mixins/with-watchers'; -export default Route.extend(WithWatchers, { +export default class VersionsRoute extends Route.extend(WithWatchers) { model() { const job = this.modelFor('jobs.job'); return job && job.get('versions').then(() => job); - }, + } startWatchers(controller, model) { if (model) { controller.set('watcher', this.watchVersions.perform(model)); } - }, + } - watchVersions: watchRelationship('versions'), - watchers: collect('watchVersions'), -}); + @watchRelationship('versions') watchVersions; + @collect('watchVersions') watchers; +} diff --git a/ui/app/routes/jobs/run.js b/ui/app/routes/jobs/run.js index 3f45bf006..c7a9de74f 100644 --- a/ui/app/routes/jobs/run.js +++ b/ui/app/routes/jobs/run.js @@ -1,33 +1,35 @@ import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; +import classic from 'ember-classic-decorator'; -export default Route.extend({ - can: service(), - store: service(), - system: service(), +@classic +export default class RunRoute extends Route { + @service can; + @service store; + @service system; - breadcrumbs: Object.freeze([ + breadcrumbs = [ { label: 'Run', args: ['jobs.run'], }, - ]), + ]; beforeModel() { if (this.can.cannot('run job')) { this.transitionTo('jobs'); } - }, + } model() { return this.store.createRecord('job', { namespace: this.get('system.activeNamespace'), }); - }, + } resetController(controller, isExiting) { if (isExiting) { controller.model.deleteRecord(); } - }, -}); + } +} diff --git a/ui/app/routes/not-found.js b/ui/app/routes/not-found.js index a7df89c01..2c7ece2a2 100644 --- a/ui/app/routes/not-found.js +++ b/ui/app/routes/not-found.js @@ -1,10 +1,10 @@ import Route from '@ember/routing/route'; import EmberError from '@ember/error'; -export default Route.extend({ +export default class NotFoundRoute extends Route { model() { const err = new EmberError('Page not found'); err.code = '404'; this.controllerFor('application').set('error', err); - }, -}); + } +} diff --git a/ui/app/routes/servers.js b/ui/app/routes/servers.js index 62ee43b89..5976dfaff 100644 --- a/ui/app/routes/servers.js +++ b/ui/app/routes/servers.js @@ -3,26 +3,28 @@ import Route from '@ember/routing/route'; import RSVP from 'rsvp'; import WithForbiddenState from 'nomad-ui/mixins/with-forbidden-state'; import notifyForbidden from 'nomad-ui/utils/notify-forbidden'; +import classic from 'ember-classic-decorator'; -export default Route.extend(WithForbiddenState, { - store: service(), - system: service(), +@classic +export default class ServersRoute extends Route.extend(WithForbiddenState) { + @service store; + @service system; - breadcrumbs: Object.freeze([ + breadcrumbs = [ { label: 'Servers', args: ['servers.index'], }, - ]), + ]; beforeModel() { return this.get('system.leader'); - }, + } model() { return RSVP.hash({ nodes: this.store.findAll('node'), agents: this.store.findAll('agent'), }).catch(notifyForbidden(this)); - }, -}); + } +} diff --git a/ui/app/routes/servers/server.js b/ui/app/routes/servers/server.js index 17aa8b10c..ab399d639 100644 --- a/ui/app/routes/servers/server.js +++ b/ui/app/routes/servers/server.js @@ -1,4 +1,4 @@ import Route from '@ember/routing/route'; import WithModelErrorHandling from 'nomad-ui/mixins/with-model-error-handling'; -export default Route.extend(WithModelErrorHandling); +export default class ServerRoute extends Route.extend(WithModelErrorHandling) {} diff --git a/ui/app/serializers/agent.js b/ui/app/serializers/agent.js index 5cbe4c3ad..75b4ce111 100644 --- a/ui/app/serializers/agent.js +++ b/ui/app/serializers/agent.js @@ -1,12 +1,12 @@ import ApplicationSerializer from './application'; import AdapterError from '@ember-data/adapter/error'; -export default ApplicationSerializer.extend({ - attrs: { +export default class AgentSerializer extends ApplicationSerializer { + attrs = { datacenter: 'dc', address: 'Addr', serfPort: 'Port', - }, + }; normalize(typeHash, hash) { if (!hash) { @@ -24,14 +24,14 @@ export default ApplicationSerializer.extend({ hash.Region = hash.Tags && hash.Tags.region; hash.RpcPort = hash.Tags && hash.Tags.port; - return this._super(typeHash, hash); - }, + return super.normalize(typeHash, hash); + } normalizeResponse(store, typeClass, hash, ...args) { - return this._super(store, typeClass, hash.Members || [], ...args); - }, + return super.normalizeResponse(store, typeClass, hash.Members || [], ...args); + } normalizeSingleResponse(store, typeClass, hash, id, ...args) { - return this._super(store, typeClass, hash.findBy('Name', id), id, ...args); - }, -}); + return super.normalizeSingleResponse(store, typeClass, hash.findBy('Name', id), id, ...args); + } +} diff --git a/ui/app/serializers/allocation.js b/ui/app/serializers/allocation.js index 43ace830f..7095d786a 100644 --- a/ui/app/serializers/allocation.js +++ b/ui/app/serializers/allocation.js @@ -8,13 +8,13 @@ const taskGroupFromJob = (job, taskGroupName) => { return taskGroup ? taskGroup : null; }; -export default ApplicationSerializer.extend({ - system: service(), +export default class AllocationSerializer extends ApplicationSerializer { + @service system; - attrs: { + attrs = { taskGroupName: 'TaskGroup', states: 'TaskStates', - }, + }; normalize(typeHash, hash) { // Transform the map-based TaskStates object into an array-based @@ -63,6 +63,6 @@ export default ApplicationSerializer.extend({ // The Job definition for an allocation is only included in findRecord responses. hash.AllocationTaskGroup = !hash.Job ? null : taskGroupFromJob(hash.Job, hash.TaskGroup); - return this._super(typeHash, hash); - }, -}); + return super.normalize(typeHash, hash); + } +} diff --git a/ui/app/serializers/application.js b/ui/app/serializers/application.js index 9546f2972..43247a3a7 100644 --- a/ui/app/serializers/application.js +++ b/ui/app/serializers/application.js @@ -5,19 +5,19 @@ import JSONSerializer from 'ember-data/serializers/json'; import { pluralize, singularize } from 'ember-inflector'; import removeRecord from '../utils/remove-record'; -export default JSONSerializer.extend({ - primaryKey: 'ID', +export default class Application extends JSONSerializer { + primaryKey = 'ID'; keyForAttribute(attr) { return attr.camelize().capitalize(); - }, + } keyForRelationship(attr, relationshipType) { const key = `${singularize(attr) .camelize() .capitalize()}ID`; return relationshipType === 'hasMany' ? pluralize(key) : key; - }, + } // Modeled after the pushPayload for ember-data/serializers/rest pushPayload(store, payload) { @@ -41,13 +41,13 @@ export default JSONSerializer.extend({ }); store.push(documentHash); - }, + } normalizeFindAllResponse(store, modelClass) { - const result = this._super(...arguments); + const result = super.normalizeFindAllResponse(...arguments); this.cullStore(store, modelClass.modelName, result.data); return result; - }, + } // When records are removed server-side, and therefore don't show up in requests, // the local copies of those records need to be unloaded from the store. @@ -65,9 +65,9 @@ export default JSONSerializer.extend({ newRecords.removeObject(newRecord); } }); - }, + } modelNameFromPayloadKey(key) { return singularize(key.dasherize()); - }, -}); + } +} diff --git a/ui/app/serializers/deployment.js b/ui/app/serializers/deployment.js index 456eb1b25..1c57286f3 100644 --- a/ui/app/serializers/deployment.js +++ b/ui/app/serializers/deployment.js @@ -1,11 +1,13 @@ import { get } from '@ember/object'; import { assign } from '@ember/polyfills'; import ApplicationSerializer from './application'; +import classic from 'ember-classic-decorator'; -export default ApplicationSerializer.extend({ - attrs: { +@classic +export default class DeploymentSerializer extends ApplicationSerializer { + attrs = { versionNumber: 'JobVersion', - }, + }; normalize(typeHash, hash) { if (hash) { @@ -29,8 +31,8 @@ export default ApplicationSerializer.extend({ hash.JobID = hash.JobForLatestID = JSON.stringify([hash.JobID, hash.Namespace]); } - return this._super(typeHash, hash); - }, + return super.normalize(typeHash, hash); + } extractRelationships(modelClass, hash) { const namespace = this.store.adapterFor(modelClass.modelName).get('namespace'); @@ -44,7 +46,7 @@ export default ApplicationSerializer.extend({ }, }, }, - this._super(modelClass, hash) + super.extractRelationships(modelClass, hash) ); - }, -}); + } +} diff --git a/ui/app/serializers/drain-strategy.js b/ui/app/serializers/drain-strategy.js index 033cc45a6..dfaf0c5c8 100644 --- a/ui/app/serializers/drain-strategy.js +++ b/ui/app/serializers/drain-strategy.js @@ -1,6 +1,6 @@ import ApplicationSerializer from './application'; -export default ApplicationSerializer.extend({ +export default class DrainStrategy extends ApplicationSerializer { normalize(typeHash, hash) { // TODO API: finishedAt is always marshaled as a date even when unset. // To simplify things, unset it here when it's the empty date value. @@ -8,6 +8,6 @@ export default ApplicationSerializer.extend({ hash.ForceDeadline = null; } - return this._super(typeHash, hash); - }, -}); + return super.normalize(typeHash, hash); + } +} diff --git a/ui/app/serializers/evaluation.js b/ui/app/serializers/evaluation.js index 18e08271e..b4575ba2e 100644 --- a/ui/app/serializers/evaluation.js +++ b/ui/app/serializers/evaluation.js @@ -3,8 +3,8 @@ import { get } from '@ember/object'; import { assign } from '@ember/polyfills'; import ApplicationSerializer from './application'; -export default ApplicationSerializer.extend({ - system: service(), +export default class Evaluation extends ApplicationSerializer { + @service system; normalize(typeHash, hash) { const failures = hash.FailedTGAllocs || {}; @@ -27,6 +27,6 @@ export default ApplicationSerializer.extend({ hash.CreateTimeNanos = hash.CreateTime % 1000000; hash.CreateTime = Math.floor(hash.CreateTime / 1000000); - return this._super(typeHash, hash); - }, -}); + return super.normalize(typeHash, hash); + } +} diff --git a/ui/app/serializers/fragment.js b/ui/app/serializers/fragment.js index 838b442f3..435e04210 100644 --- a/ui/app/serializers/fragment.js +++ b/ui/app/serializers/fragment.js @@ -1,3 +1,3 @@ import ApplicationSerializer from './application'; -export default ApplicationSerializer.extend({}); +export default class Fragment extends ApplicationSerializer {} diff --git a/ui/app/serializers/job-plan.js b/ui/app/serializers/job-plan.js index e44e47d6f..13bd4feb3 100644 --- a/ui/app/serializers/job-plan.js +++ b/ui/app/serializers/job-plan.js @@ -2,13 +2,13 @@ import { assign } from '@ember/polyfills'; import ApplicationSerializer from './application'; import { get } from '@ember/object'; -export default ApplicationSerializer.extend({ +export default class JobPlan extends ApplicationSerializer { normalize(typeHash, hash) { const failures = hash.FailedTGAllocs || {}; hash.FailedTGAllocs = Object.keys(failures).map(key => { return assign({ Name: key }, failures[key] || {}); }); hash.PreemptionIDs = (get(hash, 'Annotations.PreemptedAllocs') || []).mapBy('ID'); - return this._super(...arguments); - }, -}); + return super.normalize(...arguments); + } +} diff --git a/ui/app/serializers/job-summary.js b/ui/app/serializers/job-summary.js index ed464e819..da9c93bb0 100644 --- a/ui/app/serializers/job-summary.js +++ b/ui/app/serializers/job-summary.js @@ -1,7 +1,7 @@ import { get } from '@ember/object'; import ApplicationSerializer from './application'; -export default ApplicationSerializer.extend({ +export default class JobSummary extends ApplicationSerializer { normalize(modelClass, hash) { // Transform the map-based Summary object into an array-based // TaskGroupSummary fragment list @@ -29,6 +29,6 @@ export default ApplicationSerializer.extend({ ); } - return this._super(modelClass, hash); - }, -}); + return super.normalize(modelClass, hash); + } +} diff --git a/ui/app/serializers/job-version.js b/ui/app/serializers/job-version.js index 4e250d5d8..bc2962a53 100644 --- a/ui/app/serializers/job-version.js +++ b/ui/app/serializers/job-version.js @@ -1,10 +1,10 @@ import { assign } from '@ember/polyfills'; import ApplicationSerializer from './application'; -export default ApplicationSerializer.extend({ - attrs: { +export default class JobVersionSerializer extends ApplicationSerializer { + attrs = { number: 'Version', - }, + }; normalizeFindHasManyResponse(store, modelClass, hash, id, requestType) { const zippedVersions = hash.Versions.map((version, index) => @@ -16,6 +16,13 @@ export default ApplicationSerializer.extend({ SubmitTimeNanos: version.SubmitTime % 1000000, }) ); - return this._super(store, modelClass, zippedVersions, hash, id, requestType); - }, -}); + return super.normalizeFindHasManyResponse( + store, + modelClass, + zippedVersions, + hash, + id, + requestType + ); + } +} diff --git a/ui/app/serializers/job.js b/ui/app/serializers/job.js index a43badba1..ded935c00 100644 --- a/ui/app/serializers/job.js +++ b/ui/app/serializers/job.js @@ -2,10 +2,10 @@ import { assign } from '@ember/polyfills'; import ApplicationSerializer from './application'; import queryString from 'query-string'; -export default ApplicationSerializer.extend({ - attrs: { +export default class JobSerializer extends ApplicationSerializer { + attrs = { parameterized: 'ParameterizedJob', - }, + }; normalize(typeHash, hash) { hash.NamespaceID = hash.Namespace; @@ -45,8 +45,8 @@ export default ApplicationSerializer.extend({ }); } - return this._super(typeHash, hash); - }, + return super.normalize(typeHash, hash); + } extractRelationships(modelClass, hash) { const namespace = @@ -58,7 +58,7 @@ export default ApplicationSerializer.extend({ .buildURL(modelName, hash.ID, hash, 'findRecord') .split('?'); - return assign(this._super(...arguments), { + return assign(super.extractRelationships(...arguments), { allocations: { links: { related: buildURL(`${jobURL}/allocations`, { namespace }), @@ -85,8 +85,8 @@ export default ApplicationSerializer.extend({ }, }, }); - }, -}); + } +} function buildURL(path, queryParams) { const qpString = queryString.stringify(queryParams); diff --git a/ui/app/serializers/namespace.js b/ui/app/serializers/namespace.js index ccd2350ed..5b35364e8 100644 --- a/ui/app/serializers/namespace.js +++ b/ui/app/serializers/namespace.js @@ -1,5 +1,5 @@ import ApplicationSerializer from './application'; -export default ApplicationSerializer.extend({ - primaryKey: 'Name', -}); +export default class Namespace extends ApplicationSerializer { + primaryKey = 'Name'; +} diff --git a/ui/app/serializers/network.js b/ui/app/serializers/network.js index b5767df2e..3310db18c 100644 --- a/ui/app/serializers/network.js +++ b/ui/app/serializers/network.js @@ -1,12 +1,12 @@ import ApplicationSerializer from './application'; import isIp from 'is-ip'; -export default ApplicationSerializer.extend({ - attrs: { +export default class NetworkSerializer extends ApplicationSerializer { + attrs = { cidr: 'CIDR', ip: 'IP', mbits: 'MBits', - }, + }; normalize(typeHash, hash) { const ip = hash.IP; @@ -31,6 +31,6 @@ export default ApplicationSerializer.extend({ hash.Ports = reservedPorts.concat(dynamicPorts).sortBy('name'); - return this._super(...arguments); - }, -}); + return super.normalize(...arguments); + } +} diff --git a/ui/app/serializers/node-attributes.js b/ui/app/serializers/node-attributes.js index 458c81f15..cbd2c5c2e 100644 --- a/ui/app/serializers/node-attributes.js +++ b/ui/app/serializers/node-attributes.js @@ -1,7 +1,7 @@ import ApplicationSerializer from './application'; -export default ApplicationSerializer.extend({ +export default class NodeAttributes extends ApplicationSerializer { normalize(typeHash, hash) { - return this._super(typeHash, { NodeAttributes: hash }); - }, -}); + return super.normalize(typeHash, { NodeAttributes: hash }); + } +} diff --git a/ui/app/serializers/node-event.js b/ui/app/serializers/node-event.js index 606c6dadd..169b65faf 100644 --- a/ui/app/serializers/node-event.js +++ b/ui/app/serializers/node-event.js @@ -1,7 +1,7 @@ import ApplicationSerializer from './application'; -export default ApplicationSerializer.extend({ - attrs: { +export default class NodeEventSerializer extends ApplicationSerializer { + attrs = { time: 'Timestamp', - }, -}); + }; +} diff --git a/ui/app/serializers/node.js b/ui/app/serializers/node.js index 1084f442c..4eb05b976 100644 --- a/ui/app/serializers/node.js +++ b/ui/app/serializers/node.js @@ -2,13 +2,13 @@ import { assign } from '@ember/polyfills'; import { inject as service } from '@ember/service'; import ApplicationSerializer from './application'; -export default ApplicationSerializer.extend({ - config: service(), +export default class NodeSerializer extends ApplicationSerializer { + @service config; - attrs: { + attrs = { isDraining: 'Drain', httpAddr: 'HTTPAddr', - }, + }; normalize(modelClass, hash) { // Transform map-based objects into array-based fragment lists @@ -20,8 +20,8 @@ export default ApplicationSerializer.extend({ const hostVolumes = hash.HostVolumes || {}; hash.HostVolumes = Object.keys(hostVolumes).map(key => hostVolumes[key]); - return this._super(modelClass, hash); - }, + return super.normalize(modelClass, hash); + } extractRelationships(modelClass, hash) { const { modelName } = modelClass; @@ -36,5 +36,5 @@ export default ApplicationSerializer.extend({ }, }, }; - }, -}); + } +} diff --git a/ui/app/serializers/plugin.js b/ui/app/serializers/plugin.js index 6f87159d7..1eed76606 100644 --- a/ui/app/serializers/plugin.js +++ b/ui/app/serializers/plugin.js @@ -12,7 +12,7 @@ const unmap = (hash, propKey) => return record; }); -export default ApplicationSerializer.extend({ +export default class Plugin extends ApplicationSerializer { normalize(typeHash, hash) { hash.PlainId = hash.ID; @@ -28,6 +28,6 @@ export default ApplicationSerializer.extend({ hash.Nodes = unmap(nodes, 'NodeID'); hash.Controllers = unmap(controllers, 'NodeID'); - return this._super(typeHash, hash); - }, -}); + return super.normalize(typeHash, hash); + } +} diff --git a/ui/app/serializers/policy.js b/ui/app/serializers/policy.js index b9cfa02ed..a7fd7b1fc 100644 --- a/ui/app/serializers/policy.js +++ b/ui/app/serializers/policy.js @@ -1,8 +1,8 @@ import ApplicationSerializer from './application'; -export default ApplicationSerializer.extend({ +export default class Policy extends ApplicationSerializer { normalize(typeHash, hash) { hash.ID = hash.Name; - return this._super(typeHash, hash); - }, -}); + return super.normalize(typeHash, hash); + } +} diff --git a/ui/app/serializers/reschedule-event.js b/ui/app/serializers/reschedule-event.js index 775ddac65..3bbe12904 100644 --- a/ui/app/serializers/reschedule-event.js +++ b/ui/app/serializers/reschedule-event.js @@ -1,6 +1,6 @@ import ApplicationSerializer from './application'; -export default ApplicationSerializer.extend({ +export default class RescheduleEvent extends ApplicationSerializer { normalize(typeHash, hash) { // Time is in the form of nanoseconds since epoch, but JS dates // only understand time to the millisecond precision. So store @@ -12,6 +12,6 @@ export default ApplicationSerializer.extend({ hash.PreviousAllocationId = hash.PrevAllocID ? hash.PrevAllocID : null; hash.PreviousNodeId = hash.PrevNodeID ? hash.PrevNodeID : null; - return this._super(typeHash, hash); - }, -}); + return super.normalize(typeHash, hash); + } +} diff --git a/ui/app/serializers/resources.js b/ui/app/serializers/resources.js index a657ed43b..43c9e23b7 100644 --- a/ui/app/serializers/resources.js +++ b/ui/app/serializers/resources.js @@ -1,10 +1,10 @@ import ApplicationSerializer from './application'; -export default ApplicationSerializer.extend({ - attrs: { +export default class ResourcesSerializer extends ApplicationSerializer { + attrs = { cpu: 'CPU', memory: 'MemoryMB', disk: 'DiskMB', iops: 'IOPS', - }, -}); + }; +} diff --git a/ui/app/serializers/service.js b/ui/app/serializers/service.js index da532204c..107c79f28 100644 --- a/ui/app/serializers/service.js +++ b/ui/app/serializers/service.js @@ -1,15 +1,15 @@ import ApplicationSerializer from './application'; -export default ApplicationSerializer.extend({ - attrs: { +export default class ServiceSerializer extends ApplicationSerializer { + attrs = { connect: 'Connect', - }, + }; normalize(typeHash, hash) { if (!hash.Tags) { hash.Tags = []; } - return this._super(typeHash, hash); - }, -}); + return super.normalize(typeHash, hash); + } +} diff --git a/ui/app/serializers/task-event.js b/ui/app/serializers/task-event.js index f9955ed45..dfa43f4b6 100644 --- a/ui/app/serializers/task-event.js +++ b/ui/app/serializers/task-event.js @@ -1,9 +1,9 @@ import ApplicationSerializer from './application'; -export default ApplicationSerializer.extend({ - attrs: { +export default class TaskEventSerializer extends ApplicationSerializer { + attrs = { message: 'DisplayMessage', - }, + }; normalize(typeHash, hash) { // Time is in the form of nanoseconds since epoch, but JS dates @@ -13,6 +13,6 @@ export default ApplicationSerializer.extend({ hash.TimeNanos = hash.Time % 1000000; hash.Time = Math.floor(hash.Time / 1000000); - return this._super(typeHash, hash); - }, -}); + return super.normalize(typeHash, hash); + } +} diff --git a/ui/app/serializers/task-group-deployment-summary.js b/ui/app/serializers/task-group-deployment-summary.js index 84c30f323..0441af6be 100644 --- a/ui/app/serializers/task-group-deployment-summary.js +++ b/ui/app/serializers/task-group-deployment-summary.js @@ -1,9 +1,9 @@ import ApplicationSerializer from './application'; -export default ApplicationSerializer.extend({ +export default class TaskGroupDeploymentSummary extends ApplicationSerializer { normalize(typeHash, hash) { hash.PlacedCanaryAllocations = hash.PlacedCanaries || []; delete hash.PlacedCanaries; - return this._super(typeHash, hash); - }, -}); + return super.normalize(typeHash, hash); + } +} diff --git a/ui/app/serializers/task-group.js b/ui/app/serializers/task-group.js index 08c5bacd8..d7b0d7c4d 100644 --- a/ui/app/serializers/task-group.js +++ b/ui/app/serializers/task-group.js @@ -1,7 +1,7 @@ import { copy } from 'ember-copy'; import ApplicationSerializer from './application'; -export default ApplicationSerializer.extend({ +export default class TaskGroup extends ApplicationSerializer { normalize(typeHash, hash) { // Provide EphemeralDisk to each task hash.Tasks.forEach(task => { @@ -14,6 +14,6 @@ export default ApplicationSerializer.extend({ const volumes = hash.Volumes || {}; hash.Volumes = Object.keys(volumes).map(key => volumes[key]); - return this._super(typeHash, hash); - }, -}); + return super.normalize(typeHash, hash); + } +} diff --git a/ui/app/serializers/task-state.js b/ui/app/serializers/task-state.js index 0ec3369a1..11a29e25b 100644 --- a/ui/app/serializers/task-state.js +++ b/ui/app/serializers/task-state.js @@ -1,6 +1,6 @@ import ApplicationSerializer from './application'; -export default ApplicationSerializer.extend({ +export default class TaskState extends ApplicationSerializer { normalize(typeHash, hash) { // TODO API: finishedAt is always marshaled as a date even when unset. // To simplify things, unset it here when it's the empty date value. @@ -8,6 +8,6 @@ export default ApplicationSerializer.extend({ hash.FinishedAt = null; } - return this._super(typeHash, hash); - }, -}); + return super.normalize(typeHash, hash); + } +} diff --git a/ui/app/serializers/task.js b/ui/app/serializers/task.js index 425b76811..ea09c5dcf 100644 --- a/ui/app/serializers/task.js +++ b/ui/app/serializers/task.js @@ -1,6 +1,6 @@ import ApplicationSerializer from './application'; -export default ApplicationSerializer.extend({ +export default class Task extends ApplicationSerializer { normalize(typeHash, hash) { // Lift the reserved resource numbers out of the Resources object const resources = hash.Resources; @@ -11,6 +11,6 @@ export default ApplicationSerializer.extend({ hash.ReservedEphemeralDisk = hash.EphemeralDisk.SizeMB; } - return this._super(typeHash, hash); - }, -}); + return super.normalize(typeHash, hash); + } +} diff --git a/ui/app/serializers/token.js b/ui/app/serializers/token.js index 06a73676c..0e3beb302 100644 --- a/ui/app/serializers/token.js +++ b/ui/app/serializers/token.js @@ -1,16 +1,16 @@ import { copy } from 'ember-copy'; import ApplicationSerializer from './application'; -export default ApplicationSerializer.extend({ - primaryKey: 'AccessorID', +export default class TokenSerializer extends ApplicationSerializer { + primaryKey = 'AccessorID'; - attrs: { + attrs = { secret: 'SecretID', - }, + }; normalize(typeHash, hash) { hash.PolicyIDs = hash.Policies; hash.PolicyNames = copy(hash.Policies); - return this._super(typeHash, hash); - }, -}); + return super.normalize(typeHash, hash); + } +} diff --git a/ui/app/serializers/volume.js b/ui/app/serializers/volume.js index 78e5a49b7..2b6e055f1 100644 --- a/ui/app/serializers/volume.js +++ b/ui/app/serializers/volume.js @@ -1,12 +1,12 @@ import { set, get } from '@ember/object'; import ApplicationSerializer from './application'; -export default ApplicationSerializer.extend({ - attrs: { +export default class VolumeSerializer extends ApplicationSerializer { + attrs = { externalId: 'ExternalID', - }, + }; - embeddedRelationships: Object.freeze(['writeAllocations', 'readAllocations']), + embeddedRelationships = ['writeAllocations', 'readAllocations']; // Volumes treat Allocations as embedded records. Ember has an // EmbeddedRecords mixin, but it assumes an application is using @@ -35,15 +35,15 @@ export default ApplicationSerializer.extend({ hash.ReadAllocations = Object.keys(readAllocs).map(bindIDToAlloc(readAllocs)); hash.WriteAllocations = Object.keys(writeAllocs).map(bindIDToAlloc(writeAllocs)); - const normalizedHash = this._super(typeHash, hash); + const normalizedHash = super.normalize(typeHash, hash); return this.extractEmbeddedRecords(this, this.store, typeHash, normalizedHash); - }, + } keyForRelationship(attr, relationshipType) { //Embedded relationship attributes don't end in IDs if (this.embeddedRelationships.includes(attr)) return attr.capitalize(); - return this._super(attr, relationshipType); - }, + return super.keyForRelationship(attr, relationshipType); + } // Convert the embedded relationship arrays into JSONAPI included records extractEmbeddedRecords(serializer, store, typeHash, partial) { @@ -84,7 +84,7 @@ export default ApplicationSerializer.extend({ }); return partial; - }, + } normalizeEmbeddedRelationship(store, relationshipMeta, relationshipHash) { const modelName = relationshipMeta.type; @@ -92,5 +92,5 @@ export default ApplicationSerializer.extend({ const serializer = store.serializerFor(modelName); return serializer.normalize(modelClass, relationshipHash, null); - }, -}); + } +} diff --git a/ui/app/services/breadcrumbs.js b/ui/app/services/breadcrumbs.js index f01a7f680..fd44b9e7a 100644 --- a/ui/app/services/breadcrumbs.js +++ b/ui/app/services/breadcrumbs.js @@ -1,15 +1,18 @@ import { getOwner } from '@ember/application'; import Service, { inject as service } from '@ember/service'; import { computed } from '@ember/object'; +import classic from 'ember-classic-decorator'; -export default Service.extend({ - router: service(), +@classic +export default class BreadcrumbsService extends Service { + @service router; // currentURL is only used to listen to all transitions. // currentRouteName has all information necessary to compute breadcrumbs, // but it doesn't change when a transition to the same route with a different // model occurs. - breadcrumbs: computed('router.{currentURL,currentRouteName}', function() { + @computed('router.{currentURL,currentRouteName}') + get breadcrumbs() { const owner = getOwner(this); const allRoutes = (this.get('router.currentRouteName') || '') .split('.') @@ -38,5 +41,5 @@ export default Service.extend({ }); return crumbs; - }), -}); + } +} diff --git a/ui/app/services/config.js b/ui/app/services/config.js index 80e38e1ea..3bed95cc5 100644 --- a/ui/app/services/config.js +++ b/ui/app/services/config.js @@ -3,12 +3,12 @@ import Service from '@ember/service'; import { get } from '@ember/object'; import config from '../config/environment'; -export default Service.extend({ +export default class ConfigService extends Service { unknownProperty(path) { return get(config, path); - }, + } - isDev: equal('environment', 'development'), - isProd: equal('environment', 'production'), - isTest: equal('environment', 'test'), -}); + @equal('environment', 'development') isDev; + @equal('environment', 'production') isProd; + @equal('environment', 'test') isTest; +} diff --git a/ui/app/services/sockets.js b/ui/app/services/sockets.js index b629aa077..29a38b0c7 100644 --- a/ui/app/services/sockets.js +++ b/ui/app/services/sockets.js @@ -2,7 +2,7 @@ import Service from '@ember/service'; import config from 'nomad-ui/config/environment'; import { getOwner } from '@ember/application'; -export default Service.extend({ +export default class SocketsService extends Service { getTaskStateSocket(taskState, command) { const mirageEnabled = config.environment !== 'production' && @@ -34,5 +34,5 @@ export default Service.extend({ `&command=${encodeURIComponent(`["${command}"]`)}` ); } - }, -}); + } +} diff --git a/ui/app/services/stats-trackers-registry.js b/ui/app/services/stats-trackers-registry.js index 751ac1d45..8c2fb2ab7 100644 --- a/ui/app/services/stats-trackers-registry.js +++ b/ui/app/services/stats-trackers-registry.js @@ -14,23 +14,24 @@ let registry; const exists = (tracker, prop) => tracker.get(prop) && !tracker.get(prop).isDestroyed && !tracker.get(prop).isDestroying; -export default Service.extend({ - token: service(), +export default class StatsTrackersRegistryService extends Service { + @service token; - init() { - this._super(...arguments); + constructor() { + super(...arguments); // The LRUMap limits the number of trackers tracked by making room for // new entries beyond the limit by removing the least recently used entry. registry = new LRUMap(MAX_STAT_TRACKERS); - }, + } // A read-only way of getting a reference to the registry. // Since this could be overwritten by a bad actor, it isn't // used in getTracker - registryRef: computed(function() { + @computed + get registryRef() { return registry; - }), + } getTracker(resource) { if (!resource) return; @@ -56,5 +57,5 @@ export default Service.extend({ registry.set(key, tracker); return tracker; - }, -}); + } +} diff --git a/ui/app/services/system.js b/ui/app/services/system.js index bf71e4b3e..3b293d5c4 100644 --- a/ui/app/services/system.js +++ b/ui/app/services/system.js @@ -4,12 +4,15 @@ import PromiseObject from '../utils/classes/promise-object'; import PromiseArray from '../utils/classes/promise-array'; import { namespace } from '../adapters/application'; import jsonWithDefault from '../utils/json-with-default'; +import classic from 'ember-classic-decorator'; -export default Service.extend({ - token: service(), - store: service(), +@classic +export default class SystemService extends Service { + @service token; + @service store; - leader: computed('activeRegion', function() { + @computed('activeRegion') + get leader() { const token = this.token; return PromiseObject.create({ @@ -23,9 +26,10 @@ export default Service.extend({ return leader; }), }); - }), + } - defaultRegion: computed(function() { + @computed + get defaultRegion() { const token = this.token; return PromiseObject.create({ promise: token @@ -35,94 +39,94 @@ export default Service.extend({ return { region: json.ServerRegion }; }), }); - }), + } - regions: computed(function() { + @computed + get regions() { const token = this.token; return PromiseArray.create({ promise: token.authorizedRawRequest(`/${namespace}/regions`).then(jsonWithDefault([])), }); - }), + } - activeRegion: computed('regions.[]', { - get() { - const regions = this.regions; - const region = window.localStorage.nomadActiveRegion; + @computed('regions.[]') + get activeRegion() { + const regions = this.regions; + const region = window.localStorage.nomadActiveRegion; - if (regions.includes(region)) { - return region; - } - - return null; - }, - set(key, value) { - if (value == null) { - window.localStorage.removeItem('nomadActiveRegion'); - return; - } else { - // All localStorage values are strings. Stringify first so - // the return value is consistent with what is persisted. - const strValue = value + ''; - window.localStorage.nomadActiveRegion = strValue; - return strValue; - } - }, - }), - - shouldShowRegions: computed('regions.[]', function() { - return this.get('regions.length') > 1; - }), - - shouldIncludeRegion: computed( - 'activeRegion', - 'defaultRegion.region', - 'shouldShowRegions', - function() { - return this.shouldShowRegions && this.activeRegion !== this.get('defaultRegion.region'); + if (regions.includes(region)) { + return region; } - ), - namespaces: computed('activeRegion', function() { + return null; + } + + set activeRegion(value) { + if (value == null) { + window.localStorage.removeItem('nomadActiveRegion'); + return; + } else { + // All localStorage values are strings. Stringify first so + // the return value is consistent with what is persisted. + const strValue = value + ''; + window.localStorage.nomadActiveRegion = strValue; + return strValue; + } + } + + @computed('regions.[]') + get shouldShowRegions() { + return this.get('regions.length') > 1; + } + + @computed('activeRegion', 'defaultRegion.region', 'shouldShowRegions') + get shouldIncludeRegion() { + return this.shouldShowRegions && this.activeRegion !== this.get('defaultRegion.region'); + } + + @computed('activeRegion') + get namespaces() { return PromiseArray.create({ promise: this.store.findAll('namespace').then(namespaces => namespaces.compact()), }); - }), + } - shouldShowNamespaces: computed('namespaces.[]', function() { + @computed('namespaces.[]') + get shouldShowNamespaces() { const namespaces = this.namespaces.toArray(); return namespaces.length && namespaces.some(namespace => namespace.get('id') !== 'default'); - }), + } - activeNamespace: computed('namespaces.[]', { - get() { - const namespaceId = window.localStorage.nomadActiveNamespace || 'default'; - const namespace = this.namespaces.findBy('id', namespaceId); + @computed('namespaces.[]') + get activeNamespace() { + const namespaceId = window.localStorage.nomadActiveNamespace || 'default'; + const namespace = this.namespaces.findBy('id', namespaceId); - if (namespace) { - return namespace; - } + if (namespace) { + return namespace; + } - // If the namespace in localStorage is no longer in the cluster, it needs to - // be cleared from localStorage + // If the namespace in localStorage is no longer in the cluster, it needs to + // be cleared from localStorage + window.localStorage.removeItem('nomadActiveNamespace'); + return this.namespaces.findBy('id', 'default'); + } + + set activeNamespace(value) { + if (value == null) { window.localStorage.removeItem('nomadActiveNamespace'); - return this.namespaces.findBy('id', 'default'); - }, - set(key, value) { - if (value == null) { - window.localStorage.removeItem('nomadActiveNamespace'); - return; - } else if (typeof value === 'string') { - window.localStorage.nomadActiveNamespace = value; - return this.namespaces.findBy('id', value); - } else { - window.localStorage.nomadActiveNamespace = value.get('name'); - return value; - } - }, - }), + return; + } else if (typeof value === 'string') { + window.localStorage.nomadActiveNamespace = value; + return this.namespaces.findBy('id', value); + } else { + window.localStorage.nomadActiveNamespace = value.get('name'); + return value; + } + } reset() { this.set('activeNamespace', null); - }, -}); + } +} diff --git a/ui/app/services/token.js b/ui/app/services/token.js index 5e6dc70e6..78d43feb1 100644 --- a/ui/app/services/token.js +++ b/ui/app/services/token.js @@ -6,29 +6,31 @@ import { assign } from '@ember/polyfills'; import { task } from 'ember-concurrency'; import queryString from 'query-string'; import fetch from 'nomad-ui/utils/fetch'; +import classic from 'ember-classic-decorator'; -export default Service.extend({ - store: service(), - system: service(), +@classic +export default class TokenService extends Service { + @service store; + @service system; - aclEnabled: true, + aclEnabled = true; - secret: computed({ - get() { - return window.localStorage.nomadTokenSecret; - }, - set(key, value) { - if (value == null) { - window.localStorage.removeItem('nomadTokenSecret'); - } else { - window.localStorage.nomadTokenSecret = value; - } + @computed + get secret() { + return window.localStorage.nomadTokenSecret; + } - return value; - }, - }), + set secret(value) { + if (value == null) { + window.localStorage.removeItem('nomadTokenSecret'); + } else { + window.localStorage.nomadTokenSecret = value; + } - fetchSelfToken: task(function*() { + return value; + } + + @task(function*() { const TokenAdapter = getOwner(this).lookup('adapter:token'); try { return yield TokenAdapter.findSelf(); @@ -39,14 +41,16 @@ export default Service.extend({ } return null; } - }), + }) + fetchSelfToken; - selfToken: computed('secret', 'fetchSelfToken.lastSuccessful.value', function() { + @computed('secret', 'fetchSelfToken.lastSuccessful.value') + get selfToken() { if (this.secret) return this.get('fetchSelfToken.lastSuccessful.value'); - return; - }), + return undefined; + } - fetchSelfTokenPolicies: task(function*() { + @task(function*() { try { if (this.selfToken) { return yield this.selfToken.get('policies'); @@ -57,16 +61,18 @@ export default Service.extend({ } catch (e) { return []; } - }), + }) + fetchSelfTokenPolicies; - selfTokenPolicies: alias('fetchSelfTokenPolicies.lastSuccessful.value'), + @alias('fetchSelfTokenPolicies.lastSuccessful.value') selfTokenPolicies; - fetchSelfTokenAndPolicies: task(function*() { + @task(function*() { yield this.fetchSelfToken.perform(); if (this.aclEnabled) { yield this.fetchSelfTokenPolicies.perform(); } - }), + }) + fetchSelfTokenAndPolicies; // All non Ember Data requests should go through authorizedRequest. // However, the request that gets regions falls into that category. @@ -83,7 +89,7 @@ export default Service.extend({ } return fetch(url, assign(options, { headers, credentials })); - }, + } authorizedRequest(url, options) { if (this.get('system.shouldIncludeRegion')) { @@ -94,14 +100,14 @@ export default Service.extend({ } return this.authorizedRawRequest(url, options); - }, + } reset() { this.fetchSelfToken.cancelAll({ resetState: true }); this.fetchSelfTokenPolicies.cancelAll({ resetState: true }); this.fetchSelfTokenAndPolicies.cancelAll({ resetState: true }); - }, -}); + } +} function addParams(url, params) { const paramsStr = queryString.stringify(params); diff --git a/ui/app/services/user-settings.js b/ui/app/services/user-settings.js index f9841ad57..e551dd98b 100644 --- a/ui/app/services/user-settings.js +++ b/ui/app/services/user-settings.js @@ -1,6 +1,6 @@ import Service from '@ember/service'; import localStorageProperty from 'nomad-ui/utils/properties/local-storage'; -export default Service.extend({ - pageSize: localStorageProperty('nomadPageSize', 25), -}); +export default class UserSettingsService extends Service { + @localStorageProperty('nomadPageSize', 25) pageSize; +} diff --git a/ui/app/services/watch-list.js b/ui/app/services/watch-list.js index ab068650d..b22fc97e8 100644 --- a/ui/app/services/watch-list.js +++ b/ui/app/services/watch-list.js @@ -5,23 +5,24 @@ import Service from '@ember/service'; let list = {}; -export default Service.extend({ - _list: computed(function() { +export default class WatchListService extends Service { + @computed + get _list() { return copy(list, true); - }), + } - list: readOnly('_list'), + @readOnly('_list') list; - init() { - this._super(...arguments); + constructor() { + super(...arguments); list = {}; - }, + } getIndexFor(url) { return list[url] || 1; - }, + } setIndexFor(url, value) { list[url] = +value; - }, -}); + } +} diff --git a/ui/app/templates/clients/client.hbs b/ui/app/templates/clients/client.hbs index 5b2e7089d..470d66e2b 100644 --- a/ui/app/templates/clients/client.hbs +++ b/ui/app/templates/clients/client.hbs @@ -126,7 +126,7 @@ @client={{model}} @isDisabled={{cannot "write client"}} @onDrain={{action "drainNotify"}} - @onError={{action "drainError"}} /> + @onError={{action "setDrainError"}} /> diff --git a/ui/app/templates/components/exec/task-contents.hbs b/ui/app/templates/components/exec/task-contents.hbs index e36bea0fc..615723f3d 100644 --- a/ui/app/templates/components/exec/task-contents.hbs +++ b/ui/app/templates/components/exec/task-contents.hbs @@ -7,7 +7,7 @@ {{/if}} -{{#if openInNewWindow}} +{{#if shouldOpenInNewWindow}} {{x-icon "exit" class="show-on-hover"}} diff --git a/ui/app/templates/components/exec/task-group-parent.hbs b/ui/app/templates/components/exec/task-group-parent.hbs index 2109ca14e..72ea41808 100644 --- a/ui/app/templates/components/exec/task-group-parent.hbs +++ b/ui/app/templates/components/exec/task-group-parent.hbs @@ -5,19 +5,19 @@ {{#if isOpen}}