mirror of https://github.com/rancher/ui.git
454 lines
14 KiB
JavaScript
454 lines
14 KiB
JavaScript
import { reject, Promise as EmberPromise } from 'rsvp';
|
|
import { computed, get } from '@ember/object';
|
|
import { equal, alias } from '@ember/object/computed';
|
|
import { inject as service } from '@ember/service';
|
|
import Mixin from '@ember/object/mixin';
|
|
import { ucFirst, sortableNumericSuffix } from 'ui/utils/util';
|
|
import C from 'ui/utils/constants';
|
|
import { downloadResourceYaml } from 'shared/utils/download-files';
|
|
|
|
const defaultStateMap = {
|
|
'activating': {icon: 'icon icon-tag', color: 'text-info' },
|
|
'active': {icon: 'icon icon-circle-o', color: 'text-success'},
|
|
'available': {icon: 'icon icon-circle-o', color: 'text-success'},
|
|
'bound': {icon: 'icon icon-circle', color: 'text-success'},
|
|
'backedup': {icon: 'icon icon-backup', color: 'text-success'},
|
|
'created': {icon: 'icon icon-tag', color: 'text-info' },
|
|
'creating': {icon: 'icon icon-tag', color: 'text-info' },
|
|
'deactivating': {icon: 'icon icon-adjust', color: 'text-info' },
|
|
'degraded': {icon: 'icon icon-alert', color: 'text-warning'},
|
|
'disconnected': {icon: 'icon icon-alert', color: 'text-warning'},
|
|
'error': {icon: 'icon icon-alert', color: 'text-error' },
|
|
'erroring': {icon: 'icon icon-alert', color: 'text-error' },
|
|
'expired': {icon: 'icon icon-alert', color: 'text-warning'},
|
|
'healthy': {icon: 'icon icon-circle-o', color: 'text-success'},
|
|
'inactive': {icon: 'icon icon-circle', color: 'text-error' },
|
|
'initializing': {icon: 'icon icon-alert', color: 'text-warning'},
|
|
'migrating': {icon: 'icon icon-info', color: 'text-info' },
|
|
'paused': {icon: 'icon icon-info', color: 'text-info' },
|
|
'provisioning': {icon: 'icon icon-circle', color: 'text-info' },
|
|
'pending': {icon: 'icon icon-tag', color: 'text-info' },
|
|
'purged': {icon: 'icon icon-purged', color: 'text-error' },
|
|
'purging': {icon: 'icon icon-purged', color: 'text-info' },
|
|
'reconnecting': {icon: 'icon icon-alert', color: 'text-error' },
|
|
'registering': {icon: 'icon icon-tag', color: 'text-info' },
|
|
'released': {icon: 'icon icon-alert', color: 'text-warning'},
|
|
'reinitializing': {icon: 'icon icon-alert', color: 'text-warning'},
|
|
'removed': {icon: 'icon icon-trash', color: 'text-error' },
|
|
'removing': {icon: 'icon icon-trash', color: 'text-info' },
|
|
'requested': {icon: 'icon icon-tag', color: 'text-info' },
|
|
'restarting': {icon: 'icon icon-adjust', color: 'text-info' },
|
|
'restoring': {icon: 'icon icon-medicalcross', color: 'text-info' },
|
|
'running': {icon: 'icon icon-circle-o', color: 'text-success'},
|
|
'starting': {icon: 'icon icon-adjust', color: 'text-info' },
|
|
'stopped': {icon: 'icon icon-circle', color: 'text-error' },
|
|
'stopping': {icon: 'icon icon-adjust', color: 'text-info' },
|
|
'unavailable': {icon: 'icon icon-alert', color: 'text-error' },
|
|
'unhealthy': {icon: 'icon icon-alert', color: 'text-error' },
|
|
'unknown': {icon: 'icon icon-help', color: 'text-warning'},
|
|
'updating': {icon: 'icon icon-tag', color: 'text-info' },
|
|
'waiting': {icon: 'icon icon-tag', color: 'text-info' },
|
|
};
|
|
|
|
const stateColorSortMap = {
|
|
'error': 1,
|
|
'warning': 2,
|
|
'info': 3,
|
|
'success': 4,
|
|
'other': 5,
|
|
};
|
|
|
|
export default Mixin.create({
|
|
endpointSvc: service('endpoint'), // Some machine drivers have a property called 'endpoint'
|
|
cookies: service(),
|
|
growl: service(),
|
|
intl: service(),
|
|
session: service(),
|
|
|
|
modalService: service('modal'),
|
|
reservedKeys: ['waitInterval','waitTimeout'],
|
|
|
|
state: null,
|
|
transitioning: null,
|
|
transitioningMessage: null,
|
|
transitioningProgress: null,
|
|
|
|
availableActions: computed(() => {
|
|
/*
|
|
For custom actions not in _availableActions below, Override me and return [
|
|
{
|
|
enabled: true/false, // Whether it's enabled or greyed out
|
|
detail: true/false, // If true, this action will only be shown on detailed screens
|
|
label: 'Delete', // Label shown on hover or in menu
|
|
icon: 'icon icon-trash',// Icon shown on screen
|
|
action: 'promptDelete', // Action to call on the controller when clicked
|
|
altAction: 'delete' // Action to call on the controller when alt+clicked
|
|
divider: true, // Just this will make a divider
|
|
},
|
|
...
|
|
]
|
|
*/
|
|
return [];
|
|
}),
|
|
|
|
_availableActions: computed('availableActions.[]', 'links.{self,yaml}', 'canEdit', 'canRemove', function() {
|
|
const out = get(this, 'availableActions').slice();
|
|
|
|
let nextSort=1;
|
|
out.forEach((entry) => {
|
|
if ( !entry.sort ) {
|
|
entry.sort = nextSort++;
|
|
}
|
|
});
|
|
|
|
const l = get(this, 'links');
|
|
|
|
out.push({
|
|
sort: -99,
|
|
label: 'action.edit',
|
|
icon: 'icon icon-edit',
|
|
action: 'edit',
|
|
enabled: get(this, 'canEdit'),
|
|
});
|
|
|
|
out.push({
|
|
sort: -98,
|
|
label: 'action.clone',
|
|
action: 'clone',
|
|
icon: 'icon icon-copy',
|
|
enabled: get(this, 'canClone'),
|
|
});
|
|
|
|
// Normal actions go here in the sort order
|
|
|
|
out.push({
|
|
sort: 94,
|
|
divider: true
|
|
});
|
|
|
|
if ( l.yaml ) {
|
|
out.push({
|
|
sort: 95,
|
|
divider: true
|
|
});
|
|
|
|
out.push({
|
|
sort: 96,
|
|
label: 'action.editYaml',
|
|
icon: 'icon icon-edit',
|
|
action: 'editYaml',
|
|
});
|
|
|
|
out.push({
|
|
sort: 97,
|
|
label: 'action.downloadYaml',
|
|
icon: 'icon icon-download',
|
|
action: 'downloadYaml',
|
|
bulkable: true,
|
|
enabled: true,
|
|
});
|
|
|
|
|
|
}
|
|
|
|
out.push({
|
|
sort: 98,
|
|
label: 'action.viewInApi',
|
|
icon: 'icon icon-external-link',
|
|
action: 'goToApi',
|
|
enabled: !!l.self
|
|
});
|
|
|
|
out.push({
|
|
sort: 99,
|
|
divider: true
|
|
});
|
|
out.push({
|
|
sort: 100,
|
|
label: 'action.remove',
|
|
icon: 'icon icon-trash',
|
|
action: 'promptDelete',
|
|
altAction: 'delete',
|
|
bulkable: true,
|
|
enabled: get(this, 'canRemove'),
|
|
});
|
|
|
|
return out.sortBy('sort');
|
|
}),
|
|
|
|
canClone: computed('actions.clone', function() {
|
|
return get(this, 'actions.clone');
|
|
}),
|
|
|
|
canEdit: computed('links.update', 'actions.edit', function() {
|
|
return !!get(this, 'links.update') && get(this, 'actions.edit');
|
|
}),
|
|
|
|
canRemove: computed('links.remove', function() {
|
|
return !!get(this, 'links.remove');
|
|
}),
|
|
|
|
actions: {
|
|
promptDelete() {
|
|
get(this,'modalService').toggleModal('confirm-delete', {resources: [this]});
|
|
},
|
|
|
|
delete() {
|
|
return this.delete();
|
|
},
|
|
|
|
downloadYaml() {
|
|
downloadResourceYaml([this]);
|
|
},
|
|
|
|
editYaml(){
|
|
get(this,'modalService').toggleModal('modal-yaml', {resource: this});
|
|
},
|
|
|
|
goToApi() {
|
|
let url = get(this,'links.self'); // http://a.b.c.d/v1/things/id, a.b.c.d is where the UI is running
|
|
window.open(url, '_blank');
|
|
},
|
|
},
|
|
|
|
displayName: computed('name','id', function() {
|
|
return get(this,'name') || '('+get(this,'id')+')';
|
|
}),
|
|
|
|
sortName: computed('displayName', function() {
|
|
return sortableNumericSuffix(get(this,'displayName').toLowerCase());
|
|
}),
|
|
|
|
isTransitioning: equal('transitioning','yes'),
|
|
isError: equal('transitioning','error'),
|
|
isActive: equal('state','active'),
|
|
|
|
relevantState: computed('combinedState','state', function() {
|
|
return get(this,'combinedState') || get(this,'state') || 'unknown';
|
|
}),
|
|
|
|
// This is like this so you can override the displayed state calculation
|
|
displayState: alias('_displayState'),
|
|
_displayState: computed('relevantState', function() {
|
|
var state = get(this,'relevantState')||'';
|
|
return state.split(/-/).map((word) => {
|
|
return ucFirst(word);
|
|
}).join('-');
|
|
}),
|
|
|
|
showTransitioningMessage: computed('transitioning','transitioningMessage','displayState', function() {
|
|
var trans = get(this,'transitioning');
|
|
if (trans === 'yes' || trans === 'error') {
|
|
let message = (get(this,'transitioningMessage')||'');
|
|
if ( message.length && message.toLowerCase() !== get(this,'displayState').toLowerCase() ) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}),
|
|
|
|
stateIcon: computed('relevantState','transitioning', function() {
|
|
var trans = get(this,'transitioning');
|
|
var icon = '';
|
|
|
|
if ( trans === 'yes' ) {
|
|
icon = 'icon icon-spinner icon-spin';
|
|
} else if ( trans === 'error' ) {
|
|
icon = 'icon icon-alert';
|
|
} else{
|
|
var map = this.constructor.stateMap;
|
|
var key = (get(this,'relevantState')||'').toLowerCase();
|
|
if ( map && map[key] && map[key].icon !== undefined) {
|
|
if ( typeof map[key].icon === 'function' ) {
|
|
icon = map[key].icon(this);
|
|
} else {
|
|
icon = map[key].icon;
|
|
}
|
|
}
|
|
|
|
if ( !icon && defaultStateMap[key] && defaultStateMap[key].icon ) {
|
|
icon = defaultStateMap[key].icon;
|
|
}
|
|
|
|
if ( !icon ) {
|
|
icon = this.constructor.defaultStateIcon;
|
|
}
|
|
|
|
if ( icon.indexOf('icon ') === -1 ) {
|
|
icon = 'icon ' + icon;
|
|
}
|
|
}
|
|
|
|
return icon;
|
|
}),
|
|
|
|
stateColor: computed('relevantState','isError', function() {
|
|
if ( get(this,'isError') ) {
|
|
return 'text-error';
|
|
}
|
|
|
|
var map = this.constructor.stateMap;
|
|
var key = (get(this,'relevantState')||'').toLowerCase();
|
|
if ( map && map[key] && map[key].color !== undefined )
|
|
{
|
|
if ( typeof map[key].color === 'function' )
|
|
{
|
|
return map[key].color(this);
|
|
}
|
|
else
|
|
{
|
|
return map[key].color;
|
|
}
|
|
}
|
|
|
|
if ( defaultStateMap[key] && defaultStateMap[key].color )
|
|
{
|
|
return defaultStateMap[key].color;
|
|
}
|
|
|
|
return this.constructor.defaultStateColor;
|
|
}),
|
|
|
|
sortState: computed('stateColor','relevantState', function() {
|
|
var color = get(this,'stateColor').replace('text-','');
|
|
return (stateColorSortMap[color] || stateColorSortMap['other']) + ' ' + get(this,'relevantState');
|
|
}),
|
|
|
|
stateBackground: computed('stateColor', function() {
|
|
return get(this,'stateColor').replace("text-","bg-");
|
|
}),
|
|
|
|
cloneForNew() {
|
|
var copy = this.clone();
|
|
delete copy.id;
|
|
delete copy.actionLinks;
|
|
delete copy.links;
|
|
delete copy.uuid;
|
|
return copy;
|
|
},
|
|
|
|
serializeForNew() {
|
|
var copy = this.serialize();
|
|
delete copy.id;
|
|
delete copy.actionLinks;
|
|
delete copy.links;
|
|
delete copy.uuid;
|
|
return copy;
|
|
},
|
|
|
|
// Show growls for errors on actions
|
|
delete(/*arguments*/) {
|
|
var promise = this._super.apply(this, arguments);
|
|
return promise.catch((err) => {
|
|
get(this,'growl').fromError('Error deleting',err);
|
|
});
|
|
},
|
|
|
|
doAction(name, data, opt) {
|
|
var promise = this._super.apply(this, arguments);
|
|
|
|
if ( !opt || opt.catchGrowl !== false )
|
|
{
|
|
return promise.catch((err) => {
|
|
get(this,'growl').fromError(ucFirst(name) + ' Error', err);
|
|
return reject(err);
|
|
});
|
|
}
|
|
|
|
return promise;
|
|
},
|
|
|
|
// You really shouldn't have to use any of these.
|
|
// Needing these is a sign that the API is bad and should feel bad.
|
|
// Yet here they are, nonetheless.
|
|
waitInterval: 1000,
|
|
waitTimeout: 30000,
|
|
_waitForTestFn(testFn, msg) {
|
|
return new EmberPromise((resolve, reject) => {
|
|
// Do a first check immediately
|
|
if ( testFn.apply(this) ) {
|
|
resolve(this);
|
|
return;
|
|
}
|
|
|
|
var timeout = setTimeout(() => {
|
|
clearInterval(interval);
|
|
clearTimeout(timeout);
|
|
reject(this);
|
|
}, get(this,'waitTimeout'));
|
|
|
|
var interval = setInterval(() => {
|
|
if ( testFn.apply(this) )
|
|
{
|
|
clearInterval(interval);
|
|
clearTimeout(timeout);
|
|
resolve(this);
|
|
}
|
|
}, get(this,'waitInterval'));
|
|
}, msg||'Wait for it...');
|
|
},
|
|
|
|
waitForState(state) {
|
|
return this._waitForTestFn(function() {
|
|
return get(this,'state') === state;
|
|
}, 'Wait for state='+state);
|
|
},
|
|
|
|
waitForTransition() {
|
|
return this._waitForTestFn(function() {
|
|
return get(this,'transitioning') !== 'yes';
|
|
}, 'Wait for transition');
|
|
},
|
|
|
|
waitForAction(name) {
|
|
return this._waitForTestFn(function() {
|
|
//console.log('waitForAction('+name+'):', this.hasAction(name));
|
|
return this.hasAction(name);
|
|
}, 'Wait for action='+name);
|
|
},
|
|
|
|
hasCondition(condition, status='True') {
|
|
let entry = (get(this,'conditions')||[]).findBy('type', condition);
|
|
if ( !entry ) {
|
|
return false;
|
|
}
|
|
|
|
if ( status ) {
|
|
return ( get(entry,'status')||'').toLowerCase() === (status+'').toLowerCase();
|
|
} else {
|
|
return true;
|
|
}
|
|
},
|
|
|
|
waitForCondition(condition, status='True') {
|
|
return this._waitForTestFn(function() {
|
|
return this.hasCondition(condition, status);
|
|
}, `Wait for Condition: ${condition}: ${status}`);
|
|
},
|
|
|
|
displayUserLabelStrings: computed('labels', function() {
|
|
let out = [];
|
|
let labels = get(this,'labels')||{};
|
|
Object.keys(labels).forEach(function(key) {
|
|
if ( key.indexOf(C.LABEL.AFFINITY_PREFIX) === 0 ||
|
|
key.indexOf(C.LABEL.SYSTEM_PREFIX) === 0 ||
|
|
C.LABELS_TO_IGNORE.indexOf(key) >= 0
|
|
) {
|
|
// Skip ignored labels
|
|
return;
|
|
} else {
|
|
for ( let i = 0 ; i < C.LABEL_PREFIX_TO_IGNORE.length ; i++ ) {
|
|
if ( key.startsWith(C.LABEL_PREFIX_TO_IGNORE[i]) ) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
out.push(key + (labels[key] ? '='+labels[key] : ''));
|
|
});
|
|
|
|
return out;
|
|
}),
|
|
});
|