1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-08 07:15:23 +02:00

feat(extensions): add the ability to upload and enable an extension (#3345)

* feat(extensions): offline mode mockup

* feat(extensions): offline mode mockup

* feat(api): add support for extensionUpload API operation

* feat(extensions): offline extension upload

* feat(api): better support for extensions in offline mode

* feat(extension): update offline description

* feat(api): introduce local extension manifest

* fix(api): fix LocalExtensionManifestFile value

* feat(api): use a 5second timeout for online extension infos

* feat(extensions): add download archive link

* feat(extensions): add support for offline update

* fix(api): fix issues with offline install and online updates of extensions

* fix(extensions): fix extensions link URL

* fix(extension): hide screenshot in offline mode
This commit is contained in:
Anthony Lapenna 2019-11-20 18:16:40 +13:00 committed by GitHub
parent 8b0eb71d69
commit a85f0058ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 440 additions and 132 deletions

View file

@ -2,7 +2,8 @@ import _ from 'lodash-es';
import { ExtensionViewModel } from '../../models/extension';
angular.module('portainer.app')
.factory('ExtensionService', ['$q', 'Extension', 'StateManager', '$async', function ExtensionServiceFactory($q, Extension, StateManager, $async) {
.factory('ExtensionService', ['$q', 'Extension', 'StateManager', '$async', 'FileUploadService',
function ExtensionServiceFactory($q, Extension, StateManager, $async, FileUploadService) {
'use strict';
var service = {};
@ -20,8 +21,12 @@ angular.module('portainer.app')
service.extensionEnabled = extensionEnabled;
service.retrieveAndSaveEnabledExtensions = retrieveAndSaveEnabledExtensions;
function enable(license) {
return Extension.create({ license: license }).$promise;
function enable(license, extensionFile) {
if (extensionFile) {
return FileUploadService.uploadExtension(license, extensionFile);
} else {
return Extension.create({ license: license }).$promise;
}
}
function update(id, version) {

View file

@ -1,4 +1,4 @@
import { jsonObjectsToArrayHandler, genericHandler } from '../../docker/rest/response/handlers';
import {genericHandler, jsonObjectsToArrayHandler} from '../../docker/rest/response/handlers';
angular.module('portainer.app')
.factory('FileUploadService', ['$q', 'Upload', 'EndpointProvider', function FileUploadFactory($q, Upload, EndpointProvider) {
@ -169,5 +169,18 @@ angular.module('portainer.app')
return $q.all(queue);
};
service.uploadExtension = function(license, extensionFile) {
const payload = {
License: license,
file: extensionFile,
ArchiveFileName: extensionFile.name
};
return Upload.upload({
url: 'api/extensions/upload',
data: payload,
ignoreLoadingBar: true
});
};
return service;
}]);

View file

@ -42,9 +42,23 @@
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">
Ensure that you have a valid license.
</span>
<p class="small text-muted" ng-if="!state.offlineActivation">
Portainer will download the latest version of the extension. Ensure that you have a valid license.
</p>
<p class="small text-muted" ng-if="state.offlineActivation">
You will need to upload the extension archive manually. Ensure that you have a valid license.
</p>
<p class="small text-muted" ng-if="state.offlineActivation">
You can download the latest version of our extensions <a target="_blank" href="https://downloads.portainer.io/extensions.zip">here</a>.
</p>
<p>
<a class="small interactive" ng-if="!state.offlineActivation" ng-click="state.offlineActivation = true;">
<i class="fa fa-toggle-off space-right" aria-hidden="true"></i> Switch to offline activation
</a>
<a class="small interactive" ng-if="state.offlineActivation" ng-click="state.offlineActivation = false;">
<i class="fa fa-wifi space-right" aria-hidden="true"></i> Switch to online activation
</a>
</p>
</div>
</div>
@ -58,14 +72,25 @@
<div class="form-group" ng-show="extensionEnableForm.extension_license.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="extensionEnableForm.extension_license.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
<p ng-message="invalidLicense"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Invalid license format.</p>
</div>
</div>
</div>
<div class="form-group" ng-if="state.offlineActivation">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ngf-select ng-model="formValues.ExtensionFile" style="margin-left: 0px;">Select file</button>
<span style="margin-left: 5px;">
{{ formValues.ExtensionFile.name }}
<i class="fa fa-times red-icon" ng-if="!formValues.ExtensionFile" aria-hidden="true"></i>
</span>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-click="enableExtension()" ng-disabled="state.actionInProgress || !extensionEnableForm.$valid" button-spinner="state.actionInProgress" style="margin-left: 0px;">
<button type="button" class="btn btn-primary btn-sm" ng-click="enableExtension()" ng-disabled="state.actionInProgress || !extensionEnableForm.$valid || (state.offlineActivation && !formValues.ExtensionFile)" button-spinner="state.actionInProgress" style="margin-left: 0px;">
<span ng-hide="state.actionInProgress">Enable extension</span>
<span ng-show="state.actionInProgress">Enabling extension...</span>
</button>

View file

@ -10,7 +10,8 @@ angular.module('portainer.app')
};
$scope.formValues = {
License: ''
License: '',
ExtensionFile: null,
};
function initView() {
@ -25,10 +26,11 @@ angular.module('portainer.app')
}
$scope.enableExtension = function() {
var license = $scope.formValues.License;
const license = $scope.formValues.License;
const extensionFile = $scope.formValues.ExtensionFile;
$scope.state.actionInProgress = true;
ExtensionService.enable(license)
ExtensionService.enable(license, extensionFile)
.then(function onSuccess() {
return ExtensionService.retrieveAndSaveEnabledExtensions();
}).then(function () {

View file

@ -68,10 +68,10 @@
<btn class="btn btn-primary btn-sm" style="width: 100%; margin-left: 0;" disabled>Coming soon</btn>
</div>
<div style="margin-top: 15px;" ng-if="extension.Enabled && extension.UpdateAvailable">
<button type="button" class="btn btn-primary btn-sm" ng-click="updateExtension(extension)" ng-disabled="state.updateInProgress" button-spinner="state.updateInProgress" style="width: 100%; margin-left: 0;">
<span ng-hide="state.updateInProgress">Update</span>
<span ng-show="state.updateInProgress">Updating extension...</span>
<div style="margin-top: 15px;" ng-if="extension.Enabled && extension.UpdateAvailable && !state.offlineUpdate">
<button type="button" class="btn btn-primary btn-sm" ng-click="updateExtensionOnline(extension)" ng-disabled="state.onlineUpdateInProgress" button-spinner="state.onlineUpdateInProgress" style="width: 100%; margin-left: 0;">
<span ng-hide="state.onlineUpdateInProgress">Update via Internet</span>
<span ng-show="state.onlineUpdateInProgress">Updating extension...</span>
</button>
</div>
@ -82,8 +82,18 @@
</button>
</div>
</div>
<div style="margin-top: 15px;" ng-if="extension.Enabled && extension.UpdateAvailable">
<p>
<a class="small interactive" ng-if="!state.offlineUpdate" ng-click="state.offlineUpdate = true;">
<i class="fa fa-toggle-off space-right" aria-hidden="true"></i> Switch to offline update
</a>
<a class="small interactive" ng-if="state.offlineUpdate" ng-click="state.offlineUpdate = false;">
<i class="fa fa-wifi space-right" aria-hidden="true"></i> Switch to online update
</a>
</p>
</div>
</div>
</div>
</div>
</rd-widget-body>
@ -91,6 +101,46 @@
</div>
</div>
<div class="row" ng-if="extension && state.offlineUpdate">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal">
<div class="col-sm-12 form-section-title">
<span>
Offline update
</span>
</div>
<div class="form-group">
<div class="col-sm-12">
<p class="small text-muted" ng-if="state.offlineUpdate">
You will need to upload the extension archive manually. You can download the latest version of our extensions <a target="_blank" href="https://download.portainer.io/extensions.zip">here</a>.
</p>
</div>
</div>
<div class="form-group" ng-if="state.offlineUpdate">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ngf-select ng-model="formValues.ExtensionFile" style="margin-left: 0px;">Select file</button>
<span style="margin-left: 5px;">
{{ formValues.ExtensionFile.name }}
<i class="fa fa-times red-icon" ng-if="!formValues.ExtensionFile" aria-hidden="true"></i>
</span>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-click="updateExtensionOffline(extension)" ng-disabled="state.offlineUpdateInProgress || !formValues.ExtensionFile" button-spinner="state.offlineUpdateInProgress" style="margin-left: 0px;">
<span ng-hide="state.offlineUpdateInProgress">Update extension</span>
<span ng-show="state.offlineUpdateInProgress">Updating extension...</span>
</button>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="extension">
<div class="col-sm-12">
<rd-widget>
@ -107,7 +157,7 @@
<div class="small text-muted">
<p>
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true"></i>
Description for this extension unavailable at the moment.
Unable to provide a description in an offline environment.
</p>
</div>
</div>
@ -116,7 +166,7 @@
</div>
</div>
<div class="row" ng-if="extension">
<div class="row" ng-if="extension.Description && extension.Images">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>

View file

@ -3,15 +3,19 @@ angular.module('portainer.app')
function ($q, $scope, $transition$, $state, ExtensionService, Notifications, ModalService) {
$scope.state = {
updateInProgress: false,
deleteInProgress: false
onlineUpdateInProgress: false,
offlineUpdateInProgress: false,
deleteInProgress: false,
offlineUpdate: false,
};
$scope.formValues = {
instances: 1
instances: 1,
extensionFile: null,
};
$scope.updateExtension = updateExtension;
$scope.updateExtensionOnline = updateExtensionOnline;
$scope.updateExtensionOffline = updateExtensionOffline;
$scope.deleteExtension = deleteExtension;
$scope.enlargeImage = enlargeImage;
@ -24,7 +28,7 @@ function ($q, $scope, $transition$, $state, ExtensionService, Notifications, Mod
ExtensionService.delete(extension.Id)
.then(function onSuccess() {
Notifications.success('Extension successfully deleted');
$state.reload();
$state.go('portainer.extensions');
})
.catch(function onError(err) {
Notifications.error('Failure', err, 'Unable to delete extension');
@ -34,8 +38,8 @@ function ($q, $scope, $transition$, $state, ExtensionService, Notifications, Mod
});
}
function updateExtension(extension) {
$scope.state.updateInProgress = true;
function updateExtensionOnline(extension) {
$scope.state.onlineUpdateInProgress = true;
ExtensionService.update(extension.Id, extension.Version)
.then(function onSuccess() {
Notifications.success('Extension successfully updated');
@ -45,7 +49,24 @@ function ($q, $scope, $transition$, $state, ExtensionService, Notifications, Mod
Notifications.error('Failure', err, 'Unable to update extension');
})
.finally(function final() {
$scope.state.updateInProgress = false;
$scope.state.onlineUpdateInProgress = false;
});
}
function updateExtensionOffline(extension) {
$scope.state.offlineUpdateInProgress = true;
const extensionFile = $scope.formValues.ExtensionFile;
ExtensionService.enable(extension.License.LicenseKey, extensionFile)
.then(function onSuccess() {
Notifications.success('Extension successfully updated');
$state.reload();
})
.catch(function onError(err) {
Notifications.error('Failure', err, 'Unable to update extension');
})
.finally(function final() {
$scope.state.offlineUpdateInProgress = false;
});
}