ui/lib/shared/addon/mixins/cattle-transitioning-resour...

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