Merge branch 'release/1.15.3'

pull/1450/head 1.15.3
Anthony Lapenna 2017-11-26 10:08:04 +01:00
commit 65cde27334
53 changed files with 400 additions and 169 deletions

View File

@ -127,6 +127,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
if err == portainer.ErrSettingsNotFound { if err == portainer.ErrSettingsNotFound {
settings := &portainer.Settings{ settings := &portainer.Settings{
LogoURL: *flags.Logo, LogoURL: *flags.Logo,
DisplayDonationHeader: true,
DisplayExternalContributors: false, DisplayExternalContributors: false,
AuthenticationMethod: portainer.AuthenticationInternal, AuthenticationMethod: portainer.AuthenticationInternal,
LDAPSettings: portainer.LDAPSettings{ LDAPSettings: portainer.LDAPSettings{

View File

@ -46,6 +46,7 @@ func NewSettingsHandler(bouncer *security.RequestBouncer) *SettingsHandler {
type ( type (
publicSettingsResponse struct { publicSettingsResponse struct {
LogoURL string `json:"LogoURL"` LogoURL string `json:"LogoURL"`
DisplayDonationHeader bool `json:"DisplayDonationHeader"`
DisplayExternalContributors bool `json:"DisplayExternalContributors"` DisplayExternalContributors bool `json:"DisplayExternalContributors"`
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"` AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
@ -56,6 +57,7 @@ type (
TemplatesURL string `valid:"required"` TemplatesURL string `valid:"required"`
LogoURL string `valid:""` LogoURL string `valid:""`
BlackListedLabels []portainer.Pair `valid:""` BlackListedLabels []portainer.Pair `valid:""`
DisplayDonationHeader bool `valid:""`
DisplayExternalContributors bool `valid:""` DisplayExternalContributors bool `valid:""`
AuthenticationMethod int `valid:"required"` AuthenticationMethod int `valid:"required"`
LDAPSettings portainer.LDAPSettings `valid:""` LDAPSettings portainer.LDAPSettings `valid:""`
@ -90,6 +92,7 @@ func (handler *SettingsHandler) handleGetPublicSettings(w http.ResponseWriter, r
publicSettings := &publicSettingsResponse{ publicSettings := &publicSettingsResponse{
LogoURL: settings.LogoURL, LogoURL: settings.LogoURL,
DisplayDonationHeader: settings.DisplayDonationHeader,
DisplayExternalContributors: settings.DisplayExternalContributors, DisplayExternalContributors: settings.DisplayExternalContributors,
AuthenticationMethod: settings.AuthenticationMethod, AuthenticationMethod: settings.AuthenticationMethod,
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers, AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
@ -118,6 +121,7 @@ func (handler *SettingsHandler) handlePutSettings(w http.ResponseWriter, r *http
TemplatesURL: req.TemplatesURL, TemplatesURL: req.TemplatesURL,
LogoURL: req.LogoURL, LogoURL: req.LogoURL,
BlackListedLabels: req.BlackListedLabels, BlackListedLabels: req.BlackListedLabels,
DisplayDonationHeader: req.DisplayDonationHeader,
DisplayExternalContributors: req.DisplayExternalContributors, DisplayExternalContributors: req.DisplayExternalContributors,
LDAPSettings: req.LDAPSettings, LDAPSettings: req.LDAPSettings,
AllowBindMountsForRegularUsers: req.AllowBindMountsForRegularUsers, AllowBindMountsForRegularUsers: req.AllowBindMountsForRegularUsers,

View File

@ -73,6 +73,7 @@ type (
TemplatesURL string `json:"TemplatesURL"` TemplatesURL string `json:"TemplatesURL"`
LogoURL string `json:"LogoURL"` LogoURL string `json:"LogoURL"`
BlackListedLabels []Pair `json:"BlackListedLabels"` BlackListedLabels []Pair `json:"BlackListedLabels"`
DisplayDonationHeader bool `json:"DisplayDonationHeader"`
DisplayExternalContributors bool `json:"DisplayExternalContributors"` DisplayExternalContributors bool `json:"DisplayExternalContributors"`
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"` AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"`
LDAPSettings LDAPSettings `json:"LDAPSettings"` LDAPSettings LDAPSettings `json:"LDAPSettings"`
@ -389,7 +390,7 @@ type (
const ( const (
// APIVersion is the version number of the Portainer API. // APIVersion is the version number of the Portainer API.
APIVersion = "1.15.2" APIVersion = "1.15.3"
// DBVersion is the version number of the Portainer database. // DBVersion is the version number of the Portainer database.
DBVersion = 6 DBVersion = 6
// DefaultTemplatesURL represents the default URL for the templates definitions. // DefaultTemplatesURL represents the default URL for the templates definitions.

View File

@ -56,7 +56,7 @@ info:
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8). **NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
version: "1.15.2" version: "1.15.3"
title: "Portainer API" title: "Portainer API"
contact: contact:
email: "info@portainer.io" email: "info@portainer.io"
@ -1869,7 +1869,7 @@ definitions:
description: "Is analytics enabled" description: "Is analytics enabled"
Version: Version:
type: "string" type: "string"
example: "1.15.2" example: "1.15.3"
description: "Portainer API version" description: "Portainer API version"
PublicSettingsInspectResponse: PublicSettingsInspectResponse:
type: "object" type: "object"
@ -1880,6 +1880,10 @@ definitions:
description: "URL to a logo that will be displayed on the login page as well\ description: "URL to a logo that will be displayed on the login page as well\
\ as on top of the sidebar. Will use default Portainer logo when value is\ \ as on top of the sidebar. Will use default Portainer logo when value is\
\ empty string" \ empty string"
DisplayDonationHeader:
type: "boolean"
example: true
description: "Whether to display or not the donation message in the header."\
DisplayExternalContributors: DisplayExternalContributors:
type: "boolean" type: "boolean"
example: false example: false
@ -1983,6 +1987,10 @@ definitions:
\ when querying containers" \ when querying containers"
items: items:
$ref: "#/definitions/Settings_BlackListedLabels" $ref: "#/definitions/Settings_BlackListedLabels"
DisplayDonationHeader:
type: "boolean"
example: true
description: "Whether to display or not the donation message in the header."
DisplayExternalContributors: DisplayExternalContributors:
type: "boolean" type: "boolean"
example: false example: false
@ -2398,6 +2406,10 @@ definitions:
\ when querying containers" \ when querying containers"
items: items:
$ref: "#/definitions/Settings_BlackListedLabels" $ref: "#/definitions/Settings_BlackListedLabels"
DisplayDonationHeader:
type: "boolean"
example: true
description: "Whether to display or not the donation message in the header."
DisplayExternalContributors: DisplayExternalContributors:
type: "boolean" type: "boolean"
example: false example: false

View File

@ -0,0 +1,100 @@
<rd-header>
<rd-header-title title="About">
</rd-header-title>
<rd-header-content>
About Portainer
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<b>Portainer</b> is a <a href="https://github.com/portainer/portainer/blob/develop/LICENSE" target="_blank" >free and open-source software</a> brought to you with <span class="menu-icon fa fa-heart red-icon "></span> by <a href="https://portainer.io/" target="_blank">portainer.io</a> and <a href="https://github.com/portainer/portainer/graphs/contributors" target="_blank">contributors.</a>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header title="Help support portainer" icon="fa-heartbeat"></rd-widget-header>
<rd-widget-body>
<div class="small" style="line-height:1.65;">
<p>
It is a community effort to make <b>Portainer</b> as feature-rich as simple it is to deploy and use. We need all the help we can get!
</p>
<p>
<i class="fa fa-chevron-circle-right" aria-hidden="true"></i> <u>Fund our work</u>
<ul>
<li>Become a <a href="https://www.patreon.com/Portainerio" target="_blank"><i class="fa fa-money" aria-hidden="true"></i> patron</a></li>
<li>Consider a <a href="https://portainer.io/support.html" target="_blank">paid support plan</a></li>
<li>Make a <a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6" target="_blank"><i class="fa fa-paypal" aria-hidden="true"></i> donation</a></li>
</ul>
</p>
<p>
<i class="fa fa-chevron-circle-right" aria-hidden="true"></i> <u>Contribute</u>
<ul>
<li>Found a bug or got a feature idea? Let's talk about it on <a href="https://github.com/portainer/portainer/issues" target="_blank"><i class="fa fa-github" aria-hidden="true"></i> Github</a></li>
<li>Follow our <a href="https://portainer.readthedocs.io/en/latest/contribute.html" target="_blank">contribution guidelines</a> to build it locally and make a <a target="_blank" href="https://github.com/portainer/portainer/compare"><i class="fa fa-github" aria-hidden="true"></i> pull request</a></li>
</ul>
</p>
<p>
<i class="fa fa-chevron-circle-right" aria-hidden="true"></i> <u>Spread the word</u>
<ul>
<li>Talk to your friends and colleagues about how awesome Portainer is!</li>
<li>Follow us on <a href="https://twitter.com/portainerio" target="_blank"><i class="fa fa-twitter" aria-hidden="true"></i> Twitter</a></li>
</ul>
</p>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header title="Support and services" icon="fa-building-o"></rd-widget-header>
<rd-widget-body>
<div class="small" style="line-height:1.65;">
<p>
<i class="fa fa-chevron-circle-right" aria-hidden="true"></i> <u>Documentation</u>
<ul>
<li>Checkout our <a target="_blank" href="http://portainer.readthedocs.io"><i class="fa fa-book" aria-hidden="true"></i> online documentation</a></li>
<li>Be sure to have a look at our <a href="https://portainer.readthedocs.io/en/latest/faq.html" target="_blank">FAQ</a> and our list of <a href="https://github.com/portainer/portainer/issues" target="_blank">open issues</a></li>
</ul>
</p>
<p>
<i class="fa fa-chevron-circle-right" aria-hidden="true"></i> <u>Community support</u>
<ul>
<li>Join us on <a href="https://portainer.io/slack/" target="_blank"><i class="fa fa-slack" aria-hidden="true"></i> Slack</a></li>
<li>We're also on <a href="https://gitter.im/portainer/Lobby" target="_blank"><i class="fa fa-github-alt" aria-hidden="true"></i> Gitter</a></li>
</ul>
</p>
<p>
<i class="fa fa-chevron-circle-right" aria-hidden="true"></i> <u>Services</u>
<ul>
<li>Find out more about our <a href="https://portainer.io/support.html" target="_blank">consulting and commercial support plans</a></li>
<li>We also propose a fund-a-feature plan, reach out to us at <a target="_blank" href="mailto:info@portainer.io"><i class="fa fa-envelope-o" aria-hidden="true"></i> portainer.io</a></li>
</ul>
</p>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header title="Limitations" icon="fa-plug"></rd-widget-header>
<rd-widget-body>
<div class="small">
Portainer has full support for Docker >=1.10 and Docker Swarm >= 1.2.3, and partial support for Docker 1.9 (some features may not be available).
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@ -51,7 +51,7 @@
</th> </th>
</thead> </thead>
<tbody> <tbody>
<tr dir-paginate="config in (state.filteredConfigs = ( configs | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))"> <tr dir-paginate="config in (state.filteredConfigs = ( configs | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))" ng-class="{active: config.Checked}">
<td><input type="checkbox" ng-model="config.Checked" ng-change="selectItem(config)"/></td> <td><input type="checkbox" ng-model="config.Checked" ng-change="selectItem(config)"/></td>
<td><a ui-sref="config({id: config.Id})">{{ config.Name }}</a></td> <td><a ui-sref="config({id: config.Id})">{{ config.Name }}</a></td>
<td>{{ config.CreatedAt | getisodate }}</td> <td>{{ config.CreatedAt | getisodate }}</td>

View File

@ -12,15 +12,17 @@
<rd-widget-header icon="fa-cogs" title="Actions"></rd-widget-header> <rd-widget-header icon="fa-cogs" title="Actions"></rd-widget-header>
<rd-widget-body classes="padding"> <rd-widget-body classes="padding">
<div class="btn-group" role="group" aria-label="..."> <div class="btn-group" role="group" aria-label="...">
<button class="btn btn-success" ng-click="start()" ng-disabled="container.State.Running"><i class="fa fa-play space-right" aria-hidden="true"></i>Start</button> <button class="btn btn-success btn-responsive" ng-click="start()" ng-disabled="container.State.Running"><i class="fa fa-play space-right" aria-hidden="true"></i>Start</button>
<button class="btn btn-danger" ng-click="stop()" ng-disabled="!container.State.Running"><i class="fa fa-stop space-right" aria-hidden="true"></i>Stop</button> <button class="btn btn-danger btn-responsive" ng-click="stop()" ng-disabled="!container.State.Running"><i class="fa fa-stop space-right" aria-hidden="true"></i>Stop</button>
<button class="btn btn-danger" ng-click="kill()" ng-disabled="!container.State.Running"><i class="fa fa-bomb space-right" aria-hidden="true"></i>Kill</button> <button class="btn btn-danger btn-responsive" ng-click="kill()" ng-disabled="!container.State.Running"><i class="fa fa-bomb space-right" aria-hidden="true"></i>Kill</button>
<button class="btn btn-primary" ng-click="restart()" ng-disabled="!container.State.Running"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Restart</button> <button class="btn btn-primary btn-responsive" ng-click="restart()" ng-disabled="!container.State.Running"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Restart</button>
<button class="btn btn-primary" ng-click="pause()" ng-disabled="!container.State.Running || container.State.Paused"><i class="fa fa-pause space-right" aria-hidden="true"></i>Pause</button> <button class="btn btn-primary btn-responsive" ng-click="pause()" ng-disabled="!container.State.Running || container.State.Paused"><i class="fa fa-pause space-right" aria-hidden="true"></i>Pause</button>
<button class="btn btn-primary" ng-click="unpause()" ng-disabled="!container.State.Paused"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button> <button class="btn btn-primary btn-responsive" ng-click="unpause()" ng-disabled="!container.State.Paused"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button>
<button class="btn btn-danger" ng-click="confirmRemove()"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button> <button class="btn btn-danger btn-responsive" ng-click="confirmRemove()"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
<button class="btn btn-danger" ng-click="recreate()" ng-if="!container.Config.Labels['com.docker.swarm.service.id']"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Recreate</button> </div>
<button class="btn btn-primary" ng-click="duplicate()" ng-if="!container.Config.Labels['com.docker.swarm.service.id']"><i class="fa fa-files-o space-right" aria-hidden="true"></i>Duplicate/Edit</button> <div class="btn-group" role="group" aria-label="...">
<button class="btn btn-danger btn-responsive" ng-click="recreate()" ng-if="!container.Config.Labels['com.docker.swarm.service.id']"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Recreate</button>
<button class="btn btn-primary btn-responsive" ng-click="duplicate()" ng-if="!container.Config.Labels['com.docker.swarm.service.id']"><i class="fa fa-files-o space-right" aria-hidden="true"></i>Duplicate/Edit</button>
</div> </div>
</rd-widget-body> </rd-widget-body>
</rd-widget> </rd-widget>

View File

@ -32,7 +32,7 @@
<button type="button" class="btn btn-primary btn-responsive" ng-click="unpauseAction()" ng-disabled="!state.selectedItemCount || state.noPausedItemsSelected"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button> <button type="button" class="btn btn-primary btn-responsive" ng-click="unpauseAction()" ng-disabled="!state.selectedItemCount || state.noPausedItemsSelected"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button>
<button type="button" class="btn btn-danger btn-responsive" ng-click="confirmRemoveAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button> <button type="button" class="btn btn-danger btn-responsive" ng-click="confirmRemoveAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
</div> </div>
<a class="btn btn-primary" type="button" ui-sref="actions.create.container"><i class="fa fa-plus space-right" aria-hidden="true"></i>Add container</a> <a class="btn btn-primary btn-responsive" type="button" ui-sref="actions.create.container"><i class="fa fa-plus space-right" aria-hidden="true"></i>Add container</a>
</div> </div>
<div class="pull-right"> <div class="pull-right">
<input type="checkbox" ng-model="state.displayAll" id="displayAll" ng-change="toggleGetAll()" style="margin-top: -2px; margin-right: 5px;"/><label for="displayAll">Show all containers</label> <input type="checkbox" ng-model="state.displayAll" id="displayAll" ng-change="toggleGetAll()" style="margin-top: -2px; margin-right: 5px;"/><label for="displayAll">Show all containers</label>
@ -109,7 +109,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr dir-paginate="container in (state.filteredContainers = ( containers | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))"> <tr dir-paginate="container in (state.filteredContainers = ( containers | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))" ng-class="{active: container.Checked}">
<td><input type="checkbox" ng-model="container.Checked" ng-change="selectItem(container)"/></td> <td><input type="checkbox" ng-model="container.Checked" ng-change="selectItem(container)"/></td>
<td> <td>
<span ng-if="['starting','healthy','unhealthy'].indexOf(container.Status) !== -1" class="label label-{{ container.Status|containerstatusbadge }} interactive" uib-tooltip="This container has a health check">{{ container.Status }}</span> <span ng-if="['starting','healthy','unhealthy'].indexOf(container.Status) !== -1" class="label label-{{ container.Status|containerstatusbadge }} interactive" uib-tooltip="This container has a health check">{{ container.Status }}</span>

View File

@ -60,7 +60,7 @@ function ($scope, $state, $document, Notifications, ConfigService, Authenticatio
$scope.create = function () { $scope.create = function () {
var accessControlData = $scope.formValues.AccessControlData; var accessControlData = $scope.formValues.AccessControlData;
var userDetails = Authentication.getUserDetails(); var userDetails = Authentication.getUserDetails();
var isAdmin = userDetails.role === 1 ? true : false; var isAdmin = userDetails.role === 1;
if (!validateForm(accessControlData, isAdmin)) { if (!validateForm(accessControlData, isAdmin)) {
return; return;

View File

@ -349,6 +349,12 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
} }
} }
$scope.resetNetworkConfig = function() {
$scope.config.NetworkingConfig = {
EndpointsConfig: {}
};
};
function loadFromContainerNetworkConfig(d) { function loadFromContainerNetworkConfig(d) {
$scope.config.NetworkingConfig = { $scope.config.NetworkingConfig = {
EndpointsConfig: {} EndpointsConfig: {}
@ -550,7 +556,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
}); });
var userDetails = Authentication.getUserDetails(); var userDetails = Authentication.getUserDetails();
$scope.isAdmin = userDetails.role === 1 ? true : false; $scope.isAdmin = userDetails.role === 1;
} }
function validateForm(accessControlData, isAdmin) { function validateForm(accessControlData, isAdmin) {
@ -574,7 +580,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
var accessControlData = $scope.formValues.AccessControlData; var accessControlData = $scope.formValues.AccessControlData;
var userDetails = Authentication.getUserDetails(); var userDetails = Authentication.getUserDetails();
var isAdmin = userDetails.role === 1 ? true : false; var isAdmin = userDetails.role === 1;
if (!validateForm(accessControlData, isAdmin)) { if (!validateForm(accessControlData, isAdmin)) {
return; return;

View File

@ -9,7 +9,7 @@
<div class="col-lg-12 col-md-12 col-xs-12"> <div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget> <rd-widget>
<rd-widget-body> <rd-widget-body>
<form class="form-horizontal"> <form class="form-horizontal" autocomplete="off">
<!-- name-input --> <!-- name-input -->
<div class="form-group"> <div class="form-group">
<label for="container_name" class="col-sm-1 control-label text-left">Name</label> <label for="container_name" class="col-sm-1 control-label text-left">Name</label>
@ -28,7 +28,7 @@
<div ng-if="formValues.Registry || !fromContainer"> <div ng-if="formValues.Registry || !fromContainer">
<!-- image-and-registry --> <!-- image-and-registry -->
<div class="form-group"> <div class="form-group">
<por-image-registry image="config.Image" registry="formValues.Registry" ng-if="formValues.Registry"></por-image-registry> <por-image-registry image="config.Image" registry="formValues.Registry" ng-if="formValues.Registry" auto-complete="true"></por-image-registry>
</div> </div>
<!-- !image-and-registry --> <!-- !image-and-registry -->
<!-- always-pull --> <!-- always-pull -->
@ -296,7 +296,7 @@
<div class="form-group"> <div class="form-group">
<label for="container_network" class="col-sm-2 col-lg-1 control-label text-left">Network</label> <label for="container_network" class="col-sm-2 col-lg-1 control-label text-left">Network</label>
<div class="col-sm-9"> <div class="col-sm-9">
<select class="form-control" ng-model="config.HostConfig.NetworkMode" id="container_network"> <select class="form-control" ng-model="config.HostConfig.NetworkMode" id="container_network" ng-change="resetNetworkConfig()">
<option selected disabled hidden value="">Select a network</option> <option selected disabled hidden value="">Select a network</option>
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option> <option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
</select> </select>

View File

@ -93,7 +93,7 @@ function ($q, $scope, $state, PluginService, Notifications, NetworkService, Labe
var networkConfiguration = prepareConfiguration(); var networkConfiguration = prepareConfiguration();
var accessControlData = $scope.formValues.AccessControlData; var accessControlData = $scope.formValues.AccessControlData;
var userDetails = Authentication.getUserDetails(); var userDetails = Authentication.getUserDetails();
var isAdmin = userDetails.role === 1 ? true : false; var isAdmin = userDetails.role === 1;
if (!validateForm(accessControlData, isAdmin)) { if (!validateForm(accessControlData, isAdmin)) {
return; return;

View File

@ -59,7 +59,7 @@ function ($scope, $state, Notifications, SecretService, LabelHelper, Authenticat
var accessControlData = $scope.formValues.AccessControlData; var accessControlData = $scope.formValues.AccessControlData;
var userDetails = Authentication.getUserDetails(); var userDetails = Authentication.getUserDetails();
var isAdmin = userDetails.role === 1 ? true : false; var isAdmin = userDetails.role === 1;
if (!validateForm(accessControlData, isAdmin)) { if (!validateForm(accessControlData, isAdmin)) {
return; return;

View File

@ -82,7 +82,7 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C
}; };
$scope.addSecret = function() { $scope.addSecret = function() {
$scope.formValues.Secrets.push({}); $scope.formValues.Secrets.push({ overrideTarget: false });
}; };
$scope.removeSecret = function(index) { $scope.removeSecret = function(index) {
@ -243,7 +243,7 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C
function prepareUpdateConfig(config, input) { function prepareUpdateConfig(config, input) {
config.UpdateConfig = { config.UpdateConfig = {
Parallelism: input.Parallelism || 0, Parallelism: input.Parallelism || 0,
Delay: input.UpdateDelay || 0, Delay: input.UpdateDelay * 1000000000 || 0,
FailureAction: input.FailureAction, FailureAction: input.FailureAction,
Order: input.UpdateOrder Order: input.UpdateOrder
}; };
@ -275,6 +275,9 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C
if (secret.model) { if (secret.model) {
var s = SecretHelper.secretConfig(secret.model); var s = SecretHelper.secretConfig(secret.model);
s.File.Name = s.SecretName; s.File.Name = s.SecretName;
if (secret.overrideTarget && secret.target && secret.target !== '') {
s.File.Name = secret.target;
}
secrets.push(s); secrets.push(s);
} }
}); });
@ -387,7 +390,7 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C
var accessControlData = $scope.formValues.AccessControlData; var accessControlData = $scope.formValues.AccessControlData;
var userDetails = Authentication.getUserDetails(); var userDetails = Authentication.getUserDetails();
var isAdmin = userDetails.role === 1 ? true : false; var isAdmin = userDetails.role === 1;
if (!validateForm(accessControlData, isAdmin)) { if (!validateForm(accessControlData, isAdmin)) {
return; return;
@ -443,7 +446,7 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C
var settings = data.settings; var settings = data.settings;
$scope.allowBindMounts = settings.AllowBindMountsForRegularUsers; $scope.allowBindMounts = settings.AllowBindMountsForRegularUsers;
var userDetails = Authentication.getUserDetails(); var userDetails = Authentication.getUserDetails();
$scope.isAdmin = userDetails.role === 1 ? true : false; $scope.isAdmin = userDetails.role === 1;
}) })
.catch(function error(err) { .catch(function error(err) {
Notifications.error('Failure', err, 'Unable to initialize view'); Notifications.error('Failure', err, 'Unable to initialize view');

View File

@ -9,7 +9,7 @@
<div class="col-lg-12 col-md-12 col-xs-12"> <div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget> <rd-widget>
<rd-widget-body> <rd-widget-body>
<form class="form-horizontal"> <form class="form-horizontal" autocomplete="off">
<!-- name-input --> <!-- name-input -->
<div class="form-group"> <div class="form-group">
<label for="service_name" class="col-sm-1 control-label text-left">Name</label> <label for="service_name" class="col-sm-1 control-label text-left">Name</label>
@ -23,7 +23,7 @@
</div> </div>
<!-- image-and-registry --> <!-- image-and-registry -->
<div class="form-group"> <div class="form-group">
<por-image-registry image="formValues.Image" registry="formValues.Registry"></por-image-registry> <por-image-registry image="formValues.Image" registry="formValues.Registry" auto-complete="true"></por-image-registry>
</div> </div>
<!-- !image-and-registry --> <!-- !image-and-registry -->
<div class="col-sm-12 form-section-title"> <div class="col-sm-12 form-section-title">
@ -395,7 +395,7 @@
</div> </div>
<div class="col-sm-5"> <div class="col-sm-5">
<p class="small text-muted"> <p class="small text-muted">
Amount of time between updates. Amount of time between updates. Time in seconds.
</p> </p>
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
<form class="form-horizontal" style="margin-top: 15px;"> <form class="form-horizontal" style="margin-top: 15px;">
<div class="form-group"> <div class="form-group">
<div class="col-sm-12 small text-muted"> <div class="col-sm-12 small text-muted">
Secrets will be available under <code>/run/secrets/$SECRET_NAME</code> in containers. By default, secrets will be available under <code>/run/secrets/$SECRET_NAME</code> in containers.
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -12,16 +12,26 @@
</span> </span>
</div> </div>
<div class="col-sm-12 form-inline" style="margin-top: 10px;"> <div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="secret in formValues.Secrets" style="margin-top: 2px;"> <div ng-repeat="secret in formValues.Secrets track by $index" style="margin-top: 4px;">
<div class="input-group col-sm-4 input-group-sm"> <div class="input-group col-sm-4 input-group-sm">
<span class="input-group-addon">secret</span> <span class="input-group-addon">secret</span>
<select class="form-control" ng-model="secret.model" ng-options="secret.Name for secret in availableSecrets"> <select class="form-control" ng-model="secret.model" ng-options="secret.Name for secret in availableSecrets">
<option value="" selected="selected">Select a secret</option> <option value="" selected="selected">Select a secret</option>
</select> </select>
</div> </div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeSecret($index)"> <div class="input-group col-sm-4 input-group-sm" ng-if="applicationState.endpoint.apiVersion >= 1.30 && secret.overrideTarget">
<i class="fa fa-trash" aria-hidden="true"></i> <span class="input-group-addon">target</span>
</button> <input class="form-control" ng-model="secret.target" placeholder="/path/in/container">
</div>
<div class="input-group col-sm-3 input-group-sm">
<div class="btn-group btn-group-sm" ng-if="applicationState.endpoint.apiVersion >= 1.30">
<label class="btn btn-primary" ng-model="secret.overrideTarget" uib-btn-radio="false">Default location</label>
<label class="btn btn-primary" ng-model="secret.overrideTarget" uib-btn-radio="true">Override</label>
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeSecret($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -67,7 +67,7 @@ function ($scope, $state, $document, StackService, CodeMirrorService, Authentica
var accessControlData = $scope.formValues.AccessControlData; var accessControlData = $scope.formValues.AccessControlData;
var userDetails = Authentication.getUserDetails(); var userDetails = Authentication.getUserDetails();
var isAdmin = userDetails.role === 1 ? true : false; var isAdmin = userDetails.role === 1;
var userId = userDetails.ID; var userId = userDetails.ID;
if (!validateForm(accessControlData, isAdmin)) { if (!validateForm(accessControlData, isAdmin)) {

View File

@ -43,7 +43,7 @@ function ($q, $scope, $state, VolumeService, PluginService, ResourceControlServi
var volumeConfiguration = VolumeService.createVolumeConfiguration(name, driver, driverOptions); var volumeConfiguration = VolumeService.createVolumeConfiguration(name, driver, driverOptions);
var accessControlData = $scope.formValues.AccessControlData; var accessControlData = $scope.formValues.AccessControlData;
var userDetails = Authentication.getUserDetails(); var userDetails = Authentication.getUserDetails();
var isAdmin = userDetails.role === 1 ? true : false; var isAdmin = userDetails.role === 1;
if (!validateForm(accessControlData, isAdmin)) { if (!validateForm(accessControlData, isAdmin)) {
return; return;

View File

@ -128,7 +128,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr dir-paginate="endpoint in (state.filteredEndpoints = (endpoints | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))"> <tr dir-paginate="endpoint in (state.filteredEndpoints = (endpoints | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))" ng-class="{active: endpoint.Checked}">
<td ng-if="applicationState.application.endpointManagement"><input type="checkbox" ng-model="endpoint.Checked" ng-change="selectItem(endpoint)" /></td> <td ng-if="applicationState.application.endpointManagement"><input type="checkbox" ng-model="endpoint.Checked" ng-change="selectItem(endpoint)" /></td>
<td><i class="fa fa-star" aria-hidden="true" ng-if="endpoint.Id === activeEndpoint.Id"></i> {{ endpoint.Name }}</td> <td><i class="fa fa-star" aria-hidden="true" ng-if="endpoint.Id === activeEndpoint.Id"></i> {{ endpoint.Name }}</td>
<td>{{ endpoint.URL | stripprotocol }}</td> <td>{{ endpoint.URL | stripprotocol }}</td>

View File

@ -122,7 +122,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr dir-paginate="image in (state.filteredImages = (images | filter:{ ContainerCount: state.containersCountFilter } | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))"> <tr dir-paginate="image in (state.filteredImages = (images | filter:{ ContainerCount: state.containersCountFilter } | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))" ng-class="{active: image.Checked}">
<td><input type="checkbox" ng-model="image.Checked" ng-change="selectItem(image)" /></td> <td><input type="checkbox" ng-model="image.Checked" ng-change="selectItem(image)" /></td>
<td> <td>
<a class="monospaced" ui-sref="image({id: image.Id})">{{ image.Id|truncate:20}}</a> <a class="monospaced" ui-sref="image({id: image.Id})">{{ image.Id|truncate:20}}</a>

View File

@ -98,7 +98,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr dir-paginate="network in ( state.filteredNetworks = (networks | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))"> <tr dir-paginate="network in ( state.filteredNetworks = (networks | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))" ng-class="{active: network.Checked}">
<td><input type="checkbox" ng-model="network.Checked" ng-change="selectItem(network)"/></td> <td><input type="checkbox" ng-model="network.Checked" ng-change="selectItem(network)"/></td>
<td><a ui-sref="network({id: network.Id})">{{ network.Name | truncate:40 }}</a></td> <td><a ui-sref="network({id: network.Id})">{{ network.Name | truncate:40 }}</a></td>
<td>{{ network.StackName ? network.StackName : '-' }}</td> <td>{{ network.StackName ? network.StackName : '-' }}</td>

View File

@ -118,7 +118,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr dir-paginate="registry in (state.filteredRegistries = (registries | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))"> <tr dir-paginate="registry in (state.filteredRegistries = (registries | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))" ng-class="{active: registry.Checked}">
<td><input type="checkbox" ng-model="registry.Checked" ng-change="selectItem(registry)" /></td> <td><input type="checkbox" ng-model="registry.Checked" ng-change="selectItem(registry)" /></td>
<td> <td>
<a ui-sref="registry({id: registry.Id})">{{ registry.Name }}</a> <a ui-sref="registry({id: registry.Id})">{{ registry.Name }}</a>

View File

@ -51,7 +51,7 @@
</td> </td>
<td> <td>
<p class="small text-muted" style="margin-top: 10px;"> <p class="small text-muted" style="margin-top: 10px;">
The time window used to evaluate the restart policy (default value is 0, which is unbounded). The time window used to evaluate the restart policy (default value is 0, which is unbounded). Time in seconds.
</p> </p>
</td> </td>
</tr> </tr>

View File

@ -5,10 +5,18 @@
<rd-widget-body classes="no-padding"> <rd-widget-body classes="no-padding">
<div class="form-inline" style="padding: 10px;"> <div class="form-inline" style="padding: 10px;">
Add a secret: Add a secret:
<select class="form-control" ng-options="secret.Name for secret in secrets" ng-model="newSecret"> <select class="form-control" ng-options="secret.Name for secret in secrets" ng-model="state.addSecret.secret">
<option selected disabled hidden value="">Select a secret</option> <option selected disabled hidden value="">Select a secret</option>
</select> </select>
<a class="btn btn-default btn-sm" ng-click="addSecret(service, newSecret)"> <div class="form-group" ng-if="applicationState.endpoint.apiVersion >= 1.30 && state.addSecret.override">
Target:
<input class="form-control" ng-model="state.addSecret.target" placeholder="/path/in/container">
</div>
<div class="btn-group btn-group-sm" ng-if="applicationState.endpoint.apiVersion >= 1.30">
<label class="btn btn-primary" ng-model="state.addSecret.override" uib-btn-radio="false">Default location</label>
<label class="btn btn-primary" ng-model="state.addSecret.override" uib-btn-radio="true">Override</label>
</div>
<a class="btn btn-default btn-sm" ng-click="addSecret(service, state.addSecret)">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add secret <i class="fa fa-plus-circle" aria-hidden="true"></i> add secret
</a> </a>
</div> </div>

View File

@ -23,7 +23,7 @@
</td> </td>
<td> <td>
<p class="small text-muted" style="margin-top: 10px;"> <p class="small text-muted" style="margin-top: 10px;">
Amount of time between updates. Amount of time between updates. Time in seconds.
</p> </p>
</td> </td>
</tr> </tr>

View File

@ -68,7 +68,8 @@
<tr> <tr>
<td>Image</td> <td>Image</td>
<td> <td>
<input type="text" class="form-control" ng-model="service.Image" ng-change="updateServiceAttribute(service, 'Image')" ng-disabled="isUpdating" /> <input type="text" class="form-control" uib-typeahead="image for image in availableImages | filter:$viewValue | limitTo:5"
ng-model="service.Image" ng-change="updateServiceAttribute(service, 'Image')" id="image_name" ng-disabled="isUpdating">
</td> </td>
</tr> </tr>
<tr ng-if="applicationState.endpoint.apiVersion >= 1.30"> <tr ng-if="applicationState.endpoint.apiVersion >= 1.30">

View File

@ -1,12 +1,13 @@
angular.module('service', []) angular.module('service', [])
.controller('ServiceController', ['$q', '$scope', '$transition$', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'ConfigService', 'ConfigHelper', 'SecretService', 'SecretHelper', 'Service', 'ServiceHelper', 'LabelHelper', 'TaskService', 'NodeService', 'Notifications', 'Pagination', 'ModalService', .controller('ServiceController', ['$q', '$scope', '$transition$', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'ConfigService', 'ConfigHelper', 'SecretService', 'ImageService', 'SecretHelper', 'Service', 'ServiceHelper', 'LabelHelper', 'TaskService', 'NodeService', 'Notifications', 'Pagination', 'ModalService',
function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, ServiceService, ConfigService, ConfigHelper, SecretService, SecretHelper, Service, ServiceHelper, LabelHelper, TaskService, NodeService, Notifications, Pagination, ModalService) { function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll, ServiceService, ConfigService, ConfigHelper, SecretService, ImageService, SecretHelper, Service, ServiceHelper, LabelHelper, TaskService, NodeService, Notifications, Pagination, ModalService) {
$scope.state = {}; $scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('service_tasks'); $scope.state.pagination_count = Pagination.getPaginationCount('service_tasks');
$scope.tasks = []; $scope.tasks = [];
$scope.sortType = 'Updated'; $scope.sortType = 'Updated';
$scope.sortReverse = true; $scope.sortReverse = true;
$scope.availableImages = [];
$scope.lastVersion = 0; $scope.lastVersion = 0;
@ -74,10 +75,16 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
$scope.updateConfig = function updateConfig(service) { $scope.updateConfig = function updateConfig(service) {
updateServiceArray(service, 'ServiceConfigs', service.ServiceConfigs); updateServiceArray(service, 'ServiceConfigs', service.ServiceConfigs);
}; };
$scope.addSecret = function addSecret(service, secret) { $scope.addSecret = function addSecret(service, newSecret) {
if (secret && service.ServiceSecrets.filter(function(serviceSecret) { return serviceSecret.Id === secret.Id;}).length === 0) { if (newSecret.secret) {
service.ServiceSecrets.push({ Id: secret.Id, Name: secret.Name, FileName: secret.Name, Uid: '0', Gid: '0', Mode: 444 }); var filename = newSecret.secret.Name;
updateServiceArray(service, 'ServiceSecrets', service.ServiceSecrets); if (newSecret.override) {
filename = newSecret.target;
}
if (service.ServiceSecrets.filter(function(serviceSecret) { return serviceSecret.Id === newSecret.secret.Id && serviceSecret.FileName === filename;}).length === 0) {
service.ServiceSecrets.push({ Id: newSecret.secret.Id, Name: newSecret.secret.Name, FileName: filename, Uid: '0', Gid: '0', Mode: 444 });
updateServiceArray(service, 'ServiceSecrets', service.ServiceSecrets);
}
} }
}; };
$scope.removeSecret = function removeSecret(service, index) { $scope.removeSecret = function removeSecret(service, index) {
@ -237,16 +244,16 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
config.UpdateConfig = { config.UpdateConfig = {
Parallelism: service.UpdateParallelism, Parallelism: service.UpdateParallelism,
Delay: service.UpdateDelay, Delay: service.UpdateDelay * 1000000000,
FailureAction: service.UpdateFailureAction, FailureAction: service.UpdateFailureAction,
Order: service.UpdateOrder Order: service.UpdateOrder
}; };
config.TaskTemplate.RestartPolicy = { config.TaskTemplate.RestartPolicy = {
Condition: service.RestartCondition, Condition: service.RestartCondition,
Delay: service.RestartDelay, Delay: service.RestartDelay * 1000000000,
MaxAttempts: service.RestartMaxAttempts, MaxAttempts: service.RestartMaxAttempts,
Window: service.RestartWindow Window: service.RestartWindow * 1000000000
}; };
if (service.Ports) { if (service.Ports) {
@ -313,6 +320,12 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
service.LimitMemoryBytes = service.LimitMemoryBytes / 1024 / 1024 || 0; service.LimitMemoryBytes = service.LimitMemoryBytes / 1024 / 1024 || 0;
service.ReservationMemoryBytes = service.ReservationMemoryBytes / 1024 / 1024 || 0; service.ReservationMemoryBytes = service.ReservationMemoryBytes / 1024 / 1024 || 0;
} }
function transformDurations(service) {
service.RestartDelay = service.RestartDelay / 1000000000 || 5;
service.RestartWindow = service.RestartWindow / 1000000000 || 0;
service.UpdateDelay = service.UpdateDelay / 1000000000 || 0;
}
function initView() { function initView() {
var apiVersion = $scope.applicationState.endpoint.apiVersion; var apiVersion = $scope.applicationState.endpoint.apiVersion;
@ -326,6 +339,7 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
transformResources(service); transformResources(service);
translateServiceArrays(service); translateServiceArrays(service);
transformDurations(service);
$scope.service = service; $scope.service = service;
originalService = angular.copy(service); originalService = angular.copy(service);
@ -333,7 +347,8 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
tasks: TaskService.tasks({ service: [service.Name] }), tasks: TaskService.tasks({ service: [service.Name] }),
nodes: NodeService.nodes(), nodes: NodeService.nodes(),
secrets: apiVersion >= 1.25 ? SecretService.secrets() : [], secrets: apiVersion >= 1.25 ? SecretService.secrets() : [],
configs: apiVersion >= 1.30 ? ConfigService.configs() : [] configs: apiVersion >= 1.30 ? ConfigService.configs() : [],
availableImages: ImageService.images()
}); });
}) })
.then(function success(data) { .then(function success(data) {
@ -341,6 +356,7 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
$scope.nodes = data.nodes; $scope.nodes = data.nodes;
$scope.configs = data.configs; $scope.configs = data.configs;
$scope.secrets = data.secrets; $scope.secrets = data.secrets;
$scope.availableImages = ImageService.getUniqueTagListFromImages(data.availableImages);
// Set max cpu value // Set max cpu value
var maxCpus = 0; var maxCpus = 0;
@ -355,6 +371,9 @@ function ($q, $scope, $transition$, $state, $location, $timeout, $anchorScroll,
$scope.state.sliderMaxCpu = 32; $scope.state.sliderMaxCpu = 32;
} }
// Default values
$scope.state.addSecret = {override: false};
$timeout(function() { $timeout(function() {
$anchorScroll(); $anchorScroll();
}); });

View File

@ -87,7 +87,7 @@
</th> </th>
</thead> </thead>
<tbody> <tbody>
<tr dir-paginate="service in (state.filteredServices = ( services | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))"> <tr dir-paginate="service in (state.filteredServices = ( services | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))" ng-class="{active: service.Checked}">
<td><input type="checkbox" ng-model="service.Checked" ng-change="selectItem(service)"/></td> <td><input type="checkbox" ng-model="service.Checked" ng-change="selectItem(service)"/></td>
<td><a ui-sref="service({id: service.Id})">{{ service.Name }}</a></td> <td><a ui-sref="service({id: service.Id})">{{ service.Name }}</a></td>
<td>{{ service.StackName ? service.StackName : '-' }}</td> <td>{{ service.StackName ? service.StackName : '-' }}</td>
@ -107,7 +107,7 @@
</span> </span>
</td> </td>
<td> <td>
<a ng-if="service.Ports && service.Ports.length > 0 && swarmManagerIP" ng-repeat="p in service.Ports" class="image-tag" ng-href="http://{{swarmManagerIP}}:{{p.PublishedPort}}" target="_blank"> <a ng-if="service.Ports && service.Ports.length > 0 && swarmManagerIP && p.PublishedPort" ng-repeat="p in service.Ports" class="image-tag" ng-href="http://{{swarmManagerIP}}:{{p.PublishedPort}}" target="_blank">
<i class="fa fa-external-link" aria-hidden="true"></i> {{ p.PublishedPort }}:{{ p.TargetPort }} <i class="fa fa-external-link" aria-hidden="true"></i> {{ p.PublishedPort }}:{{ p.TargetPort }}
</a> </a>
<span ng-if="!service.Ports || service.Ports.length === 0 || !swarmManagerIP" >-</span> <span ng-if="!service.Ports || service.Ports.length === 0 || !swarmManagerIP" >-</span>

View File

@ -9,6 +9,43 @@
<rd-widget-header icon="fa-cogs" title="Application settings"></rd-widget-header> <rd-widget-header icon="fa-cogs" title="Application settings"></rd-widget-header>
<rd-widget-body> <rd-widget-body>
<form class="form-horizontal"> <form class="form-horizontal">
<div class="form-group">
<div class="col-sm-12">
<label for="toggle_donation" class="control-label text-left">
Disable donation header
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" name="toggle_donation" ng-model="formValues.donationHeader"><i></i>
</label>
</div>
</div>
<!-- logo -->
<div class="form-group">
<div class="col-sm-12">
<label for="toggle_logo" class="control-label text-left">
Use custom logo
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" name="toggle_logo" ng-model="formValues.customLogo"><i></i>
</label>
</div>
</div>
<div ng-if="formValues.customLogo">
<div class="form-group">
<span class="col-sm-12 text-muted small">
You can specify the URL to your logo here. For an optimal display, logo dimensions should be 155px by 55px.
</span>
</div>
<div class="form-group">
<label for="logo_url" class="col-sm-1 control-label text-left">
URL
</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="settings.LogoURL" id="logo_url" placeholder="https://mycompany.com/logo.png">
</div>
</div>
</div>
<!-- !logo -->
<!-- security --> <!-- security -->
<div class="col-sm-12 form-section-title"> <div class="col-sm-12 form-section-title">
Security Security
@ -36,36 +73,6 @@
</div> </div>
</div> </div>
<!-- security --> <!-- security -->
<!-- logo -->
<div class="col-sm-12 form-section-title">
Logo
</div>
<div class="form-group">
<div class="col-sm-12">
<label for="toggle_logo" class="control-label text-left">
Use custom logo
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" name="toggle_logo" ng-model="formValues.customLogo"><i></i>
</label>
</div>
</div>
<div ng-if="formValues.customLogo">
<div class="form-group">
<span class="col-sm-12 text-muted small">
You can specify the URL to your logo here. For an optimal display, logo dimensions should be 155px by 55px.
</span>
</div>
<div class="form-group">
<label for="logo_url" class="col-sm-1 control-label text-left">
URL
</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="settings.LogoURL" id="logo_url" placeholder="https://mycompany.com/logo.png">
</div>
</div>
</div>
<!-- !logo -->
<!-- app-templates --> <!-- app-templates -->
<div class="col-sm-12 form-section-title"> <div class="col-sm-12 form-section-title">
App Templates App Templates

View File

@ -9,6 +9,7 @@ function ($scope, $state, Notifications, SettingsService, StateManager, DEFAULT_
$scope.formValues = { $scope.formValues = {
customLogo: false, customLogo: false,
customTemplates: false, customTemplates: false,
donationHeader: true,
externalContributions: false, externalContributions: false,
restrictBindMounts: false, restrictBindMounts: false,
restrictPrivilegedMode: false, restrictPrivilegedMode: false,
@ -45,6 +46,7 @@ function ($scope, $state, Notifications, SettingsService, StateManager, DEFAULT_
settings.TemplatesURL = DEFAULT_TEMPLATES_URL; settings.TemplatesURL = DEFAULT_TEMPLATES_URL;
} }
settings.DisplayDonationHeader = !$scope.formValues.donationHeader;
settings.DisplayExternalContributors = !$scope.formValues.externalContributions; settings.DisplayExternalContributors = !$scope.formValues.externalContributions;
settings.AllowBindMountsForRegularUsers = !$scope.formValues.restrictBindMounts; settings.AllowBindMountsForRegularUsers = !$scope.formValues.restrictBindMounts;
settings.AllowPrivilegedModeForRegularUsers = !$scope.formValues.restrictPrivilegedMode; settings.AllowPrivilegedModeForRegularUsers = !$scope.formValues.restrictPrivilegedMode;
@ -63,6 +65,7 @@ function ($scope, $state, Notifications, SettingsService, StateManager, DEFAULT_
.then(function success(data) { .then(function success(data) {
Notifications.success('Settings updated'); Notifications.success('Settings updated');
StateManager.updateLogo(settings.LogoURL); StateManager.updateLogo(settings.LogoURL);
StateManager.updateDonationHeader(settings.DisplayDonationHeader);
StateManager.updateExternalContributions(settings.DisplayExternalContributors); StateManager.updateExternalContributions(settings.DisplayExternalContributors);
if (resetForm) { if (resetForm) {
resetFormValues(); resetFormValues();
@ -87,6 +90,7 @@ function ($scope, $state, Notifications, SettingsService, StateManager, DEFAULT_
if (settings.TemplatesURL !== DEFAULT_TEMPLATES_URL) { if (settings.TemplatesURL !== DEFAULT_TEMPLATES_URL) {
$scope.formValues.customTemplates = true; $scope.formValues.customTemplates = true;
} }
$scope.formValues.donationHeader = !settings.DisplayDonationHeader;
$scope.formValues.externalContributions = !settings.DisplayExternalContributors; $scope.formValues.externalContributions = !settings.DisplayExternalContributors;
$scope.formValues.restrictBindMounts = !settings.AllowBindMountsForRegularUsers; $scope.formValues.restrictBindMounts = !settings.AllowBindMountsForRegularUsers;
$scope.formValues.restrictPrivilegedMode = !settings.AllowPrivilegedModeForRegularUsers; $scope.formValues.restrictPrivilegedMode = !settings.AllowPrivilegedModeForRegularUsers;

View File

@ -9,75 +9,75 @@
</div> </div>
<div class="sidebar-content"> <div class="sidebar-content">
<ul class="sidebar"> <ul class="sidebar">
<li class="sidebar-title"> <li class="sidebar-title"><span>Active endpoint</span></li>
<span>Active endpoint</span>
</li>
<li class="sidebar-title"> <li class="sidebar-title">
<select class="select-endpoint form-control" ng-options="endpoint.Name for endpoint in endpoints" ng-model="activeEndpoint" ng-change="switchEndpoint(activeEndpoint)"> <select class="select-endpoint form-control" ng-options="endpoint.Name for endpoint in endpoints" ng-model="activeEndpoint" ng-change="switchEndpoint(activeEndpoint)">
</select> </select>
</li> </li>
<li class="sidebar-title"><span>Endpoint actions</span></li> <li class="sidebar-title"><span>Endpoint actions</span></li>
<li class="sidebar-list"> <li class="sidebar-list">
<a ui-sref="dashboard" ui-sref-active="active">Dashboard <span class="menu-icon fa fa-tachometer"></span></a> <a ui-sref="dashboard" ui-sref-active="active">Dashboard <span class="menu-icon fa fa-tachometer fa-fw"></span></a>
</li> </li>
<li class="sidebar-list"> <li class="sidebar-list">
<a ui-sref="templates" ui-sref-active="active">App Templates <span class="menu-icon fa fa-rocket"></span></a> <a ui-sref="templates" ui-sref-active="active">App Templates <span class="menu-icon fa fa-rocket fa-fw"></span></a>
<div class="sidebar-sublist" ng-if="toggle && displayExternalContributors && ($state.current.name === 'templates' || $state.current.name === 'templates_linuxserver')"> <div class="sidebar-sublist" ng-if="toggle && displayExternalContributors && ($state.current.name === 'templates' || $state.current.name === 'templates_linuxserver')">
<a ui-sref="templates_linuxserver" ui-sref-active="active">LinuxServer.io</a> <a ui-sref="templates_linuxserver" ui-sref-active="active">LinuxServer.io</a>
</div> </div>
</li> </li>
<li class="sidebar-list" ng-if="applicationState.endpoint.apiVersion >= 1.25 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'"> <li class="sidebar-list" ng-if="applicationState.endpoint.apiVersion >= 1.25 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
<a ui-sref="stacks" ui-sref-active="active">Stacks <span class="menu-icon fa fa-th-list"></span></a> <a ui-sref="stacks" ui-sref-active="active">Stacks <span class="menu-icon fa fa-th-list fa-fw"></span></a>
</li> </li>
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'"> <li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
<a ui-sref="services" ui-sref-active="active">Services <span class="menu-icon fa fa-list-alt"></span></a> <a ui-sref="services" ui-sref-active="active">Services <span class="menu-icon fa fa-list-alt fa-fw"></span></a>
</li> </li>
<li class="sidebar-list"> <li class="sidebar-list">
<a ui-sref="containers" ui-sref-active="active">Containers <span class="menu-icon fa fa-server"></span></a> <a ui-sref="containers" ui-sref-active="active">Containers <span class="menu-icon fa fa-server fa-fw"></span></a>
</li> </li>
<li class="sidebar-list"> <li class="sidebar-list">
<a ui-sref="images" ui-sref-active="active">Images <span class="menu-icon fa fa-clone"></span></a> <a ui-sref="images" ui-sref-active="active">Images <span class="menu-icon fa fa-clone fa-fw"></span></a>
</li> </li>
<li class="sidebar-list"> <li class="sidebar-list">
<a ui-sref="networks" ui-sref-active="active">Networks <span class="menu-icon fa fa-sitemap"></span></a> <a ui-sref="networks" ui-sref-active="active">Networks <span class="menu-icon fa fa-sitemap fa-fw"></span></a>
</li> </li>
<li class="sidebar-list"> <li class="sidebar-list">
<a ui-sref="volumes" ui-sref-active="active">Volumes <span class="menu-icon fa fa-cubes"></span></a> <a ui-sref="volumes" ui-sref-active="active">Volumes <span class="menu-icon fa fa-cubes fa-fw"></span></a>
</li> </li>
<li class="sidebar-list" ng-if="applicationState.endpoint.apiVersion >= 1.30 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'"> <li class="sidebar-list" ng-if="applicationState.endpoint.apiVersion >= 1.30 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
<a ui-sref="configs" ui-sref-active="active">Configs <span class="menu-icon fa fa-file-code-o"></span></a> <a ui-sref="configs" ui-sref-active="active">Configs <span class="menu-icon fa fa-file-code-o fa-fw"></span></a>
</li> </li>
<li class="sidebar-list" ng-if="applicationState.endpoint.apiVersion >= 1.25 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'"> <li class="sidebar-list" ng-if="applicationState.endpoint.apiVersion >= 1.25 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
<a ui-sref="secrets" ui-sref-active="active">Secrets <span class="menu-icon fa fa-user-secret"></span></a> <a ui-sref="secrets" ui-sref-active="active">Secrets <span class="menu-icon fa fa-user-secret fa-fw"></span></a>
</li> </li>
<li class="sidebar-list" ng-if="(applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC') && isAdmin"> <li class="sidebar-list" ng-if="(applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC') && isAdmin">
<a ui-sref="events" ui-sref-active="active">Events <span class="menu-icon fa fa-history"></span></a> <a ui-sref="events" ui-sref-active="active">Events <span class="menu-icon fa fa-history fa-fw"></span></a>
</li> </li>
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || (applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER')"> <li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || (applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER')">
<a ui-sref="swarm" ui-sref-active="active">Swarm <span class="menu-icon fa fa-object-group"></span></a> <a ui-sref="swarm" ui-sref-active="active">Swarm <span class="menu-icon fa fa-object-group fa-fw"></span></a>
</li> </li>
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC'"> <li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC'">
<a ui-sref="engine" ui-sref-active="active">Engine <span class="menu-icon fa fa-th"></span></a> <a ui-sref="engine" ui-sref-active="active">Engine <span class="menu-icon fa fa-th fa-fw"></span></a>
</li> </li>
<li class="sidebar-title" ng-if="!applicationState.application.authentication || isAdmin || isTeamLeader"> <li class="sidebar-title" ng-if="!applicationState.application.authentication || isAdmin || isTeamLeader">
<span>Portainer settings</span> <span>Portainer settings</span>
</li> </li>
<li class="sidebar-list" ng-if="applicationState.application.authentication && (isAdmin || isTeamLeader)"> <li class="sidebar-list" ng-if="applicationState.application.authentication && (isAdmin || isTeamLeader)">
<a ui-sref="users" ui-sref-active="active">User management <span class="menu-icon fa fa-users"></span></a> <a ui-sref="users" ui-sref-active="active">User management <span class="menu-icon fa fa-users fa-fw"></span></a>
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'users' || $state.current.name === 'user' || $state.current.name === 'teams' || $state.current.name === 'team')"> <div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'users' || $state.current.name === 'user' || $state.current.name === 'teams' || $state.current.name === 'team')">
<a ui-sref="teams" ui-sref-active="active">Teams</a> <a ui-sref="teams" ui-sref-active="active">Teams</a>
</div> </div>
</li> </li>
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin"> <li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
<a ui-sref="endpoints" ui-sref-active="active">Endpoints <span class="menu-icon fa fa-plug"></span></a> <a ui-sref="endpoints" ui-sref-active="active">Endpoints <span class="menu-icon fa fa-plug fa-fw"></span></a>
</li> </li>
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin"> <li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
<a ui-sref="registries" ui-sref-active="active">Registries <span class="menu-icon fa fa-database"></span></a> <a ui-sref="registries" ui-sref-active="active">Registries <span class="menu-icon fa fa-database fa-fw"></span></a>
</li> </li>
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin"> <li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
<a ui-sref="settings" ui-sref-active="active">Settings <span class="menu-icon fa fa-cogs"></span></a> <a ui-sref="settings" ui-sref-active="active">Settings <span class="menu-icon fa fa-cogs fa-fw"></span></a>
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'settings' || $state.current.name === 'settings_authentication') && applicationState.application.authentication && isAdmin"> <div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'settings' || $state.current.name === 'settings_authentication' || $state.current.name === 'settings_about') && applicationState.application.authentication && isAdmin">
<a ui-sref="settings_authentication" ui-sref-active="active">Authentication</a> <a ui-sref="settings_authentication" ui-sref-active="active">Authentication</a></div>
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'settings' || $state.current.name === 'settings_authentication' || $state.current.name === 'settings_about')">
<a ui-sref="settings_about" ui-sref-active="active">About</a>
</div> </div>
</li> </li>
</ul> </ul>

View File

@ -84,7 +84,7 @@
</th> </th>
</thead> </thead>
<tbody> <tbody>
<tr dir-paginate="stack in (state.filteredStacks = ( stacks | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))" ng-if="state.DisplayExternalStacks || (!state.DisplayExternalStacks && !stack.External)"> <tr dir-paginate="stack in (state.filteredStacks = ( stacks | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))" ng-if="state.DisplayExternalStacks || (!state.DisplayExternalStacks && !stack.External)" ng-class="{active: stacks.Checked}">
<td><input type="checkbox" ng-model="stack.Checked" ng-change="selectItem(stack)" ng-disabled="!stack.Id"/></td> <td><input type="checkbox" ng-model="stack.Checked" ng-change="selectItem(stack)" ng-disabled="!stack.Id"/></td>
<td> <td>
<span ng-if="stack.Id"> <span ng-if="stack.Id">

View File

@ -106,7 +106,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr dir-paginate="team in (state.filteredTeams = (teams | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))"> <tr dir-paginate="team in (state.filteredTeams = (teams | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))" ng-class="{active: team.Checked}">
<td ng-if="isAdmin"><input type="checkbox" ng-model="team.Checked" ng-change="selectItem(team)" /></td> <td ng-if="isAdmin"><input type="checkbox" ng-model="team.Checked" ng-change="selectItem(team)" /></td>
<td>{{ team.Name }}</td> <td>{{ team.Name }}</td>
<td> <td>

View File

@ -356,8 +356,8 @@
<div class="form-group" style="margin-bottom: 0"> <div class="form-group" style="margin-bottom: 0">
<div class="boxselector_wrapper"> <div class="boxselector_wrapper">
<div ng-click="updateCategories(templates, state.filters.Type)"> <div ng-click="updateCategories(templates, state.filters.Type)">
<input type="radio" id="registry_quay" ng-model="state.filters.Type" value="stack"> <input type="radio" id="template_stack" ng-model="state.filters.Type" value="stack">
<label for="registry_quay"> <label for="template_stack">
<div class="boxselector_header"> <div class="boxselector_header">
<i class="fa fa-th-list" aria-hidden="true" style="margin-right: 2px;"></i> <i class="fa fa-th-list" aria-hidden="true" style="margin-right: 2px;"></i>
Stack Stack
@ -366,8 +366,8 @@
</label> </label>
</div> </div>
<div ng-click="updateCategories(templates, state.filters.Type)"> <div ng-click="updateCategories(templates, state.filters.Type)">
<input type="radio" id="registry_custom" ng-model="state.filters.Type" value="container"> <input type="radio" id="template_container" ng-model="state.filters.Type" value="container">
<label for="registry_custom"> <label for="template_container">
<div class="boxselector_header"> <div class="boxselector_header">
<i class="fa fa-server" aria-hidden="true" style="margin-right: 2px;"></i> <i class="fa fa-server" aria-hidden="true" style="margin-right: 2px;"></i>
Container Container

View File

@ -122,7 +122,7 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer
var userDetails = Authentication.getUserDetails(); var userDetails = Authentication.getUserDetails();
var userId = userDetails.ID; var userId = userDetails.ID;
var accessControlData = $scope.formValues.AccessControlData; var accessControlData = $scope.formValues.AccessControlData;
var isAdmin = userDetails.role === 1 ? true : false; var isAdmin = userDetails.role === 1;
if (!validateForm(accessControlData, isAdmin)) { if (!validateForm(accessControlData, isAdmin)) {
return; return;
@ -241,12 +241,13 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer
$scope.templatesKey = templatesKey; $scope.templatesKey = templatesKey;
var userDetails = Authentication.getUserDetails(); var userDetails = Authentication.getUserDetails();
$scope.isAdmin = userDetails.role === 1 ? true : false; $scope.isAdmin = userDetails.role === 1;
var endpointMode = $scope.applicationState.endpoint.mode; var endpointMode = $scope.applicationState.endpoint.mode;
var apiVersion = $scope.applicationState.endpoint.apiVersion; var apiVersion = $scope.applicationState.endpoint.apiVersion;
if (endpointMode.provider === 'DOCKER_SWARM_MODE' && endpointMode.role === 'MANAGER' && apiVersion >= 1.25) { if (templatesKey !== 'linuxserver.io'
&& endpointMode.provider === 'DOCKER_SWARM_MODE' && endpointMode.role === 'MANAGER' && apiVersion >= 1.25) {
$scope.state.filters.Type = 'stack'; $scope.state.filters.Type = 'stack';
$scope.state.showDeploymentSelector = true; $scope.state.showDeploymentSelector = true;
} }

View File

@ -168,7 +168,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr dir-paginate="user in (state.filteredUsers = (users | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))"> <tr dir-paginate="user in (state.filteredUsers = (users | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))" ng-class="{active: user.Checked}">
<td ng-if="isAdmin"><input type="checkbox" ng-model="user.Checked" ng-change="selectItem(user)" /></td> <td ng-if="isAdmin"><input type="checkbox" ng-model="user.Checked" ng-change="selectItem(user)" /></td>
<td>{{ user.Username }}</td> <td>{{ user.Username }}</td>
<td> <td>

View File

@ -87,7 +87,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr dir-paginate="volume in (state.filteredVolumes = (volumes | filter:{dangling: state.danglingVolumesOnly} | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))"> <tr dir-paginate="volume in (state.filteredVolumes = (volumes | filter:{dangling: state.danglingVolumesOnly} | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))" ng-class="{active: volume.Checked}">
<td><input type="checkbox" ng-model="volume.Checked" ng-change="selectItem(volume)"/></td> <td><input type="checkbox" ng-model="volume.Checked" ng-change="selectItem(volume)"/></td>
<td> <td>
<a ui-sref="volume({id: volume.Id})" class="monospaced">{{ volume.Id|truncate:25 }}</a> <a ui-sref="volume({id: volume.Id})" class="monospaced">{{ volume.Id|truncate:25 }}</a>

View File

@ -1,6 +1,6 @@
angular angular
.module('portainer') .module('portainer')
.directive('rdHeaderTitle', ['Authentication', function rdHeaderTitle(Authentication) { .directive('rdHeaderTitle', ['Authentication', 'StateManager', function rdHeaderTitle(Authentication, StateManager) {
var directive = { var directive = {
requires: '^rdHeader', requires: '^rdHeader',
scope: { scope: {
@ -8,9 +8,10 @@ angular
}, },
link: function (scope, iElement, iAttrs) { link: function (scope, iElement, iAttrs) {
scope.username = Authentication.getUserDetails().username; scope.username = Authentication.getUserDetails().username;
scope.displayDonationHeader = StateManager.getState().application.displayDonationHeader;
}, },
transclude: true, transclude: true,
template: '<div class="page white-space-normal">{{title}}<span class="header_title_content" ng-transclude></span><span class="pull-right user-box" ng-if="username"><i class="fa fa-user-circle-o" aria-hidden="true"></i> {{username}}</span></div>', template: '<div class="page white-space-normal">{{title}}<span class="header_title_content" ng-transclude></span><span class="pull-right user-box" ng-if="username"><i class="fa fa-user-circle-o" aria-hidden="true"></i> {{username}}</span><a ng-if="displayDonationHeader" ui-sref="settings_about" class="pull-right" style="font-size:14px;margin-right:15px;margin-top:2px;"><span class="fa fa-heart fa-fw red-icon"></span> Help support portainer</a></div>',
restrict: 'E' restrict: 'E'
}; };
return directive; return directive;

View File

@ -3,6 +3,7 @@ angular.module('portainer').component('porImageRegistry', {
controller: 'porImageRegistryController', controller: 'porImageRegistryController',
bindings: { bindings: {
'image': '=', 'image': '=',
'registry': '=' 'registry': '=',
'autoComplete': '<'
} }
}); });

View File

@ -1,12 +1,14 @@
<div> <div>
<label for="image_name" class="col-sm-1 control-label text-left">Name</label> <label for="image_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11 col-md-6"> <div class="col-sm-11 col-md-6">
<input type="text" class="form-control" ng-model="$ctrl.image" id="image_name" placeholder="e.g. myImage:myTag"> <input type="text" class="form-control" uib-typeahead="image for image in $ctrl.availableImages | filter:$viewValue | limitTo:5"
ng-model="$ctrl.image" id="image_name" placeholder="e.g. myImage:myTag">
</div> </div>
<label for="image_registry" class="col-sm-2 col-md-1 margin-sm-top control-label text-left"> <label for="image_registry" class="col-sm-2 col-md-1 margin-sm-top control-label text-left">
Registry Registry
</label> </label>
<div class="col-sm-10 col-md-4 margin-sm-top"> <div class="col-sm-10 col-md-4 margin-sm-top">
<select ng-options="registry as registry.Name for registry in $ctrl.availableRegistries" ng-model="$ctrl.registry" id="image_registry" class="form-control"></select> <select ng-options="registry as registry.Name for registry in $ctrl.availableRegistries" ng-model="$ctrl.registry" id="image_registry"
class="form-control"></select>
</div> </div>
</div> </div>

View File

@ -1,27 +1,29 @@
angular.module('portainer') angular.module('portainer')
.controller('porImageRegistryController', ['$q', 'RegistryService', 'DockerHubService', 'Notifications', .controller('porImageRegistryController', ['$q', 'RegistryService', 'DockerHubService', 'ImageService', 'Notifications',
function ($q, RegistryService, DockerHubService, Notifications) { function ($q, RegistryService, DockerHubService, ImageService, Notifications) {
var ctrl = this; var ctrl = this;
function initComponent() { function initComponent() {
$q.all({ $q.all({
registries: RegistryService.registries(), registries: RegistryService.registries(),
dockerhub: DockerHubService.dockerhub() dockerhub: DockerHubService.dockerhub(),
}) availableImages: ctrl.autoComplete ? ImageService.images() : []
.then(function success(data) { })
var dockerhub = data.dockerhub; .then(function success(data) {
var registries = data.registries; var dockerhub = data.dockerhub;
ctrl.availableRegistries = [dockerhub].concat(registries); var registries = data.registries;
if (!ctrl.registry.Id) { ctrl.availableImages = ImageService.getUniqueTagListFromImages(data.availableImages);
ctrl.registry = dockerhub; ctrl.availableRegistries = [dockerhub].concat(registries);
} else { if (!ctrl.registry.Id) {
ctrl.registry = _.find(ctrl.availableRegistries, { 'Id': ctrl.registry.Id }); ctrl.registry = dockerhub;
} else {
ctrl.registry = _.find(ctrl.availableRegistries, { 'Id': ctrl.registry.Id });
}
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve registries');
});
} }
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve registries');
});
}
initComponent(); initComponent();
}]); }]);

View File

@ -2,6 +2,7 @@ function SettingsViewModel(data) {
this.TemplatesURL = data.TemplatesURL; this.TemplatesURL = data.TemplatesURL;
this.LogoURL = data.LogoURL; this.LogoURL = data.LogoURL;
this.BlackListedLabels = data.BlackListedLabels; this.BlackListedLabels = data.BlackListedLabels;
this.DisplayDonationHeader = data.DisplayDonationHeader;
this.DisplayExternalContributors = data.DisplayExternalContributors; this.DisplayExternalContributors = data.DisplayExternalContributors;
this.AuthenticationMethod = data.AuthenticationMethod; this.AuthenticationMethod = data.AuthenticationMethod;
this.LDAPSettings = data.LDAPSettings; this.LDAPSettings = data.LDAPSettings;

View File

@ -1,4 +1,5 @@
function TemplateLSIOViewModel(data) { function TemplateLSIOViewModel(data) {
this.Type = data.type;
this.Title = data.title; this.Title = data.title;
this.Note = data.description; this.Note = data.description;
this.Categories = data.category ? data.category : []; this.Categories = data.category ? data.category : [];

View File

@ -31,13 +31,13 @@ function ServiceViewModel(data, runningTasks, allTasks, nodes) {
} }
if (data.Spec.TaskTemplate.RestartPolicy) { if (data.Spec.TaskTemplate.RestartPolicy) {
this.RestartCondition = data.Spec.TaskTemplate.RestartPolicy.Condition; this.RestartCondition = data.Spec.TaskTemplate.RestartPolicy.Condition || 'any';
this.RestartDelay = data.Spec.TaskTemplate.RestartPolicy.Delay; this.RestartDelay = data.Spec.TaskTemplate.RestartPolicy.Delay || 5000000000;
this.RestartMaxAttempts = data.Spec.TaskTemplate.RestartPolicy.MaxAttempts; this.RestartMaxAttempts = data.Spec.TaskTemplate.RestartPolicy.MaxAttempts || 0;
this.RestartWindow = data.Spec.TaskTemplate.RestartPolicy.Window; this.RestartWindow = data.Spec.TaskTemplate.RestartPolicy.Window || 0;
} else { } else {
this.RestartCondition = 'none'; this.RestartCondition = 'any';
this.RestartDelay = 0; this.RestartDelay = 5000000000;
this.RestartMaxAttempts = 0; this.RestartMaxAttempts = 0;
this.RestartWindow = 0; this.RestartWindow = 0;
} }

View File

@ -537,6 +537,18 @@ function configureRoutes($stateProvider) {
} }
} }
}) })
.state('settings_about', {
url: '^/settings/about',
views: {
'content@': {
templateUrl: 'app/components/about/about.html'
},
'sidebar@': {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('settings_authentication', { .state('settings_authentication', {
url: '^/settings/authentication', url: '^/settings/authentication',
views: { views: {

View File

@ -152,5 +152,14 @@ angular.module('portainer.services')
return deferred.promise; return deferred.promise;
}; };
service.getUniqueTagListFromImages = function (availableImages) {
return _.flatten(_.map(availableImages, function (image) {
_.remove(image.RepoTags, function (item) {
return item.indexOf('<none>') !== -1;
});
return image.RepoTags ? _.uniqWith(image.RepoTags, _.isEqual) : [];
}));
};
return service; return service;
}]); }]);

View File

@ -29,6 +29,11 @@ angular.module('portainer.services')
LocalStorage.storeApplicationState(state.application); LocalStorage.storeApplicationState(state.application);
}; };
manager.updateDonationHeader = function(displayDonationHeader) {
state.application.displayDonationHeader = displayDonationHeader;
LocalStorage.storeApplicationState(state.application);
};
manager.initialize = function () { manager.initialize = function () {
var deferred = $q.defer(); var deferred = $q.defer();
@ -55,6 +60,7 @@ angular.module('portainer.services')
state.application.endpointManagement = status.EndpointManagement; state.application.endpointManagement = status.EndpointManagement;
state.application.version = status.Version; state.application.version = status.Version;
state.application.logo = settings.LogoURL; state.application.logo = settings.LogoURL;
state.application.displayDonationHeader = settings.DisplayDonationHeader;
state.application.displayExternalContributors = settings.DisplayExternalContributors; state.application.displayExternalContributors = settings.DisplayExternalContributors;
LocalStorage.storeApplicationState(state.application); LocalStorage.storeApplicationState(state.application);
deferred.resolve(state); deferred.resolve(state);

View File

@ -368,6 +368,10 @@ ul.sidebar .sidebar-list a.active {
font-size: 90%; font-size: 90%;
} }
ul.sidebar .sidebar-list a.active .menu-icon {
text-indent: 25px;
}
ul.sidebar .sidebar-list .sidebar-sublist a { ul.sidebar .sidebar-list .sidebar-sublist a {
text-indent: 35px; text-indent: 35px;
font-size: 12px; font-size: 12px;

View File

@ -1,6 +1,6 @@
{ {
"name": "portainer", "name": "portainer",
"version": "1.15.2", "version": "1.15.3",
"homepage": "https://github.com/portainer/portainer", "homepage": "https://github.com/portainer/portainer",
"authors": [ "authors": [
"Anthony Lapenna <anthony.lapenna at gmail dot com>" "Anthony Lapenna <anthony.lapenna at gmail dot com>"

View File

@ -1,5 +1,5 @@
Name: portainer Name: portainer
Version: 1.15.2 Version: 1.15.3
Release: 0 Release: 0
License: Zlib License: Zlib
Summary: A lightweight docker management UI Summary: A lightweight docker management UI

View File

@ -55,6 +55,29 @@ module.exports = function (grunt) {
grunt.registerTask('run-dev', ['build', 'shell:run', 'watch:build']); grunt.registerTask('run-dev', ['build', 'shell:run', 'watch:build']);
grunt.registerTask('clear', ['clean:app']); grunt.registerTask('clear', ['clean:app']);
// Load content of `vendor.yml` to src.jsVendor, src.cssVendor and src.angularVendor
grunt.registerTask('vendor', 'vendor:<minified|regular>', function(min) {
// Argument `min` defaults to 'minified'
var minification = (min === '') ? 'minified' : min;
var vendorFile = grunt.file.readYAML('vendor.yml');
for (var filelist in vendorFile) {
if (vendorFile.hasOwnProperty(filelist)) {
var list = vendorFile[filelist][minification];
// Check if any of the files is missing
for (var itemIndex in list) {
if (list.hasOwnProperty(itemIndex)) {
var item = list[itemIndex];
if (!grunt.file.exists(item)) {
grunt.fail.warn('Dependency file ' + item + ' not found.');
}
}
}
// If none is missing, save the list
grunt.config('src.' + filelist + 'Vendor', list);
}
}
});
// Project configuration. // Project configuration.
grunt.initConfig({ grunt.initConfig({
distdir: 'dist/public', distdir: 'dist/public',
@ -232,13 +255,4 @@ module.exports = function (grunt) {
} }
}); });
grunt.registerTask('vendor', 'vendor:<min|reg>', function(min) {
// The content of `vendor.yml` is loaded to src.jsVendor, src.cssVendor and src.angularVendor
// Argument `min` selects between the 'regular' or 'minified' sets
var m = ( min === '' ) ? 'minified' : min;
var v = grunt.file.readYAML('vendor.yml');
for (type in v) { if (v.hasOwnProperty(type)) {
grunt.config('src.'+type+'Vendor',v[type][m]);
}}
});
}; };

View File

@ -2,7 +2,7 @@
"author": "Portainer.io", "author": "Portainer.io",
"name": "portainer", "name": "portainer",
"homepage": "http://portainer.io", "homepage": "http://portainer.io",
"version": "1.15.2", "version": "1.15.3",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git@github.com:portainer/portainer.git" "url": "git@github.com:portainer/portainer.git"

View File

@ -21,7 +21,6 @@ js:
- bower_components/jquery/dist/jquery.min.js - bower_components/jquery/dist/jquery.min.js
- bower_components/bootstrap/dist/js/bootstrap.min.js - bower_components/bootstrap/dist/js/bootstrap.min.js
- bower_components/bootbox.js/bootbox.js - bower_components/bootbox.js/bootbox.js
- bower_components/Chart.js/Chart.min.js
- bower_components/filesize/lib/filesize.min.js - bower_components/filesize/lib/filesize.min.js
- bower_components/lodash/dist/lodash.min.js - bower_components/lodash/dist/lodash.min.js
- bower_components/moment/min/moment.min.js - bower_components/moment/min/moment.min.js