mirror of
https://github.com/kemko/nomad.git
synced 2026-01-06 02:15:43 +03:00
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:
@@ -15,6 +15,9 @@ module.exports = {
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
legacyDecorators: true,
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
'ember'
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Watchable from './watchable';
|
||||
|
||||
export default Watchable.extend({
|
||||
queryParamsToAttrs: Object.freeze({
|
||||
export default class PluginAdapter extends Watchable {
|
||||
queryParamsToAttrs = {
|
||||
type: 'type',
|
||||
}),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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') {
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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' },
|
||||
];
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 '';
|
||||
}
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {}
|
||||
}
|
||||
|
||||
@@ -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') },
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 };
|
||||
});
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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() {}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
];
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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]);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
});
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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() {}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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() {}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = '';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)]);
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user