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