Copy over more services for polling
Use a class for rollout status response Add some better errors Add override styles for success case
This commit is contained in:
parent
d936d778da
commit
128cf0a28d
8 changed files with 318 additions and 72 deletions
|
@ -43,12 +43,16 @@
|
|||
<code>{{deployment.name}}</code>: {{deployment.message || 'Waiting for deployment information...'}}
|
||||
</li>
|
||||
</div>
|
||||
<div ng-if="$ctrl.state === 'deployed'">
|
||||
Configuration successfully deployed!
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="$ctrl.state === 'error'">
|
||||
</div>
|
||||
<div ng-if="$ctrl.state === 'deployed'" class="modal-footer co-alert co-alert-success">
|
||||
Configuration successfully rolled out and deployed!
|
||||
<br>Note: The web interface of the Quay app may take a few minutes to come up.
|
||||
</div>
|
||||
<div ng-if="$ctrl.state === 'error'" class="modal-footer alert alert-danger">
|
||||
{{ $ctrl.errorMessage }}
|
||||
<div ng-if="$ctrl.offerRollback">
|
||||
// todo
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {Component, EventEmitter, Inject} from 'ng-metadata/core';
|
||||
import {Component, EventEmitter, Inject, OnDestroy } from 'ng-metadata/core';
|
||||
import {AngularPollChannel, PollHandle} from "../../services/services.types";
|
||||
const templateUrl = require('./kube-deploy-modal.component.html');
|
||||
const styleUrl = require('./kube-deploy-modal.css');
|
||||
|
||||
|
@ -8,12 +9,19 @@ type DeploymentRollout = {
|
|||
message: string
|
||||
};
|
||||
|
||||
type DeploymentStatus = {
|
||||
name: string,
|
||||
numPods: number,
|
||||
message?: string,
|
||||
pollHandler?: PollHandle,
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'kube-deploy-modal',
|
||||
templateUrl,
|
||||
styleUrls: [ styleUrl ],
|
||||
})
|
||||
export class KubeDeployModalComponent {
|
||||
export class KubeDeployModalComponent implements OnDestroy {
|
||||
private state
|
||||
: 'loadingDeployments'
|
||||
| 'readyToDeploy'
|
||||
|
@ -21,13 +29,13 @@ export class KubeDeployModalComponent {
|
|||
| 'cyclingDeployments'
|
||||
| 'deployed'
|
||||
| 'error';
|
||||
|
||||
private errorMessage: string;
|
||||
|
||||
private deploymentsStatus: { name: string, numPods: number, message?: string }[] = [];
|
||||
private offerRollback: boolean;
|
||||
private deploymentsStatus: DeploymentStatus[] = [];
|
||||
private deploymentsCycled: number = 0;
|
||||
private onDestroyListeners: Function[] = [];
|
||||
|
||||
constructor(@Inject('ApiService') private ApiService) {
|
||||
constructor(@Inject('ApiService') private ApiService, @Inject('AngularPollChannel') private AngularPollChannel: AngularPollChannel) {
|
||||
this.state = 'loadingDeployments';
|
||||
|
||||
ApiService.scGetNumDeployments().then(resp => {
|
||||
|
@ -42,6 +50,14 @@ export class KubeDeployModalComponent {
|
|||
})
|
||||
}
|
||||
|
||||
// Call all listeners of the onDestroy
|
||||
ngOnDestroy(): any {
|
||||
this.onDestroyListeners.forEach(fn => {
|
||||
fn()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
deployConfiguration(): void {
|
||||
this.ApiService.scDeployConfiguration().then(() => {
|
||||
this.state = 'deployingConfiguration';
|
||||
|
@ -62,34 +78,45 @@ export class KubeDeployModalComponent {
|
|||
|
||||
watchDeployments(): void {
|
||||
this.deploymentsStatus.forEach(deployment => {
|
||||
// Query each deployment every 500ms, and stop polling once it's either available or failed
|
||||
const id: number = window.setInterval(() => {
|
||||
const params = {
|
||||
'deployment': deployment.name
|
||||
};
|
||||
|
||||
this.ApiService.scGetDeploymentRolloutStatus(null, params).then((deploymentRollout: DeploymentRollout) => {
|
||||
if (deploymentRollout.status === 'available') {
|
||||
window.clearInterval(id);
|
||||
|
||||
this.deploymentsCycled++;
|
||||
if (this.deploymentsCycled === this.deploymentsStatus.length) {
|
||||
this.state = 'deployed';
|
||||
}
|
||||
} else if (deploymentRollout.status === 'progressing') {
|
||||
deployment.message = deploymentRollout.message;
|
||||
} else { // deployment rollout failed
|
||||
window.clearInterval(id);
|
||||
|
||||
deployment.message = deploymentRollout.message;
|
||||
}
|
||||
}).catch(err => {
|
||||
window.clearInterval(id);
|
||||
this.state = 'error';
|
||||
this.errorMessage = `Could not cycle the deployments with the new configuration. Error: ${err.toString()}`;
|
||||
});
|
||||
}, 500);
|
||||
this.AngularPollChannel.create( {
|
||||
// Have to mock the scope object for the poll channel since we're calling into angular1 code
|
||||
// We register the onDestroy function to be called later when this object is destroyed
|
||||
'$on': (_, onDestruction) => { this.onDestroyListeners.push(onDestruction) }
|
||||
}, this.getDeploymentStatus(deployment), 5000 /* 5 seconds */)
|
||||
.start();
|
||||
});
|
||||
}
|
||||
|
||||
// Query each deployment every 5s, and stop polling once it's either available or failed
|
||||
getDeploymentStatus(deployment: DeploymentStatus): (boolean) => void {
|
||||
return (continue_callback: (shouldContinue: boolean) => void) => {
|
||||
const params = {
|
||||
'deployment': deployment.name
|
||||
};
|
||||
|
||||
this.ApiService.scGetDeploymentRolloutStatus(null, params).then((deploymentRollout: DeploymentRollout) => {
|
||||
if (deploymentRollout.status === 'available') {
|
||||
continue_callback(false);
|
||||
|
||||
this.deploymentsCycled++;
|
||||
if (this.deploymentsCycled === this.deploymentsStatus.length) {
|
||||
this.state = 'deployed';
|
||||
}
|
||||
} else if (deploymentRollout.status === 'progressing') {
|
||||
continue_callback(true);
|
||||
deployment.message = deploymentRollout.message;
|
||||
} else { // deployment rollout failed
|
||||
this.state = 'error';
|
||||
continue_callback(false);
|
||||
deployment.message = deploymentRollout.message;
|
||||
this.errorMessage = `Could not cycle deployments: ${deploymentRollout.message}`;
|
||||
this.offerRollback = true;
|
||||
}
|
||||
}).catch(err => {
|
||||
continue_callback(false);
|
||||
this.state = 'error';
|
||||
this.errorMessage = `Could not cycle the deployments with the new configuration. Error: ${err.toString()}`;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
107
config_app/js/services/angular-poll-channel.js
vendored
Normal file
107
config_app/js/services/angular-poll-channel.js
vendored
Normal file
|
@ -0,0 +1,107 @@
|
|||
/**
|
||||
* Specialized class for conducting an HTTP poll, while properly preventing multiple calls.
|
||||
*/
|
||||
angular.module('quay-config').factory('AngularPollChannel',
|
||||
['ApiService', '$timeout', 'DocumentVisibilityService', 'CORE_EVENT', '$rootScope',
|
||||
function(ApiService, $timeout, DocumentVisibilityService, CORE_EVENT, $rootScope) {
|
||||
var _PollChannel = function(scope, requester, opt_sleeptime) {
|
||||
this.scope_ = scope;
|
||||
this.requester_ = requester;
|
||||
this.sleeptime_ = opt_sleeptime || (60 * 1000 /* 60s */);
|
||||
this.timer_ = null;
|
||||
|
||||
this.working = false;
|
||||
this.polling = false;
|
||||
this.skipping = false;
|
||||
|
||||
var that = this;
|
||||
|
||||
var visibilityHandler = $rootScope.$on(CORE_EVENT.DOC_VISIBILITY_CHANGE, function() {
|
||||
// If the poll channel was skipping because the visibility was hidden, call it immediately.
|
||||
if (that.skipping && !DocumentVisibilityService.isHidden()) {
|
||||
that.call_();
|
||||
}
|
||||
});
|
||||
|
||||
scope.$on('$destroy', function() {
|
||||
that.stop();
|
||||
visibilityHandler();
|
||||
});
|
||||
};
|
||||
|
||||
_PollChannel.prototype.setSleepTime = function(sleepTime) {
|
||||
this.sleeptime_ = sleepTime;
|
||||
this.stop();
|
||||
this.start(true);
|
||||
};
|
||||
|
||||
_PollChannel.prototype.stop = function() {
|
||||
if (this.timer_) {
|
||||
$timeout.cancel(this.timer_);
|
||||
this.timer_ = null;
|
||||
this.polling = false;
|
||||
}
|
||||
|
||||
this.skipping = false;
|
||||
this.working = false;
|
||||
};
|
||||
|
||||
_PollChannel.prototype.start = function(opt_skipFirstCall) {
|
||||
// Make sure we invoke call outside the normal digest cycle, since
|
||||
// we'll call $scope.$apply ourselves.
|
||||
var that = this;
|
||||
setTimeout(function() {
|
||||
if (opt_skipFirstCall) {
|
||||
that.setupTimer_();
|
||||
return;
|
||||
}
|
||||
|
||||
that.call_();
|
||||
}, 0);
|
||||
};
|
||||
|
||||
_PollChannel.prototype.call_ = function() {
|
||||
if (this.working) { return; }
|
||||
|
||||
// If the document is currently hidden, skip the call.
|
||||
if (DocumentVisibilityService.isHidden()) {
|
||||
this.skipping = true;
|
||||
this.setupTimer_();
|
||||
return;
|
||||
}
|
||||
|
||||
var that = this;
|
||||
this.working = true;
|
||||
|
||||
$timeout(function() {
|
||||
that.requester_(function(status) {
|
||||
if (status) {
|
||||
that.working = false;
|
||||
that.skipping = false;
|
||||
that.setupTimer_();
|
||||
} else {
|
||||
that.stop();
|
||||
}
|
||||
});
|
||||
}, 0);
|
||||
};
|
||||
|
||||
_PollChannel.prototype.setupTimer_ = function() {
|
||||
if (this.timer_) { return; }
|
||||
|
||||
var that = this;
|
||||
this.polling = true;
|
||||
this.timer_ = $timeout(function() {
|
||||
that.timer_ = null;
|
||||
that.call_();
|
||||
}, this.sleeptime_)
|
||||
};
|
||||
|
||||
var service = {
|
||||
'create': function(scope, requester, opt_sleeptime) {
|
||||
return new _PollChannel(scope, requester, opt_sleeptime);
|
||||
}
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
60
config_app/js/services/document-visibility-service.js
Normal file
60
config_app/js/services/document-visibility-service.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* Helper service which fires off events when the document's visibility changes, as well as allowing
|
||||
* other Angular code to query the state of the document's visibility directly.
|
||||
*/
|
||||
angular.module('quay-config').constant('CORE_EVENT', {
|
||||
DOC_VISIBILITY_CHANGE: 'core.event.doc_visibility_change'
|
||||
});
|
||||
|
||||
angular.module('quay-config').factory('DocumentVisibilityService', ['$rootScope', '$document', 'CORE_EVENT',
|
||||
function($rootScope, $document, CORE_EVENT) {
|
||||
var document = $document[0],
|
||||
features,
|
||||
detectedFeature;
|
||||
|
||||
function broadcastChangeEvent() {
|
||||
$rootScope.$broadcast(CORE_EVENT.DOC_VISIBILITY_CHANGE,
|
||||
document[detectedFeature.propertyName]);
|
||||
}
|
||||
|
||||
features = {
|
||||
standard: {
|
||||
eventName: 'visibilitychange',
|
||||
propertyName: 'hidden'
|
||||
},
|
||||
moz: {
|
||||
eventName: 'mozvisibilitychange',
|
||||
propertyName: 'mozHidden'
|
||||
},
|
||||
ms: {
|
||||
eventName: 'msvisibilitychange',
|
||||
propertyName: 'msHidden'
|
||||
},
|
||||
webkit: {
|
||||
eventName: 'webkitvisibilitychange',
|
||||
propertyName: 'webkitHidden'
|
||||
}
|
||||
};
|
||||
|
||||
Object.keys(features).some(function(feature) {
|
||||
if (document[features[feature].propertyName] !== undefined) {
|
||||
detectedFeature = features[feature];
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (detectedFeature) {
|
||||
$document.on(detectedFeature.eventName, broadcastChangeEvent);
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Is the window currently hidden or not.
|
||||
*/
|
||||
isHidden: function() {
|
||||
if (detectedFeature) {
|
||||
return document[detectedFeature.propertyName];
|
||||
}
|
||||
}
|
||||
};
|
||||
}]);
|
12
config_app/js/services/services.types.ts
Normal file
12
config_app/js/services/services.types.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
export interface AngularPollChannel {
|
||||
create(scope: { '$on': Function },
|
||||
requester: (boolean) => void, // function that receives a callback to continue or halt polling
|
||||
opt_sleeptime?: number
|
||||
): PollHandle,
|
||||
}
|
||||
|
||||
export interface PollHandle {
|
||||
start(opt_skipFirstCall?: boolean): void,
|
||||
stop(): void,
|
||||
setSleepTime(sleepTime: number): void,
|
||||
}
|
Reference in a new issue