mirror of https://github.com/portainer/portainer
style(extensions): minor update to extension UX/UI (#2538)
* style(extensions): update extension icons * style(extensions): style update * feat(extensions): update extension UX * style(extensions): update extension style * style(extension-details): update screenshot default size * style(extensions): update overview diagram image * refactor(support): fix support URLspull/2539/head
parent
f222b3cb1a
commit
5c2e714e69
|
@ -35,6 +35,10 @@ func (handler *Handler) extensionInspect(w http.ResponseWriter, r *http.Request)
|
||||||
for _, p := range extensions {
|
for _, p := range extensions {
|
||||||
if p.ID == extensionID {
|
if p.ID == extensionID {
|
||||||
extension = p
|
extension = p
|
||||||
|
if extension.DescriptionURL != "" {
|
||||||
|
description, _ := client.Get(extension.DescriptionURL, 10)
|
||||||
|
extension.Description = string(description)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ func (handler *Handler) extensionList(w http.ResponseWriter, r *http.Request) *h
|
||||||
if storeDetails {
|
if storeDetails {
|
||||||
definitions, err := handler.ExtensionManager.FetchExtensionDefinitions()
|
definitions, err := handler.ExtensionManager.FetchExtensionDefinitions()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extension definitions", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
for idx := range definitions {
|
for idx := range definitions {
|
||||||
|
|
|
@ -485,6 +485,7 @@ type (
|
||||||
Name string `json:"Name,omitempty"`
|
Name string `json:"Name,omitempty"`
|
||||||
ShortDescription string `json:"ShortDescription,omitempty"`
|
ShortDescription string `json:"ShortDescription,omitempty"`
|
||||||
Description string `json:"Description,omitempty"`
|
Description string `json:"Description,omitempty"`
|
||||||
|
DescriptionURL string `json:"DescriptionURL,omitempty"`
|
||||||
Price string `json:"Price,omitempty"`
|
Price string `json:"Price,omitempty"`
|
||||||
PriceDescription string `json:"PriceDescription,omitempty"`
|
PriceDescription string `json:"PriceDescription,omitempty"`
|
||||||
Deal bool `json:"Deal,omitempty"`
|
Deal bool `json:"Deal,omitempty"`
|
||||||
|
@ -492,7 +493,7 @@ type (
|
||||||
License LicenseInformation `json:"License,omitempty"`
|
License LicenseInformation `json:"License,omitempty"`
|
||||||
Version string `json:"Version"`
|
Version string `json:"Version"`
|
||||||
UpdateAvailable bool `json:"UpdateAvailable"`
|
UpdateAvailable bool `json:"UpdateAvailable"`
|
||||||
ProductID int `json:"ProductId,omitempty"`
|
ShopURL string `json:"ShopURL,omitempty"`
|
||||||
Images []string `json:"Images,omitempty"`
|
Images []string `json:"Images,omitempty"`
|
||||||
Logo string `json:"Logo,omitempty"`
|
Logo string `json:"Logo,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -149,8 +149,8 @@
|
||||||
<span ng-show="state.testInProgress">Test in progress...</span>
|
<span ng-show="state.testInProgress">Test in progress...</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.updateInProgress || !state.validConfiguration" ng-click="updateConfiguration()" button-spinner="state.updateInProgress">
|
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.updateInProgress || !state.validConfiguration" ng-click="updateConfiguration()" button-spinner="state.updateInProgress">
|
||||||
<span ng-hide="state.updateInProgress">Update configuration</span>
|
<span ng-hide="state.updateInProgress">Save configuration</span>
|
||||||
<span ng-show="state.updateInProgress">Updating configuration...</span>
|
<span ng-show="state.updateInProgress">Saving configuration...</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -64,8 +64,8 @@
|
||||||
<a ui-sref="portainer.registries.registry.repositories({id: item.Id})" ng-if="$ctrl.registryManagement" class="space-left">
|
<a ui-sref="portainer.registries.registry.repositories({id: item.Id})" ng-if="$ctrl.registryManagement" class="space-left">
|
||||||
<i class="fa fa-search" aria-hidden="true"></i> Browse
|
<i class="fa fa-search" aria-hidden="true"></i> Browse
|
||||||
</a>
|
</a>
|
||||||
<a ui-sref="portainer.extensions.extension({id: 1})" ng-if="!$ctrl.registryManagement" class="space-left">
|
<a ui-sref="portainer.extensions.extension({id: 1})" ng-if="!$ctrl.registryManagement" class="space-left" style="color: #767676" tooltip-append-to-body="true" tooltip-placement="bottom" tooltip-class="portainer-tooltip" uib-tooltip="Feature available via an extension">
|
||||||
<i class="fa fa-search" aria-hidden="true"></i> Browse ( <extension-tooltip></extension-tooltip> )
|
<i class="fa fa-search" aria-hidden="true"></i> Browse (extension)
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
<!-- extension -->
|
<!-- extension -->
|
||||||
<div class="blocklist-item" ng-click="$ctrl.goToExtensionView()">
|
<div class="blocklist-item" ng-click="$ctrl.goToExtensionView()" ng-class="{ 'blocklist-item--disabled': !$ctrl.model.Available }">
|
||||||
<div class="blocklist-item-box">
|
<div class="blocklist-item-box">
|
||||||
<!-- extension-image -->
|
<!-- extension-image -->
|
||||||
<span ng-if="$ctrl.model.Logo">
|
<span ng-if="$ctrl.model.Logo" style="width: 75px; text-align: center;">
|
||||||
<img class="blocklist-item-logo" ng-src="{{ $ctrl.model.Logo }}" />
|
<!-- <img class="blocklist-item-logo" ng-src="{{ $ctrl.model.Logo }}" /> -->
|
||||||
|
<i class="{{ $ctrl.model.Logo }} fa fa-4x blue-icon" aria-hidden="true"></i>
|
||||||
</span>
|
</span>
|
||||||
<span class="blocklist-item-logo" ng-if="!$ctrl.model.Logo">
|
<span class="blocklist-item-logo" ng-if="!$ctrl.model.Logo">
|
||||||
<i class="fa fa-bolt fa-4x blue-icon" style="margin-left: 14px;" aria-hidden="true"></i>
|
<i class="fa fa-bolt fa-4x blue-icon" style="margin-left: 14px;" aria-hidden="true"></i>
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('ExtensionItemController', ['$state',
|
.controller('ExtensionItemController', ['$state',
|
||||||
function ($state) {
|
function($state) {
|
||||||
|
|
||||||
var ctrl = this;
|
var ctrl = this;
|
||||||
ctrl.$onInit = $onInit;
|
ctrl.$onInit = $onInit;
|
||||||
ctrl.goToExtensionView = goToExtensionView;
|
ctrl.goToExtensionView = goToExtensionView;
|
||||||
|
|
||||||
function goToExtensionView() {
|
function goToExtensionView() {
|
||||||
$state.go('portainer.extensions.extension', { id: ctrl.model.Id });
|
if (ctrl.model.Available) {
|
||||||
}
|
$state.go('portainer.extensions.extension', { id: ctrl.model.Id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function $onInit() {
|
function $onInit() {
|
||||||
if (ctrl.currentDate === ctrl.model.License.Expiration) {
|
if (ctrl.currentDate === ctrl.model.License.Expiration) {
|
||||||
ctrl.model.Expired = true;
|
ctrl.model.Expired = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
<i class="fa fa-bolt orange-icon" aria-hidden="true" tooltip-append-to-body="true" tooltip-placement="bottom" tooltip-class="portainer-tooltip" uib-tooltip="Feature available via a plug-in"></i>
|
|
|
@ -1,3 +0,0 @@
|
||||||
angular.module('portainer.app').component('extensionTooltip', {
|
|
||||||
templateUrl: 'app/portainer/components/extension-tooltip/extension-tooltip.html'
|
|
||||||
});
|
|
|
@ -11,7 +11,7 @@ function ExtensionViewModel(data) {
|
||||||
this.License = data.License;
|
this.License = data.License;
|
||||||
this.Version = data.Version;
|
this.Version = data.Version;
|
||||||
this.UpdateAvailable = data.UpdateAvailable;
|
this.UpdateAvailable = data.UpdateAvailable;
|
||||||
this.ProductId = data.ProductId;
|
this.ShopURL = data.ShopURL;
|
||||||
this.Images = data.Images;
|
this.Images = data.Images;
|
||||||
this.Logo = data.Logo;
|
this.Logo = data.Logo;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,54 @@
|
||||||
</rd-header>
|
</rd-header>
|
||||||
|
|
||||||
<information-panel title-text="Information">
|
<information-panel title-text="Information">
|
||||||
<span class="small text-muted">
|
<span class="text-muted" style="font-size: 90%;">
|
||||||
<p>
|
<p>
|
||||||
Content to be defined
|
Portainer CE is a great way of managing clusters, provisioning containers and services and
|
||||||
|
managing container environment lifecycles. To extend the benefit of Portainer CE even
|
||||||
|
more, and to address the needs of larger, complex or critical environments, the Portainer
|
||||||
|
team provides a growing range of low-cost Extensions.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
As the diagram shows, running a successful production container environment requires a
|
||||||
|
range of capability across a number of complex technical areas.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p style="text-align: center; margin: 15px 0 15px 0;">
|
||||||
|
<img src="images/extensions_overview_diagram.png" alt="extensions overview">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Available through a simple subscription process from the menu, Portainer Extensions
|
||||||
|
answers this need and provides a simple way to enhance the functionality that Portainer
|
||||||
|
makes available through incremental capability in important areas.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The vision for Portainer is to be the standard management layer for container platforms. In
|
||||||
|
order to achieve this vision, Portainer CE will be extended across a range of new functional
|
||||||
|
areas. In order to ensure that Portainer remains the best choice for managing production
|
||||||
|
container platforms, the Portainer team have chosen a modular extensible design approach,
|
||||||
|
where additional capability can be added to the Portainer CE core as needed, and at very
|
||||||
|
low cost.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The advantage of an extensible design is clear: While a range of capability is available, only
|
||||||
|
necessary functionality is added as and when needed.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Our first extension is <a ui-sref="portainer.extensions.extension({id: 1})">Registry Manager</a>, available now. Others (such as
|
||||||
|
Single Sign On and Operations Management) are scheduled for the early part of 2019.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Portainer CE is the core of the Portainer management environments. Portainer CE will
|
||||||
|
continue to be developed and made freely available as part of our deep commitment to our
|
||||||
|
Open Source heritage and our user community. Portainer CE will always deliver great
|
||||||
|
functionality and remain the industry standard toolset for managing container-based
|
||||||
|
platforms.
|
||||||
</p>
|
</p>
|
||||||
</span>
|
</span>
|
||||||
</information-panel>
|
</information-panel>
|
||||||
|
@ -61,7 +106,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" ng-if="extensions">
|
<div class="row" ng-if="extensions && extensions.length > 0">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<extension-list
|
<extension-list
|
||||||
current-date="state.currentDate"
|
current-date="state.currentDate"
|
||||||
|
@ -69,3 +114,12 @@
|
||||||
></extension-list>
|
></extension-list>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<information-panel title-text="Error" ng-if="extensions && extensions.length === 0">
|
||||||
|
<span class="small text-muted">
|
||||||
|
<p>
|
||||||
|
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true"></i>
|
||||||
|
Portainer must be connected to the Internet to fetch the list of available extensions.
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
</information-panel>
|
||||||
|
|
|
@ -1,58 +1,59 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('ExtensionsController', ['$scope', '$state', 'ExtensionService', 'Notifications',
|
.controller('ExtensionsController', ['$scope', '$state', 'ExtensionService', 'Notifications',
|
||||||
function ($scope, $state, ExtensionService, Notifications) {
|
function($scope, $state, ExtensionService, Notifications) {
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
actionInProgress: false,
|
actionInProgress: false,
|
||||||
currentDate: moment().format('YYYY-MM-dd')
|
currentDate: moment().format('YYYY-MM-dd')
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.formValues = {
|
$scope.formValues = {
|
||||||
License: ''
|
License: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
ExtensionService.extensions(true)
|
ExtensionService.extensions(true)
|
||||||
.then(function onSuccess(data) {
|
.then(function onSuccess(data) {
|
||||||
$scope.extensions = data;
|
$scope.extensions = data;
|
||||||
})
|
})
|
||||||
.catch(function onError(err) {
|
.catch(function onError(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to access extension store');
|
$scope.extensions = [];
|
||||||
});
|
Notifications.error('Failure', err, 'Unable to access extension store');
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$scope.enableExtension = function() {
|
$scope.enableExtension = function() {
|
||||||
var license = $scope.formValues.License;
|
var license = $scope.formValues.License;
|
||||||
|
|
||||||
$scope.state.actionInProgress = true;
|
$scope.state.actionInProgress = true;
|
||||||
ExtensionService.enable(license)
|
ExtensionService.enable(license)
|
||||||
.then(function onSuccess() {
|
.then(function onSuccess() {
|
||||||
Notifications.success('Extension successfully enabled');
|
Notifications.success('Extension successfully enabled');
|
||||||
$state.reload();
|
$state.reload();
|
||||||
})
|
})
|
||||||
.catch(function onError(err) {
|
.catch(function onError(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to enable extension');
|
Notifications.error('Failure', err, 'Unable to enable extension');
|
||||||
})
|
})
|
||||||
.finally(function final() {
|
.finally(function final() {
|
||||||
$scope.state.actionInProgress = false;
|
$scope.state.actionInProgress = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
$scope.isValidLicenseFormat = function(form) {
|
$scope.isValidLicenseFormat = function(form) {
|
||||||
var valid = true;
|
var valid = true;
|
||||||
|
|
||||||
if (!$scope.formValues.License) {
|
if (!$scope.formValues.License) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNaN($scope.formValues.License[0])) {
|
if (isNaN($scope.formValues.License[0])) {
|
||||||
valid = false;
|
valid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
form.extension_license.$setValidity('invalidLicense', valid);
|
form.extension_license.$setValidity('invalidLicense', valid);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
initView();
|
initView();
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -51,11 +51,17 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 15px;" ng-if="!extension.Enabled && extension.Available">
|
<div style="margin-top: 15px;" ng-if="!extension.Enabled && extension.Available">
|
||||||
<a href="https://2-portainer.pi.bypronto.com/checkout/?add-to-cart={{ extension.ProductId }}&quantity={{ formValues.instances }}" target="_blank" class="btn btn-primary btn-sm" style="width: 100%; margin-left: 0;">
|
<a href="{{ extension.ShopURL }}&quantity={{ formValues.instances }}" target="_blank" class="btn btn-primary btn-sm" style="width: 100%; margin-left: 0;">
|
||||||
Buy
|
Buy
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 10px;" ng-if="!extension.Enabled && extension.Available">
|
||||||
|
<a ui-sref="portainer.extensions" class="btn btn-primary btn-sm" style="width: 100%; margin-left: 0;">
|
||||||
|
Add license key
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 15px;" ng-if="!extension.Enabled && !extension.Available">
|
<div style="margin-top: 15px;" ng-if="!extension.Enabled && !extension.Available">
|
||||||
<btn class="btn btn-primary btn-sm" style="width: 100%; margin-left: 0;" disabled>Coming soon</btn>
|
<btn class="btn btn-primary btn-sm" style="width: 100%; margin-left: 0;" disabled>Coming soon</btn>
|
||||||
</div>
|
</div>
|
||||||
|
@ -92,10 +98,16 @@
|
||||||
Description
|
Description
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group" ng-if="extension.Description">
|
||||||
<span class="small text-muted">
|
<div class="text-muted" style="font-size: 90%;" ng-bind-html="extension.Description"></div>
|
||||||
{{ extension.Description }}
|
</div>
|
||||||
</span>
|
<div class="form-group" ng-if="!extension.Description">
|
||||||
|
<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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</rd-widget-body>
|
</rd-widget-body>
|
||||||
</rd-widget>
|
</rd-widget>
|
||||||
|
@ -113,7 +125,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align: center;">
|
<div style="text-align: center;">
|
||||||
<div ng-repeat="image in extension.Images" style="margin-top: 25px; cursor: zoom-in;">
|
<div ng-repeat="image in extension.Images" style="margin-top: 25px; cursor: zoom-in;">
|
||||||
<img ng-src="{{image}}" ng-click="enlargeImage(image)"/>
|
<img ng-src="{{image}}" ng-click="enlargeImage(image)" style="max-width: 1024px;" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</rd-widget-body>
|
</rd-widget-body>
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 15px;" ng-disabled="!formValues.hostCount">
|
<div style="margin-top: 15px;" ng-disabled="!formValues.hostCount">
|
||||||
<a href="https://2-portainer.pi.bypronto.com/checkout/?add-to-cart={{ product.ProductId }}&quantity={{ formValues.hostCount }}" target="_blank" class="btn btn-primary btn-sm" style="width: 100%; margin-left: 0;">
|
<a href="https://portainer.io/checkout/?add-to-cart={{ product.ProductId }}&quantity={{ formValues.hostCount }}" target="_blank" class="btn btn-primary btn-sm" style="width: 100%; margin-left: 0;">
|
||||||
Buy
|
Buy
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -174,6 +174,11 @@ a[ng-click]{
|
||||||
box-shadow: 0 3px 10px -2px rgba(161, 170, 166, 0.5);
|
box-shadow: 0 3px 10px -2px rgba(161, 170, 166, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.blocklist-item--disabled {
|
||||||
|
cursor: auto;
|
||||||
|
background-color: #ececec;
|
||||||
|
}
|
||||||
|
|
||||||
.blocklist-item--selected {
|
.blocklist-item--selected {
|
||||||
border: 2px solid #bbbbbb;
|
border: 2px solid #bbbbbb;
|
||||||
background-color: #ececec;
|
background-color: #ececec;
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 79 KiB |
Loading…
Reference in New Issue