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

745 lines
19 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';
function terminatedIcon(inst) {
if ( get(inst, 'exitCode') === 0 ) {
return 'icon icon-dot-circlefill';
} else {
return 'icon icon-circle';
}
}
function terminatedColor(inst) {
if ( get(inst, 'exitCode') === 0 ) {
return 'text-success';
} else {
return 'text-error';
}
}
const defaultStateMap = {
'aborted': {
icon: 'icon icon-alert',
color: 'text-warning'
},
'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'
},
'building': {
icon: 'icon icon-circle-o',
color: 'text-success'
},
'created': {
icon: 'icon icon-tag',
color: 'text-info'
},
'creating': {
icon: 'icon icon-tag',
color: 'text-info'
},
'denied': {
icon: 'icon icon-adjust',
color: 'text-error'
},
'deactivating': {
icon: 'icon icon-adjust',
color: 'text-info'
},
'degraded': {
icon: 'icon icon-alert',
color: 'text-warning'
},
'disconnected': {
icon: 'icon icon-alert',
color: 'text-warning'
},
'disabled': {
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'
},
'fail': {
icon: 'icon icon-alert',
color: 'text-error'
},
'failed': {
icon: 'icon icon-alert',
color: 'text-error'
},
'healthy': {
icon: 'icon icon-circle-o',
color: 'text-success'
},
'locked': {
icon: 'icon icon-adjust',
color: 'text-warning'
},
'in-progress': {
icon: 'icon icon-tag',
color: 'text-info'
},
'inactive': {
icon: 'icon icon-circle',
color: 'text-error'
},
'initializing': {
icon: 'icon icon-alert',
color: 'text-warning'
},
'migrating': {
icon: 'icon icon-info',
color: 'text-info'
},
'pass': {
icon: 'icon icon-circle-o',
color: 'text-success'
},
'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'
},
'succeeded': {
icon: 'icon icon-dot-circlefill',
color: 'text-success'
},
'success': {
icon: 'icon icon-circle-o',
color: 'text-success'
},
'suspended': {
icon: 'icon icon-pause',
color: 'text-info'
},
'skipped': {
icon: 'icon icon-circle-o',
color: 'text-info'
},
'terminated': {
icon: terminatedIcon,
color: terminatedColor
},
'unavailable': {
icon: 'icon icon-alert',
color: 'text-error'
},
'unhealthy': {
icon: 'icon icon-alert',
color: 'text-error'
},
'unknown': {
icon: 'icon icon-help',
color: 'text-warning'
},
'untriggered': {
icon: 'icon icon-tag',
color: 'text-success'
},
'updating': {
icon: 'icon icon-tag',
color: 'text-warning'
},
'upgrading': {
icon: 'icon icon-tag',
color: 'text-warning'
},
'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 shown or not. Anything other than exactly false will be shown.
bulkable: true/false, // If true, the action is shown in bulk actions on sortable-tables
single: true/false, // If exactly false, the action is not shown on individual resource actions (with bulkable=true for a bulk-only action)
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.[]', 'canBulkRemove', 'canClone', 'canDownloadYaml', 'canEdit', 'canEditYaml', 'canRemove', 'canViewYaml', 'getAltActionDelete', 'grafanaUrl', 'links.{self,yaml}', 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
});
out.push({
sort: 95,
label: 'action.editYaml',
icon: 'icon icon-edit',
action: 'editYaml',
enabled: !!l.yaml && get(this, 'canEditYaml'),
});
out.push({
sort: 96,
label: 'action.viewYaml',
icon: 'icon icon-file',
action: 'viewYaml',
enabled: get(this, 'canViewYaml'),
});
out.push({
sort: 97,
label: 'action.downloadYaml',
icon: 'icon icon-download',
action: 'downloadYaml',
bulkable: true,
single: false,
enabled: get(this, 'canDownloadYaml'),
});
out.push({
sort: 98,
label: 'action.viewInApi',
icon: 'icon icon-external-link',
action: 'goToApi',
enabled: !!l.self
});
out.push({
sort: 99,
label: 'action.viewInGrafana',
icon: 'icon icon-link',
action: 'goToGrafana',
enabled: !!get(this, 'grafanaUrl'),
});
out.push({
sort: 100,
divider: true
});
out.push({
sort: 101,
label: 'action.remove',
icon: 'icon icon-trash',
action: 'promptDelete',
altAction: get(this, 'getAltActionDelete'),
bulkable: get(this, 'canBulkRemove'),
enabled: get(this, 'canRemove'),
});
return out.sortBy('sort');
}),
getAltActionDelete: computed('action.remove', function() { // eslint-disable-line
// eks clusters with session tokens can't be deleted with alt actions because of the verification of keys that needs to occur
return 'delete';
}),
canBulkRemove: computed('action.remove', function() { // eslint-disable-line
return true;
}),
canClone: computed('actions.clone', function() {
return !!get(this, 'actions.clone');
}),
canEditYaml: alias('canEdit'),
canViewYaml: computed('canEditYaml', 'links.[]', 'links.yaml', function() {
return !!get(this, 'links.yaml') && !get(this, 'canEditYaml');
}),
canDownloadYaml: computed('links.{@each,yaml}', function() {
return !!get(this, 'links.yaml');
}),
canEdit: computed('actions.edit', 'links.[]', 'links.update', function() {
return !!get(this, 'links.update') && !!get(this, 'actions.edit');
}),
canRemove: computed('links.{@each,remove}', function() {
return !!get(this, 'links.remove');
}),
actions: {
promptDelete() {
get(this, 'modalService').toggleModal('confirm-delete', {
escToClose: true,
resources: [this]
});
},
delete() {
return this.delete();
},
downloadYaml() {
downloadResourceYaml([this]);
},
editYaml(){
get(this, 'modalService').toggleModal('modal-yaml', {
escToClose: true,
resource: this
});
},
viewYaml(){
get(this, 'modalService').toggleModal('modal-yaml', {
escToClose: true,
resource: this,
readOnly: true
});
},
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');
},
goToGrafana() {
let url = get(this, 'grafanaUrl');
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', 'intl.locale', function() {
const intl = get(this, 'intl');
const state = get(this, 'relevantState') || '';
const key = `resourceState.${ (state || 'unknown').toLowerCase() }`;
if ( intl.locale && intl.exists(key) ) {
return intl.t(key);
}
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('constructor.{defaultStateIcon,stateMap}', '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 ) {
let tmp = defaultStateMap[key].icon;
if ( typeof tmp === 'function' ) {
icon = tmp(this);
} else {
icon = tmp;
}
}
if ( !icon ) {
icon = this.constructor.defaultStateIcon;
}
if ( icon.indexOf('icon ') === -1 ) {
icon = `icon ${ icon }`;
}
}
return icon;
}),
stateColor: computed('constructor.{defaultStateColor,stateMap}', 'isError', 'relevantState', 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 ) {
let tmp = defaultStateMap[key].color;
if ( typeof tmp === 'function' ) {
return tmp(this);
} else {
return tmp;
}
}
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.actionLinks;
delete copy.appId;
delete copy.id;
delete copy.links;
delete copy.name;
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(`Failed while: ${ msg }`);
}, 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((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;
}),
displayTaintsStrings: computed('nodeTaints', 'taints', function() {
const out = [];
const taints = get(this, 'nodeTaints') || get(this, 'taints') || [];
taints.forEach((taint) => {
out.push(`${ get(taint, 'key') }${ get(taint, 'value') ? `=${ get(taint, 'value') }` : '' }:${ get(taint, 'effect') }`);
});
return out.sort();
}),
});