UI: Migrate to ES6 classes (#8144)

* Add massaged results of class codemod

Manual interventions:
• decorators on the same line for service and controller
  injections and most computed property macros
• preserving import order when possible, both per-line
  and intra-line
• moving new imports to the bottom
• removal of classic decorator for trivial cases
• conversion of init to constructor when appropriate

* Add fixes for ESLint getter-return

…I GUESS

* Remove unnecessary fetch-setting

Originally this was failing because it only had a getter.
I tried replacing it with a computed property and that
succeeded, but since we have already stopped using
jQuery, we might as well remove it.

* Change with-namespace-ids mixin to a base class

This is a merge of 5d9fce5.

* Change URL-generation for job-updating

The id-processing in the WatchableNamespaceIds adapter was
happening twice; this removes urlForUpdate record so it
only happens once. @DingoEatingFuzz figured it out! 🥳

* Fix query parameters structures

I’d think the codemod would handle this if it’s a requirement
but apparently not, is it a bug?

* Add manually-converted classes

I don’t know why the codemod ignored these files 🧐

* Remove problem field

It appears this gets turned into a getter-only computed property
somehow, which causes problems when subclasses override it.

* Rename clashing action

* Convert field to overridable computed property

StatsTimeSeries defines description as a computed property,
which isn’t possible when this is a class field.

* Rename clashing property

* Remove superfluous uses of Object.freeze

This is no longer needed!
https://guides.emberjs.com/release/upgrading/current-edition/native-classes/#toc_properties-and-fields

* Data cannot be a field in the base class and a CP in the child classes

* Remove stray commented-out line

Co-authored-by: Michael Lange <dingoeatingfuzz@gmail.com>
This commit is contained in:
Buck Doyle
2020-06-16 12:18:48 -05:00
committed by GitHub
263 changed files with 5271 additions and 3571 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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;
},
});
}
}

View File

@@ -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'),
},
});
},
});
}
}

View File

@@ -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 };
}
});
},
});
}
}

View File

@@ -1,14 +1,14 @@
import Watchable from './watchable';
import addToPath from 'nomad-ui/utils/add-to-path';
export default Watchable.extend({
export default class 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,
},
});
},
});
}
}

View File

@@ -1,7 +1,7 @@
import Watchable from './watchable';
export default Watchable.extend({
queryParamsToAttrs: Object.freeze({
export default class PluginAdapter extends Watchable {
queryParamsToAttrs = {
type: 'type',
}),
});
};
}

View File

@@ -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';
}

View File

@@ -1,10 +1,10 @@
import { inject as service } from '@ember/service';
import { default as ApplicationAdapter, namespace } from './application';
export default ApplicationAdapter.extend({
store: service(),
export default class 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);
});
},
});
}
}

View File

@@ -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',
}),
});
};
}

View File

@@ -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') {

View File

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

View File

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

View File

@@ -8,46 +8,48 @@ import { run } from '@ember/runloop';
import { task, timeout } from 'ember-concurrency';
import { lazyClick } from '../helpers/lazy-click';
import AllocationStatsTracker from 'nomad-ui/utils/classes/allocation-stats-tracker';
import classic from 'ember-classic-decorator';
import { classNames, tagName } from '@ember-decorators/component';
export default Component.extend({
store: service(),
token: service(),
@classic
@tagName('tr')
@classNames('allocation-row', 'is-interactive')
export default class AllocationRow extends Component {
@service store;
@service token;
tagName: 'tr',
classNames: ['allocation-row', 'is-interactive'],
allocation: null,
allocation = null;
// Used to determine whether the row should mention the node or the job
context: null,
context = null;
// Internal state
statsError: false,
statsError = false;
enablePolling: overridable(() => !Ember.testing),
@overridable(() => !Ember.testing) enablePolling;
stats: computed('allocation', 'allocation.isRunning', function() {
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;

View File

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

View File

@@ -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' },
];
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
},
});
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,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);
}
}

View File

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

View File

@@ -1,58 +1,59 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { filterBy } from '@ember/object/computed';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: '',
@classic
@tagName('')
export default class Browser extends Component {
model = null;
model: null,
allocation: computed('model', function() {
@computed('model')
get allocation() {
if (this.model.allocation) {
return this.model.allocation;
} else {
return this.model;
}
}),
}
taskState: computed('model', function() {
@computed('model')
get taskState() {
if (this.model.allocation) {
return this.model;
}
return;
}),
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;
}
),
});
}
}

View File

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

View File

@@ -1,36 +1,38 @@
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import { computed } from '@ember/object';
import { gt } from '@ember/object/computed';
import { equal } from '@ember/object/computed';
import { action, computed } from '@ember/object';
import { equal, gt } from '@ember/object/computed';
import RSVP from 'rsvp';
import Log from 'nomad-ui/utils/classes/log';
import timeout from 'nomad-ui/utils/timeout';
import { classNames } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
token: service(),
@classic
@classNames('boxed-section', 'task-log')
export default class File extends Component {
@service token;
classNames: ['boxed-section', 'task-log'],
'data-test-file-viewer' = true;
'data-test-file-viewer': true,
allocation: null,
taskState: null,
file: null,
stat: null, // { Name, IsDir, Size, FileMode, ModTime, ContentType }
allocation = null;
taskState = null;
file = null;
stat = null; // { Name, IsDir, Size, FileMode, ModTime, ContentType }
// When true, request logs from the server agent
useServer: false,
useServer = false;
// When true, logs cannot be fetched from either the client or the server
noConnection: false,
noConnection = false;
clientTimeout: 1000,
serverTimeout: 5000,
clientTimeout = 1000;
serverTimeout = 5000;
mode: 'head',
mode = 'head';
fileComponent: computed('stat.ContentType', function() {
@computed('stat.ContentType')
get fileComponent() {
const contentType = this.stat.ContentType || '';
if (contentType.startsWith('image/')) {
@@ -40,21 +42,23 @@ export default Component.extend({
} else {
return 'unknown';
}
}),
}
isLarge: gt('stat.Size', 50000),
@gt('stat.Size', 50000) isLarge;
fileTypeIsUnknown: equal('fileComponent', 'unknown'),
isStreamable: equal('fileComponent', 'stream'),
isStreaming: false,
@equal('fileComponent', 'unknown') fileTypeIsUnknown;
@equal('fileComponent', 'stream') isStreamable;
isStreaming = false;
catUrl: computed('allocation.id', 'taskState.name', 'file', function() {
@computed('allocation.id', 'taskState.name', 'file')
get catUrl() {
const taskUrlPrefix = this.taskState ? `${this.taskState.name}/` : '';
const encodedPath = encodeURIComponent(`${taskUrlPrefix}${this.file}`);
return `/v1/client/fs/cat/${this.allocation.id}?path=${encodedPath}`;
}),
}
fetchMode: computed('isLarge', 'mode', function() {
@computed('isLarge', 'mode')
get fetchMode() {
if (this.mode === 'streaming') {
return 'stream';
}
@@ -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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,23 +1,27 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { classNames, tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
tagName: 'figure',
classNames: 'image-file',
'data-test-image-file': true,
@classic
@tagName('figure')
@classNames('image-file')
export default class ImageFile extends Component {
'data-test-image-file' = true;
src: null,
alt: null,
size: null,
src = null;
alt = null;
size = null;
// Set by updateImageMeta
width: 0,
height: 0,
width = 0;
height = 0;
fileName: computed('src', function() {
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,
});
},
});
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,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();
},
});
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,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() {}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,47 +5,52 @@ import { computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import { task, timeout } from 'ember-concurrency';
import { lazyClick } from '../helpers/lazy-click';
import { classNames, tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';
export default Component.extend({
store: service(),
token: service(),
statsTrackersRegistry: service('stats-trackers-registry'),
@classic
@tagName('tr')
@classNames('task-row', 'is-interactive')
export default class TaskRow extends Component {
@service store;
@service token;
@service('stats-trackers-registry') statsTrackersRegistry;
tagName: 'tr',
classNames: ['task-row', 'is-interactive'],
task: null,
task = null;
// Internal state
statsError: false,
statsError = false;
enablePolling: computed(function() {
@computed
get enablePolling() {
return !Ember.testing;
}),
}
// Since all tasks for an allocation share the same tracker, use the registry
stats: computed('task', 'task.isRunning', function() {
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();
}
},
});
}
}

View File

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

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