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:
parent
8b0eb71d69
commit
a85f0058ee
17 changed files with 440 additions and 132 deletions
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}]);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue