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

feat(extensions): introduce extension support (#2527)

* wip

* wip: missing repository & tags removal

* feat(registry): private registry management

* style(plugin-details): update view

* wip

* wip

* wip

* feat(plugins): add license info

* feat(plugins): browse feature preview

* feat(registry-configure): add the ability to configure registry management

* style(app): update text in app

* feat(plugins): add plugin version number

* feat(plugins): wip plugin upgrade process

* feat(plugins): wip plugin upgrade

* feat(plugins): add the ability to update a plugin

* feat(plugins): init plugins at startup time

* feat(plugins): add the ability to remove a plugin

* feat(plugins): update to latest plugin definitions

* feat(plugins): introduce plugin-tooltip component

* refactor(app): relocate plugin files to app/plugins

* feat(plugins): introduce PluginDefinitionsURL constant

* feat(plugins): update the flags used by the plugins

* feat(plugins): wip

* feat(plugins): display a label when a plugin has expired

* wip

* feat(registry-creation): update registry creation logic

* refactor(registry-creation): change name/ids for inputs

* feat(api): pass registry type to management configuration

* feat(api): unstrip /v2 in regsitry proxy

* docs(api): add TODO

* feat(store): mockup-1

* feat(store): mockup 2

* feat(store): mockup 2

* feat(store): update mockup-2

* feat(app): add unauthenticated event check

* update gruntfile

* style(support): update support views

* style(support): update product views

* refactor(extensions): refactor plugins to extensions

* feat(extensions): add a deal property

* feat(extensions): introduce ExtensionManager

* style(extensions): update extension details style

* feat(extensions): display license/company when enabling extension

* feat(extensions): update extensions views

* feat(extensions): use ProductId defined in extension schema

* style(app): remove padding left for form section title elements

* style(support): use per host model

* refactor(extensions): multiple refactors related to extensions mecanism

* feat(extensions): update tls file path for registry extension

* feat(extensions): update registry management configuration

* feat(extensions): send license in header to extension proxy

* fix(proxy): fix invalid default loopback address

* feat(extensions): add header X-RegistryManagement-ForceNew for specific operations

* feat(extensions): add the ability to display screenshots

* feat(extensions): center screenshots

* style(extensions): tune style

* feat(extensions-details): open full screen image on click (#2517)

* feat(extension-details): show magnifying glass on images

* feat(extensions): support extension logo

* feat(extensions): update support logos

* refactor(lint): fix lint issues
This commit is contained in:
Anthony Lapenna 2018-12-09 16:49:27 +13:00 committed by GitHub
parent f5dc663879
commit 6fd5ddc802
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
100 changed files with 3519 additions and 268 deletions

View file

@ -198,6 +198,28 @@ angular.module('portainer.app', [])
}
};
var extensions = {
name: 'portainer.extensions',
url: '/extensions',
views: {
'content@': {
templateUrl: 'app/portainer/views/extensions/extensions.html',
controller: 'ExtensionsController'
}
}
};
var extension = {
name: 'portainer.extensions.extension',
url: '/extension/:id',
views: {
'content@': {
templateUrl: 'app/portainer/views/extensions/inspect/extension.html',
controller: 'ExtensionController'
}
}
};
var registries = {
name: 'portainer.registries',
url: '/registries',
@ -335,7 +357,22 @@ angular.module('portainer.app', [])
url: '/support',
views: {
'content@': {
templateUrl: 'app/portainer/views/support/support.html'
templateUrl: 'app/portainer/views/support/support.html',
controller: 'SupportController'
}
},
params: {
product: {}
}
};
var supportProduct = {
name: 'portainer.support.product',
url: '/product',
views: {
'content@': {
templateUrl: 'app/portainer/views/support/product/product.html',
controller: 'SupportProductController'
}
}
};
@ -457,6 +494,8 @@ angular.module('portainer.app', [])
$stateRegistryProvider.register(init);
$stateRegistryProvider.register(initEndpoint);
$stateRegistryProvider.register(initAdmin);
$stateRegistryProvider.register(extensions);
$stateRegistryProvider.register(extension);
$stateRegistryProvider.register(registries);
$stateRegistryProvider.register(registry);
$stateRegistryProvider.register(registryAccess);
@ -470,6 +509,7 @@ angular.module('portainer.app', [])
$stateRegistryProvider.register(stack);
$stateRegistryProvider.register(stackCreation);
$stateRegistryProvider.register(support);
$stateRegistryProvider.register(supportProduct);
$stateRegistryProvider.register(tags);
$stateRegistryProvider.register(updatePassword);
$stateRegistryProvider.register(users);

View file

@ -61,6 +61,12 @@
<a ui-sref="portainer.registries.registry.access({id: item.Id})" ng-if="$ctrl.accessManagement">
<i class="fa fa-users" aria-hidden="true"></i> Manage access
</a>
<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
</a>
<a ui-sref="portainer.extensions.extension({id: 1})" ng-if="!$ctrl.registryManagement" class="space-left">
<i class="fa fa-search" aria-hidden="true"></i> Browse ( <extension-tooltip></extension-tooltip> )
</a>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">

View file

@ -9,6 +9,7 @@ angular.module('portainer.app').component('registriesDatatable', {
orderBy: '@',
reverseOrder: '<',
accessManagement: '<',
removeAction: '<'
removeAction: '<',
registryManagement: '<'
}
});

View file

@ -0,0 +1,8 @@
angular.module('portainer.app').component('extensionItem', {
templateUrl: 'app/portainer/components/extension-list/extension-item/extensionItem.html',
controller: 'ExtensionItemController',
bindings: {
model: '<',
currentDate: '<'
}
});

View file

@ -0,0 +1,46 @@
<!-- extension -->
<div class="blocklist-item" ng-click="$ctrl.goToExtensionView()">
<div class="blocklist-item-box">
<!-- extension-image -->
<span ng-if="$ctrl.model.Logo">
<img class="blocklist-item-logo" ng-src="{{ $ctrl.model.Logo }}" />
</span>
<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>
</span>
<!-- !extension-image -->
<!-- extension-details -->
<span class="col-sm-12">
<!-- blocklist-item-line1 -->
<div class="blocklist-item-line">
<span>
<span class="blocklist-item-title">
{{ $ctrl.model.Name }}
</span>
</span>
<span>
<span class="label label-primary" ng-if="!$ctrl.model.Enabled && !$ctrl.model.Available">coming soon</span>
<span class="label label-warning" ng-if="!$ctrl.model.Enabled && $ctrl.model.Deal">deal</span>
<span class="label label-danger" ng-if="$ctrl.model.Enabled && $ctrl.model.Expired">expired</span>
<span class="label label-success" ng-if="$ctrl.model.Enabled && !$ctrl.model.Expired">enabled</span>
<span class="label label-primary" ng-if="$ctrl.model.Enabled && $ctrl.model.UpdateAvailable && !$ctrl.model.Expired">update available</span>
</span>
</div>
<!-- !blocklist-item-line1 -->
<!-- blocklist-item-line2 -->
<div class="blocklist-item-line">
<span>
<span class="blocklist-item-desc">
{{ $ctrl.model.ShortDescription }}
</span>
</span>
<span ng-if="$ctrl.model.License.Company">
<span class="small text-muted">Licensed to {{ $ctrl.model.License.Company }} - Expires on {{ $ctrl.model.License.Expiration }}</span>
</span>
</div>
<!-- !blocklist-item-line2 -->
</span>
<!-- !extension-details -->
</div>
<!-- !extension -->
</div>

View file

@ -0,0 +1,18 @@
angular.module('portainer.app')
.controller('ExtensionItemController', ['$state',
function ($state) {
var ctrl = this;
ctrl.$onInit = $onInit;
ctrl.goToExtensionView = goToExtensionView;
function goToExtensionView() {
$state.go('portainer.extensions.extension', { id: ctrl.model.Id });
}
function $onInit() {
if (ctrl.currentDate === ctrl.model.License.Expiration) {
ctrl.model.Expired = true;
}
}
}]);

View file

@ -0,0 +1,7 @@
angular.module('portainer.app').component('extensionList', {
templateUrl: 'app/portainer/components/extension-list/extensionList.html',
bindings: {
extensions: '<',
currentDate: '<'
}
});

View file

@ -0,0 +1,20 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa fa-bolt" aria-hidden="true" style="margin-right: 2px;"></i> Available extensions
</div>
</div>
<div class="blocklist">
<extension-item ng-repeat="extension in $ctrl.extensions"
model="extension"
current-date="$ctrl.currentDate"
></extension-item>
</div>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -0,0 +1 @@
<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>

View file

@ -0,0 +1,3 @@
angular.module('portainer.app').component('extensionTooltip', {
templateUrl: 'app/portainer/components/extension-tooltip/extension-tooltip.html'
});

View file

@ -0,0 +1,81 @@
<form class="form-horizontal" name="registryFormAzure" ng-submit="$ctrl.formAction()">
<div class="col-sm-12 form-section-title">
Azure registry details
</div>
<!-- name-input -->
<div class="form-group">
<label for="registry_name" class="col-sm-3 col-lg-2 control-label text-left">Name</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="registry_name" name="registry_name" ng-model="$ctrl.model.Name" placeholder="my-azure-registry" required auto-focus>
</div>
</div>
<div class="form-group" ng-show="registryFormAzure.registry_name.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormAzure.registry_name.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !name-input -->
<!-- url-input -->
<div class="form-group" ng-if="$ctrl.model.Type === 2 || $ctrl.model.Type === 3">
<label for="registry_url" class="col-sm-3 col-lg-2 control-label text-left">
Registry URL
<portainer-tooltip position="bottom" message="URL of an Azure Container Registry. Any protocol will be stripped."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="registry_url" name="registry_url" ng-model="$ctrl.model.URL" placeholder="myproject.azurecr.io" required>
</div>
</div>
<div class="form-group" ng-show="registryFormAzure.registry_url.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormAzure.registry_url.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- url-input -->
<!-- credentials-user -->
<div class="form-group">
<label for="registry_username" class="col-sm-3 col-lg-2 control-label text-left">Username</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="registry_username" name="registry_username" ng-model="$ctrl.model.Username" required>
</div>
</div>
<div class="form-group" ng-show="registryFormAzure.registry_username.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormAzure.registry_username.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !credentials-user -->
<!-- credentials-password -->
<div class="form-group">
<label for="registry_password" class="col-sm-3 col-lg-2 control-label text-left">Password</label>
<div class="col-sm-9 col-lg-10">
<input type="password" class="form-control" id="registry_password" name="registry_password" ng-model="$ctrl.model.Password" required>
</div>
</div>
<div class="form-group" ng-show="registryFormAzure.registry_password.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormAzure.registry_password.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !credentials-password -->
<!-- actions -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-sm" ng-click="$ctrl.formAction()" ng-disabled="$ctrl.actionInProgress || !registryFormAzure.$valid" button-spinner="$ctrl.actionInProgress">
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
<span ng-show="$ctrl.actionInProgress">In progress...</span>
</button>
</div>
</div>
<!-- !actions -->
</form>

View file

@ -0,0 +1,9 @@
angular.module('portainer.app').component('registryFormAzure', {
templateUrl: 'app/portainer/components/forms/registry-form-azure/registry-form-azure.html',
bindings: {
model: '=',
formAction: '<',
formActionLabel: '@',
actionInProgress: '<'
}
});

View file

@ -0,0 +1,105 @@
<form class="form-horizontal" name="registryFormCustom" ng-submit="$ctrl.formAction()">
<div class="col-sm-12 form-section-title">
Important notice
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
Docker requires you to connect to a <a href="https://docs.docker.com/registry/deploying/#running-a-domain-registry" target="_blank">secure registry</a>.
You can find more information about how to connect to an insecure registry <a href="https://docs.docker.com/registry/insecure/" target="_blank">in the Docker documentation</a>.
</span>
</div>
<div class="col-sm-12 form-section-title">
Custom registry details
</div>
<!-- name-input -->
<div class="form-group">
<label for="registry_name" class="col-sm-3 col-lg-2 control-label text-left">Name</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="registry_name" name="registry_name" ng-model="$ctrl.model.Name" placeholder="my-custom-registry" required auto-focus>
</div>
</div>
<div class="form-group" ng-show="registryFormCustom.registry_name.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormCustom.registry_name.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !name-input -->
<!-- url-input -->
<div class="form-group" ng-if="$ctrl.model.Type === 2 || $ctrl.model.Type === 3">
<label for="registry_url" class="col-sm-3 col-lg-2 control-label text-left">
Registry URL
<portainer-tooltip position="bottom" message="URL or IP address of a Docker registry. Any protocol will be stripped."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="registry_url" name="registry_url" ng-model="$ctrl.model.URL" placeholder="10.0.0.10:5000 or myregistry.domain.tld" required>
</div>
</div>
<div class="form-group" ng-show="registryFormCustom.registry_url.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormCustom.registry_url.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- url-input -->
<!-- authentication-checkbox -->
<div class="form-group">
<div class="col-sm-12">
<label for="registry_auth" class="control-label text-left">
Authentication
<portainer-tooltip position="bottom" message="Enable this option if you need to specify credentials to connect to this registry."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="$ctrl.model.Authentication"><i></i>
</label>
</div>
</div>
<!-- !authentication-checkbox -->
<div ng-if="$ctrl.model.Authentication">
<!-- credentials-user -->
<div class="form-group">
<label for="registry_username" class="col-sm-3 col-lg-2 control-label text-left">Username</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="registry_username" name="registry_username" ng-model="$ctrl.model.Username" required>
</div>
</div>
<div class="form-group" ng-show="registryFormCustom.registry_username.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormCustom.registry_username.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !credentials-user -->
<!-- credentials-password -->
<div class="form-group">
<label for="registry_password" class="col-sm-3 col-lg-2 control-label text-left">Password</label>
<div class="col-sm-9 col-lg-10">
<input type="password" class="form-control" id="registry_password" name="registry_password" ng-model="$ctrl.model.Password" required>
</div>
</div>
<div class="form-group" ng-show="registryFormCustom.registry_password.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormCustom.registry_password.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !credentials-password -->
</div>
<!-- actions -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-sm" ng-click="$ctrl.formAction()" ng-disabled="$ctrl.actionInProgress || !registryFormCustom.$valid" button-spinner="$ctrl.actionInProgress">
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
<span ng-show="$ctrl.actionInProgress">In progress...</span>
</button>
</div>
</div>
<!-- !actions -->
</form>

View file

@ -0,0 +1,9 @@
angular.module('portainer.app').component('registryFormCustom', {
templateUrl: 'app/portainer/components/forms/registry-form-custom/registry-form-custom.html',
bindings: {
model: '=',
formAction: '<',
formActionLabel: '@',
actionInProgress: '<'
}
});

View file

@ -0,0 +1,48 @@
<form class="form-horizontal" name="registryFormQuay" ng-submit="$ctrl.formAction()">
<div class="col-sm-12 form-section-title">
Quay account details
</div>
<!-- credentials-user -->
<div class="form-group">
<label for="registry_username" class="col-sm-3 col-lg-2 control-label text-left">Username</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="registry_username" name="registry_username" ng-model="$ctrl.model.Username" required auto-focus>
</div>
</div>
<div class="form-group" ng-show="registryFormQuay.registry_username.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormQuay.registry_username.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !credentials-user -->
<!-- credentials-password -->
<div class="form-group">
<label for="registry_password" class="col-sm-3 col-lg-2 control-label text-left">Password</label>
<div class="col-sm-9 col-lg-10">
<input type="password" class="form-control" id="registry_password" name="registry_password" ng-model="$ctrl.model.Password" required>
</div>
</div>
<div class="form-group" ng-show="registryFormQuay.registry_password.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="registryFormQuay.registry_password.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<!-- !credentials-password -->
<!-- actions -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-sm" ng-click="$ctrl.formAction()" ng-disabled="$ctrl.actionInProgress || !registryFormQuay.$valid" button-spinner="$ctrl.actionInProgress">
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
<span ng-show="$ctrl.actionInProgress">In progress...</span>
</button>
</div>
</div>
<!-- !actions -->
</form>

View file

@ -0,0 +1,9 @@
angular.module('portainer.app').component('registryFormQuay', {
templateUrl: 'app/portainer/components/forms/registry-form-quay/registry-form-quay.html',
bindings: {
model: '=',
formAction: '<',
formActionLabel: '@',
actionInProgress: '<'
}
});

View file

@ -0,0 +1,9 @@
angular.module('portainer.app').component('productItem', {
templateUrl: 'app/portainer/components/product-list/product-item/productItem.html',
controller: 'ProductItemController',
bindings: {
model: '<',
currentDate: '<',
goTo: '<'
}
});

View file

@ -0,0 +1,41 @@
<!-- extension -->
<div class="blocklist-item" ng-click="$ctrl.goTo($ctrl.model)">
<div class="blocklist-item-box">
<!-- extension-image -->
<span class="blocklist-item-logo">
<img class="blocklist-item-logo" src="images/support_{{ $ctrl.model.Id }}.png" />
</span>
<!-- !extension-image -->
<!-- extension-details -->
<span class="col-sm-12">
<!-- blocklist-item-line1 -->
<div class="blocklist-item-line">
<span>
<span class="blocklist-item-title">
{{ $ctrl.model.Name }}
</span>
</span>
<span>
<span class="label label-danger" ng-if="$ctrl.model.Enabled && $ctrl.model.Expired">expired</span>
<span class="label label-success" ng-if="$ctrl.model.Enabled && !$ctrl.model.Expired">enabled</span>
<span class="label label-primary" ng-if="$ctrl.model.Enabled && $ctrl.model.UpdateAvailable && !$ctrl.model.Expired">update available</span>
</span>
</div>
<!-- !blocklist-item-line1 -->
<!-- blocklist-item-line2 -->
<div class="blocklist-item-line">
<span>
<span class="blocklist-item-desc">
{{ $ctrl.model.ShortDescription }}
</span>
</span>
<span ng-if="$ctrl.model.License.Company">
<span class="small text-muted">Licensed to {{ $ctrl.model.License.Company }} - Expires on {{ $ctrl.model.License.Expiration }}</span>
</span>
</div>
<!-- !blocklist-item-line2 -->
</span>
<!-- !extension-details -->
</div>
<!-- !extension -->
</div>

View file

@ -0,0 +1,18 @@
angular.module('portainer.app')
.controller('ProductItemController', ['$state',
function ($state) {
var ctrl = this;
ctrl.$onInit = $onInit;
ctrl.goToExtensionView = goToExtensionView;
function goToExtensionView() {
$state.go('portainer.extensions.extension', { id: ctrl.model.Id });
}
function $onInit() {
if (ctrl.currentDate === ctrl.model.License.Expiration) {
ctrl.model.Expired = true;
}
}
}]);

View file

@ -0,0 +1,10 @@
angular.module('portainer.app').component('productList', {
templateUrl: 'app/portainer/components/product-list/productList.html',
bindings: {
titleText: '@',
products: '<',
goTo: '<'
// extensions: '<',
// currentDate: '<'
}
});

View file

@ -0,0 +1,21 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa fa-bolt" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
</div>
</div>
<div class="blocklist">
<product-item ng-repeat="product in $ctrl.products"
model="product"
current-date="$ctrl.currentDate"
go-to="$ctrl.goTo"
></product-item>
</div>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -0,0 +1,17 @@
function ExtensionViewModel(data) {
this.Id = data.Id;
this.Name = data.Name;
this.Enabled = data.Enabled;
this.Description = data.Description;
this.Price = data.Price;
this.PriceDescription = data.PriceDescription;
this.Available = data.Available;
this.Deal = data.Deal;
this.ShortDescription = data.ShortDescription;
this.License = data.License;
this.Version = data.Version;
this.UpdateAvailable = data.UpdateAvailable;
this.ProductId = data.ProductId;
this.Images = data.Images;
this.Logo = data.Logo;
}

View file

@ -1,5 +1,6 @@
function RegistryViewModel(data) {
this.Id = data.Id;
this.Type = data.Type;
this.Name = data.Name;
this.URL = data.URL;
this.Authentication = data.Authentication;
@ -9,3 +10,44 @@ function RegistryViewModel(data) {
this.AuthorizedTeams = data.AuthorizedTeams;
this.Checked = false;
}
function RegistryManagementConfigurationDefaultModel(registry) {
this.Authentication = false;
this.Password = '';
this.TLS = false;
this.TLSSkipVerify = false;
this.TLSCACertFile = null;
this.TLSCertFile = null;
this.TLSKeyFile = null;
if (registry.Type === 1 || registry.Type === 2 ) {
this.Authentication = true;
this.Username = registry.Username;
this.TLS = true;
}
if (registry.Type === 3 && registry.Authentication) {
this.Authentication = true;
this.Username = registry.Username;
}
}
function RegistryDefaultModel() {
this.Type = 3;
this.URL = '';
this.Name = '';
this.Authentication = false;
this.Username = '';
this.Password = '';
}
function RegistryCreateRequest(model) {
this.Name = model.Name;
this.Type = model.Type;
this.URL = model.URL;
this.Authentication = model.Authentication;
if (model.Authentication) {
this.Username = model.Username;
this.Password = model.Password;
}
}

View file

@ -4,4 +4,5 @@ function StatusViewModel(data) {
this.EndpointManagement = data.EndpointManagement;
this.Analytics = data.Analytics;
this.Version = data.Version;
this.EnabledExtensions = data.EnabledExtensions;
}

View file

@ -1,11 +1,12 @@
angular.module('portainer.app')
.factory('Extensions', ['$resource', 'EndpointProvider', 'API_ENDPOINT_ENDPOINTS', function Extensions($resource, EndpointProvider, API_ENDPOINT_ENDPOINTS) {
.factory('Extension', ['$resource', 'API_ENDPOINT_EXTENSIONS',
function ExtensionFactory($resource, API_ENDPOINT_EXTENSIONS) {
'use strict';
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/extensions/:type', {
endpointId: EndpointProvider.endpointID
},
{
register: { method: 'POST' },
deregister: { method: 'DELETE', params: { type: '@type' } }
return $resource(API_ENDPOINT_EXTENSIONS + '/:id/:action', {}, {
create: { method: 'POST' },
query: { method: 'GET', isArray: true },
get: { method: 'GET', params: { id: '@id' } },
delete: { method: 'DELETE', params: { id: '@id' } },
update: { method: 'POST', params: { id: '@id', action: 'update' } }
});
}]);

View file

@ -0,0 +1,12 @@
// TODO: legacy extension management
angular.module('portainer.app')
.factory('LegacyExtensions', ['$resource', 'EndpointProvider', 'API_ENDPOINT_ENDPOINTS', function LegacyExtensions($resource, EndpointProvider, API_ENDPOINT_ENDPOINTS) {
'use strict';
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/extensions/:type', {
endpointId: EndpointProvider.endpointID
},
{
register: { method: 'POST' },
deregister: { method: 'DELETE', params: { type: '@type' } }
});
}]);

View file

@ -7,6 +7,7 @@ angular.module('portainer.app')
get: { method: 'GET', params: { id: '@id' } },
update: { method: 'PUT', params: { id: '@id' } },
updateAccess: { method: 'PUT', params: { id: '@id', action: 'access' } },
remove: { method: 'DELETE', params: { id: '@id'} }
remove: { method: 'DELETE', params: { id: '@id'} },
configure: { method: 'POST', params: { id: '@id', action: 'configure' } }
});
}]);

View file

@ -1,19 +1,65 @@
angular.module('portainer.app')
.factory('ExtensionService', ['Extensions', function ExtensionServiceFactory(Extensions) {
.factory('ExtensionService', ['$q', 'Extension', function ExtensionServiceFactory($q, Extension) {
'use strict';
var service = {};
service.registerStoridgeExtension = function(url) {
var payload = {
Type: 1,
URL: url
};
return Extensions.register(payload).$promise;
service.enable = function(license) {
return Extension.create({ license: license }).$promise;
};
service.deregisterStoridgeExtension = function() {
return Extensions.deregister({ type: 1 }).$promise;
service.update = function(id, version) {
return Extension.update({ id: id, version: version }).$promise;
};
service.delete = function(id) {
return Extension.delete({ id: id }).$promise;
};
service.extensions = function(store) {
var deferred = $q.defer();
Extension.query({ store: store }).$promise
.then(function success(data) {
var extensions = data.map(function (item) {
return new ExtensionViewModel(item);
});
deferred.resolve(extensions);
})
.catch(function error(err) {
deferred.reject({msg: 'Unable to retrieve extensions', err: err});
});
return deferred.promise;
};
service.extension = function(id) {
var deferred = $q.defer();
Extension.get({ id: id }).$promise
.then(function success(data) {
var extension = new ExtensionViewModel(data);
deferred.resolve(extension);
})
.catch(function error(err) {
deferred.reject({msg: 'Unable to retrieve extension details', err: err});
});
return deferred.promise;
};
service.registryManagementEnabled = function() {
var deferred = $q.defer();
service.extensions(false)
.then(function onSuccess(extensions) {
var extensionAvailable = _.find(extensions, { Id: 1, Enabled: true }) ? true : false;
deferred.resolve(extensionAvailable);
})
.catch(function onError(err) {
deferred.reject(err);
});
return deferred.promise;
};
return service;

View file

@ -0,0 +1,21 @@
// TODO: legacy extension management
angular.module('portainer.app')
.factory('LegacyExtensionService', ['LegacyExtensions', function LegacyExtensionServiceFactory(LegacyExtensions) {
'use strict';
var service = {};
service.registerStoridgeExtension = function(url) {
var payload = {
Type: 1,
URL: url
};
return LegacyExtensions.register(payload).$promise;
};
service.deregisterStoridgeExtension = function() {
return LegacyExtensions.deregister({ type: 1 }).$promise;
};
return service;
}]);

View file

@ -1,5 +1,5 @@
angular.module('portainer.app')
.factory('RegistryService', ['$q', 'Registries', 'DockerHubService', 'RegistryHelper', 'ImageHelper', function RegistryServiceFactory($q, Registries, DockerHubService, RegistryHelper, ImageHelper) {
.factory('RegistryService', ['$q', 'Registries', 'DockerHubService', 'RegistryHelper', 'ImageHelper', 'FileUploadService', function RegistryServiceFactory($q, Registries, DockerHubService, RegistryHelper, ImageHelper, FileUploadService) {
'use strict';
var service = {};
@ -54,17 +54,13 @@ angular.module('portainer.app')
return Registries.update({ id: registry.Id }, registry).$promise;
};
service.createRegistry = function(name, URL, authentication, username, password) {
var payload = {
Name: name,
URL: URL,
Authentication: authentication
};
if (authentication) {
payload.Username = username;
payload.Password = password;
}
return Registries.create({}, payload).$promise;
service.configureRegistry = function(id, registryManagementConfigurationModel) {
return FileUploadService.configureRegistry(id, registryManagementConfigurationModel);
};
service.createRegistry = function(model) {
var payload = new RegistryCreateRequest(model);
return Registries.create(payload).$promise;
};
service.retrieveRegistryFromRepository = function(repository) {

View file

@ -80,6 +80,13 @@ angular.module('portainer.app')
});
};
service.configureRegistry = function(registryId, registryManagementConfigurationModel) {
return Upload.upload({
url: 'api/registries/' + registryId + '/configure',
data: registryManagementConfigurationModel
});
};
service.executeEndpointJob = function (imageName, file, endpointId, nodeName) {
return Upload.upload({
url: 'api/endpoints/' + endpointId + '/job?method=file&nodeName=' + nodeName,

View file

@ -1,6 +1,7 @@
// TODO: legacy extension management
angular.module('portainer.app')
.factory('ExtensionManager', ['$q', 'PluginService', 'SystemService', 'ExtensionService',
function ExtensionManagerFactory($q, PluginService, SystemService, ExtensionService) {
.factory('LegacyExtensionManager', ['$q', 'PluginService', 'SystemService', 'LegacyExtensionService',
function ExtensionManagerFactory($q, PluginService, SystemService, LegacyExtensionService) {
'use strict';
var service = {};
@ -60,7 +61,7 @@ function ExtensionManagerFactory($q, PluginService, SystemService, ExtensionServ
.then(function success(data) {
var managerIP = data.Swarm.NodeAddr;
var storidgeAPIURL = 'tcp://' + managerIP + ':8282';
return ExtensionService.registerStoridgeExtension(storidgeAPIURL);
return LegacyExtensionService.registerStoridgeExtension(storidgeAPIURL);
})
.then(function success(data) {
deferred.resolve(data);
@ -73,7 +74,7 @@ function ExtensionManagerFactory($q, PluginService, SystemService, ExtensionServ
}
function deregisterStoridgeExtension() {
return ExtensionService.deregisterStoridgeExtension();
return LegacyExtensionService.deregisterStoridgeExtension();
}
return service;

View file

@ -25,6 +25,14 @@ angular.module('portainer.app')
return buttons;
};
service.enlargeImage = function(image) {
bootbox.dialog({
message: '<img src="' + image + '" style="width:100%" />',
className: 'image-zoom-modal',
onEscape: true
});
};
service.confirm = function(options){
var box = bootbox.confirm({
title: options.title,

View file

@ -0,0 +1,71 @@
<rd-header>
<rd-header-title title-text="Extensions"></rd-header-title>
<rd-header-content>Portainer extensions</rd-header-content>
</rd-header>
<information-panel title-text="Information">
<span class="small text-muted">
<p>
Content to be defined
</p>
</span>
</information-panel>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal" name="extensionEnableForm">
<div class="col-sm-12 form-section-title">
Enable extension
</div>
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">
Ensure that you have a valid license.
</span>
</div>
</div>
<div class="form-group">
<label for="extension_license" class="col-sm-2 control-label text-left">License</label>
<div class="col-sm-10">
<input type="text" name="extension_license" class="form-control" ng-model="formValues.License" ng-change="isValidLicenseFormat(extensionEnableForm)" required placeholder="Enter a license key here">
</div>
</div>
<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="invalidLicense"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Invalid license format.</p>
</div>
</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;">
<span ng-hide="state.actionInProgress">Enable extension</span>
<span ng-show="state.actionInProgress">Enabling extension...</span>
</button>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="extensions">
<div class="col-sm-12">
<extension-list
current-date="state.currentDate"
extensions="extensions"
></extension-list>
</div>
</div>

View file

@ -0,0 +1,58 @@
angular.module('portainer.app')
.controller('ExtensionsController', ['$scope', '$state', 'ExtensionService', 'Notifications',
function ($scope, $state, ExtensionService, Notifications) {
$scope.state = {
actionInProgress: false,
currentDate: moment().format('YYYY-MM-dd')
};
$scope.formValues = {
License: ''
};
function initView() {
ExtensionService.extensions(true)
.then(function onSuccess(data) {
$scope.extensions = data;
})
.catch(function onError(err) {
Notifications.error('Failure', err, 'Unable to access extension store');
});
}
$scope.enableExtension = function() {
var license = $scope.formValues.License;
$scope.state.actionInProgress = true;
ExtensionService.enable(license)
.then(function onSuccess() {
Notifications.success('Extension successfully enabled');
$state.reload();
})
.catch(function onError(err) {
Notifications.error('Failure', err, 'Unable to enable extension');
})
.finally(function final() {
$scope.state.actionInProgress = false;
});
};
$scope.isValidLicenseFormat = function(form) {
var valid = true;
if (!$scope.formValues.License) {
return;
}
if (isNaN($scope.formValues.License[0])) {
valid = false;
}
form.extension_license.$setValidity('invalidLicense', valid);
};
initView();
}]);

View file

@ -0,0 +1,122 @@
<rd-header>
<rd-header-title title-text="Extension details"></rd-header-title>
<rd-header-content>
<a ui-sref="portainer.extensions">Portainer extensions</a> &gt; {{ extension.Name }}
</rd-header-content>
</rd-header>
<div class="row" ng-if="extension">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<div style="display: flex;">
<div style="flex-grow: 4; display: flex; flex-direction: column; justify-content: space-between;">
<div class="form-group">
<div class="text-muted" style="font-size: 150%;">
{{ extension.Name }} extension
</div>
<div class="small text-muted" style="margin-top: 5px;">
By <a href="https://portainer.io" href="_blank">Portainer.io</a>
</div>
</div>
<div class="form-group">
<div class="text-muted">
{{ extension.ShortDescription }}
</div>
</div>
</div>
<div style="flex-grow: 1; border-left: 1px solid #777;">
<div class="form-group" style="margin-left: 40px;">
<div style="font-size: 125%; border-bottom: 2px solid #2d3e63; padding-bottom: 5px;">
{{ extension.Enabled ? 'Enabled' : extension.Price }}
</div>
<div class="small text-muted col-sm-12" style="margin: 15px 0 15px 0;" ng-if="!extension.Enabled">
{{ extension.PriceDescription }}
</div>
<div style="margin-top: 10px; margin-bottom: 95px;" ng-if="!extension.Enabled && extension.Available">
<label for="instances_qty" class="col-sm-7 control-label text-left" style="margin-top: 7px;">Instances</label>
<div class="col-sm-5">
<input type="number" class="form-control" ng-model="formValues.instances" id="instances_qty" placeholder="1" min="1">
</div>
</div>
<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;">
Buy
</a>
</div>
<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>
</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>
</button>
</div>
<div style="margin-top: 5px;" ng-if="extension.Enabled">
<button type="button" class="btn btn-danger btn-sm" ng-click="deleteExtension(extension)" ng-disabled="state.deleteInProgress" button-spinner="state.deleteInProgress" style="width: 100%; margin-left: 0;">
<span ng-hide="state.deleteInProgress">Delete</span>
<span ng-show="state.deleteInProgress">Deleting extension...</span>
</button>
</div>
</div>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="extension">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<div class="col-sm-12 form-section-title">
<span>
Description
</span>
</div>
<div class="form-group">
<span class="small text-muted">
{{ extension.Description }}
</span>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="extension">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<div class="col-sm-12 form-section-title">
<span>
Screenshots
</span>
</div>
<div style="text-align: center;">
<div ng-repeat="image in extension.Images" style="margin-top: 25px; cursor: zoom-in;">
<img ng-src="{{image}}" ng-click="enlargeImage(image)"/>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>

View file

@ -0,0 +1,63 @@
angular.module('portainer.app')
.controller('ExtensionController', ['$q', '$scope', '$transition$', '$state', 'ExtensionService', 'Notifications', 'ModalService',
function ($q, $scope, $transition$, $state, ExtensionService, Notifications, ModalService) {
$scope.state = {
updateInProgress: false,
deleteInProgress: false
};
$scope.formValues = {
instances: 1
};
$scope.updateExtension = updateExtension;
$scope.deleteExtension = deleteExtension;
$scope.enlargeImage = enlargeImage;
function enlargeImage(image) {
ModalService.enlargeImage(image);
}
function deleteExtension(extension) {
$scope.state.deleteInProgress = true;
ExtensionService.delete(extension.Id)
.then(function onSuccess() {
Notifications.success('Extension successfully deleted');
$state.reload();
})
.catch(function onError(err) {
Notifications.error('Failure', err, 'Unable to delete extension');
})
.finally(function final() {
$scope.state.deleteInProgress = false;
});
}
function updateExtension(extension) {
$scope.state.updateInProgress = true;
ExtensionService.update(extension.Id, extension.Version)
.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.updateInProgress = false;
});
}
function initView() {
ExtensionService.extension($transition$.params().id)
.then(function onSuccess(extension) {
$scope.extension = extension;
})
.catch(function onError(err) {
Notifications.error('Failure', err, 'Unable to retrieve extension information');
});
}
initView();
}]);

View file

@ -17,22 +17,13 @@
</information-panel>
<information-panel
ng-if="!applicationState.UI.dismissedInfoPanels['home-info-01']"
title-text="Information"
dismiss-action="dismissInformationPanel('home-info-01')">
ng-if="!isAdmin && endpoints.length === 0"
title-text="Information">
<span class="small text-muted">
<p ng-if="endpoints.length > 0">
Welcome to Portainer ! Click on any endpoint in the list below to access management features.
</p>
<p ng-if="!isAdmin && endpoints.length === 0">
<p>
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
You do not have access to any environment. Please contact your administrator.
</p>
<p ng-if="isAdmin && !applicationState.application.snapshot">
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Endpoint snapshot is disabled.
</p>
</span>
</information-panel>

View file

@ -1,6 +1,6 @@
angular.module('portainer.app')
.controller('HomeController', ['$q', '$scope', '$state', 'Authentication', 'EndpointService', 'EndpointHelper', 'GroupService', 'Notifications', 'EndpointProvider', 'StateManager', 'ExtensionManager', 'ModalService', 'MotdService', 'SystemService',
function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, GroupService, Notifications, EndpointProvider, StateManager, ExtensionManager, ModalService, MotdService, SystemService) {
.controller('HomeController', ['$q', '$scope', '$state', 'Authentication', 'EndpointService', 'EndpointHelper', 'GroupService', 'Notifications', 'EndpointProvider', 'StateManager', 'LegacyExtensionManager', 'ModalService', 'MotdService', 'SystemService',
function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, GroupService, Notifications, EndpointProvider, StateManager, LegacyExtensionManager, ModalService, MotdService, SystemService) {
$scope.goToEdit = function(id) {
$state.go('portainer.endpoints.endpoint', { id: id });
@ -87,7 +87,7 @@ function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, G
EndpointProvider.setEndpointID(endpoint.Id);
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
EndpointProvider.setOfflineModeFromStatus(endpoint.Status);
ExtensionManager.initEndpointExtensions(endpoint)
LegacyExtensionManager.initEndpointExtensions(endpoint)
.then(function success(data) {
var extensions = data;
return StateManager.updateEndpointState(endpoint, extensions);

View file

@ -2,40 +2,38 @@ angular.module('portainer.app')
.controller('CreateRegistryController', ['$scope', '$state', 'RegistryService', 'Notifications',
function ($scope, $state, RegistryService, Notifications) {
$scope.selectQuayRegistry = selectQuayRegistry;
$scope.selectAzureRegistry = selectAzureRegistry;
$scope.selectCustomRegistry = selectCustomRegistry;
$scope.create = createRegistry;
$scope.state = {
RegistryType: 'quay',
actionInProgress: false
};
$scope.formValues = {
Name: 'Quay',
URL: 'quay.io',
Authentication: true,
Username: '',
Password: ''
};
function selectQuayRegistry() {
$scope.model.Name = 'Quay';
$scope.model.URL = 'quay.io';
$scope.model.Authentication = true;
}
$scope.selectQuayRegistry = function() {
$scope.formValues.Name = 'Quay';
$scope.formValues.URL = 'quay.io';
$scope.formValues.Authentication = true;
};
function selectAzureRegistry() {
$scope.model.Name = '';
$scope.model.URL = '';
$scope.model.Authentication = true;
}
$scope.selectCustomRegistry = function() {
$scope.formValues.Name = '';
$scope.formValues.URL = '';
$scope.formValues.Authentication = false;
};
function selectCustomRegistry() {
$scope.model.Name = '';
$scope.model.URL = '';
$scope.model.Authentication = false;
}
$scope.addRegistry = function() {
var registryName = $scope.formValues.Name;
var registryURL = $scope.formValues.URL.replace(/^https?\:\/\//i, '');
var authentication = $scope.formValues.Authentication;
var username = $scope.formValues.Username;
var password = $scope.formValues.Password;
function createRegistry() {
$scope.model.URL = $scope.model.URL.replace(/^https?\:\/\//i, '');
$scope.state.actionInProgress = true;
RegistryService.createRegistry(registryName, registryURL, authentication, username, password)
RegistryService.createRegistry($scope.model)
.then(function success() {
Notifications.success('Registry successfully created');
$state.go('portainer.registries');
@ -46,5 +44,11 @@ function ($scope, $state, RegistryService, Notifications) {
.finally(function final() {
$scope.state.actionInProgress = false;
});
};
}
function initView() {
$scope.model = new RegistryDefaultModel();
}
initView();
}]);

View file

@ -13,11 +13,13 @@
<div class="col-sm-12 form-section-title">
Registry provider
</div>
<div class="form-group"></div>
<div class="form-group" style="margin-bottom: 0">
<div class="boxselector_wrapper">
<div ng-click="selectQuayRegistry()">
<input type="radio" id="registry_quay" ng-model="state.RegistryType" value="quay">
<input type="radio" id="registry_quay" ng-model="model.Type" ng-value="1">
<label for="registry_quay">
<div class="boxselector_header">
<i class="fa fa-database" aria-hidden="true" style="margin-right: 2px;"></i>
@ -26,8 +28,18 @@
<p>Quay container registry</p>
</label>
</div>
<div ng-click="selectAzureRegistry()">
<input type="radio" id="registry_azure" ng-model="model.Type" ng-value="2">
<label for="registry_azure">
<div class="boxselector_header">
<i class="fab fa-microsoft" aria-hidden="true" style="margin-right: 2px;"></i>
Azure
</div>
<p>Azure container registry</p>
</label>
</div>
<div ng-click="selectCustomRegistry()">
<input type="radio" id="registry_custom" ng-model="state.RegistryType" value="custom">
<input type="radio" id="registry_custom" ng-model="model.Type" ng-value="3">
<label for="registry_custom">
<div class="boxselector_header">
<i class="fa fa-database" aria-hidden="true" style="margin-right: 2px;"></i>
@ -38,81 +50,28 @@
</div>
</div>
</div>
<div class="col-sm-12 form-section-title" ng-if="state.RegistryType === 'custom'">
Important notice
</div>
<div class="form-group" ng-if="state.RegistryType === 'custom'">
<span class="col-sm-12 text-muted small">
Docker requires you to connect to a <a href="https://docs.docker.com/registry/deploying/#running-a-domain-registry" target="_blank">secure registry</a>.
You can find more information about how to connect to an insecure registry <a href="https://docs.docker.com/registry/insecure/" target="_blank">in the Docker documentation</a>.
</span>
</div>
<div class="col-sm-12 form-section-title">
Registry details
</div>
<!-- name-input -->
<div class="form-group" ng-if="state.RegistryType === 'custom'">
<label for="registry_name" class="col-sm-3 col-lg-2 control-label text-left">Name</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="registry_name" ng-model="formValues.Name" placeholder="e.g. my-registry">
</div>
</div>
<!-- !name-input -->
<!-- registry-url-input -->
<div class="form-group" ng-if="state.RegistryType === 'custom'">
<label for="registry_url" class="col-sm-3 col-lg-2 control-label text-left">
Registry URL
<portainer-tooltip position="bottom" message="URL or IP address of a Docker registry. Any protocol will be stripped."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="registry_url" ng-model="formValues.URL" placeholder="e.g. 10.0.0.10:5000 or myregistry.domain.tld">
</div>
</div>
<!-- !registry-url-input -->
<!-- authentication-checkbox -->
<div class="form-group" ng-if="state.RegistryType === 'custom'">
<div class="col-sm-12">
<label for="registry_auth" class="control-label text-left">
Authentication
<portainer-tooltip position="bottom" message="Enable this option if you need to specify credentials to connect to this registry."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="formValues.Authentication"><i></i>
</label>
</div>
</div>
<!-- !authentication-checkbox -->
<!-- authentication-credentials -->
<div ng-if="formValues.Authentication || state.RegistryType === 'quay'">
<!-- credentials-user -->
<div class="form-group">
<label for="credentials_username" class="col-sm-3 col-lg-2 control-label text-left">Username</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="credentials_username" ng-model="formValues.Username">
</div>
</div>
<!-- !credentials-user -->
<!-- credentials-password -->
<div class="form-group">
<label for="credentials_password" class="col-sm-3 col-lg-2 control-label text-left">Password</label>
<div class="col-sm-9 col-lg-10">
<input type="password" class="form-control" id="credentials_password" ng-model="formValues.Password">
</div>
</div>
<!-- !credentials-password -->
</div>
<!-- !authentication-credentials -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress || !formValues.Name || !formValues.URL || (formValues.Authentication && (!formValues.Username || !formValues.Password))" ng-click="addRegistry()" button-spinner="state.actionInProgress">
<span ng-hide="state.actionInProgress">Add registry</span>
<span ng-show="state.actionInProgress">Adding registry...</span>
</button>
</div>
</div>
<registry-form-quay ng-if="model.Type === 1"
model="model"
form-action="create"
form-action-label="Add registry"
action-in-progress="state.actionInProgress"
></registry-form-quay>
<registry-form-azure ng-if="model.Type === 2"
model="model"
form-action="create"
form-action-label="Add registry"
action-in-progress="state.actionInProgress"
></registry-form-azure>
<registry-form-custom ng-if="model.Type === 3"
model="model"
form-action="create"
form-action-label="Add registry"
action-in-progress="state.actionInProgress"
></registry-form-custom>
</form>
</rd-widget-body>
</rd-widget>

View file

@ -73,9 +73,10 @@
<registries-datatable
title-text="Registries" title-icon="fa-database"
dataset="registries" table-key="registries"
order-by="Name"
order-by="Name"
access-management="applicationState.application.authentication"
remove-action="removeAction"
registry-management="registryManagementAvailable"
></registries-datatable>
</div>
</div>

View file

@ -1,6 +1,6 @@
angular.module('portainer.app')
.controller('RegistriesController', ['$q', '$scope', '$state', 'RegistryService', 'DockerHubService', 'ModalService', 'Notifications',
function ($q, $scope, $state, RegistryService, DockerHubService, ModalService, Notifications) {
.controller('RegistriesController', ['$q', '$scope', '$state', 'RegistryService', 'DockerHubService', 'ModalService', 'Notifications', 'ExtensionService',
function ($q, $scope, $state, RegistryService, DockerHubService, ModalService, Notifications, ExtensionService) {
$scope.state = {
actionInProgress: false
@ -60,11 +60,13 @@ function ($q, $scope, $state, RegistryService, DockerHubService, ModalService, N
function initView() {
$q.all({
registries: RegistryService.registries(),
dockerhub: DockerHubService.dockerhub()
dockerhub: DockerHubService.dockerhub(),
registryManagement: ExtensionService.registryManagementEnabled()
})
.then(function success(data) {
$scope.registries = data.registries;
$scope.dockerhub = data.dockerhub;
$scope.registryManagementAvailable = data.registryManagement;
})
.catch(function error(err) {
$scope.registries = [];

View file

@ -45,6 +45,9 @@
<li class="sidebar-title" ng-if="!applicationState.application.authentication || isAdmin || isTeamLeader">
<span>Settings</span>
</li>
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
<a ui-sref="portainer.extensions" ui-sref-active="active">Extensions <span class="menu-icon fa fa-bolt fa-fw"></span></a>
</li>
<li class="sidebar-list" ng-if="applicationState.application.authentication && (isAdmin || isTeamLeader)">
<a ui-sref="portainer.users" ui-sref-active="active">Users <span class="menu-icon fa fa-users fa-fw"></span></a>
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'portainer.users' || $state.current.name === 'portainer.users.user' || $state.current.name === 'portainer.teams' || $state.current.name === 'portainer.teams.team')">

View file

@ -0,0 +1,84 @@
<rd-header>
<rd-header-title title-text="Support option details"></rd-header-title>
<rd-header-content>
<a ui-sref="portainer.support">Portainer support</a> &gt; {{ product.Name }}
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<div style="display: flex;">
<div style="flex-grow: 4; display: flex; flex-direction: column; justify-content: space-between;">
<div class="form-group">
<div class="text-muted" style="font-size: 150%;">
{{ product.Name }}
</div>
<div class="small text-muted" style="margin-top: 5px;">
By <a href="https://portainer.io" href="_blank">Portainer.io</a>
</div>
</div>
<div class="form-group">
<div class="text-muted">
{{ product.ShortDescription }}
</div>
</div>
</div>
<div style="flex-grow: 1; border-left: 1px solid #777;">
<div class="form-group" style="margin-left: 40px;">
<div style="font-size: 125%; border-bottom: 2px solid #2d3e63; padding-bottom: 5px;">
{{ product.Price }}
</div>
<div class="small text-muted col-sm-12" style="margin: 15px 0 15px 0;">
{{ product.PriceDescription }}
</div>
<div style="margin-top: 10px; margin-bottom: 95px;">
<label for="endpoint_count" class="col-sm-7 control-label text-left" style="margin-top: 7px;">Hosts</label>
<div class="col-sm-5">
<input type="number" class="form-control" ng-model="formValues.hostCount" id="endpoint_count" placeholder="10" min="10">
</div>
</div>
<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;">
Buy
</a>
</div>
</div>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="product">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<div class="col-sm-12 form-section-title">
<span>
Description
</span>
</div>
<div class="form-group">
<span class="small text-muted" style="white-space: pre-line;">{{ product.Description }}</span>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>

View file

@ -0,0 +1,14 @@
angular.module('portainer.app')
.controller('SupportProductController', ['$scope', '$transition$',
function($scope, $transition$) {
$scope.formValues = {
hostCount: 10
};
function initView() {
$scope.product = $transition$.params().product;
}
initView();
}]);

View file

@ -1,38 +1,34 @@
<rd-header>
<rd-header-title title-text="Support">
<rd-header-title title-text="Portainer support">
</rd-header-title>
<rd-header-content>
Portainer support
Commercial support options
</rd-header-content>
</rd-header>
<information-panel title-text="Information">
<span class="small text-muted">
<p>
Business support is a subscription service and is delivered by Portainer developers directly. Portainer Business Support is available in Standard and Critical levels, which offer a range of availability and response time options.
</p>
<p>
Once acquired through an in-app purchase, a support subscription will enable private access to the Portainer Support Portal at the appropriate service level.
</p>
<p>
Business support includes comprehensive assistance and issue resolution for Portainer software, as well as the ability to ask “how to” questions.
</p>
<p>
Issues outside Portainer (such as those relating to third party software or hardware) will be diagnosed, verified and the client will be referred to the relevant supplier for support. Portainer support will investigate and resolve any bugs that are identified as part of the support case.
</p>
</span>
</information-panel>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header title-text="Portainer support options" icon="fa-life-ring"></rd-widget-header>
<rd-widget-body>
<div class="small" style="line-height:1.65;">
<p>
Portainer.io offers multiple commercial support options.
</p>
<p>
<i class="fa fa-chevron-circle-right" aria-hidden="true"></i> <u>Per incident</u>
<ul>
<li>$USD 100</li>
<li><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6" target="_blank"><i class="fab fa-paypal" aria-hidden="true"></i> Buy it here</a></li>
</ul>
</p>
<p>
<i class="fa fa-chevron-circle-right" aria-hidden="true"></i> <u>Per Portainer instance</u>
<ul>
<li>$USD 1200 per year</li>
<li>Unlimited incidents</li>
<li>4 named users</li>
<li><a target="_blank" href="mailto:info@portainer.io"><i class="fa fa-envelope" aria-hidden="true"></i> Contact us</a></li>
</ul>
</p>
</div>
</rd-widget-body>
</rd-widget>
<product-list
title-text="Available support options"
products="products"
go-to="goToProductView"
></product-list>
</div>
</div>

View file

@ -0,0 +1,35 @@
angular.module('portainer.app')
.controller('SupportController', ['$scope', '$state',
function($scope, $state) {
$scope.goToProductView = function(product) {
$state.go('portainer.support.product', { product: product });
};
function initView() {
var supportProducts = [
{
Id: 1,
Name: 'Business Support Standard',
ShortDescription: '11x5 support with 4 hour response',
Price: 'USD 120',
PriceDescription: 'Price per month per host (minimum 10 hosts)',
Description: 'Portainer Business Support Standard:\n\n* 7am 6pm business days, local time.\n* 4 Hour response for issues, 4 named support contacts.\n\nPortainer support provides you with an easy way to interact directly with the Portainer development team; whether you have an issue with the product, think you have found a bug, or need help on how to use Portainer, we are here to help. Support is initiated from our web based ticketing system, and support is provided either by Slack messaging, Zoom remote support, or email.\n\nPrice is per Docker Host, with a 10 Host minimum, and is an annual support subscription.',
ProductId: '1163'
},
{
Id: 2,
Name: 'Business Support Critical',
ShortDescription: '24x7 support with 1 hour response',
Price: 'USD 240',
PriceDescription: 'Price per month per host (minimum 10 hosts)',
Description: 'Portainer Business Support Critical:\n\n* 24x7\n* 1 Hour response for issues, 4 named support contacts.\n\nPortainer support provides you with advanced support for critical requirements. Business Support Critical is an easy way to interact directly with the Portainer development team; whether you have an issue with the product, think you have found a bug, or need help on how to use Portainer, we are here to help. Support is initiated from our web based ticketing system, and support is provided either by Slack messaging, Zoom remote support, or email.\n\nPrice is per Docker Host, with a 10 Host minimum, and is an annual support subscription.',
ProductId: '1162'
}
];
$scope.products = supportProducts;
}
initView();
}]);