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

Merge branch 'release/1.12.2'

This commit is contained in:
Anthony Lapenna 2017-03-28 15:18:33 +02:00
commit 9e06cfbdf0
61 changed files with 2102 additions and 987 deletions

View file

@ -22,11 +22,13 @@ Some of the open issues are labeled with prefix `exp/`, this is used to mark the
* **beginner**: a task that should be accessible with users not familiar with the codebase * **beginner**: a task that should be accessible with users not familiar with the codebase
* **intermediate**: a task that require some understanding of the project codebase or some experience in * **intermediate**: a task that require some understanding of the project codebase or some experience in
either AngularJS or Golang either AngularJS or Golang
* **advanced**: a task that require a deep understanding of the project codebase
You can have a use Github filters to list these issues: You can have a use Github filters to list these issues:
* beginner labeled issues: https://github.com/portainer/portainer/labels/exp%2Fbeginner * beginner labeled issues: https://github.com/portainer/portainer/labels/exp%2Fbeginner
* intermediate labeled issues: https://github.com/portainer/portainer/labels/exp%2Fintermediate * intermediate labeled issues: https://github.com/portainer/portainer/labels/exp%2Fintermediate
* advanced labeled issues: https://github.com/portainer/portainer/labels/exp%2Fadvanced
### Linting ### Linting

View file

@ -18,7 +18,7 @@
<img src="http://portainer.io/images/screenshots/portainer.gif" width="77%"/> <img src="http://portainer.io/images/screenshots/portainer.gif" width="77%"/>
You can try out the public demo instance: http://demo.portainer.io/ (login with the username **demo** and the password **tryportainer**). You can try out the public demo instance: http://demo.portainer.io/ (login with the username **admin** and the password **tryportainer**).
Please note that the public demo cluster is **reset every 15min**. Please note that the public demo cluster is **reset every 15min**.
@ -43,7 +43,7 @@ Please note that the public demo cluster is **reset every 15min**.
**_Portainer_** has full support for the following Docker versions: **_Portainer_** has full support for the following Docker versions:
* Docker 1.10 to Docker 1.12 (including `swarm-mode`) * Docker 1.10 to Docker 17.03 (including `swarm-mode`)
* Docker Swarm >= 1.2.3 * Docker Swarm >= 1.2.3
Partial support for the following Docker versions (some features may not be available): Partial support for the following Docker versions (some features may not be available):

View file

@ -49,7 +49,7 @@ func NewStore(storePath string) (*Store, error) {
store.ResourceControlService.store = store store.ResourceControlService.store = store
store.VersionService.store = store store.VersionService.store = store
_, err := os.Stat(storePath) _, err := os.Stat(storePath + "/" + databaseFileName)
if err != nil && os.IsNotExist(err) { if err != nil && os.IsNotExist(err) {
store.checkForDataMigration = false store.checkForDataMigration = false
} else if err != nil { } else if err != nil {

View file

@ -11,6 +11,7 @@ import (
"os" "os"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/orcaman/concurrent-map"
) )
// DockerHandler represents an HTTP API handler for proxying requests to the Docker API. // DockerHandler represents an HTTP API handler for proxying requests to the Docker API.
@ -19,7 +20,7 @@ type DockerHandler struct {
Logger *log.Logger Logger *log.Logger
EndpointService portainer.EndpointService EndpointService portainer.EndpointService
ProxyFactory ProxyFactory ProxyFactory ProxyFactory
proxies map[portainer.EndpointID]http.Handler proxies cmap.ConcurrentMap
} }
// NewDockerHandler returns a new instance of DockerHandler. // NewDockerHandler returns a new instance of DockerHandler.
@ -30,7 +31,7 @@ func NewDockerHandler(mw *middleWareService, resourceControlService portainer.Re
ProxyFactory: ProxyFactory{ ProxyFactory: ProxyFactory{
ResourceControlService: resourceControlService, ResourceControlService: resourceControlService,
}, },
proxies: make(map[portainer.EndpointID]http.Handler), proxies: cmap.New(),
} }
h.PathPrefix("/{id}/").Handler( h.PathPrefix("/{id}/").Handler(
mw.authenticated(http.HandlerFunc(h.proxyRequestsToDockerAPI))) mw.authenticated(http.HandlerFunc(h.proxyRequestsToDockerAPI)))
@ -72,13 +73,16 @@ func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r
return return
} }
proxy := handler.proxies[endpointID] var proxy http.Handler
if proxy == nil { item, ok := handler.proxies.Get(string(endpointID))
if !ok {
proxy, err = handler.createAndRegisterEndpointProxy(endpoint) proxy, err = handler.createAndRegisterEndpointProxy(endpoint)
if err != nil { if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger) Error(w, err, http.StatusBadRequest, handler.Logger)
return return
} }
} else {
proxy = item.(http.Handler)
} }
http.StripPrefix("/"+id, proxy).ServeHTTP(w, r) http.StripPrefix("/"+id, proxy).ServeHTTP(w, r)
} }
@ -105,6 +109,6 @@ func (handler *DockerHandler) createAndRegisterEndpointProxy(endpoint *portainer
proxy = handler.ProxyFactory.newSocketProxy(endpointURL.Path) proxy = handler.ProxyFactory.newSocketProxy(endpointURL.Path)
} }
handler.proxies[endpoint.ID] = proxy handler.proxies.Set(string(endpoint.ID), proxy)
return proxy, nil return proxy, nil
} }

View file

@ -20,7 +20,6 @@ type EndpointHandler struct {
authorizeEndpointManagement bool authorizeEndpointManagement bool
EndpointService portainer.EndpointService EndpointService portainer.EndpointService
FileService portainer.FileService FileService portainer.FileService
// server *Server
} }
const ( const (
@ -214,7 +213,7 @@ func (handler *EndpointHandler) handlePutEndpointAccess(w http.ResponseWriter, r
} }
type putEndpointAccessRequest struct { type putEndpointAccessRequest struct {
AuthorizedUsers []int `valid:"required"` AuthorizedUsers []int `valid:"-"`
} }
// handlePutEndpoint handles PUT requests on /endpoints/:id // handlePutEndpoint handles PUT requests on /endpoints/:id

View file

@ -176,7 +176,7 @@ type (
const ( const (
// APIVersion is the version number of Portainer API. // APIVersion is the version number of Portainer API.
APIVersion = "1.12.1" APIVersion = "1.12.2"
// DBVersion is the version number of Portainer database. // DBVersion is the version number of Portainer database.
DBVersion = 1 DBVersion = 1
) )

View file

@ -51,7 +51,7 @@ angular.module('portainer', [
'user', 'user',
'users', 'users',
'volumes']) 'volumes'])
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider) { .config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', '$uibTooltipProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider, $uibTooltipProvider) {
'use strict'; 'use strict';
localStorageServiceProvider localStorageServiceProvider
@ -73,6 +73,13 @@ angular.module('portainer', [
$urlRouterProvider.otherwise('/auth'); $urlRouterProvider.otherwise('/auth');
$uibTooltipProvider.setTriggers({
'mouseenter': 'mouseleave',
'click': 'click',
'focus': 'blur',
'outsideClick': 'outsideClick'
});
$stateProvider $stateProvider
.state('root', { .state('root', {
abstract: true, abstract: true,
@ -566,4 +573,4 @@ angular.module('portainer', [
.constant('ENDPOINTS_ENDPOINT', 'api/endpoints') .constant('ENDPOINTS_ENDPOINT', 'api/endpoints')
.constant('TEMPLATES_ENDPOINT', 'api/templates') .constant('TEMPLATES_ENDPOINT', 'api/templates')
.constant('PAGINATION_MAX_ITEMS', 10) .constant('PAGINATION_MAX_ITEMS', 10)
.constant('UI_VERSION', 'v1.12.1'); .constant('UI_VERSION', 'v1.12.2');

View file

@ -104,6 +104,7 @@ function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Au
else if (data.length === 0 && userDetails.role === 1) { else if (data.length === 0 && userDetails.role === 1) {
$state.go('endpointInit'); $state.go('endpointInit');
} else if (data.length === 0 && userDetails.role === 2) { } else if (data.length === 0 && userDetails.role === 2) {
Authentication.logout();
$scope.authData.error = 'User not allowed. Please contact your administrator.'; $scope.authData.error = 'User not allowed. Please contact your administrator.';
} }
}) })

View file

@ -13,12 +13,12 @@
<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-primary" ng-click="start()" ng-if="!container.State.Running"><i class="fa fa-play space-right" aria-hidden="true"></i>Start</button> <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-danger" ng-click="stop()" ng-if="container.State.Running"><i class="fa fa-stop space-right" aria-hidden="true"></i>Stop</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" ng-click="kill()" ng-if="container.State.Running"><i class="fa fa-bomb space-right" aria-hidden="true"></i>Kill</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-primary" ng-click="restart()" ng-if="container.State.Running"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Restart</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" ng-click="pause()" ng-if="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="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-if="container.State.Paused"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</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-danger" ng-click="remove()" ng-disabled="container.State.Running"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button> <button class="btn btn-danger" ng-click="remove()" ng-disabled="container.State.Running"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
</div> </div>
</rd-widget-body> </rd-widget-body>
@ -101,11 +101,14 @@
<!-- name-and-registry-inputs --> <!-- name-and-registry-inputs -->
<div class="form-group"> <div class="form-group">
<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-7"> <div class="col-sm-11 col-md-6">
<input type="text" class="form-control" ng-model="config.Image" id="image_name" placeholder="e.g. myImage:myTag"> <input type="text" class="form-control" ng-model="config.Image" id="image_name" placeholder="e.g. myImage:myTag">
</div> </div>
<label for="image_registry" class="col-sm-1 control-label text-left">Registry</label> <label for="image_registry" class="col-sm-2 margin-sm-top control-label text-left">
<div class="col-sm-3"> Registry
<portainer-tooltip position="bottom" message="A registry to pull the image from. Leave empty to use the official Docker registry."></portainer-tooltip>
</label>
<div class="col-sm-10 col-md-3 margin-sm-top">
<input type="text" class="form-control" ng-model="config.Registry" id="image_registry" placeholder="optional"> <input type="text" class="form-control" ng-model="config.Registry" id="image_registry" placeholder="optional">
</div> </div>
</div> </div>
@ -119,7 +122,7 @@
<!-- !tag-note --> <!-- !tag-note -->
<div class="form-group"> <div class="form-group">
<div class="col-sm-12"> <div class="col-sm-12">
<button type="button" class="btn btn-default btn-sm" ng-disabled="!config.Image" ng-click="commit()">Create</button> <button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Image" ng-click="commit()">Create</button>
<i id="createImageSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i> <i id="createImageSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div> </div>
</div> </div>

View file

@ -25,15 +25,15 @@
<rd-widget-taskbar classes="col-lg-12"> <rd-widget-taskbar classes="col-lg-12">
<div class="pull-left"> <div class="pull-left">
<div class="btn-group" role="group" aria-label="..."> <div class="btn-group" role="group" aria-label="...">
<button type="button" class="btn btn-primary btn-responsive" ng-click="startAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-play space-right" aria-hidden="true"></i>Start</button> <button type="button" class="btn btn-success btn-responsive" ng-click="startAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-play space-right" aria-hidden="true"></i>Start</button>
<button type="button" class="btn btn-primary btn-responsive" ng-click="stopAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-stop space-right" aria-hidden="true"></i>Stop</button> <button type="button" class="btn btn-danger btn-responsive" ng-click="stopAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-stop space-right" aria-hidden="true"></i>Stop</button>
<button type="button" class="btn btn-primary btn-responsive" ng-click="killAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-bomb space-right" aria-hidden="true"></i>Kill</button> <button type="button" class="btn btn-danger btn-responsive" ng-click="killAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-bomb space-right" aria-hidden="true"></i>Kill</button>
<button type="button" class="btn btn-primary btn-responsive" ng-click="restartAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Restart</button> <button type="button" class="btn btn-primary btn-responsive" ng-click="restartAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Restart</button>
<button type="button" class="btn btn-primary btn-responsive" ng-click="pauseAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-pause space-right" aria-hidden="true"></i>Pause</button> <button type="button" class="btn btn-primary btn-responsive" ng-click="pauseAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-pause space-right" aria-hidden="true"></i>Pause</button>
<button type="button" class="btn btn-primary btn-responsive" ng-click="unpauseAction()" ng-disabled="!state.selectedItemCount"><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"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button>
<button type="button" class="btn btn-danger btn-responsive" ng-click="removeAction()" 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="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
</div> </div>
<a class="btn btn-default btn-responsive" type="button" ui-sref="actions.create.container">Add container</a> <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>
</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>
@ -103,8 +103,8 @@
<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))">
<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><span class="label label-{{ container.Status|containerstatusbadge }}">{{ container.Status }}</span></td> <td><span class="label label-{{ container.Status|containerstatusbadge }}">{{ container.Status }}</span></td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername}}</a></td> <td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername|truncate: 40}}</a></td>
<td ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|containername}}</a></td> <td ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|containername|truncate: 40}}</a></td>
<td><a ui-sref="image({id: container.Image})">{{ container.Image | hideshasum }}</a></td> <td><a ui-sref="image({id: container.Image})">{{ container.Image | hideshasum }}</a></td>
<td ng-if="state.displayIP">{{ container.IP ? container.IP : '-' }}</td> <td ng-if="state.displayIP">{{ container.IP ? container.IP : '-' }}</td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">{{ container.hostIP }}</td> <td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">{{ container.hostIP }}</td>

View file

@ -47,7 +47,7 @@ angular.module('containers', [])
angular.forEach($scope.containers, function (container) { angular.forEach($scope.containers, function (container) {
if (container.Metadata) { if (container.Metadata) {
var containerRC = container.Metadata.ResourceControl; var containerRC = container.Metadata.ResourceControl;
if (containerRC && containerRC.OwnerId != $scope.user.ID) { if (containerRC && containerRC.OwnerId !== $scope.user.ID) {
angular.forEach(users, function (user) { angular.forEach(users, function (user) {
if (containerRC.OwnerId === user.Id) { if (containerRC.OwnerId === user.Id) {
container.Owner = user.Username; container.Owner = user.Username;

View file

@ -37,7 +37,7 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
}; };
$scope.addVolume = function() { $scope.addVolume = function() {
$scope.formValues.Volumes.push({ name: '', containerPath: '' }); $scope.formValues.Volumes.push({ name: '', containerPath: '', readOnly: false, type: 'volume' });
}; };
$scope.removeVolume = function(index) { $scope.removeVolume = function(index) {

View file

@ -18,88 +18,98 @@
</div> </div>
</div> </div>
<!-- !name-input --> <!-- !name-input -->
<div class="col-sm-12 form-section-title">
Image configuration
</div>
<!-- image-and-registry-inputs --> <!-- image-and-registry-inputs -->
<div class="form-group"> <div class="form-group">
<label for="container_image" class="col-sm-1 control-label text-left">Image</label> <label for="container_image" class="col-sm-1 control-label text-left">Image</label>
<div class="col-sm-7"> <div class="col-sm-11 col-md-6">
<input type="text" class="form-control" ng-model="config.Image" id="container_image" placeholder="e.g. ubuntu:trusty"> <input type="text" class="form-control" ng-model="config.Image" id="container_image" placeholder="e.g. ubuntu:trusty">
</div> </div>
<label for="image_registry" class="col-sm-1 control-label text-left">Registry</label> <label for="image_registry" class="col-sm-2 margin-sm-top control-label text-left">
<div class="col-sm-3"> Registry
<input type="text" class="form-control" ng-model="formValues.Registry" id="image_registry" placeholder="leave empty to use DockerHub"> <portainer-tooltip position="bottom" message="A registry to pull the image from. Leave empty to use the official Docker registry."></portainer-tooltip>
</div>
<div class="col-sm-offset-1 col-sm-11">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="formValues.alwaysPull"> Always pull image before creating
</label> </label>
</div> <div class="col-sm-10 col-md-3 margin-sm-top">
<input type="text" class="form-control" ng-model="formValues.Registry" id="image_registry" placeholder="e.g. myregistry.mydomain">
</div> </div>
</div> </div>
<!-- !image-and-registry-inputs --> <!-- !image-and-registry-inputs -->
<!-- restart-policy --> <!-- always-pull -->
<div class="form-group"> <div class="form-group">
<label class="col-sm-1 control-label text-left">Restart policy</label> <div class="col-sm-12">
<div class="col-sm-11"> <label for="ownership" class="control-label text-left">
<label class="radio-inline"> Always pull the image
<input type="radio" name="container_restart_policy" ng-model="config.HostConfig.RestartPolicy.Name" value="no"> <portainer-tooltip position="bottom" message="When enabled, Portainer will automatically try to pull the specified image before creating the container."></portainer-tooltip>
Never
</label> </label>
<label class="radio-inline"> <label class="switch" style="margin-left: 20px;">
<input type="radio" name="container_restart_policy" ng-model="config.HostConfig.RestartPolicy.Name" value="always"> <input type="checkbox" ng-model="formValues.alwaysPull"><i></i>
Always
</label>
<label class="radio-inline">
<input type="radio" name="container_restart_policy" ng-model="config.HostConfig.RestartPolicy.Name" value="on-failure">
<span class="radio-value">On failure</span>
</label>
<label class="radio-inline">
<input type="radio" name="container_restart_policy" ng-model="config.HostConfig.RestartPolicy.Name" value="unless-stopped">
<span class="radio-value">Unless stopped</span>
</label> </label>
</div> </div>
</div> </div>
<!-- !restart-policy --> <!-- !always-pull -->
<div class="col-sm-12 form-section-title">
Ports configuration
</div>
<!-- publish-exposed-ports -->
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Publish all exposed ports
<portainer-tooltip position="bottom" message="When enabled, Portainer will let Docker automatically map a random port on the host to each one defined in the image Dockerfile."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="config.HostConfig.PublishAllPorts"><i></i>
</label>
</div>
</div>
<!-- !publish-exposed-ports -->
<!-- port-mapping --> <!-- port-mapping -->
<div class="form-group"> <div class="form-group">
<label for="container_ports" class="col-sm-1 control-label text-left">Port mapping</label> <div class="col-sm-12">
<div class="col-sm-11"> <label class="control-label text-left">Port mapping</label>
<div class="checkbox"> <span class="label label-default interactive" style="margin-left: 10px;" ng-click="addPortBinding()">
<label> <i class="fa fa-plus-circle" aria-hidden="true"></i> map additional port
<input type="checkbox" ng-model="config.HostConfig.PublishAllPorts"> Publish all exposed ports
</label>
</div>
<span class="label label-default interactive" ng-click="addPortBinding()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> map port
</span> </span>
</div> </div>
<!-- port-mapping-input-list --> <!-- port-mapping-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;"> <div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="portBinding in config.HostConfig.PortBindings" style="margin-top: 2px;"> <div ng-repeat="portBinding in config.HostConfig.PortBindings" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm"> <!-- host-port -->
<div class="input-group col-sm-4 input-group-sm">
<span class="input-group-addon">host</span> <span class="input-group-addon">host</span>
<input type="text" class="form-control" ng-model="portBinding.hostPort" placeholder="e.g. 80 or 1.2.3.4:80 (optional)"> <input type="text" class="form-control" ng-model="portBinding.hostPort" placeholder="e.g. 80 or 1.2.3.4:80 (optional)">
</div> </div>
<div class="input-group col-sm-5 input-group-sm"> <!-- !host-port -->
<span style="margin: 0 10px 0 10px;">
<i class="fa fa-long-arrow-right" aria-hidden="true"></i>
</span>
<!-- container-port -->
<div class="input-group col-sm-4 input-group-sm">
<span class="input-group-addon">container</span> <span class="input-group-addon">container</span>
<input type="text" class="form-control" ng-model="portBinding.containerPort" placeholder="e.g. 80"> <input type="text" class="form-control" ng-model="portBinding.containerPort" placeholder="e.g. 80">
</div> </div>
<div class="input-group col-sm-1 input-group-sm"> <!-- !container-port -->
<select class="form-control" ng-model="portBinding.protocol"> <!-- protocol-actions -->
<option value="tcp">tcp</option> <div class="input-group col-sm-3 input-group-sm">
<option value="udp">udp</option> <div class="btn-group btn-group-sm">
</select> <label class="btn btn-primary" ng-model="portBinding.protocol" uib-btn-radio="'tcp'">TCP</label>
<span class="input-group-btn"> <label class="btn btn-primary" ng-model="portBinding.protocol" uib-btn-radio="'udp'">UDP</label>
<button class="btn btn-default" type="button" ng-click="removePortBinding($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div> </div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removePortBinding($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
<!-- !protocol-actions -->
</div> </div>
</div> </div>
<!-- !port-mapping-input-list --> <!-- !port-mapping-input-list -->
</div> </div>
<!-- !port-mapping --> <!-- !port-mapping -->
<div class="col-sm-12 form-section-title" ng-if="applicationState.application.authentication">
Access control
</div>
<!-- ownership --> <!-- ownership -->
<div class="form-group" ng-if="applicationState.application.authentication"> <div class="form-group" ng-if="applicationState.application.authentication">
<div class="col-sm-12"> <div class="col-sm-12">
@ -108,11 +118,11 @@
<portainer-tooltip position="bottom" message="When setting the ownership value to private, only you and the administrators will be able to see and manage this object. When choosing public, everybody will be able to access it."></portainer-tooltip> <portainer-tooltip position="bottom" message="When setting the ownership value to private, only you and the administrators will be able to see and manage this object. When choosing public, everybody will be able to access it."></portainer-tooltip>
</label> </label>
<div class="btn-group btn-group-sm" style="margin-left: 20px;"> <div class="btn-group btn-group-sm" style="margin-left: 20px;">
<label class="btn btn-default" ng-model="formValues.Ownership" uib-btn-radio="'private'"> <label class="btn btn-primary" ng-model="formValues.Ownership" uib-btn-radio="'private'">
<i class="fa fa-eye-slash" aria-hidden="true"></i> <i class="fa fa-eye-slash" aria-hidden="true"></i>
Private Private
</label> </label>
<label class="btn btn-default" ng-model="formValues.Ownership" uib-btn-radio="'public'"> <label class="btn btn-primary" ng-model="formValues.Ownership" uib-btn-radio="'public'">
<i class="fa fa-eye" aria-hidden="true"></i> <i class="fa fa-eye" aria-hidden="true"></i>
Public Public
</label> </label>
@ -120,6 +130,18 @@
</div> </div>
</div> </div>
<!-- !ownership --> <!-- !ownership -->
<!-- actions -->
<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="!config.Image" ng-click="create()">Start container</button>
<a type="button" class="btn btn-default btn-sm" ui-sref="containers">Cancel</a>
<i id="createContainerSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
<!-- !actions -->
</form> </form>
</rd-widget-body> </rd-widget-body>
</rd-widget> </rd-widget>
@ -129,13 +151,16 @@
<div class="row"> <div class="row">
<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-header icon="fa-cog" title="Advanced container settings"></rd-widget-header>
<rd-widget-body> <rd-widget-body>
<ul class="nav nav-tabs"> <ul class="nav nav-pills nav-justified">
<li class="active interactive"><a data-target="#command" data-toggle="tab">Command</a></li> <li class="active interactive"><a data-target="#command" data-toggle="tab">Command</a></li>
<li class="interactive"><a data-target="#volumes" data-toggle="tab">Volumes</a></li> <li class="interactive"><a data-target="#volumes" data-toggle="tab">Volumes</a></li>
<li class="interactive"><a data-target="#network" data-toggle="tab">Network</a></li> <li class="interactive"><a data-target="#network" data-toggle="tab">Network</a></li>
<li class="interactive"><a data-target="#env" data-toggle="tab">Env</a></li>
<li class="interactive"><a data-target="#labels" data-toggle="tab">Labels</a></li> <li class="interactive"><a data-target="#labels" data-toggle="tab">Labels</a></li>
<li class="interactive"><a data-target="#security" data-toggle="tab">Security/Host</a></li> <li class="interactive"><a data-target="#restart-policy" data-toggle="tab">Restart policy</a></li>
<li class="interactive"><a data-target="#runtime" data-toggle="tab">Runtime</a></li>
</ul> </ul>
<!-- tab-content --> <!-- tab-content -->
<div class="tab-content"> <div class="tab-content">
@ -144,7 +169,7 @@
<form class="form-horizontal" style="margin-top: 15px;"> <form class="form-horizontal" style="margin-top: 15px;">
<!-- command-input --> <!-- command-input -->
<div class="form-group"> <div class="form-group">
<label for="container_command" class="col-sm-1 control-label text-left">Command</label> <label for="container_command" class="col-sm-2 col-lg-1 control-label text-left">Command</label>
<div class="col-sm-9"> <div class="col-sm-9">
<input type="text" class="form-control" ng-model="config.Cmd" id="container_command" placeholder="e.g. /usr/bin/nginx -t -c /mynginx.conf"> <input type="text" class="form-control" ng-model="config.Cmd" id="container_command" placeholder="e.g. /usr/bin/nginx -t -c /mynginx.conf">
</div> </div>
@ -152,7 +177,7 @@
<!-- !command-input --> <!-- !command-input -->
<!-- entrypoint-input --> <!-- entrypoint-input -->
<div class="form-group"> <div class="form-group">
<label for="container_entrypoint" class="col-sm-1 control-label text-left">Entry Point</label> <label for="container_entrypoint" class="col-sm-2 col-lg-1 control-label text-left">Entry Point</label>
<div class="col-sm-9"> <div class="col-sm-9">
<input type="text" class="form-control" ng-model="config.Entrypoint" id="container_entrypoint" placeholder="e.g. /bin/sh -c"> <input type="text" class="form-control" ng-model="config.Entrypoint" id="container_entrypoint" placeholder="e.g. /bin/sh -c">
</div> </div>
@ -160,7 +185,7 @@
<!-- !entrypoint-input --> <!-- !entrypoint-input -->
<!-- workdir-user-input --> <!-- workdir-user-input -->
<div class="form-group"> <div class="form-group">
<label for="container_workingdir" class="col-sm-1 control-label text-left">Working Dir</label> <label for="container_workingdir" class="col-sm-2 col-lg-1 control-label text-left">Working Dir</label>
<div class="col-sm-4"> <div class="col-sm-4">
<input type="text" class="form-control" ng-model="config.WorkingDir" id="container_workingdir" placeholder="e.g. /myapp"> <input type="text" class="form-control" ng-model="config.WorkingDir" id="container_workingdir" placeholder="e.g. /myapp">
</div> </div>
@ -172,8 +197,8 @@
<!-- !workdir-user-input --> <!-- !workdir-user-input -->
<!-- console --> <!-- console -->
<div class="form-group"> <div class="form-group">
<label for="container_console" class="col-sm-1 control-label text-left">Console</label> <label for="container_console" class="col-sm-2 col-lg-1 control-label text-left">Console</label>
<div class="col-sm-11"> <div class="col-sm-10 col-lg-11">
<div class="col-sm-4"> <div class="col-sm-4">
<label class="radio-inline"> <label class="radio-inline">
<input type="radio" name="container_console" ng-model="formValues.Console" value="both"> <input type="radio" name="container_console" ng-model="formValues.Console" value="both">
@ -187,7 +212,7 @@
</label> </label>
</div> </div>
</div> </div>
<div class="col-sm-offset-1 col-sm-11"> <div class="col-sm-offset-2 col-sm-10 col-lg-offset-1 col-lg-11">
<div class="col-sm-4"> <div class="col-sm-4">
<label class="radio-inline"> <label class="radio-inline">
<input type="radio" name="container_console" ng-model="formValues.Console" value="tty"> <input type="radio" name="container_console" ng-model="formValues.Console" value="tty">
@ -203,35 +228,6 @@
</div> </div>
</div> </div>
<!-- !console --> <!-- !console -->
<!-- environment-variables -->
<div class="form-group">
<label for="container_env" class="col-sm-1 control-label text-left">Environment variables</label>
<div class="col-sm-11">
<span class="label label-default interactive" ng-click="addEnvironmentVariable()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> environment variable
</span>
</div>
<!-- environment-variable-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
<div ng-repeat="variable in config.Env" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span>
<input type="text" class="form-control" ng-model="variable.name" placeholder="e.g. FOO">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="variable.value" placeholder="e.g. bar">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeEnvironmentVariable($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !environment-variable-input-list -->
</div>
<!-- !environment-variables -->
</form> </form>
</div> </div>
<!-- !tab-command --> <!-- !tab-command -->
@ -240,39 +236,64 @@
<form class="form-horizontal" style="margin-top: 15px;"> <form class="form-horizontal" style="margin-top: 15px;">
<!-- volumes --> <!-- volumes -->
<div class="form-group"> <div class="form-group">
<label for="container_volumes" class="col-sm-1 control-label text-left">Volumes</label> <div class="col-sm-12" style="margin-top: 5px;">
<div class="col-sm-11"> <label class="control-label text-left">Volume mapping</label>
<span class="label label-default interactive" ng-click="addVolume()"> <span class="label label-default interactive" style="margin-left: 10px;" ng-click="addVolume()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> volume <i class="fa fa-plus-circle" aria-hidden="true"></i> map additional volume
</span> </span>
</div> </div>
<!-- volumes-input-list --> <!-- volumes-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;"> <div class="form-inline" style="margin-top: 10px;">
<div ng-repeat="volume in formValues.Volumes" style="margin-top: 2px;"> <div ng-repeat="volume in formValues.Volumes">
<div class="input-group col-sm-1 input-group-sm"> <!-- volume-line1 -->
<div class="checkbox"> <div class="col-sm-12 form-inline" style="margin-top: 10px;">
<label> <!-- container-path -->
<input type="checkbox" ng-model="volume.readOnly"> Read-only <div class="input-group input-group-sm col-sm-6">
</label> <span class="input-group-addon">container</span>
<input type="text" class="form-control" ng-model="volume.containerPath" placeholder="e.g. /path/in/container">
</div> </div>
<!-- !container-path -->
<!-- volume-type -->
<div class="input-group col-sm-5" style="margin-left: 5px;">
<div class="btn-group btn-group-sm">
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'volume'" ng-click="volume.name = ''">Volume</label>
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'bind'" ng-click="volume.name = ''">Bind</label>
</div> </div>
<div class="input-group col-sm-5 input-group-sm"> <button class="btn btn-sm btn-danger" type="button" ng-click="removeVolume($index)">
<span class="input-group-addon"><input type="checkbox" ng-model="volume.isPath" ng-click="resetVolumePath($index)">Path</span> <i class="fa fa-trash" aria-hidden="true"></i>
<select class="form-control" ng-model="volume.name" ng-if="!volume.isPath"> </button>
</div>
<!-- !volume-type -->
</div>
<!-- !volume-line1 -->
<!-- volume-line2 -->
<div class="col-sm-12 form-inline" style="margin-top: 5px;">
<i class="fa fa-long-arrow-right" aria-hidden="true"></i>
<!-- volume -->
<div class="input-group input-group-sm col-sm-6" ng-if="volume.type === 'volume'">
<span class="input-group-addon">volume</span>
<select class="form-control" ng-model="volume.name">
<option selected disabled hidden value="">Select a volume</option> <option selected disabled hidden value="">Select a volume</option>
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option> <option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option>
</select> </select>
<input ng-if="volume.isPath" type="text" class="form-control" ng-model="volume.name" placeholder="e.g. /path/on/host">
</div> </div>
<div class="input-group col-sm-5 input-group-sm"> <!-- !volume -->
<span class="input-group-addon">container</span> <!-- bind -->
<input type="text" class="form-control" ng-model="volume.containerPath" placeholder="e.g. /path/in/container"> <div class="input-group input-group-sm col-sm-6" ng-if="volume.type === 'bind'">
<span class="input-group-btn"> <span class="input-group-addon">host</span>
<button class="btn btn-default" type="button" ng-click="removeVolume($index)"> <input type="text" class="form-control" ng-model="volume.name" placeholder="e.g. /path/on/host">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div> </div>
<!-- !bind -->
<!-- read-only -->
<div class="input-group input-group-sm col-sm-5" style="margin-left: 5px;">
<div class="btn-group btn-group-sm">
<label class="btn btn-primary" ng-model="volume.readOnly" uib-btn-radio="false">Writable</label>
<label class="btn btn-primary" ng-model="volume.readOnly" uib-btn-radio="true">Read-only</label>
</div>
</div>
<!-- !read-only -->
</div>
<!-- !volume-line2 -->
</div> </div>
</div> </div>
<!-- !volumes-input-list --> <!-- !volumes-input-list -->
@ -291,7 +312,7 @@
</div> </div>
<!-- network-input --> <!-- network-input -->
<div class="form-group"> <div class="form-group">
<label for="container_network" class="col-sm-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">
<option selected disabled hidden value="">Select a network</option> <option selected disabled hidden value="">Select a network</option>
@ -302,7 +323,7 @@
<!-- !network-input --> <!-- !network-input -->
<!-- container-name-input --> <!-- container-name-input -->
<div class="form-group" ng-if="config.HostConfig.NetworkMode == 'container'"> <div class="form-group" ng-if="config.HostConfig.NetworkMode == 'container'">
<label for="container_network_container" class="col-sm-1 control-label text-left">Container</label> <label for="container_network_container" class="col-sm-2 col-lg-1 control-label text-left">Container</label>
<div class="col-sm-9"> <div class="col-sm-9">
<select ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'" ng-options="container|containername for container in runningContainers" class="form-control" ng-model="formValues.NetworkContainer"> <select ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'" ng-options="container|containername for container in runningContainers" class="form-control" ng-model="formValues.NetworkContainer">
<option selected disabled hidden value="">Select a container</option> <option selected disabled hidden value="">Select a container</option>
@ -315,7 +336,7 @@
<!-- !container-name-input --> <!-- !container-name-input -->
<!-- hostname-input --> <!-- hostname-input -->
<div class="form-group"> <div class="form-group">
<label for="container_hostname" class="col-sm-1 control-label text-left">Hostname</label> <label for="container_hostname" class="col-sm-2 col-lg-1 control-label text-left">Hostname</label>
<div class="col-sm-9"> <div class="col-sm-9">
<input type="text" class="form-control" ng-model="config.Hostname" id="container_hostname" placeholder="e.g. web01"> <input type="text" class="form-control" ng-model="config.Hostname" id="container_hostname" placeholder="e.g. web01">
</div> </div>
@ -323,7 +344,7 @@
<!-- !hostname-input --> <!-- !hostname-input -->
<!-- domainname-input --> <!-- domainname-input -->
<div class="form-group"> <div class="form-group">
<label for="container_domainname" class="col-sm-1 control-label text-left">Domain Name</label> <label for="container_domainname" class="col-sm-2 col-lg-1 control-label text-left">Domain Name</label>
<div class="col-sm-9"> <div class="col-sm-9">
<input type="text" class="form-control" ng-model="config.Domainname" id="container_domainname" placeholder="e.g. example.com"> <input type="text" class="form-control" ng-model="config.Domainname" id="container_domainname" placeholder="e.g. example.com">
</div> </div>
@ -331,24 +352,22 @@
<!-- !domainname --> <!-- !domainname -->
<!-- extra-hosts-variables --> <!-- extra-hosts-variables -->
<div class="form-group"> <div class="form-group">
<label for="container_extrahosts" class="col-sm-1 control-label text-left">Extra Hosts</label> <div class="col-sm-12" style="margin-top: 5px;">
<div class="col-sm-11"> <label class="control-label text-left">Hosts file entries</label>
<span class="label label-default interactive" ng-click="addExtraHost()"> <span class="label label-default interactive" style="margin-left: 10px;" ng-click="addExtraHost()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> extra host <i class="fa fa-plus-circle" aria-hidden="true"></i> add additional entry
</span> </span>
</div> </div>
<!-- extra-hosts-input-list --> <!-- extra-hosts-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;"> <div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="variable in formValues.ExtraHosts" style="margin-top: 2px;"> <div ng-repeat="variable in formValues.ExtraHosts" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm"> <div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span> <span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="variable.value" placeholder="e.g. host:IP"> <input type="text" class="form-control" ng-model="variable.value" placeholder="e.g. host:IP">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeExtraHost($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div> </div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeExtraHost($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div> </div>
</div> </div>
<!-- !extra-hosts-input-list --> <!-- !extra-hosts-input-list -->
@ -362,14 +381,14 @@
<form class="form-horizontal" style="margin-top: 15px;"> <form class="form-horizontal" style="margin-top: 15px;">
<!-- labels --> <!-- labels -->
<div class="form-group"> <div class="form-group">
<label for="container_labels" class="col-sm-1 control-label text-left">Labels</label> <div class="col-sm-12" style="margin-top: 5px;">
<div class="col-sm-11"> <label class="control-label text-left">Labels</label>
<span class="label label-default interactive" ng-click="addLabel()"> <span class="label label-default interactive" style="margin-left: 10px;" ng-click="addLabel()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> label <i class="fa fa-plus-circle" aria-hidden="true"></i> add label
</span> </span>
</div> </div>
<!-- labels-input-list --> <!-- labels-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;"> <div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="label in formValues.Labels" style="margin-top: 2px;"> <div ng-repeat="label in formValues.Labels" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm"> <div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span> <span class="input-group-addon">name</span>
@ -378,12 +397,10 @@
<div class="input-group col-sm-5 input-group-sm"> <div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span> <span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar"> <input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeLabel($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div> </div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeLabel($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div> </div>
</div> </div>
<!-- !labels-input-list --> <!-- !labels-input-list -->
@ -392,35 +409,86 @@
</form> </form>
</div> </div>
<!-- !tab-labels --> <!-- !tab-labels -->
<!-- tab-security --> <!-- tab-env -->
<div class="tab-pane" id="security"> <div class="tab-pane" id="env">
<form class="form-horizontal" style="margin-top: 15px;">
<!-- environment-variables -->
<div class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Environment variables</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addEnvironmentVariable()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add environment variable
</span>
</div>
<!-- environment-variable-input-list -->
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="variable in config.Env" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span>
<input type="text" class="form-control" ng-model="variable.name" placeholder="e.g. FOO">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="variable.value" placeholder="e.g. bar">
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeEnvironmentVariable($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
<!-- !environment-variable-input-list -->
</div>
<!-- !environment-variables -->
</form>
</div>
<!-- !tab-labels -->
<!-- tab-restart-policy -->
<div class="tab-pane" id="restart-policy">
<form class="form-horizontal" style="margin-top: 15px;">
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Restart policy
</label>
<div class="btn-group btn-group-sm" style="margin-left: 20px;">
<label class="btn btn-primary" ng-model="config.HostConfig.RestartPolicy.Name" uib-btn-radio="'no'">
Never
</label>
<label class="btn btn-primary" ng-model="config.HostConfig.RestartPolicy.Name" uib-btn-radio="'always'">
Always
</label>
<label class="btn btn-primary" ng-model="config.HostConfig.RestartPolicy.Name" uib-btn-radio="'on-failure'">
On failure
</label>
<label class="btn btn-primary" ng-model="config.HostConfig.RestartPolicy.Name" uib-btn-radio="'unless-stopped'">
Unless stopped
</label>
</div>
</div>
</div>
</form>
</div>
<!-- !tab-restart-policy -->
<!-- tab-runtime -->
<div class="tab-pane" id="runtime">
<form class="form-horizontal" style="margin-top: 15px;"> <form class="form-horizontal" style="margin-top: 15px;">
<!-- privileged-mode --> <!-- privileged-mode -->
<div class="form-group"> <div class="form-group">
<div class="col-sm-12"> <div class="col-sm-12">
<div class="checkbox"> <label for="ownership" class="control-label text-left">
<label> Privileged mode
<input type="checkbox" ng-model="config.HostConfig.Privileged"> Privileged mode </label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="config.HostConfig.Privileged"><i></i>
</label> </label>
</div>
</div> </div>
</div> </div>
<!-- !privileged-mode --> <!-- !privileged-mode -->
</form> </form>
</div> </div>
<!-- !tab-security --> <!-- !tab-runtime -->
</div> </div>
</rd-widget-body> </rd-widget-body>
</rd-widget> </rd-widget>
</div> </div>
</div> </div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12" style="text-align: center;">
<div>
<i id="createContainerSpinner" class="fa fa-cog fa-3x fa-spin" style="margin-bottom: 5px; display: none;"></i>
</div>
<button type="button" class="btn btn-default btn-lg" ng-click="create()">Create</button>
<a type="button" class="btn btn-default btn-lg" ui-sref="containers">Cancel</a>
</div>
</div>

View file

@ -18,36 +18,45 @@
</div> </div>
</div> </div>
<!-- !name-input --> <!-- !name-input -->
<div class="col-sm-12 form-section-title">
Network configuration
</div>
<!-- subnet-gateway-inputs --> <!-- subnet-gateway-inputs -->
<div class="form-group"> <div class="form-group">
<label for="network_subnet" class="col-sm-1 control-label text-left">Subnet</label> <label for="network_subnet" class="col-sm-2 col-lg-1 control-label text-left">Subnet</label>
<div class="col-sm-5"> <div class="col-sm-4 col-lg-5">
<input type="text" class="form-control" ng-model="formValues.Subnet" id="network_subnet" placeholder="e.g. 172.20.0.0/16"> <input type="text" class="form-control" ng-model="formValues.Subnet" id="network_subnet" placeholder="e.g. 172.20.0.0/16">
</div> </div>
<label for="network_gateway" class="col-sm-1 control-label text-left">Gateway</label> <label for="network_gateway" class="col-sm-2 col-lg-1 control-label text-left">Gateway</label>
<div class="col-sm-5"> <div class="col-sm-4 col-lg-5">
<input type="text" class="form-control" ng-model="formValues.Gateway" id="network_gateway" placeholder="e.g. 172.20.10.11"> <input type="text" class="form-control" ng-model="formValues.Gateway" id="network_gateway" placeholder="e.g. 172.20.10.11">
</div> </div>
</div> </div>
<!-- !subnet-gateway-inputs --> <!-- !subnet-gateway-inputs -->
<div class="col-sm-12 form-section-title">
Driver configuration
</div>
<!-- driver-input --> <!-- driver-input -->
<div class="form-group"> <div class="form-group">
<label for="network_driver" class="col-sm-1 control-label text-left">Driver</label> <label for="network_driver" class="col-sm-2 col-lg-1 control-label text-left">Driver</label>
<div class="col-sm-11"> <div class="col-sm-10">
<input type="text" class="form-control" ng-model="config.Driver" id="network_driver" placeholder="e.g. driverName"> <input type="text" class="form-control" ng-model="config.Driver" id="network_driver" placeholder="e.g. driverName">
</div> </div>
</div> </div>
<!-- !driver-input --> <!-- !driver-input -->
<!-- driver-options --> <!-- driver-options -->
<div class="form-group"> <div class="form-group">
<label for="network_driveropts" class="col-sm-1 control-label text-left">Driver options</label> <div class="col-sm-12" style="margin-top: 5px;">
<div class="col-sm-11"> <label class="control-label text-left">
<span class="label label-default interactive" ng-click="addDriverOption()"> Driver options
<i class="fa fa-plus-circle" aria-hidden="true"></i> driver option <portainer-tooltip position="bottom" message="Driver options are specific to the selected driver. Please refer to the selected driver documentation."></portainer-tooltip>
</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addDriverOption()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add driver option
</span> </span>
</div> </div>
<!-- driver-options-input-list --> <!-- driver-options-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;"> <div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="option in formValues.DriverOptions" style="margin-top: 2px;"> <div ng-repeat="option in formValues.DriverOptions" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm"> <div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span> <span class="input-group-addon">name</span>
@ -56,38 +65,28 @@
<div class="input-group col-sm-5 input-group-sm"> <div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span> <span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="option.value" placeholder="e.g. true"> <input type="text" class="form-control" ng-model="option.value" placeholder="e.g. true">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeDriverOption($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div> </div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeDriverOption($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div> </div>
</div> </div>
<!-- !driver-options-input-list --> <!-- !driver-options-input-list -->
</div> </div>
<!-- !driver-options --> <!-- !driver-options -->
<!-- internal --> <div class="col-sm-12 form-section-title">
<div class="form-group"> Advanced configuration
<div class="col-sm-12">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="config.Internal"> Restrict external access to the network
</label>
</div> </div>
</div>
</div>
<!-- !internal -->
<!-- labels --> <!-- labels -->
<div class="form-group"> <div class="form-group">
<label for="service_env" class="col-sm-1 control-label text-left">Labels</label> <div class="col-sm-12" style="margin-top: 5px;">
<div class="col-sm-11"> <label class="control-label text-left">Labels</label>
<span class="label label-default interactive" ng-click="addLabel()"> <span class="label label-default interactive" style="margin-left: 10px;" ng-click="addLabel()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> label <i class="fa fa-plus-circle" aria-hidden="true"></i> add label
</span> </span>
</div> </div>
<!-- labels-input-list --> <!-- labels-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;"> <div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="label in formValues.Labels" style="margin-top: 2px;"> <div ng-repeat="label in formValues.Labels" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm"> <div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span> <span class="input-group-addon">name</span>
@ -96,29 +95,41 @@
<div class="input-group col-sm-5 input-group-sm"> <div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span> <span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar"> <input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeLabel($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div> </div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeLabel($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div> </div>
</div> </div>
<!-- !labels-input-list --> <!-- !labels-input-list -->
</div> </div>
<!-- !labels--> <!-- !labels-->
<!-- internal -->
<div class="form-group">
<div class="col-sm-12">
<label for="ownership" class="control-label text-left">
Restrict external access to the network
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="formValues.alwaysPull"><i></i>
</label>
</div>
</div>
<!-- !internal -->
<!-- actions -->
<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="!config.Name" ng-click="create()">Create network</button>
<a type="button" class="btn btn-default btn-sm" ui-sref="networks">Cancel</a>
<i id="createNetworkSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
<!-- !actions -->
</form> </form>
</rd-widget-body> </rd-widget-body>
</rd-widget> </rd-widget>
</div> </div>
</div> </div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12" style="text-align: center;">
<div>
<i id="createNetworkSpinner" class="fa fa-cog fa-3x fa-spin" style="margin-bottom: 5px; display: none;"></i>
</div>
<button type="button" class="btn btn-default btn-lg" ng-disabled="!config.Name" ng-click="create()">Create</button>
<a type="button" class="btn btn-default btn-lg" ui-sref="networks">Cancel</a>
</div>
</div>

View file

@ -44,7 +44,7 @@ function ($scope, $state, Service, Volume, Network, ImageHelper, Authentication,
}; };
$scope.addVolume = function() { $scope.addVolume = function() {
$scope.formValues.Volumes.push({ name: '', containerPath: '' }); $scope.formValues.Volumes.push({ Source: '', Target: '', ReadOnly: false, Type: 'volume' });
}; };
$scope.removeVolume = function(index) { $scope.removeVolume = function(index) {

View file

@ -18,75 +18,95 @@
</div> </div>
</div> </div>
<!-- !name-input --> <!-- !name-input -->
<div class="col-sm-12 form-section-title">
Image configuration
</div>
<!-- image-and-registry-inputs --> <!-- image-and-registry-inputs -->
<div class="form-group"> <div class="form-group">
<label for="service_image" class="col-sm-1 control-label text-left">Image</label> <label for="service_image" class="col-sm-1 control-label text-left">Image</label>
<div class="col-sm-7"> <div class="col-sm-11 col-md-6">
<input type="text" class="form-control" ng-model="formValues.Image" id="service_image" placeholder="e.g. nginx:latest"> <input type="text" class="form-control" ng-model="formValues.Image" id="service_image" placeholder="e.g. nginx:latest">
</div> </div>
<label for="image_registry" class="col-sm-1 control-label text-left">Registry</label> <label for="image_registry" class="col-sm-2 margin-sm-top control-label text-left">
<div class="col-sm-3"> Registry
<input type="text" class="form-control" ng-model="formValues.Registry" id="image_registry" placeholder="leave empty to use DockerHub"> <portainer-tooltip position="bottom" message="A registry to pull the image from. Leave empty to use the official Docker registry."></portainer-tooltip>
</label>
<div class="col-sm-10 col-md-3 margin-sm-top">
<input type="text" class="form-control" ng-model="formValues.Registry" id="image_registry" placeholder="e.g. myregistry.mydomain">
</div> </div>
</div> </div>
<!-- !image-and-registry-inputs --> <!-- !image-and-registry-inputs -->
<div class="col-sm-12 form-section-title">
Scheduling
</div>
<!-- scheduling-mode --> <!-- scheduling-mode -->
<div class="form-group"> <div class="form-group">
<label class="col-sm-1 control-label text-left">Scheduling mode</label> <div class="col-sm-12">
<div class="col-sm-11"> <label class="control-label text-left">
<label class="radio-inline"> Scheduling mode
<input type="radio" name="service_scheduling" ng-model="formValues.Mode" value="global">
Global
</label> </label>
<label class="radio-inline"> <div class="btn-group btn-group-sm" style="margin-left: 20px;">
<input type="radio" name="service_scheduling" ng-model="formValues.Mode" value="replicated"> <label class="btn btn-primary" ng-model="formValues.Mode" uib-btn-radio="'global'">Global</label>
Replicated <label class="btn btn-primary" ng-model="formValues.Mode" uib-btn-radio="'replicated'">Replicated</label>
</div>
</div>
</div>
<div class="form-group form-inline" ng-if="formValues.Mode === 'replicated'">
<div class="col-sm-12">
<label class="control-label text-left">
Replicas
</label> </label>
<input type="number" class="form-control" ng-model="formValues.Replicas" id="replicas" placeholder="e.g. 3" style="margin-left: 20px;">
</div> </div>
</div> </div>
<div class="form-group" ng-if="formValues.Mode === 'replicated'">
<label for="replicas" class="col-sm-1 control-label text-left">Replicas</label>
<div class="col-sm-1">
<input type="number" class="form-control" ng-model="formValues.Replicas" id="replicas" placeholder="e.g. 3">
</div>
<div class="col-sm-10"></div>
</div>
<!-- !scheduling-mode --> <!-- !scheduling-mode -->
<div class="col-sm-12 form-section-title">
Ports configuration
</div>
<!-- port-mapping --> <!-- port-mapping -->
<div class="form-group"> <div class="form-group">
<label for="container_ports" class="col-sm-1 control-label text-left">Port mapping</label> <div class="col-sm-12" style="margin-top: 5px;">
<div class="col-sm-11"> <label class="control-label text-left">Port mapping</label>
<span class="label label-default interactive" ng-click="addPortBinding()"> <span class="label label-default interactive" style="margin-left: 10px;" ng-click="addPortBinding()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> map port <i class="fa fa-plus-circle" aria-hidden="true"></i> map additional port
</span> </span>
</div> </div>
<!-- port-mapping-input-list --> <div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
<div ng-repeat="portBinding in formValues.Ports" style="margin-top: 2px;"> <div ng-repeat="portBinding in formValues.Ports" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm"> <!-- host-port -->
<div class="input-group col-sm-4 input-group-sm">
<span class="input-group-addon">host</span> <span class="input-group-addon">host</span>
<input type="text" class="form-control" ng-model="portBinding.PublishedPort" placeholder="e.g. 8080"> <input type="text" class="form-control" ng-model="portBinding.PublishedPort" placeholder="e.g. 80 or 1.2.3.4:80 (optional)">
</div> </div>
<div class="input-group col-sm-5 input-group-sm"> <!-- !host-port -->
<span style="margin: 0 10px 0 10px;">
<i class="fa fa-long-arrow-right" aria-hidden="true"></i>
</span>
<!-- container-port -->
<div class="input-group col-sm-4 input-group-sm">
<span class="input-group-addon">container</span> <span class="input-group-addon">container</span>
<input type="text" class="form-control" ng-model="portBinding.TargetPort" placeholder="e.g. 80"> <input type="text" class="form-control" ng-model="portBinding.TargetPort" placeholder="e.g. 80">
</div> </div>
<div class="input-group col-sm-1 input-group-sm"> <!-- !container-port -->
<select class="form-control" ng-model="portBinding.Protocol"> <!-- protocol-actions -->
<option value="tcp">tcp</option> <div class="input-group col-sm-3 input-group-sm">
<option value="udp">udp</option> <div class="btn-group btn-group-sm">
</select> <label class="btn btn-primary" ng-model="portBinding.Protocol" uib-btn-radio="'tcp'">TCP</label>
<span class="input-group-btn"> <label class="btn btn-primary" ng-model="portBinding.Protocol" uib-btn-radio="'udp'">UDP</label>
<button class="btn btn-default" type="button" ng-click="removePortBinding($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div> </div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removePortBinding($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
<!-- !protocol-actions -->
</div> </div>
</div> </div>
<!-- !port-mapping-input-list --> <!-- !port-mapping-input-list -->
</div> </div>
<!-- !port-mapping --> <!-- !port-mapping -->
<div class="col-sm-12 form-section-title" ng-if="applicationState.application.authentication">
Access control
</div>
<!-- ownership --> <!-- ownership -->
<div class="form-group" ng-if="applicationState.application.authentication"> <div class="form-group" ng-if="applicationState.application.authentication">
<div class="col-sm-12"> <div class="col-sm-12">
@ -95,11 +115,11 @@
<portainer-tooltip position="bottom" message="When setting the ownership value to private, only you and the administrators will be able to see and manage this object. When choosing public, everybody will be able to access it."></portainer-tooltip> <portainer-tooltip position="bottom" message="When setting the ownership value to private, only you and the administrators will be able to see and manage this object. When choosing public, everybody will be able to access it."></portainer-tooltip>
</label> </label>
<div class="btn-group btn-group-sm" style="margin-left: 20px;"> <div class="btn-group btn-group-sm" style="margin-left: 20px;">
<label class="btn btn-default" ng-model="formValues.Ownership" uib-btn-radio="'private'"> <label class="btn btn-primary" ng-model="formValues.Ownership" uib-btn-radio="'private'">
<i class="fa fa-eye-slash" aria-hidden="true"></i> <i class="fa fa-eye-slash" aria-hidden="true"></i>
Private Private
</label> </label>
<label class="btn btn-default" ng-model="formValues.Ownership" uib-btn-radio="'public'"> <label class="btn btn-primary" ng-model="formValues.Ownership" uib-btn-radio="'public'">
<i class="fa fa-eye" aria-hidden="true"></i> <i class="fa fa-eye" aria-hidden="true"></i>
Public Public
</label> </label>
@ -107,6 +127,18 @@
</div> </div>
</div> </div>
<!-- !ownership --> <!-- !ownership -->
<!-- actions -->
<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="!formValues.Image" ng-click="create()">Create service</button>
<a type="button" class="btn btn-default btn-sm" ui-sref="services">Cancel</a>
<i id="createServiceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
<!-- !actions -->
</form> </form>
</rd-widget-body> </rd-widget-body>
</rd-widget> </rd-widget>
@ -117,7 +149,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>
<ul class="nav nav-tabs"> <ul class="nav nav-pills nav-justified">
<li class="active interactive"><a data-target="#command" data-toggle="tab">Command</a></li> <li class="active interactive"><a data-target="#command" data-toggle="tab">Command</a></li>
<li class="interactive"><a data-target="#volumes" data-toggle="tab">Volumes</a></li> <li class="interactive"><a data-target="#volumes" data-toggle="tab">Volumes</a></li>
<li class="interactive"><a data-target="#network" data-toggle="tab">Network</a></li> <li class="interactive"><a data-target="#network" data-toggle="tab">Network</a></li>
@ -131,7 +163,7 @@
<form class="form-horizontal" style="margin-top: 15px;"> <form class="form-horizontal" style="margin-top: 15px;">
<!-- command-input --> <!-- command-input -->
<div class="form-group"> <div class="form-group">
<label for="service_command" class="col-sm-1 control-label text-left">Command</label> <label for="service_command" class="col-sm-2 col-lg-1 control-label text-left">Command</label>
<div class="col-sm-9"> <div class="col-sm-9">
<input type="text" class="form-control" ng-model="formValues.Command" id="service_command" placeholder="e.g. /usr/bin/nginx -t -c /mynginx.conf"> <input type="text" class="form-control" ng-model="formValues.Command" id="service_command" placeholder="e.g. /usr/bin/nginx -t -c /mynginx.conf">
</div> </div>
@ -139,7 +171,7 @@
<!-- !command-input --> <!-- !command-input -->
<!-- entrypoint-input --> <!-- entrypoint-input -->
<div class="form-group"> <div class="form-group">
<label for="service_entrypoint" class="col-sm-1 control-label text-left">Entrypoint</label> <label for="service_entrypoint" class="col-sm-2 col-lg-1 control-label text-left">Entrypoint</label>
<div class="col-sm-9"> <div class="col-sm-9">
<input type="text" class="form-control" ng-model="formValues.EntryPoint" id="service_entrypoint" placeholder="e.g. /bin/sh -c"> <input type="text" class="form-control" ng-model="formValues.EntryPoint" id="service_entrypoint" placeholder="e.g. /bin/sh -c">
</div> </div>
@ -147,7 +179,7 @@
<!-- !entrypoint-input --> <!-- !entrypoint-input -->
<!-- workdir-user-input --> <!-- workdir-user-input -->
<div class="form-group"> <div class="form-group">
<label for="service_workingdir" class="col-sm-1 control-label text-left">Working Dir</label> <label for="service_workingdir" class="col-sm-2 col-lg-1 control-label text-left">Working Dir</label>
<div class="col-sm-4"> <div class="col-sm-4">
<input type="text" class="form-control" ng-model="formValues.WorkingDir" id="service_workingdir" placeholder="e.g. /myapp"> <input type="text" class="form-control" ng-model="formValues.WorkingDir" id="service_workingdir" placeholder="e.g. /myapp">
</div> </div>
@ -159,14 +191,14 @@
<!-- !workdir-user-input --> <!-- !workdir-user-input -->
<!-- environment-variables --> <!-- environment-variables -->
<div class="form-group"> <div class="form-group">
<label for="service_env" class="col-sm-1 control-label text-left">Environment variables</label> <div class="col-sm-12" style="margin-top: 5px;">
<div class="col-sm-11"> <label class="control-label text-left">Environment variables</label>
<span class="label label-default interactive" ng-click="addEnvironmentVariable()"> <span class="label label-default interactive" style="margin-left: 10px;" ng-click="addEnvironmentVariable()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> environment variable <i class="fa fa-plus-circle" aria-hidden="true"></i> add environment variable
</span> </span>
</div> </div>
<!-- environment-variable-input-list --> <!-- environment-variable-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;"> <div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="variable in formValues.Env" style="margin-top: 2px;"> <div ng-repeat="variable in formValues.Env" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm"> <div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span> <span class="input-group-addon">name</span>
@ -175,12 +207,10 @@
<div class="input-group col-sm-5 input-group-sm"> <div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span> <span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="variable.value" placeholder="e.g. bar"> <input type="text" class="form-control" ng-model="variable.value" placeholder="e.g. bar">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeEnvironmentVariable($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div> </div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeEnvironmentVariable($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div> </div>
</div> </div>
<!-- !environment-variable-input-list --> <!-- !environment-variable-input-list -->
@ -194,38 +224,65 @@
<form class="form-horizontal" style="margin-top: 15px;"> <form class="form-horizontal" style="margin-top: 15px;">
<!-- volumes --> <!-- volumes -->
<div class="form-group"> <div class="form-group">
<label for="service_volumes" class="col-sm-1 control-label text-left">Volumes</label> <div class="col-sm-12" style="margin-top: 5px;">
<div class="col-sm-11"> <label class="control-label text-left">Volume mapping</label>
<span class="label label-default interactive" ng-click="addVolume()"> <span class="label label-default interactive" style="margin-left: 10px;" ng-click="addVolume()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> volume <i class="fa fa-plus-circle" aria-hidden="true"></i> map additional volume
</span> </span>
</div> </div>
<!-- volumes-input-list --> <!-- volumes-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;"> <div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="volume in formValues.Volumes" style="margin-top: 2px;"> <div ng-repeat="volume in formValues.Volumes">
<div class="input-group col-sm-1 input-group-sm"> <div class="col-sm-12" style="margin-top: 10px;">
<div class="checkbox"> <!-- volume-line1 -->
<label> <div class="col-sm-12 form-inline">
<input type="checkbox" ng-model="volume.ReadOnly"> Read-only <!-- container-path -->
</label> <div class="input-group input-group-sm col-sm-6">
<span class="input-group-addon">container</span>
<input type="text" class="form-control" ng-model="volume.Source" placeholder="e.g. /path/in/container">
</div> </div>
<!-- !container-path -->
<!-- volume-type -->
<div class="input-group col-sm-5" style="margin-left: 5px;">
<div class="btn-group btn-group-sm">
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'volume'" ng-click="volume.name = ''">Volume</label>
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'bind'" ng-click="volume.Name = ''">Bind</label>
</div> </div>
<div class="input-group col-sm-5 input-group-sm"> <button class="btn btn-sm btn-danger" type="button" ng-click="removeVolume($index)">
<span class="input-group-addon"><input type="checkbox" ng-model="volume.Bind">bind</span> <i class="fa fa-trash" aria-hidden="true"></i>
<select class="form-control" ng-model="volume.Source" ng-if="!volume.Bind"> </button>
</div>
<!-- !volume-type -->
</div>
<!-- !volume-line1 -->
<!-- volume-line2 -->
<div class="col-sm-12 form-inline" style="margin-top: 5px;">
<i class="fa fa-long-arrow-right" aria-hidden="true"></i>
<!-- volume -->
<div class="input-group input-group-sm col-sm-6" ng-if="volume.Type === 'volume'">
<span class="input-group-addon">volume</span>
<select class="form-control" ng-model="volume.Target">
<option selected disabled hidden value="">Select a volume</option> <option selected disabled hidden value="">Select a volume</option>
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option> <option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option>
</select> </select>
<input ng-if="volume.Bind" type="text" class="form-control" ng-model="volume.Source" placeholder="e.g. /path/on/host">
</div> </div>
<div class="input-group col-sm-5 input-group-sm"> <!-- !volume -->
<span class="input-group-addon">container</span> <!-- bind -->
<input type="text" class="form-control" ng-model="volume.Target" placeholder="e.g. /path/in/container"> <div class="input-group input-group-sm col-sm-6" ng-if="volume.Type === 'bind'">
<span class="input-group-btn"> <span class="input-group-addon">host</span>
<button class="btn btn-default" type="button" ng-click="removeVolume($index)"> <input type="text" class="form-control" ng-model="volume.Target" placeholder="e.g. /path/on/host">
<i class="fa fa-minus" aria-hidden="true"></i> </div>
</button> <!-- !bind -->
</span> <!-- read-only -->
<div class="input-group input-group-sm col-sm-5" style="margin-left: 5px;">
<div class="btn-group btn-group-sm">
<label class="btn btn-primary" ng-model="volume.ReadOnly" uib-btn-radio="false">Writable</label>
<label class="btn btn-primary" ng-model="volume.ReadOnly" uib-btn-radio="true">Read-only</label>
</div>
</div>
<!-- !read-only -->
</div>
<!-- !volume-line2 -->
</div> </div>
</div> </div>
</div> </div>
@ -240,7 +297,7 @@
<form class="form-horizontal" style="margin-top: 15px;"> <form class="form-horizontal" style="margin-top: 15px;">
<!-- network-input --> <!-- network-input -->
<div class="form-group"> <div class="form-group">
<label for="container_network" class="col-sm-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="formValues.Network"> <select class="form-control" ng-model="formValues.Network">
<option selected disabled hidden value="">Select a network</option> <option selected disabled hidden value="">Select a network</option>
@ -252,27 +309,22 @@
<!-- !network-input --> <!-- !network-input -->
<!-- extra-networks --> <!-- extra-networks -->
<div class="form-group"> <div class="form-group">
<label for="service_extra_networks" class="col-sm-1 control-label text-left">Extra networks</label> <div class="col-sm-12" style="margin-top: 5px;">
<div class="col-sm-11"> <label class="control-label text-left">Extra networks</label>
<span class="label label-default interactive" ng-click="addExtraNetwork()"> <span class="label label-default interactive" style="margin-left: 10px;" ng-click="addExtraNetwork()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> network <i class="fa fa-plus-circle" aria-hidden="true"></i> add extra network
</span> </span>
</div> </div>
<!-- network-input-list --> <!-- network-input-list -->
<div style="margin-top: 10px;"> <div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div class="col-sm-12" ng-repeat="network in formValues.ExtraNetworks" style="margin-top: 5px;"> <div ng-repeat="network in formValues.ExtraNetworks" style="margin-top: 2px;">
<div class="input-group col-sm-9 input-group-sm col-sm-offset-1">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeExtraNetwork($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
<select class="form-control" ng-model="network.Name"> <select class="form-control" ng-model="network.Name">
<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>
</div> <button class="btn btn-sm btn-danger" type="button" ng-click="removeExtraNetwork($index)">
<div class="col-sm-2"></div> <i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div> </div>
</div> </div>
<!-- !network-input-list --> <!-- !network-input-list -->
@ -286,14 +338,14 @@
<form class="form-horizontal" style="margin-top: 15px;"> <form class="form-horizontal" style="margin-top: 15px;">
<!-- labels --> <!-- labels -->
<div class="form-group"> <div class="form-group">
<label for="service_env" class="col-sm-1 control-label text-left">Labels</label> <div class="col-sm-12" style="margin-top: 5px;">
<div class="col-sm-11"> <label class="control-label text-left">Service labels</label>
<span class="label label-default interactive" ng-click="addLabel()"> <span class="label label-default interactive" style="margin-left: 10px;" ng-click="addLabel()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> label <i class="fa fa-plus-circle" aria-hidden="true"></i> add service label
</span> </span>
</div> </div>
<!-- labels-input-list --> <!-- labels-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;"> <div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="label in formValues.Labels" style="margin-top: 2px;"> <div ng-repeat="label in formValues.Labels" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm"> <div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span> <span class="input-group-addon">name</span>
@ -302,12 +354,10 @@
<div class="input-group col-sm-5 input-group-sm"> <div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span> <span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar"> <input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeLabel($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div> </div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeLabel($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div> </div>
</div> </div>
<!-- !labels-input-list --> <!-- !labels-input-list -->
@ -315,14 +365,14 @@
<!-- !labels--> <!-- !labels-->
<!-- container-labels --> <!-- container-labels -->
<div class="form-group"> <div class="form-group">
<label for="service_env" class="col-sm-1 control-label text-left">Container labels</label> <div class="col-sm-12" style="margin-top: 5px;">
<div class="col-sm-11"> <label class="control-label text-left">Container labels</label>
<span class="label label-default interactive" ng-click="addContainerLabel()"> <span class="label label-default interactive" style="margin-left: 10px;" ng-click="addContainerLabel()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> container label <i class="fa fa-plus-circle" aria-hidden="true"></i> add container label
</span> </span>
</div> </div>
<!-- container-labels-input-list --> <!-- container-labels-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;"> <div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="label in formValues.ContainerLabels" style="margin-top: 2px;"> <div ng-repeat="label in formValues.ContainerLabels" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm"> <div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span> <span class="input-group-addon">name</span>
@ -331,12 +381,10 @@
<div class="input-group col-sm-5 input-group-sm"> <div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span> <span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar"> <input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeContainerLabel($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div> </div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeContainerLabel($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div> </div>
</div> </div>
<!-- !container-labels-input-list --> <!-- !container-labels-input-list -->
@ -350,11 +398,11 @@
<form class="form-horizontal" style="margin-top: 15px;"> <form class="form-horizontal" style="margin-top: 15px;">
<!-- parallelism-input --> <!-- parallelism-input -->
<div class="form-group"> <div class="form-group">
<label for="parallelism" class="col-sm-1 control-label text-left">Parallelism</label> <label for="parallelism" class="col-sm-2 col-lg-1 control-label text-left">Parallelism</label>
<div class="col-sm-1"> <div class="col-sm-2">
<input type="number" class="form-control" ng-model="formValues.Parallelism" id="parallelism" placeholder="e.g. 1"> <input type="number" class="form-control" ng-model="formValues.Parallelism" id="parallelism" placeholder="e.g. 1">
</div> </div>
<div class="col-sm-10"> <div class="col-sm-8">
<p class="small text-muted" style="margin-top: 10px;"> <p class="small text-muted" style="margin-top: 10px;">
Maximum number of tasks to be updated simultaneously (0 to update all at once). Maximum number of tasks to be updated simultaneously (0 to update all at once).
</p> </p>
@ -363,11 +411,11 @@
<!-- !parallelism-input --> <!-- !parallelism-input -->
<!-- delay-input --> <!-- delay-input -->
<div class="form-group"> <div class="form-group">
<label for="update-delay" class="col-sm-1 control-label text-left">Delay</label> <label for="update-delay" class="col-sm-2 col-lg-1 control-label text-left">Delay</label>
<div class="col-sm-2"> <div class="col-sm-2">
<input type="number" class="form-control" ng-model="formValues.UpdateDelay" id="update-delay" placeholder="e.g. 10"> <input type="number" class="form-control" ng-model="formValues.UpdateDelay" id="update-delay" placeholder="e.g. 10">
</div> </div>
<div class="col-sm-9"> <div class="col-sm-8">
<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.
</p> </p>
@ -376,40 +424,20 @@
<!-- !delay-input --> <!-- !delay-input -->
<!-- failureAction-input --> <!-- failureAction-input -->
<div class="form-group"> <div class="form-group">
<label for="failure_action" class="col-sm-1 control-label text-left">Failure Action</label> <div class="col-sm-12">
<div class="col-sm-3"> <label class="control-label text-left">Failure action</label>
<label class="radio-inline"> <div class="btn-group btn-group-sm" style="margin-left: 20px;">
<input type="radio" name="failure_action" ng-model="formValues.FailureAction" value="continue"> <label class="btn btn-primary" ng-model="formValues.FailureAction" uib-btn-radio="'continue'">Continue</label>
Continue <label class="btn btn-primary" ng-model="formValues.FailureAction" uib-btn-radio="'pause'">Pause</label>
</label> </div>
<label class="radio-inline">
<input type="radio" name="failure_action" ng-model="formValues.FailureAction" value="pause">
Pause
</label>
</div> </div>
<div class="col-sm-8"></div>
</div> </div>
<!-- !failureAction-input --> <!-- !failureAction-input -->
</form> </form>
</div> </div>
<!-- !tab-update-config --> <!-- !tab-update-config -->
<!-- tab-security -->
<div class="tab-pane" id="security">
</div>
<!-- !tab-security -->
</div> </div>
</rd-widget-body> </rd-widget-body>
</rd-widget> </rd-widget>
</div> </div>
</div> </div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12" style="text-align: center;">
<div>
<i id="createServiceSpinner" class="fa fa-cog fa-3x fa-spin" style="margin-bottom: 5px; display: none;"></i>
</div>
<button type="button" class="btn btn-default btn-lg" ng-click="create()">Create</button>
<a type="button" class="btn btn-default btn-lg" ui-sref="services">Cancel</a>
</div>
</div>

View file

@ -18,6 +18,9 @@
</div> </div>
</div> </div>
<!-- !name-input --> <!-- !name-input -->
<div class="col-sm-12 form-section-title">
Driver configuration
</div>
<!-- driver-input --> <!-- driver-input -->
<div class="form-group"> <div class="form-group">
<label for="volume_driver" class="col-sm-1 control-label text-left">Driver</label> <label for="volume_driver" class="col-sm-1 control-label text-left">Driver</label>
@ -28,14 +31,17 @@
<!-- !driver-input --> <!-- !driver-input -->
<!-- driver-options --> <!-- driver-options -->
<div class="form-group"> <div class="form-group">
<label for="volume_driveropts" class="col-sm-1 control-label text-left">Driver options</label> <div class="col-sm-12" style="margin-top: 5px;">
<div class="col-sm-11"> <label class="control-label text-left">
<span class="label label-default interactive" ng-click="addDriverOption()"> Driver options
<i class="fa fa-plus-circle" aria-hidden="true"></i> driver option <portainer-tooltip position="bottom" message="Driver options are specific to the selected driver. Please refer to the selected driver documentation."></portainer-tooltip>
</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addDriverOption()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add driver option
</span> </span>
</div> </div>
<!-- driver-options-input-list --> <!-- driver-options-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;"> <div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="option in formValues.DriverOptions" style="margin-top: 2px;"> <div ng-repeat="option in formValues.DriverOptions" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm"> <div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span> <span class="input-group-addon">name</span>
@ -44,17 +50,18 @@
<div class="input-group col-sm-5 input-group-sm"> <div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span> <span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="option.value" placeholder="e.g. /path/on/host"> <input type="text" class="form-control" ng-model="option.value" placeholder="e.g. /path/on/host">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeDriverOption($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div> </div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeDriverOption($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div> </div>
</div> </div>
<!-- !driver-options-input-list --> <!-- !driver-options-input-list -->
</div> </div>
<!-- !driver-options --> <!-- !driver-options -->
<div class="col-sm-12 form-section-title" ng-if="applicationState.application.authentication">
Access control
</div>
<!-- ownership --> <!-- ownership -->
<div class="form-group" ng-if="applicationState.application.authentication"> <div class="form-group" ng-if="applicationState.application.authentication">
<div class="col-sm-12"> <div class="col-sm-12">
@ -63,11 +70,11 @@
<portainer-tooltip position="bottom" message="When setting the ownership value to private, only you and the administrators will be able to see and manage this object. When choosing public, everybody will be able to access it."></portainer-tooltip> <portainer-tooltip position="bottom" message="When setting the ownership value to private, only you and the administrators will be able to see and manage this object. When choosing public, everybody will be able to access it."></portainer-tooltip>
</label> </label>
<div class="btn-group btn-group-sm" style="margin-left: 20px;"> <div class="btn-group btn-group-sm" style="margin-left: 20px;">
<label class="btn btn-default" ng-model="formValues.Ownership" uib-btn-radio="'private'"> <label class="btn btn-primary" ng-model="formValues.Ownership" uib-btn-radio="'private'">
<i class="fa fa-eye-slash" aria-hidden="true"></i> <i class="fa fa-eye-slash" aria-hidden="true"></i>
Private Private
</label> </label>
<label class="btn btn-default" ng-model="formValues.Ownership" uib-btn-radio="'public'"> <label class="btn btn-primary" ng-model="formValues.Ownership" uib-btn-radio="'public'">
<i class="fa fa-eye" aria-hidden="true"></i> <i class="fa fa-eye" aria-hidden="true"></i>
Public Public
</label> </label>
@ -75,18 +82,20 @@
</div> </div>
</div> </div>
<!-- !ownership --> <!-- !ownership -->
<!-- actions -->
<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-click="create()">Create volume</button>
<a type="button" class="btn btn-default btn-sm" ui-sref="volumes">Cancel</a>
<i id="createVolumeSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
<!-- !actions -->
</form> </form>
</rd-widget-body> </rd-widget-body>
</rd-widget> </rd-widget>
</div> </div>
</div> </div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12" style="text-align: center;">
<div>
<i id="createVolumeSpinner" class="fa fa-cog fa-3x fa-spin" style="margin-bottom: 5px; display: none;"></i>
</div>
<button type="button" class="btn btn-default btn-lg" ng-click="create()">Create</button>
<a type="button" class="btn btn-default btn-lg" ui-sref="volumes">Cancel</a>
</div>
</div>

View file

@ -85,7 +85,7 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12"> <div class="col-xs-12 col-md-6">
<a ui-sref="containers"> <a ui-sref="containers">
<rd-widget> <rd-widget>
<rd-widget-body> <rd-widget-body>
@ -102,7 +102,7 @@
</rd-widget> </rd-widget>
</a> </a>
</div> </div>
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12"> <div class="col-xs-12 col-md-6">
<a ui-sref="images"> <a ui-sref="images">
<rd-widget> <rd-widget>
<rd-widget-body> <rd-widget-body>
@ -118,7 +118,7 @@
</rd-widget> </rd-widget>
</a> </a>
</div> </div>
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12"> <div class="col-xs-12 col-md-6">
<a ui-sref="volumes"> <a ui-sref="volumes">
<rd-widget> <rd-widget>
<rd-widget-body> <rd-widget-body>
@ -134,7 +134,7 @@
</rd-widget> </rd-widget>
</a> </a>
</div> </div>
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12"> <div class="col-xs-12 col-md-6">
<a ui-sref="networks"> <a ui-sref="networks">
<rd-widget> <rd-widget>
<rd-widget-body> <rd-widget-body>

View file

@ -14,25 +14,33 @@
<form class="form-horizontal"> <form class="form-horizontal">
<!-- name-input --> <!-- name-input -->
<div class="form-group"> <div class="form-group">
<label for="container_name" class="col-sm-2 control-label text-left">Name</label> <label for="container_name" class="col-sm-3 col-lg-2 control-label text-left">Name</label>
<div class="col-sm-10"> <div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="container_name" ng-model="endpoint.Name" placeholder="e.g. docker-prod01"> <input type="text" class="form-control" id="container_name" ng-model="endpoint.Name" placeholder="e.g. docker-prod01">
</div> </div>
</div> </div>
<!-- !name-input --> <!-- !name-input -->
<!-- endpoint-url-input --> <!-- endpoint-url-input -->
<div class="form-group"> <div class="form-group">
<label for="endpoint_url" class="col-sm-2 control-label text-left">Endpoint URL</label> <label for="endpoint_url" class="col-sm-3 col-lg-2 control-label text-left">
<div class="col-sm-10"> Endpoint URL
<portainer-tooltip position="bottom" message="URL or IP address of a Docker host. The Docker API must be exposed over a TCP port. Please refer to the Docker documentation to configure it."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input ng-disabled="endpointType === 'local'" type="text" class="form-control" id="endpoint_url" ng-model="endpoint.URL" placeholder="e.g. 10.0.0.10:2375 or mydocker.mydomain.com:2375"> <input ng-disabled="endpointType === 'local'" type="text" class="form-control" id="endpoint_url" ng-model="endpoint.URL" placeholder="e.g. 10.0.0.10:2375 or mydocker.mydomain.com:2375">
</div> </div>
</div> </div>
<!-- !endpoint-url-input --> <!-- !endpoint-url-input -->
<!-- tls-checkbox --> <!-- tls-checkbox -->
<div class="form-group" ng-if="endpointType === 'remote'"> <div class="form-group" ng-if="endpointType === 'remote'">
<label for="tls" class="col-sm-2 control-label text-left">TLS</label> <div class="col-sm-12">
<div class="col-sm-10"> <label for="tls" class="control-label text-left">
<input type="checkbox" name="tls" ng-model="endpoint.TLS"> TLS
<portainer-tooltip position="bottom" message="Enable this option if you need to specify TLS certificates to connect to the Docker endpoint."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="endpoint.TLS"><i></i>
</label>
</div> </div>
</div> </div>
<!-- !tls-checkbox --> <!-- !tls-checkbox -->

View file

@ -1,7 +1,7 @@
<div class="page-wrapper"> <div class="page-wrapper">
<!-- simple box --> <!-- simple box -->
<div class="container simple-box"> <div class="container simple-box">
<div class="col-md-8 col-md-offset-2 col-sm-8 col-sm-offset-2"> <div class="col-md-8 col-md-offset-2 col-sm-10 col-sm-offset-1">
<!-- simple box logo --> <!-- simple box logo -->
<div class="row"> <div class="row">
<img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo"> <img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo">
@ -14,8 +14,8 @@
<!-- init-endpoint form --> <!-- init-endpoint form -->
<form class="form-horizontal" style="margin: 20px;" enctype="multipart/form-data" method="POST"> <form class="form-horizontal" style="margin: 20px;" enctype="multipart/form-data" method="POST">
<!-- comment --> <!-- comment -->
<div class="form-group"> <div class="form-group" style="text-align: center;">
<p>Connect Portainer to a Docker engine or Swarm cluster endpoint.</p> <h4>Connect Portainer to a Docker engine or Swarm cluster endpoint</h4>
</div> </div>
<!-- !comment input --> <!-- !comment input -->
<!-- endpoin-type radio --> <!-- endpoin-type radio -->
@ -54,25 +54,33 @@
<div ng-if="formValues.endpointType === 'remote'" style="margin-top: 25px;"> <div ng-if="formValues.endpointType === 'remote'" style="margin-top: 25px;">
<!-- name-input --> <!-- name-input -->
<div class="form-group"> <div class="form-group">
<label for="container_name" class="col-sm-3 control-label text-left">Name</label> <label for="container_name" class="col-sm-4 col-lg-3 control-label text-left">Name</label>
<div class="col-sm-9"> <div class="col-sm-8 col-lg-9">
<input type="text" class="form-control" id="container_name" ng-model="formValues.Name" placeholder="e.g. docker-prod01"> <input type="text" class="form-control" id="container_name" ng-model="formValues.Name" placeholder="e.g. docker-prod01">
</div> </div>
</div> </div>
<!-- !name-input --> <!-- !name-input -->
<!-- endpoint-url-input --> <!-- endpoint-url-input -->
<div class="form-group"> <div class="form-group">
<label for="endpoint_url" class="col-sm-3 control-label text-left">Endpoint URL</label> <label for="endpoint_url" class="col-sm-4 col-lg-3 control-label text-left">
<div class="col-sm-9"> Endpoint URL
<portainer-tooltip position="bottom" message="URL or IP address of a Docker host. The Docker API must be exposed over a TCP port. Please refer to the Docker documentation to configure it."></portainer-tooltip>
</label>
<div class="col-sm-8 col-lg-9">
<input type="text" class="form-control" id="endpoint_url" ng-model="formValues.URL" placeholder="e.g. 10.0.0.10:2375 or mydocker.mydomain.com:2375"> <input type="text" class="form-control" id="endpoint_url" ng-model="formValues.URL" placeholder="e.g. 10.0.0.10:2375 or mydocker.mydomain.com:2375">
</div> </div>
</div> </div>
<!-- !endpoint-url-input --> <!-- !endpoint-url-input -->
<!-- tls-checkbox --> <!-- tls-checkbox -->
<div class="form-group"> <div class="form-group">
<label for="tls" class="col-sm-3 control-label text-left">TLS</label> <div class="col-sm-12">
<div class="col-sm-9"> <label for="tls" class="control-label text-left">
<input type="checkbox" name="tls" ng-model="formValues.TLS"> TLS
<portainer-tooltip position="bottom" message="Enable this option if you need to specify TLS certificates to connect to the Docker endpoint."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="formValues.TLS"><i></i>
</label>
</div> </div>
</div> </div>
<!-- !tls-checkbox --> <!-- !tls-checkbox -->

View file

@ -29,25 +29,33 @@
<form class="form-horizontal"> <form class="form-horizontal">
<!-- name-input --> <!-- name-input -->
<div class="form-group"> <div class="form-group">
<label for="container_name" class="col-sm-2 control-label text-left">Name</label> <label for="container_name" class="col-sm-3 col-lg-2 control-label text-left">Name</label>
<div class="col-sm-10"> <div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="container_name" ng-model="formValues.Name" placeholder="e.g. docker-prod01"> <input type="text" class="form-control" id="container_name" ng-model="formValues.Name" placeholder="e.g. docker-prod01">
</div> </div>
</div> </div>
<!-- !name-input --> <!-- !name-input -->
<!-- endpoint-url-input --> <!-- endpoint-url-input -->
<div class="form-group"> <div class="form-group">
<label for="endpoint_url" class="col-sm-2 control-label text-left">Endpoint URL</label> <label for="endpoint_url" class="col-sm-3 col-lg-2 control-label text-left">
<div class="col-sm-10"> Endpoint URL
<portainer-tooltip position="bottom" message="URL or IP address of a Docker host. The Docker API must be exposed over a TCP port. Please refer to the Docker documentation to configure it."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="endpoint_url" ng-model="formValues.URL" placeholder="e.g. 10.0.0.10:2375 or mydocker.mydomain.com:2375"> <input type="text" class="form-control" id="endpoint_url" ng-model="formValues.URL" placeholder="e.g. 10.0.0.10:2375 or mydocker.mydomain.com:2375">
</div> </div>
</div> </div>
<!-- !endpoint-url-input --> <!-- !endpoint-url-input -->
<!-- tls-checkbox --> <!-- tls-checkbox -->
<div class="form-group"> <div class="form-group">
<label for="tls" class="col-sm-2 control-label text-left">TLS</label> <div class="col-sm-12">
<div class="col-sm-10"> <label for="tls" class="control-label text-left">
<input type="checkbox" name="tls" ng-model="formValues.TLS"> TLS
<portainer-tooltip position="bottom" message="Enable this option if you need to specify TLS certificates to connect to the Docker endpoint."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="formValues.TLS"><i></i>
</label>
</div> </div>
</div> </div>
<!-- !tls-checkbox --> <!-- !tls-checkbox -->

View file

@ -7,28 +7,42 @@
</rd-header-content> </rd-header-content>
</rd-header> </rd-header>
<div class="row" ng-if="RepoTags.length > 0"> <div class="row" ng-if="image.RepoTags.length > 0">
<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-header icon="fa fa-tags" title="Image tags"></rd-widget-header> <rd-widget-header icon="fa fa-tags" title="Image tags"></rd-widget-header>
<rd-widget-body classes="no-padding"> <rd-widget-body>
<div style="margin: 5px 10px;"> <form class="form-horizontal">
<span ng-repeat="tag in RepoTags" class="label label-primary image-tag space-right"> <div class="form-group">
<a data-toggle="tooltip" class="interactive" title="Push to registry" ng-click="pushImage(tag)"> <div class="row">
<i class="fa fa-upload white-icon" aria-hidden="true"></i> <div class="pull-left" ng-repeat="tag in image.RepoTags" style="display:table">
<div class="input-group col-md-1" style="padding:0 15px">
<span class="input-group-addon">{{ tag }}</span>
<span class="input-group-btn">
<a data-toggle="tooltip" class="btn btn-primary interactive" title="Push to registry" ng-click="pushImage(tag)">
<span class="fa fa-upload white-icon" aria-hidden="true"></span>
</a> </a>
{{ tag }} <a data-toggle="tooltip" class="btn btn-primary interactive" title="Pull from registry" ng-click="pullImage(tag)">
<a data-toggle="tooltip" class="interactive" title="Remove tag" ng-click="removeImage(tag)"> <span class="fa fa-download white-icon" aria-hidden="true"></span>
<i class="fa fa-trash-o white-icon" aria-hidden="true"></i> </a>
<a data-toggle="tooltip" class="btn btn-primary interactive" title="Remove tag" ng-click="removeTag(tag)">
<span class="fa fa-trash-o white-icon" aria-hidden="true"></span>
</a> </a>
</span> </span>
</div> </div>
<div style="margin: 5px 10px;"> </div>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted"> <span class="small text-muted">
Note: you can click on the upload icon to push an image Note: you can click on the upload icon <span class="fa fa-upload" aria-hidden="true"></span> to push an image
and on the trash icon to delete a tag or on the download icon <span class="fa fa-download" aria-hidden="true"></span> to pull an image
or on the trash icon <span class="fa fa-trash-o" aria-hidden="true"></span> to delete a tag.
</span> </span>
</div> </div>
</div>
</form>
</rd-widget-body> </rd-widget-body>
</rd-widget> </rd-widget>
</div> </div>
@ -43,12 +57,15 @@
<!-- name-and-registry-inputs --> <!-- name-and-registry-inputs -->
<div class="form-group"> <div class="form-group">
<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-7"> <div class="col-sm-11 col-md-6">
<input type="text" class="form-control" ng-model="config.Image" id="image_name" placeholder="e.g. myImage:myTag"> <input type="text" class="form-control" ng-model="config.Image" id="image_name" placeholder="e.g. myImage:myTag">
</div> </div>
<label for="image_registry" class="col-sm-1 control-label text-left">Registry</label> <label for="image_registry" class="col-sm-2 margin-sm-top control-label text-left">
<div class="col-sm-3"> Registry
<input type="text" class="form-control" ng-model="config.Registry" id="image_registry" placeholder="optional"> <portainer-tooltip position="bottom" message="A registry to pull the image from. Leave empty to use the official Docker registry."></portainer-tooltip>
</label>
<div class="col-sm-10 col-md-3 margin-sm-top">
<input type="text" class="form-control" ng-model="config.Registry" id="image_registry" placeholder="e.g. myregistry.mydomain">
</div> </div>
</div> </div>
<!-- !name-and-registry-inputs --> <!-- !name-and-registry-inputs -->
@ -61,7 +78,7 @@
<!-- !tag-note --> <!-- !tag-note -->
<div class="form-group"> <div class="form-group">
<div class="col-sm-12"> <div class="col-sm-12">
<button type="button" class="btn btn-default btn-sm" ng-disabled="!config.Image" ng-click="tagImage()">Tag</button> <button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Image" ng-click="tagImage()">Tag</button>
<i id="pullImageSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i> <i id="pullImageSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div> </div>
</div> </div>
@ -121,33 +138,33 @@
<tbody> <tbody>
<tr> <tr>
<td>CMD</td> <td>CMD</td>
<td><code>{{ image.ContainerConfig.Cmd|command }}</code></td> <td><code>{{ image.Command|command }}</code></td>
</tr> </tr>
<tr ng-if="image.ContainerConfig.Entrypoint"> <tr ng-if="image.Entrypoint">
<td>ENTRYPOINT</td> <td>ENTRYPOINT</td>
<td><code>{{ image.ContainerConfig.Entrypoint|command }}</code></td> <td><code>{{ image.Entrypoint|command }}</code></td>
</tr> </tr>
<tr ng-if="image.ContainerConfig.ExposedPorts"> <tr ng-if="image.ExposedPorts.length > 0">
<td>EXPOSE</td> <td>EXPOSE</td>
<td> <td>
<span class="label label-default space-right" ng-repeat="port in exposedPorts"> <span class="label label-default space-right" ng-repeat="port in image.ExposedPorts">
{{ port }} {{ port }}
</span> </span>
</td> </td>
</tr> </tr>
<tr ng-if="image.ContainerConfig.Volumes"> <tr ng-if="image.Volumes.length > 0">
<td>VOLUME</td> <td>VOLUME</td>
<td> <td>
<span class="label label-default space-right" ng-repeat="volume in volumes"> <span class="label label-default space-right" ng-repeat="volume in image.Volumes">
{{ volume }} {{ volume }}
</span> </span>
</td> </td>
</tr> </tr>
<tr> <tr ng-if="image.Env.length > 0">
<td>ENV</td> <td>ENV</td>
<td> <td>
<table class="table table-bordered table-condensed"> <table class="table table-bordered table-condensed">
<tr ng-repeat="var in image.ContainerConfig.Env"> <tr ng-repeat="var in image.Env">
<td>{{ var|key: '=' }}</td> <td>{{ var|key: '=' }}</td>
<td>{{ var|value: '=' }}</td> <td>{{ var|value: '=' }}</td>
</tr> </tr>

View file

@ -1,89 +1,109 @@
angular.module('image', []) angular.module('image', [])
.controller('ImageController', ['$scope', '$stateParams', '$state', 'Image', 'ImageHelper', 'Messages', .controller('ImageController', ['$scope', '$stateParams', '$state', 'ImageService', 'Messages',
function ($scope, $stateParams, $state, Image, ImageHelper, Messages) { function ($scope, $stateParams, $state, ImageService, Messages) {
$scope.RepoTags = [];
$scope.config = { $scope.config = {
Image: '', Image: '',
Registry: '' Registry: ''
}; };
// Get RepoTags from the /images/query endpoint instead of /image/json,
// for backwards compatibility with Docker API versions older than 1.21
function getRepoTags(imageId) {
Image.query({}, function (d) {
d.forEach(function(image) {
if (image.Id === imageId && image.RepoTags[0] !== '<none>:<none>') {
$scope.RepoTags = image.RepoTags;
}
});
});
}
$scope.tagImage = function() { $scope.tagImage = function() {
$('#loadingViewSpinner').show(); $('#loadingViewSpinner').show();
var image = $scope.config.Image; var image = $scope.config.Image;
var registry = $scope.config.Registry; var registry = $scope.config.Registry;
var imageConfig = ImageHelper.createImageConfigForCommit(image, registry);
Image.tag({id: $stateParams.id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) { ImageService.tagImage($stateParams.id, image, registry)
.then(function success(data) {
Messages.send('Image successfully tagged'); Messages.send('Image successfully tagged');
$('#loadingViewSpinner').hide();
$state.go('image', {id: $stateParams.id}, {reload: true}); $state.go('image', {id: $stateParams.id}, {reload: true});
}, function(e) { })
.catch(function error(err) {
Messages.error("Failure", err, "Unable to tag image");
})
.finally(function final() {
$('#loadingViewSpinner').hide(); $('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to tag image");
}); });
}; };
$scope.pushImage = function(tag) { $scope.pushImage = function(tag) {
$('#loadingViewSpinner').show(); $('#loadingViewSpinner').show();
Image.push({tag: tag}, function (d) { ImageService.pushImage(tag)
if (d[d.length-1].error) { .then(function success() {
Messages.error("Unable to push image", {}, d[d.length-1].error);
} else {
Messages.send('Image successfully pushed'); Messages.send('Image successfully pushed');
})
.catch(function error(err) {
Messages.error("Failure", err, "Unable to push image tag");
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
};
$scope.pullImage = function(tag) {
$('#loadingViewSpinner').show();
var image = $scope.config.Image;
var registry = $scope.config.Registry;
ImageService.pullImage(image, registry)
.then(function success(data) {
Messages.send('Image successfully pulled', image);
})
.catch(function error(err){
Messages.error("Failure", err, "Unable to pull image");
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
};
$scope.removeTag = function(id) {
$('#loadingViewSpinner').show();
ImageService.deleteImage(id, false)
.then(function success() {
if ($scope.image.RepoTags.length === 1) {
Messages.send('Image successfully deleted', id);
$state.go('images', {}, {reload: true});
} else {
Messages.send('Tag successfully deleted', id);
$state.go('image', {id: $stateParams.id}, {reload: true});
} }
})
.catch(function error(err) {
Messages.error("Failure", err, 'Unable to remove image');
})
.finally(function final() {
$('#loadingViewSpinner').hide(); $('#loadingViewSpinner').hide();
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to push image");
}); });
}; };
$scope.removeImage = function (id) { $scope.removeImage = function (id) {
$('#loadingViewSpinner').show(); $('#loadingViewSpinner').show();
Image.remove({id: id}, function (d) { ImageService.deleteImage(id, false)
if (d[0].message) { .then(function success() {
$('#loadingViewSpinner').hide(); Messages.send('Image successfully deleted', id);
Messages.error("Unable to remove image", {}, d[0].message);
} else {
// If last message key is 'Deleted' or if it's 'Untagged' and there is only one tag associated to the image
// then assume the image is gone and send to images page
if (d[d.length-1].Deleted || (d[d.length-1].Untagged && $scope.RepoTags.length === 1)) {
Messages.send('Image successfully deleted');
$state.go('images', {}, {reload: true}); $state.go('images', {}, {reload: true});
} else { })
Messages.send('Tag successfully deleted'); .catch(function error(err) {
$state.go('image', {id: $stateParams.id}, {reload: true}); Messages.error("Failure", err, 'Unable to remove image');
} })
} .finally(function final() {
}, function (e) {
$('#loadingViewSpinner').hide(); $('#loadingViewSpinner').hide();
Messages.error("Failure", e, 'Unable to remove image');
}); });
}; };
function retrieveImageDetails() {
$('#loadingViewSpinner').show(); $('#loadingViewSpinner').show();
Image.get({id: $stateParams.id}, function (d) { ImageService.image($stateParams.id)
$scope.image = d; .then(function success(data) {
if (d.RepoTags) { $scope.image = data;
$scope.RepoTags = d.RepoTags; })
} else { .catch(function error(err) {
getRepoTags(d.Id); Messages.error("Failure", err, "Unable to retrieve image details");
} $state.go('images');
})
.finally(function final() {
$('#loadingViewSpinner').hide(); $('#loadingViewSpinner').hide();
$scope.exposedPorts = d.ContainerConfig.ExposedPorts ? Object.keys(d.ContainerConfig.ExposedPorts) : [];
$scope.volumes = d.ContainerConfig.Volumes ? Object.keys(d.ContainerConfig.Volumes) : [];
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve image info");
}); });
}
retrieveImageDetails();
}]); }]);

View file

@ -18,12 +18,15 @@
<!-- name-and-registry-inputs --> <!-- name-and-registry-inputs -->
<div class="form-group"> <div class="form-group">
<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-7"> <div class="col-sm-11 col-md-6">
<input type="text" class="form-control" ng-model="config.Image" id="image_name" placeholder="e.g. ubuntu:trusty"> <input type="text" class="form-control" ng-model="config.Image" id="image_name" placeholder="e.g. ubuntu:trusty">
</div> </div>
<label for="image_registry" class="col-sm-1 control-label text-left">Registry</label> <label for="image_registry" class="col-sm-2 margin-sm-top control-label text-left">
<div class="col-sm-3"> Registry
<input type="text" class="form-control" ng-model="config.Registry" id="image_registry" placeholder="leave empty to use DockerHub"> <portainer-tooltip position="bottom" message="A registry to pull the image from. Leave empty to use the official Docker registry."></portainer-tooltip>
</label>
<div class="col-sm-10 col-md-3 margin-sm-top">
<input type="text" class="form-control" ng-model="config.Registry" id="image_registry" placeholder="e.g. myregistry.mydomain">
</div> </div>
</div> </div>
<!-- !name-and-registry-inputs --> <!-- !name-and-registry-inputs -->
@ -36,7 +39,7 @@
<!-- !tag-note --> <!-- !tag-note -->
<div class="form-group"> <div class="form-group">
<div class="col-sm-12"> <div class="col-sm-12">
<button type="button" class="btn btn-default btn-sm" ng-disabled="!config.Image" ng-click="pullImage()">Pull</button> <button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Image" ng-click="pullImage()">Pull</button>
<i id="pullImageSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i> <i id="pullImageSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div> </div>
</div> </div>

View file

@ -1,6 +1,6 @@
angular.module('images', []) angular.module('images', [])
.controller('ImagesController', ['$scope', '$state', 'Config', 'Image', 'ImageHelper', 'Messages', 'Pagination', 'ModalService', .controller('ImagesController', ['$scope', '$state', 'Config', 'ImageService', 'Messages', 'Pagination', 'ModalService',
function ($scope, $state, Config, Image, ImageHelper, Messages, Pagination, ModalService) { function ($scope, $state, Config, ImageService, Messages, Pagination, ModalService) {
$scope.state = {}; $scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('images'); $scope.state.pagination_count = Pagination.getPaginationCount('images');
$scope.sortType = 'RepoTags'; $scope.sortType = 'RepoTags';
@ -42,20 +42,15 @@ function ($scope, $state, Config, Image, ImageHelper, Messages, Pagination, Moda
$('#pullImageSpinner').show(); $('#pullImageSpinner').show();
var image = $scope.config.Image; var image = $scope.config.Image;
var registry = $scope.config.Registry; var registry = $scope.config.Registry;
var imageConfig = ImageHelper.createImageConfigForContainer(image, registry); ImageService.pullImage(image, registry)
Image.create(imageConfig, function (data) { .then(function success(data) {
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
if (err) {
var detail = data[data.length - 1];
$('#pullImageSpinner').hide();
Messages.error('Error', {}, detail.error);
} else {
$('#pullImageSpinner').hide();
$state.reload(); $state.reload();
} })
}, function (e) { .catch(function error(err) {
Messages.error("Failure", err, "Unable to pull image");
})
.finally(function final() {
$('#pullImageSpinner').hide(); $('#pullImageSpinner').hide();
Messages.error("Failure", e, "Unable to pull image");
}); });
}; };
@ -79,18 +74,16 @@ function ($scope, $state, Config, Image, ImageHelper, Messages, Pagination, Moda
angular.forEach($scope.images, function (i) { angular.forEach($scope.images, function (i) {
if (i.Checked) { if (i.Checked) {
counter = counter + 1; counter = counter + 1;
Image.remove({id: i.Id, force: force}, function (d) { ImageService.deleteImage(i.Id, force)
if (d[0].message) { .then(function success(data) {
$('#loadImagesSpinner').hide();
Messages.error("Unable to remove image", {}, d[0].message);
} else {
Messages.send("Image deleted", i.Id); Messages.send("Image deleted", i.Id);
var index = $scope.images.indexOf(i); var index = $scope.images.indexOf(i);
$scope.images.splice(index, 1); $scope.images.splice(index, 1);
} })
complete(); .catch(function error(err) {
}, function (e) { Messages.error("Failure", err, 'Unable to remove image');
Messages.error("Failure", e, 'Unable to remove image'); })
.finally(function final() {
complete(); complete();
}); });
} }
@ -98,19 +91,19 @@ function ($scope, $state, Config, Image, ImageHelper, Messages, Pagination, Moda
}; };
function fetchImages() { function fetchImages() {
Image.query({}, function (d) { $('#loadImagesSpinner').show();
$scope.images = d.map(function (item) { ImageService.images()
return new ImageViewModel(item); .then(function success(data) {
}); $scope.images = data;
$('#loadImagesSpinner').hide(); })
}, function (e) { .catch(function error(err) {
$('#loadImagesSpinner').hide(); Messages.error("Failure", err, "Unable to retrieve images");
Messages.error("Failure", e, "Unable to retrieve images");
$scope.images = []; $scope.images = [];
})
.finally(function final() {
$('#loadImagesSpinner').hide();
}); });
} }
Config.$promise.then(function (c) {
fetchImages(); fetchImages();
});
}]); }]);

View file

@ -37,8 +37,8 @@
<!-- !tag-note --> <!-- !tag-note -->
<div class="form-group"> <div class="form-group">
<div class="col-sm-12"> <div class="col-sm-12">
<button type="button" class="btn btn-default btn-sm" ng-disabled="!config.Name" ng-click="createNetwork()">Create</button> <button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Name" ng-click="createNetwork()">Create</button>
<button type="button" class="btn btn-default btn-sm" ui-sref="actions.create.network">Advanced settings...</button> <button type="button" class="btn btn-primary btn-sm" ui-sref="actions.create.network">Advanced settings...</button>
<i id="createNetworkSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i> <i id="createNetworkSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div> </div>
</div> </div>

View file

@ -177,8 +177,8 @@
<span class="input-group-addon fit-text-size">value</span> <span class="input-group-addon fit-text-size">value</span>
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar" ng-change="updateLabel(node, label)"> <input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar" ng-change="updateLabel(node, label)">
<span class="input-group-btn"> <span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeLabel(node, $index)"> <button class="btn btn-sm btn-danger" type="button" ng-click="removeLabel(node, $index)">
<i class="fa fa-minus" aria-hidden="true"></i> <i class="fa fa-trash" aria-hidden="true"></i>
</button> </button>
</span> </span>
</div> </div>

View file

@ -0,0 +1,66 @@
<div ng-if="service.ServiceConstraints">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Placement constraints">
<div class="nopadding">
<a class="btn btn-default btn-sm pull-right" ng-click="isUpdating || addPlacementConstraint(service)" ng-disabled="isUpdating">
<i class="fa fa-plus-circle" aria-hidden="true"></i> placement constraint
</a>
</div>
</rd-widget-header>
<rd-widget-body ng-if="service.ServiceConstraints.length === 0">
<p>There are no placement constraints for this service.</p>
</rd-widget-body>
<rd-widget-body ng-if="service.ServiceConstraints.length > 0" classes="no-padding">
<table class="table" >
<thead>
<tr>
<th>Name</th>
<th>Operator</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="constraint in service.ServiceConstraints">
<td>
<div class="input-group input-group-sm">
<input type="text" class="form-control" ng-model="constraint.key" placeholder="e.g. node.role" ng-change="updatePlacementConstraint(service, constraint)" ng-disabled="isUpdating">
</div>
</td>
<td>
<div class="input-group input-group-sm">
<select name="constraintOperator" class="form-control" ng-model="constraint.operator" ng-change="updatePlacementConstraint(service, constraint)" ng-disabled="isUpdating">
<option value="==">==</option>
<option value="!=">!=</option>
</select>
</div>
</td>
<td>
<div class="input-group input-group-sm">
<input type="text" class="form-control" ng-model="constraint.value" placeholder="e.g. manager" ng-change="updatePlacementConstraint(service, constraint)" ng-disabled="isUpdating">
<span class="input-group-btn">
<button class="btn btn-sm btn-danger" type="button" ng-click="removePlacementConstraint(service, $index)" ng-disabled="isUpdating">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</span>
</div>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
<rd-widget-footer>
<div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['ServiceConstraints'])" ng-click="updateService(service)">Apply changes</button>
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a ng-click="cancelChanges(service, ['ServiceConstraints'])">Reset changes</a></li>
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
</ul>
</div>
</div>
</rd-widget-footer>
</rd-widget>
</div>

View file

@ -0,0 +1,56 @@
<div>
<rd-widget>
<rd-widget-header icon="fa-list-alt" title="Container spec"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>CMD</td>
<td><code ng-if="service.Command">{{ service.Command|command }}</code></td>
<td>
<p class="small text-muted" style="margin-top: 10px;">
Command to execute.
</p>
</td>
</tr>
<tr>
<td>Args</td>
<td><code ng-if="service.Arguments">{{ service.Arguments }}</code></td>
<td>
<p class="small text-muted" style="margin-top: 10px;">
Arguments passed to command in container.
</p>
</td>
</tr>
<tr>
<td>User</td>
<td>{{ service.User }}</td>
<td>
<p class="small text-muted" style="margin-top: 10px;">
Username or UID.
</p>
</td>
</tr>
<tr>
<td>Working directory</td>
<td>{{ service.Dir }}</td>
<td>
<p class="small text-muted" style="margin-top: 10px;">
Working directory inside the container.
</p>
</td>
</tr>
<tr>
<td>Stop grace period</td>
<td>{{ service.StopGracePeriod }}</td>
<td>
<p class="small text-muted" style="margin-top: 10px;">
Time to wait before force killing a container (default none).
</p>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -0,0 +1,59 @@
<div>
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Container labels">
<div class="nopadding">
<a class="btn btn-default btn-sm pull-right" ng-click="isUpdating ||addContainerLabel(service)" ng-disabled="isUpdating">
<i class="fa fa-plus-circle" aria-hidden="true"></i> container label
</a>
</div>
</rd-widget-header>
<rd-widget-body ng-if="service.ServiceContainerLabels.length === 0">
<p>There are no container labels for this service.</p>
</rd-widget-body>
<rd-widget-body ng-if="service.ServiceContainerLabels.length > 0" classes="no-padding">
<table class="table" >
<thead>
<tr>
<th>Label</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="label in service.ServiceContainerLabels">
<td>
<div class="input-group input-group-sm">
<span class="input-group-addon fit-text-size">name</span>
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo" ng-change="updateContainerLabel(service, label)" ng-disabled="isUpdating">
</div>
</td>
<td>
<div class="input-group input-group-sm">
<span class="input-group-addon fit-text-size">value</span>
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar" ng-change="updateContainerLabel(service, label)" ng-disabled="isUpdating">
<span class="input-group-btn">
<button class="btn btn-sm btn-danger" type="button" ng-click="removeContainerLabel(service, $index)" ng-disabled="isUpdating">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</span>
</div>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
<rd-widget-footer>
<div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['ServiceContainerLabels'])" ng-click="updateService(service)">Apply changes</button>
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a ng-click="cancelChanges(service, ['ServiceContainerLabels'])">Reset changes</a></li>
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
</ul>
</div>
</div>
</rd-widget-footer>
</rd-widget>
</div>

View file

@ -0,0 +1,59 @@
<div ng-if="service.EnvironmentVariables">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Environment variables">
<div class="nopadding">
<a class="btn btn-default btn-sm pull-right" ng-click="isUpdating ||addEnvironmentVariable(service)" ng-disabled="isUpdating">
<i class="fa fa-plus-circle" aria-hidden="true"></i> environment variable
</a>
</div>
</rd-widget-header>
<rd-widget-body ng-if="service.EnvironmentVariables.length === 0">
<p>There are no environment variables for this service.</p>
</rd-widget-body>
<rd-widget-body ng-if="service.EnvironmentVariables.length > 0" classes="no-padding">
<table class="table" >
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="var in service.EnvironmentVariables">
<td>
<div class="input-group input-group-sm">
<span class="input-group-addon fit-text-size">name</span>
<input type="text" class="form-control" ng-model="var.key" ng-disabled="var.added || isUpdating" placeholder="e.g. FOO">
</div>
</td>
<td>
<div class="input-group input-group-sm">
<span class="input-group-addon fit-text-size">value</span>
<input type="text" class="form-control" ng-model="var.value" ng-change="updateEnvironmentVariable(service, var)" placeholder="e.g. bar" ng-disabled="isUpdating">
<span class="input-group-btn">
<button class="btn btn-sm btn-danger" type="button" ng-click="removeEnvironmentVariable(service, $index)" ng-disabled="isUpdating">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</span>
</div>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
<rd-widget-footer>
<div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['EnvironmentVariables'])" ng-click="updateService(service)">Apply changes</button>
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a ng-click="cancelChanges(service, ['EnvironmentVariables'])">Reset changes</a></li>
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
</ul>
</div>
</div>
</rd-widget-footer>
</rd-widget>
</div>

View file

@ -0,0 +1,67 @@
<div ng-if="service.ServiceMounts">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Mounts">
<div class="nopadding">
<a class="btn btn-default btn-sm pull-right" ng-click="isUpdating ||addMount(service)" ng-disabled="isUpdating">
<i class="fa fa-plus-circle" aria-hidden="true"></i> mount
</a>
</div>
</rd-widget-header>
<rd-widget-body ng-if="service.ServiceMounts.length === 0">
<p>There are no mounts for this service.</p>
</rd-widget-body>
<rd-widget-body ng-if="service.ServiceMounts.length > 0" classes="no-padding">
<table class="table" >
<thead>
<tr>
<th>Type</th>
<th>Source</th>
<th>Target</th>
<th>Read only</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="mount in service.ServiceMounts">
<td>
<select name="mountType" class="form-control" ng-model="mount.Type" ng-disabled="isUpdating">
<option value="volume">Volume</option>
<option value="bind">Bind</option>
</select>
</td>
<td>
<input type="text" class="form-control" ng-model="mount.Source" placeholder="e.g. /tmp/portainer/data" ng-change="updateMount(service, mount)" ng-disabled="isUpdating">
</td>
<td>
<input type="text" class="form-control" ng-model="mount.Target" placeholder="e.g. /tmp/portainer/data" ng-change="updateMount(service, mount)" ng-disabled="isUpdating">
</td>
<td>
<input type="checkbox" class="form-control" ng-model="mount.ReadOnly" ng-change="updateMount(service, mount)" ng-disabled="isUpdating">
</td>
<td>
<span class="input-group-btn">
<button class="btn btn-sm btn-danger" type="button" ng-click="removeMount(service, $index)" ng-disabled="isUpdating">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</span>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
<rd-widget-footer>
<div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['ServiceMounts'])" ng-click="updateService(service)">Apply changes</button>
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a ng-click="cancelChanges(service, ['ServiceMounts'])">Reset changes</a></li>
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
</ul>
</div>
</div>
</rd-widget-footer>
</rd-widget>
</div>

View file

@ -0,0 +1,26 @@
<div>
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Networks"></rd-widget-header>
<rd-widget-body ng-if="!service.VirtualIPs || service.VirtualIPs.length === 0">
<p>This service is not connected to any networks.</p>
</rd-widget-body>
<rd-widget-body ng-if="service.VirtualIPs && service.VirtualIPs.length > 0" classes="no-padding">
<table class="table" >
<thead>
<tr>
<th>ID</th>
<th>IP address</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="network in service.VirtualIPs">
<td>
<a ui-sref="network({id: network.NetworkID})">{{ network.NetworkID }}</a>
</td>
<td>{{ network.Addr }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -0,0 +1,71 @@
<div>
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Published ports">
<div class="nopadding">
<a class="btn btn-default btn-sm pull-right" ng-click="isUpdating ||addPublishedPort(service)" ng-disabled="isUpdating">
<i class="fa fa-plus-circle" aria-hidden="true"></i> port mapping
</a>
</div>
</rd-widget-header>
<rd-widget-body ng-if="!service.Ports || service.Ports.length === 0">
<p>This service has no ports published.</p>
</rd-widget-body>
<rd-widget-body ng-if="service.Ports && service.Ports.length > 0" classes="no-padding">
<table class="table" >
<thead>
<tr>
<th>Host port</th>
<th>Container port</th>
<th>Protocol</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="portBinding in service.Ports">
<td>
<div class="input-group input-group-sm">
<span class="input-group-addon">host</span>
<input type="number" class="form-control" ng-model="portBinding.PublishedPort" placeholder="e.g. 8080" ng-change="updatePublishedPort(service, mapping)" ng-disabled="isUpdating">
</div>
</td>
<td>
<div class="input-group input-group-sm">
<span class="input-group-addon">container</span>
<input type="number" class="form-control" ng-model="portBinding.TargetPort" placeholder="e.g. 80" ng-change="updatePublishedPort(service, mapping)" ng-disabled="isUpdating">
</div>
</td>
<td>
<div class="input-group input-group-sm">
<select class="selectpicker form-control" ng-model="portBinding.Protocol" ng-change="updatePublishedPort(service, mapping)" ng-disabled="isUpdating">
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</td>
<td>
<span class="input-group-btn">
<button class="btn btn-sm btn-danger" type="button" ng-click="removePortPublishedBinding(service, $index)" ng-disabled="isUpdating">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</span>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
<rd-widget-footer>
<div class="btn-toolbar" role="toolbar"
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['Ports'])" ng-click="updateService(service)">Apply changes</button>
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a ng-click="cancelChanges(service, ['Ports'])">Reset changes</a></li>
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
</ul>
</div>
</div>
</rd-widget-footer>
</rd-widget>
</div>

View file

@ -0,0 +1,36 @@
<div>
<rd-widget>
<rd-widget-header icon="fa-list-alt" title="Resource limits and reservations">
</rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>CPU limits</td>
<td ng-if="service.LimitNanoCPUs">
{{ service.LimitNanoCPUs / 1000000000 }}
</td>
<td ng-if="!service.LimitNanoCPUs">None</td>
</tr>
<tr>
<td>Memory limits</td>
<td ng-if="service.LimitMemoryBytes">{{service.LimitMemoryBytes|humansize}}</td>
<td ng-if="!service.LimitMemoryBytes">None</td>
</tr>
<tr>
<td>CPU reservation</td>
<td ng-if="service.ReservationNanoCPUs">
{{service.ReservationNanoCPUs / 1000000000}}
</td>
<td ng-if="!service.ReservationNanoCPUs">None</td>
</tr>
<tr>
<td>Memory reservation</td>
<td ng-if="service.ReservationMemoryBytes">{{service.ReservationMemoryBytes|humansize}}</td>
<td ng-if="!service.ReservationMemoryBytes">None</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -0,0 +1,76 @@
<div>
<rd-widget>
<rd-widget-header icon="fa-list-alt" title="Restart policy">
</rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Restart condition</td>
<td>
<div class="input-group input-group-sm">
<select class="selectpicker form-control" ng-model="service.RestartCondition" ng-change="updateServiceAttribute(service, 'RestartCondition')" ng-disabled="isUpdating">
<option value="none">None</option>
<option value="on-failure">On failure</option>
<option value="any">Any</option>
</select>
</div>
</td>
<td>
<p class="small text-muted" style="margin-top: 10px;">
Condition for restart.
</p>
</td>
</tr>
<tr>
<td>Restart delay</td>
<td>
<input class="input-sm" type="number" ng-model="service.RestartDelay" ng-change="updateServiceAttribute(service, 'RestartDelay')" ng-disabled="isUpdating"/>
</td>
<td>
<p class="small text-muted" style="margin-top: 10px;">
Delay between restart attempts. Time in seconds.
</p>
</td>
</tr>
<tr>
<td>Restart max attempts</td>
<td>
<input class="input-sm" type="number" ng-model="service.RestartMaxAttempts" ng-change="updateServiceAttribute(service, 'RestartMaxAttempts')" ng-disabled="isUpdating"/>
</td>
<td>
<p class="small text-muted" style="margin-top: 10px;">
Maximum attempts to restart a given container before giving up (default value is 0, which is ignored).
</p>
</td>
</tr>
<tr>
<td>Restart window</td>
<td>
<input class="input-sm" type="number" ng-model="service.RestartWindow" ng-change="updateServiceAttribute(service, 'RestartWindow')" ng-disabled="isUpdating"/>
</td>
<td>
<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).
</p>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
<rd-widget-footer>
<div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['RestartCondition', 'RestartDelay', 'RestartMaxAttempts', 'RestartWindow'])" ng-click="updateService(service)">Apply changes</button>
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a ng-click="cancelChanges(service, ['RestartCondition', 'RestartDelay', 'RestartMaxAttempts', 'RestartWindow'])">Reset changes</a></li>
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
</ul>
</div>
</div>
</rd-widget-footer>
</rd-widget>
</div>

View file

@ -0,0 +1,63 @@
<div>
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Service labels">
<div class="nopadding">
<a class="btn btn-default btn-sm pull-right" ng-click="isUpdating || addLabel(service)" ng-disabled="isUpdating">
<i class="fa fa-plus-circle" aria-hidden="true"></i> label
</a>
</div>
</rd-widget-header>
<rd-widget-body ng-if="service.ServiceLabels.length === 0">
<p>There are no labels for this service.</p>
</rd-widget-body>
<rd-widget-body classes="no-padding" ng-if="service.ServiceLabels.length > 0">
<table class="table">
<thead>
<tr>
<th>
Label
</th>
<th>
Value
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="label in service.ServiceLabels">
<td>
<div class="input-group input-group-sm">
<span class="input-group-addon fit-text-size">name</span>
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo" ng-change="updateLabel(service, label)" ng-disabled="isUpdating">
</div>
</td>
<td>
<div class="input-group input-group-sm">
<span class="input-group-addon fit-text-size">value</span>
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar" ng-change="updateLabel(service, label)" ng-disabled="isUpdating">
<span class="input-group-btn">
<button class="btn btn-sm btn-danger" type="button" ng-click="removeLabel(service, $index)" ng-disabled="isUpdating">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</span>
</div>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
<rd-widget-footer>
<div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['ServiceLabels'])" ng-click="updateService(service)">Apply changes</button>
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a ng-click="cancelChanges(service, ['ServiceLabels'])">Reset changes</a></li>
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
</ul>
</div>
</div>
</rd-widget-footer>
</rd-widget>
</div>

View file

@ -0,0 +1,65 @@
<div ng-if="tasks.length > 0">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Associated tasks">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>
<a ui-sref="service" ng-click="order('Status')">
Status
<span ng-show="sortType == 'Status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="service" ng-click="order('Slot')">
Slot
<span ng-show="sortType == 'Slot' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Slot' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-if="displayNode">
<a ui-sref="service" ng-click="order('Node')">
Node
<span ng-show="sortType == 'Node' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Node' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="service" ng-click="order('Updated')">
Last update
<span ng-show="sortType == 'Updated' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Updated' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="task in (filteredTasks = ( tasks | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><a ui-sref="task({ id: task.Id })">{{ task.Id }}</a></td>
<td><span class="label label-{{ task.Status|taskstatusbadge }}">{{ task.Status }}</span></td>
<td>{{ task.Slot }}</td>
<td ng-if="displayNode">{{ task.Node }}</td>
<td>{{ task.Updated|getisodate }}</td>
</tr>
</tbody>
</table>
<div ng-if="tasks" class="pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</rd-widget-body>
</rd-widget>
</div>

View file

@ -0,0 +1,68 @@
<div>
<rd-widget>
<rd-widget-header icon="fa-list-alt" title="Update configuration">
</rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Update Parallelism</td>
<td>
<input class="input-sm" type="number" ng-model="service.UpdateParallelism" ng-change="updateServiceAttribute(service, 'UpdateParallelism')" ng-disabled="isUpdating"/>
</td>
<td>
<p class="small text-muted" style="margin-top: 10px;">
Maximum number of tasks to be updated simultaneously (0 to update all at once).
</p>
</td>
</tr>
<tr>
<td>Update Delay</td>
<td>
<input class="input-sm" type="number" ng-model="service.UpdateDelay" ng-change="updateServiceAttribute(service, 'UpdateDelay')" ng-disabled="isUpdating"/>
</td>
<td>
<p class="small text-muted" style="margin-top: 10px;">
Amount of time between updates.
</p>
</td>
</tr>
<tr>
<td>Update Failure Action</td>
<td>
<div class="form-group">
<label class="radio-inline">
<input type="radio" name="failure_action" ng-model="service.UpdateFailureAction" value="continue" ng-change="updateServiceAttribute(service, 'UpdateFailureAction')" ng-disabled="isUpdating">
Continue
</label>
<label class="radio-inline">
<input type="radio" name="failure_action" ng-model="service.UpdateFailureAction" value="pause" ng-change="updateServiceAttribute(service, 'UpdateFailureAction')" ng-disabled="isUpdating">
Pause
</label>
</div>
</td>
<td>
<p class="small text-muted" style="margin-top: 10px;">
Action taken on failure to start after update.
</p>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
<rd-widget-footer>
<div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['UpdateFailureAction', 'UpdateDelay', 'UpdateParallelism'])" ng-click="updateService(service)">Apply changes</button>
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a ng-click="cancelChanges(service, ['UpdateFailureAction', 'UpdateDelay', 'UpdateParallelism'])">Reset changes</a></li>
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
</ul>
</div>
</div>
</rd-widget-footer>
</rd-widget>
</div>

View file

@ -11,7 +11,16 @@
</rd-header> </rd-header>
<div class="row"> <div class="row">
<div class="col-lg-12 col-md-12 col-xs-12"> <div ng-if="isUpdating" class="col-lg-12 col-md-12 col-xs-12">
<div class="alert alert-info" role="alert" id="service-update-alert">
<p>This service is being updated. Editing this service is currently disabled.</p>
<a ui-sref="service({id: service.Id}, {reload: true})">Refresh to see if this service has finished updated.</a>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-9 col-md-9 col-xs-9">
<rd-widget> <rd-widget>
<rd-widget-header icon="fa-list-alt" title="Service details"></rd-widget-header> <rd-widget-header icon="fa-list-alt" title="Service details"></rd-widget-header>
<rd-widget-body classes="no-padding"> <rd-widget-body classes="no-padding">
@ -19,23 +28,32 @@
<tbody> <tbody>
<tr> <tr>
<td>Name</td> <td>Name</td>
<td ng-if="!service.EditName"> <td ng-if="applicationState.endpoint.apiVersion <= 1.24">
{{ service.Name }} <input type="text" class="form-control" ng-model="service.Name" ng-change="updateServiceAttribute(service, 'Name')" ng-disabled="isUpdating">
<a href="" data-toggle="tooltip" title="Edit service name" ng-click="service.EditName = true;"><i class="fa fa-edit"></i></a>
</td> </td>
<td ng-if="service.EditName"> <td ng-if="applicationState.endpoint.apiVersion >= 1.25">
<input type="text" class="containerNameInput" ng-model="service.newServiceName"> {{ service.Name }}
<a class="interactive" ng-click="service.EditName = false;"><i class="fa fa-times"></i></a>
<a class="interactive" ng-click="renameService(service)"><i class="fa fa-check-square-o"></i></a>
</td> </td>
</tr> </tr>
<tr> <tr>
<td>ID</td> <td>ID</td>
<td> <td>
{{ service.Id }} {{ service.Id }}
<button class="btn btn-xs btn-danger" ng-click="removeService()"><i class="fa fa-trash space-right" aria-hidden="true"></i>Delete this service</button> <button class="btn btn-xs btn-danger" ng-click="removeService()"><i class="fa fa-trash space-right" aria-hidden="true" ng-disabled="isUpdating"></i>Delete this service</button>
</td> </td>
</tr> </tr>
<tr ng-if="service.CreatedAt">
<td>Created at</td>
<td>{{ service.CreatedAt|getisodate}}</td>
</tr>
<tr ng-if="service.UpdatedAt">
<td>Last updated at</td>
<td>{{ service.UpdatedAt|getisodate }}</td>
</tr>
<tr ng-if="service.Version">
<td>Version</td>
<td>{{ service.Version }}</td>
</tr>
<tr> <tr>
<td>Scheduling mode</td> <td>Scheduling mode</td>
<td>{{ service.Mode }}</td> <td>{{ service.Mode }}</td>
@ -43,251 +61,90 @@
<tr ng-if="service.Mode === 'replicated'"> <tr ng-if="service.Mode === 'replicated'">
<td>Replicas</td> <td>Replicas</td>
<td> <td>
<span ng-if="service.Mode === 'replicated' && !service.EditReplicas"> <span ng-if="service.Mode === 'replicated'">
{{ service.Replicas }} <input class="input-sm" type="number" ng-model="service.Replicas" ng-change="updateServiceAttribute(service, 'Replicas')" ng-disabled="isUpdating" />
<a class="interactive" ng-click="service.EditReplicas = true;"><i class="fa fa-arrows-v" aria-hidden="true"></i> Scale</a>
</span>
<span ng-if="service.Mode === 'replicated' && service.EditReplicas">
<input class="input-sm" type="number" ng-model="service.newServiceReplicas" />
<a class="interactive" ng-click="service.EditReplicas = false;"><i class="fa fa-times"></i></a>
<a class="interactive" ng-click="scaleService(service)"><i class="fa fa-check-square-o"></i></a>
</span> </span>
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Image</td> <td>Image</td>
<td ng-if="!service.EditImage">
{{ service.Image }}
<a href="" data-toggle="tooltip" title="Edit service image" ng-click="service.EditImage = true;"><i class="fa fa-edit"></i></a>
</td>
<td ng-if="service.EditImage">
<input type="text" class="containerNameInput" ng-model="service.newServiceImage">
<a class="interactive" ng-click="service.EditImage = false;"><i class="fa fa-times"></i></a>
<a class="interactive" ng-click="changeServiceImage(service)"><i class="fa fa-check-square-o"></i></a>
</td>
</tr>
<tr ng-if="service.Ports">
<td>Published ports</td>
<td> <td>
<div ng-repeat="mapping in service.Ports"> <input type="text" class="form-control" ng-model="service.Image" ng-change="updateServiceAttribute(service, 'Image')" ng-disabled="isUpdating" />
{{ mapping.TargetPort }} <i class="fa fa-long-arrow-right"></i> {{ mapping.PublishedPort }}
</div>
</td>
</tr>
<tr ng-if="service.EnvironmentVariables">
<td>Environment variables</td>
<td>
<div class="form-group">
<div class="col-sm-11 nopadding">
<span class="label label-default interactive fit-text-size" ng-click="addEnvironmentVariable(service)">
<i class="fa fa-plus-circle" aria-hidden="true"></i> environment variable
</span>
</div>
<!-- environment-variable-input-list -->
<div class="col-sm-11 form-inline nopadding" style="margin-top: 10px;">
<div ng-repeat="var in service.EnvironmentVariables" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon fit-text-size">name</span>
<input type="text" class="form-control" ng-model="var.key" ng-disabled="var.added" placeholder="e.g. FOO">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon fit-text-size">value</span>
<input type="text" class="form-control" ng-model="var.value" ng-change="updateEnvironmentVariable(service, var)" placeholder="e.g. bar">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeEnvironmentVariable(service, $index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !environment-variable-input-list -->
</div>
</td>
</tr>
<tr>
<td>Labels</td>
<td>
<div class="form-group">
<div class="col-sm-11 nopadding">
<span class="label label-default interactive fit-text-size" ng-click="addLabel(service)">
<i class="fa fa-plus-circle" aria-hidden="true"></i> label
</span>
</div>
<!-- labels-input-list -->
<div class="col-sm-11 form-inline nopadding" style="margin-top: 10px;">
<div ng-repeat="label in service.ServiceLabels" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon fit-text-size">name</span>
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo" ng-change="updateLabel(service, label)">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon fit-text-size">value</span>
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar" ng-change="updateLabel(service, label)">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeLabel(service, $index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !labels-input-list -->
</div>
</td>
</tr>
<tr>
<td>Container labels</td>
<td>
<div class="form-group">
<div class="col-sm-11 nopadding">
<span class="label label-default interactive fit-text-size" ng-click="addContainerLabel(service)">
<i class="fa fa-plus-circle" aria-hidden="true"></i> container label
</span>
</div>
<!-- labels-input-list -->
<div class="col-sm-11 form-inline nopadding" style="margin-top: 10px;">
<div ng-repeat="label in service.ServiceContainerLabels" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon fit-text-size">name</span>
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo" ng-change="updateLabel(service, label)">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon fit-text-size">value</span>
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar" ng-change="updateLabel(service, label)">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeContainerLabel(service, $index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !labels-input-list -->
</div>
</td>
</tr>
<tr>
<td>Update Parallelism</td>
<td>
<span ng-if="!service.EditParallelism">
{{ service.UpdateParallelism }}
<a class="interactive" ng-click="service.EditParallelism = true;"><i class="fa fa-arrows-v" aria-hidden="true"></i> Change</a>
</span>
<span ng-if="service.EditParallelism">
<input class="input-sm" type="number" ng-model="service.newServiceUpdateParallelism" />
<a class="interactive" ng-click="service.EditParallelism = false;"><i class="fa fa-times"></i></a>
<a class="interactive" ng-click="changeParallelism(service)"><i class="fa fa-check-square-o"></i></a>
</span>
</td>
</tr>
<tr>
<td>Update Delay</td>
<td>
<span ng-if="!service.EditDelay">
{{ service.UpdateDelay }}
<a class="interactive" ng-click="service.EditDelay = true;"><i class="fa fa-arrows-v" aria-hidden="true"></i> Change</a>
</span>
<span ng-if="service.EditDelay">
<input class="input-sm" type="number" ng-model="service.newServiceUpdateDelay" />
<a class="interactive" ng-click="service.EditDelay = false;"><i class="fa fa-times"></i></a>
<a class="interactive" ng-click="changeUpdateDelay(service)"><i class="fa fa-check-square-o"></i></a>
</span>
</td>
</tr>
<tr>
<td>Update Failure Action</td>
<td>
<div class="form-group">
<label class="radio-inline">
<input type="radio" name="failure_action" ng-model="service.newServiceUpdateFailureAction" value="continue" ng-change="changeUpdateFailureAction(service)">
Continue
</label>
<label class="radio-inline">
<input type="radio" name="failure_action" ng-model="service.newServiceUpdateFailureAction" value="pause" ng-change="changeUpdateFailureAction(service)">
Pause
</label>
</div>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</rd-widget-body> </rd-widget-body>
<rd-widget-footer ng-if="service.hasChanges"> <rd-widget-footer>
<div> <p class="small text-muted">
<button type="button" class="btn btn-primary" ng-click="updateService(service)">Save changes</button> Do you need help? View the Docker Service documentation <a href="https://docs.docker.com/engine/reference/commandline/service_update/" target="self">here</a>.
<button type="button" class="btn btn-default" ng-click="cancelChanges(service)">Reset</button> </p>
<div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary" ng-disabled="!hasChanges(service, ['Mode', 'Replicas', 'Image', 'Name'])" ng-click="updateService(service)">Apply changes</button>
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a ng-click="cancelChanges(service, ['Mode', 'Replicas', 'Image', 'Name'])">Reset changes</a></li>
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
</ul>
</div>
</div> </div>
</rd-widget-footer> </rd-widget-footer>
</rd-widget> </rd-widget>
</div> </div>
</div>
<div class="row" ng-if="tasks.length > 0"> <div class="col-lg-3 col-md-3 col-xs-3">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget> <rd-widget>
<rd-widget-header icon="fa-tasks" title="Associated tasks"> <rd-widget-header icon="fa-bars" title="Quick navigation"></rd-widget-header>
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</rd-widget-header>
<rd-widget-body classes="no-padding"> <rd-widget-body classes="no-padding">
<table class="table"> <ul class="nav nav-pills nav-stacked">
<thead> <li><a href ng-click="goToItem('service-env-variables')">Environment variables</a></li>
<tr> <li><a href ng-click="goToItem('service-container-labels')">Container labels</a></li>
<th>Id</th> <li><a href ng-click="goToItem('service-mounts')">Mounts</a></li>
<th> <li><a href ng-click="goToItem('service-network-specs')">Network &amp; published ports</a></li>
<a ui-sref="service" ng-click="order('Status')"> <li><a href ng-click="goToItem('service-resources')">Resource limits &amp; reservations</a></li>
Status <li><a href ng-click="goToItem('service-placement-constraints')">Placement constraints</a></li>
<span ng-show="sortType == 'Status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span> <li><a href ng-click="goToItem('service-restart-policy')">Restart policy</a></li>
<span ng-show="sortType == 'Status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span> <li><a href ng-click="goToItem('service-update-config')">Update configuration</a></li>
</a> <li><a href ng-click="goToItem('service-labels')">Service labels</a></li>
</th> <li><a href ng-click="goToItem('service-tasks')">Tasks</a></li>
<th> <ul>
<a ui-sref="service" ng-click="order('Slot')">
Slot
<span ng-show="sortType == 'Slot' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Slot' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-if="displayNode">
<a ui-sref="service" ng-click="order('Node')">
Node
<span ng-show="sortType == 'Node' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Node' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="service" ng-click="order('Updated')">
Last update
<span ng-show="sortType == 'Updated' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Updated' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="task in (filteredTasks = ( tasks | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><a ui-sref="task({ id: task.Id })">{{ task.Id }}</a></td>
<td><span class="label label-{{ task.Status|taskstatusbadge }}">{{ task.Status }}</span></td>
<td>{{ task.Slot }}</td>
<td ng-if="displayNode">{{ task.Node }}</td>
<td>{{ task.Updated|getisodate }}</td>
</tr>
</tbody>
</table>
<div ng-if="tasks" class="pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</rd-widget-body> </rd-widget-body>
</rd-widget> </rd-widget>
</div> </div>
</div> </div>
<div class="row">
<hr>
<div class="col-lg-12 col-md-12 col-xs-12">
<h3 id="container-specs">Container specification</h3>
<div id="service-container-spec" class="padding-top" ng-include="'app/components/service/includes/container-specs.html'"></div>
<div id="service-env-variables" class="padding-top" ng-include="'app/components/service/includes/environmentvariables.html'"></div>
<div id="service-container-labels" class="padding-top" ng-include="'app/components/service/includes/containerlabels.html'"></div>
<div id="service-mounts" class="padding-top" ng-include="'app/components/service/includes/mounts.html'"></div>
</div>
</div>
<div class="row">
<hr>
<div class="col-lg-12 col-md-12 col-xs-12">
<h3 id="service-network-specs">Networks &amp; ports</h3>
<div id="service-networks" class="padding-top" ng-include="'app/components/service/includes/networks.html'"></div>
<div id="service-published-ports" class="padding-top" ng-include="'app/components/service/includes/ports.html'"></div>
</div>
</div>
<div class="row">
<hr>
<div class="col-lg-12 col-md-12 col-xs-12">
<h3 id="service-specs">Service specification</h3>
<div id="service-resources" class="padding-top" ng-include="'app/components/service/includes/resources.html'"></div>
<div id="service-placement-constraints" class="padding-top" ng-include="'app/components/service/includes/constraints.html'"></div>
<div id="service-restart-policy" class="padding-top" ng-include="'app/components/service/includes/restart.html'"></div>
<div id="service-update-config" class="padding-top" ng-include="'app/components/service/includes/updateconfig.html'"></div>
<div id="service-labels" class="padding-top" ng-include="'app/components/service/includes/servicelabels.html'"></div>
<div id="service-tasks" class="padding-top" ng-include="'app/components/service/includes/tasks.html'"></div>
</div>
</div>

View file

@ -1,6 +1,6 @@
angular.module('service', []) angular.module('service', [])
.controller('ServiceController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Task', 'Node', 'Messages', 'Pagination', .controller('ServiceController', ['$scope', '$stateParams', '$state', '$location', '$anchorScroll', 'Service', 'ServiceHelper', 'Task', 'Node', 'Messages', 'Pagination',
function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Messages, Pagination) { function ($scope, $stateParams, $state, $location, $anchorScroll, Service, ServiceHelper, Task, Node, Messages, Pagination) {
$scope.state = {}; $scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('service_tasks'); $scope.state.pagination_count = Pagination.getPaginationCount('service_tasks');
@ -10,7 +10,10 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess
$scope.sortType = 'Status'; $scope.sortType = 'Status';
$scope.sortReverse = false; $scope.sortReverse = false;
var previousServiceValues = {}; $scope.lastVersion = 0;
var originalService = {};
var previousServiceValues = [];
$scope.order = function (sortType) { $scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
@ -35,85 +38,175 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess
service.EditReplicas = false; service.EditReplicas = false;
}; };
$scope.goToItem = function(hash) {
$anchorScroll(hash);
};
$scope.addEnvironmentVariable = function addEnvironmentVariable(service) { $scope.addEnvironmentVariable = function addEnvironmentVariable(service) {
service.EnvironmentVariables.push({ key: '', value: '', originalValue: '' }); service.EnvironmentVariables.push({ key: '', value: '', originalValue: '' });
service.hasChanges = true; updateServiceArray(service, 'EnvironmentVariables', service.EnvironmentVariables);
}; };
$scope.removeEnvironmentVariable = function removeEnvironmentVariable(service, index) { $scope.removeEnvironmentVariable = function removeEnvironmentVariable(service, index) {
var removedElement = service.EnvironmentVariables.splice(index, 1); var removedElement = service.EnvironmentVariables.splice(index, 1);
service.hasChanges = service.hasChanges || removedElement !== null; if (removedElement !== null) {
updateServiceArray(service, 'EnvironmentVariables', service.EnvironmentVariables);
}
}; };
$scope.updateEnvironmentVariable = function updateEnvironmentVariable(service, variable) { $scope.updateEnvironmentVariable = function updateEnvironmentVariable(service, variable) {
service.hasChanges = service.hasChanges || variable.value !== variable.originalValue; if (variable.value !== variable.originalValue || variable.key !== variable.originalKey) {
updateServiceArray(service, 'EnvironmentVariables', service.EnvironmentVariables);
}
}; };
$scope.addLabel = function addLabel(service) { $scope.addLabel = function addLabel(service) {
service.hasChanges = true;
service.ServiceLabels.push({ key: '', value: '', originalValue: '' }); service.ServiceLabels.push({ key: '', value: '', originalValue: '' });
updateServiceArray(service, 'ServiceLabels', service.ServiceLabels);
}; };
$scope.removeLabel = function removeLabel(service, index) { $scope.removeLabel = function removeLabel(service, index) {
var removedElement = service.ServiceLabels.splice(index, 1); var removedElement = service.ServiceLabels.splice(index, 1);
service.hasChanges = service.hasChanges || removedElement !== null; if (removedElement !== null) {
updateServiceArray(service, 'ServiceLabels', service.ServiceLabels);
}
}; };
$scope.updateLabel = function updateLabel(service, label) { $scope.updateLabel = function updateLabel(service, label) {
service.hasChanges = service.hasChanges || label.value !== label.originalValue; if (label.value !== label.originalValue || label.key !== label.originalKey) {
updateServiceArray(service, 'ServiceLabels', service.ServiceLabels);
}
}; };
$scope.addContainerLabel = function addContainerLabel(service) { $scope.addContainerLabel = function addContainerLabel(service) {
service.hasChanges = true;
service.ServiceContainerLabels.push({ key: '', value: '', originalValue: '' }); service.ServiceContainerLabels.push({ key: '', value: '', originalValue: '' });
updateServiceArray(service, 'ServiceContainerLabels', service.ServiceContainerLabels);
}; };
$scope.removeContainerLabel = function removeContainerLabel(service, index) { $scope.removeContainerLabel = function removeLabel(service, index) {
var removedElement = service.ServiceContainerLabels.splice(index, 1); var removedElement = service.ServiceContainerLabels.splice(index, 1);
service.hasChanges = service.hasChanges || removedElement !== null; if (removedElement !== null) {
updateServiceArray(service, 'ServiceContainerLabels', service.ServiceContainerLabels);
}
};
$scope.updateContainerLabel = function updateLabel(service, label) {
if (label.value !== label.originalValue || label.key !== label.originalKey) {
updateServiceArray(service, 'ServiceContainerLabels', service.ServiceContainerLabels);
}
};
$scope.addMount = function addMount(service) {
service.ServiceMounts.push({Type: 'volume', Source: '', Target: '', ReadOnly: false });
updateServiceArray(service, 'ServiceMounts', service.ServiceMounts);
};
$scope.removeMount = function removeMount(service, index) {
var removedElement = service.ServiceMounts.splice(index, 1);
if (removedElement !== null) {
updateServiceArray(service, 'ServiceMounts', service.ServiceMounts);
}
};
$scope.updateMount = function updateMount(service, mount) {
updateServiceArray(service, 'ServiceMounts', service.ServiceMounts);
};
$scope.addPlacementConstraint = function addPlacementConstraint(service) {
service.ServiceConstraints.push({ key: '', operator: '==', value: '' });
updateServiceArray(service, 'ServiceConstraints', service.ServiceConstraints);
};
$scope.removePlacementConstraint = function removePlacementConstraint(service, index) {
var removedElement = service.ServiceConstraints.splice(index, 1);
if (removedElement !== null) {
updateServiceArray(service, 'ServiceConstraints', service.ServiceConstraints);
}
};
$scope.updatePlacementConstraint = function updatePlacementConstraint(service, constraint) {
updateServiceArray(service, 'ServiceConstraints', service.ServiceConstraints);
}; };
$scope.changeParallelism = function changeParallelism(service) { $scope.addPublishedPort = function addPublishedPort(service) {
updateServiceAttribute(service, 'UpdateParallelism', service.newServiceUpdateParallelism); if (!service.Ports) {
service.EditParallelism = false; service.Ports = [];
}
service.Ports.push({ PublishedPort: '', TargetPort: '', Protocol: 'tcp' });
}; };
$scope.changeUpdateDelay = function changeUpdateDelay(service) { $scope.updatePublishedPort = function updatePublishedPort(service, portMapping) {
updateServiceAttribute(service, 'UpdateDelay', service.newServiceUpdateDelay); updateServiceArray(service, 'Ports', service.Ports);
service.EditDelay = false;
}; };
$scope.changeUpdateFailureAction = function changeUpdateFailureAction(service) { $scope.removePortPublishedBinding = function removePortPublishedBinding(service, index) {
updateServiceAttribute(service, 'UpdateFailureAction', service.newServiceUpdateFailureAction); var removedElement = service.Ports.splice(index, 1);
if (removedElement !== null) {
updateServiceArray(service, 'Ports', service.Ports);
}
}; };
$scope.cancelChanges = function changeServiceImage(service) { $scope.cancelChanges = function cancelChanges(service, keys) {
Object.keys(previousServiceValues).forEach(function(attribute) { if (keys) { // clean out the keys only from the list of modified keys
service[attribute] = previousServiceValues[attribute]; // reset service values keys.forEach(function(key) {
service['newService' + attribute] = previousServiceValues[attribute]; // reset edit fields var index = previousServiceValues.indexOf(key);
if (index >= 0) {
previousServiceValues.splice(index, 1);
}
});
} else { // clean out all changes
keys = Object.keys(service);
previousServiceValues = [];
}
keys.forEach(function(attribute) {
service[attribute] = originalService[attribute]; // reset service values
}); });
previousServiceValues = {}; // clear out all changes
// clear out environment variable changes
service.EnvironmentVariables = translateEnvironmentVariables(service.Env);
service.ServiceLabels = translateLabelsToServiceLabels(service.Labels);
service.ServiceContainerLabels = translateLabelsToServiceLabels(service.ContainerLabels);
service.hasChanges = false; service.hasChanges = false;
}; };
$scope.hasChanges = function(service, elements) {
var hasChanges = false;
elements.forEach(function(key) {
hasChanges = hasChanges || (previousServiceValues.indexOf(key) >= 0);
});
return hasChanges;
};
$scope.updateService = function updateService(service) { $scope.updateService = function updateService(service) {
$('#loadServicesSpinner').show(); $('#loadServicesSpinner').show();
var config = ServiceHelper.serviceToConfig(service.Model); var config = ServiceHelper.serviceToConfig(service.Model);
config.Name = service.newServiceName; config.Name = service.Name;
config.Labels = translateServiceLabelsToLabels(service.ServiceLabels); config.Labels = translateServiceLabelsToLabels(service.ServiceLabels);
config.TaskTemplate.ContainerSpec.Env = translateEnvironmentVariablesToEnv(service.EnvironmentVariables); config.TaskTemplate.ContainerSpec.Env = translateEnvironmentVariablesToEnv(service.EnvironmentVariables);
config.TaskTemplate.ContainerSpec.Labels = translateServiceLabelsToLabels(service.ServiceContainerLabels); config.TaskTemplate.ContainerSpec.Labels = translateServiceLabelsToLabels(service.ServiceContainerLabels);
config.TaskTemplate.ContainerSpec.Image = service.newServiceImage; config.TaskTemplate.ContainerSpec.Image = service.Image;
config.TaskTemplate.ContainerSpec.Secrets = service.ServiceSecrets;
if (service.Mode === 'replicated') { if (service.Mode === 'replicated') {
config.Mode.Replicated.Replicas = service.Replicas; config.Mode.Replicated.Replicas = service.Replicas;
} }
config.TaskTemplate.ContainerSpec.Mounts = service.ServiceMounts;
if (typeof config.TaskTemplate.Placement === 'undefined') {
config.TaskTemplate.Placement = {};
}
config.TaskTemplate.Placement.Constraints = translateKeyValueToConstraints(service.ServiceConstraints);
config.TaskTemplate.Resources = {
Limits: {
NanoCPUs: service.LimitNanoCPUs,
MemoryBytes: service.LimitMemoryBytes
},
Reservations: {
NanoCPUs: service.ReservationNanoCPUs,
MemoryBytes: service.ReservationMemoryBytes
}
};
config.UpdateConfig = { config.UpdateConfig = {
Parallelism: service.newServiceUpdateParallelism, Parallelism: service.UpdateParallelism,
Delay: service.newServiceUpdateDelay, Delay: service.UpdateDelay,
FailureAction: service.newServiceUpdateFailureAction FailureAction: service.UpdateFailureAction
};
config.TaskTemplate.RestartPolicy = {
Condition: service.RestartCondition,
Delay: service.RestartDelay,
MaxAttempts: service.RestartMaxAttempts,
Window: service.RestartWindow
};
config.EndpointSpec = {
Mode: config.EndpointSpec.Mode || 'vip',
Ports: service.Ports
}; };
Service.update({ id: service.Id, version: service.Version }, config, function (data) { Service.update({ id: service.Id, version: service.Version }, config, function (data) {
$('#loadServicesSpinner').hide(); $('#loadServicesSpinner').hide();
Messages.send("Service successfully updated", "Service updated"); Messages.send("Service successfully updated", "Service updated");
$state.go('service', {id: service.Id}, {reload: true}); $scope.cancelChanges({});
fetchServiceDetails();
}, function (e) { }, function (e) {
$('#loadServicesSpinner').hide(); $('#loadServicesSpinner').hide();
Messages.error("Failure", e, "Unable to update service"); Messages.error("Failure", e, "Unable to update service");
@ -138,22 +231,28 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess
}); });
}; };
function translateServiceArrays(service) {
service.ServiceSecrets = service.Secrets;
service.EnvironmentVariables = translateEnvironmentVariables(service.Env);
service.ServiceLabels = translateLabelsToServiceLabels(service.Labels);
service.ServiceContainerLabels = translateLabelsToServiceLabels(service.ContainerLabels);
service.ServiceMounts = angular.copy(service.Mounts);
service.ServiceConstraints = translateConstraintsToKeyValue(service.Constraints);
}
function fetchServiceDetails() { function fetchServiceDetails() {
$('#loadingViewSpinner').show(); $('#loadingViewSpinner').show();
Service.get({id: $stateParams.id}, function (d) { Service.get({id: $stateParams.id}, function (d) {
var service = new ServiceViewModel(d); var service = new ServiceViewModel(d);
service.newServiceName = service.Name; $scope.isUpdating = $scope.lastVersion >= service.Version;
service.newServiceImage = service.Image; if (!$scope.isUpdating) {
service.newServiceReplicas = service.Replicas; $scope.lastVersion = service.Version;
service.newServiceUpdateParallelism = service.UpdateParallelism; }
service.newServiceUpdateDelay = service.UpdateDelay;
service.newServiceUpdateFailureAction = service.UpdateFailureAction;
service.EnvironmentVariables = translateEnvironmentVariables(service.Env);
service.ServiceLabels = translateLabelsToServiceLabels(service.Labels);
service.ServiceContainerLabels = translateLabelsToServiceLabels(service.ContainerLabels);
translateServiceArrays(service);
$scope.service = service; $scope.service = service;
originalService = angular.copy(service);
Task.query({filters: {service: [service.Name]}}, function (tasks) { Task.query({filters: {service: [service.Name]}}, function (tasks) {
Node.query({}, function (nodes) { Node.query({}, function (nodes) {
$scope.displayNode = true; $scope.displayNode = true;
@ -178,13 +277,15 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess
}); });
} }
function updateServiceAttribute(service, name, newValue) { $scope.updateServiceAttribute = function updateServiceAttribute(service, name) {
// ensure we only capture the original previous value, in case we update the attribute multiple times if (service[name] !== originalService[name] || !(name in originalService)) {
if (!previousServiceValues[name]) { service.hasChanges = true;
previousServiceValues[name] = service[name];
} }
// update the attribute previousServiceValues.push(name);
service[name] = newValue; };
function updateServiceArray(service, name) {
previousServiceValues.push(name);
service.hasChanges = true; service.hasChanges = true;
} }
@ -195,7 +296,7 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess
var idx = variable.indexOf('='); var idx = variable.indexOf('=');
var keyValue = [variable.slice(0,idx), variable.slice(idx+1)]; var keyValue = [variable.slice(0,idx), variable.slice(idx+1)];
var originalValue = (keyValue.length > 1) ? keyValue[1] : ''; var originalValue = (keyValue.length > 1) ? keyValue[1] : '';
variables.push({ key: keyValue[0], value: originalValue, originalValue: originalValue, added: true}); variables.push({ key: keyValue[0], value: originalValue, originalKey: keyValue[0], originalValue: originalValue, added: true});
}); });
return variables; return variables;
} }
@ -218,7 +319,7 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess
var labels = []; var labels = [];
if (Labels) { if (Labels) {
Object.keys(Labels).forEach(function(key) { Object.keys(Labels).forEach(function(key) {
labels.push({ key: key, value: Labels[key], originalValue: Labels[key], added: true}); labels.push({ key: key, value: Labels[key], originalKey: key, originalValue: Labels[key], added: true});
}); });
} }
return labels; return labels;
@ -233,5 +334,48 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess
return Labels; return Labels;
} }
function translateConstraintsToKeyValue(constraints) {
function getOperator(constraint) {
var indexEquals = constraint.indexOf('==');
if (indexEquals >= 0) {
return [indexEquals, '=='];
}
return [constraint.indexOf('!='), '!='];
}
if (constraints) {
var keyValueConstraints = [];
constraints.forEach(function(constraint) {
var operatorIndices = getOperator(constraint);
var key = constraint.slice(0, operatorIndices[0]);
var operator = operatorIndices[1];
var value = constraint.slice(operatorIndices[0] + 2);
keyValueConstraints.push({
key: key,
value: value,
operator: operator,
originalKey: key,
originalValue: value
});
});
return keyValueConstraints;
}
return [];
}
function translateKeyValueToConstraints(keyValueConstraints) {
if (keyValueConstraints) {
var constraints = [];
keyValueConstraints.forEach(function(keyValueConstraint) {
if (keyValueConstraint.key && keyValueConstraint.key !== '' && keyValueConstraint.value && keyValueConstraint.value !== '') {
constraints.push(keyValueConstraint.key + keyValueConstraint.operator + keyValueConstraint.value);
}
});
return constraints;
}
return [];
}
fetchServiceDetails(); fetchServiceDetails();
}]); }]);

View file

@ -26,7 +26,7 @@
<rd-widget-taskbar classes="col-lg-12 col-md-12 col-xs-12"> <rd-widget-taskbar classes="col-lg-12 col-md-12 col-xs-12">
<div class="pull-left"> <div class="pull-left">
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button> <button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
<a class="btn btn-default btn-responsive" type="button" ui-sref="actions.create.service">Add service</a> <a class="btn btn-primary" type="button" ui-sref="actions.create.service"><i class="fa fa-plus space-right" aria-hidden="true"></i>Add service</a>
</div> </div>
<div class="pull-right"> <div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" /> <input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
@ -58,8 +58,22 @@
<span ng-show="sortType == 'Mode' && sortReverse" class="glyphicon glyphicon-chevron-up"></span> <span ng-show="sortType == 'Mode' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a> </a>
</th> </th>
<th>
<a ui-sref="services" ng-click="order('Ports')">
Published Ports
<span ng-show="sortType == 'Ports' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Ports' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="services" ng-click="order('UpdatedAt')">
Updated at
<span ng-show="sortType == 'UpdatedAt' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'UpdatedAt' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-if="applicationState.application.authentication"> <th ng-if="applicationState.application.authentication">
<a ui-sref="containers" ng-click="order('Metadata.ResourceControl.OwnerId')"> <a ui-sref="services" ng-click="order('Metadata.ResourceControl.OwnerId')">
Ownership Ownership
<span ng-show="sortType == 'Metadata.ResourceControl.OwnerId' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span> <span ng-show="sortType == 'Metadata.ResourceControl.OwnerId' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Metadata.ResourceControl.OwnerId' && sortReverse" class="glyphicon glyphicon-chevron-up"></span> <span ng-show="sortType == 'Metadata.ResourceControl.OwnerId' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
@ -85,6 +99,15 @@
<a class="interactive" ng-click="scaleService(service)"><i class="fa fa-check-square-o"></i></a> <a class="interactive" ng-click="scaleService(service)"><i class="fa fa-check-square-o"></i></a>
</span> </span>
</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">
<i class="fa fa-external-link" aria-hidden="true"></i> {{ p.PublishedPort }}:{{ p.TargetPort }}
</a>
<span ng-if="!service.Ports || service.Ports.length === 0 || !swarmManagerIP" >-</span>
</td>
<td>
{{ service.UpdatedAt|getisodate }}
</td>
<td ng-if="applicationState.application.authentication"> <td ng-if="applicationState.application.authentication">
<span ng-if="user.role === 1 && service.Metadata.ResourceControl"> <span ng-if="user.role === 1 && service.Metadata.ResourceControl">
<i class="fa fa-eye-slash" aria-hidden="true"></i> <i class="fa fa-eye-slash" aria-hidden="true"></i>

View file

@ -1,6 +1,6 @@
angular.module('services', []) angular.module('services', [])
.controller('ServicesController', ['$q', '$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Messages', 'Pagination', 'Task', 'Node', 'Authentication', 'UserService', 'ModalService', 'ResourceControlService', .controller('ServicesController', ['$q', '$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Messages', 'Pagination', 'Task', 'Node', 'NodeHelper', 'Authentication', 'UserService', 'ModalService', 'ResourceControlService',
function ($q, $scope, $stateParams, $state, Service, ServiceHelper, Messages, Pagination, Task, Node, Authentication, UserService, ModalService, ResourceControlService) { function ($q, $scope, $stateParams, $state, Service, ServiceHelper, Messages, Pagination, Task, Node, NodeHelper, Authentication, UserService, ModalService, ResourceControlService) {
$scope.state = {}; $scope.state = {};
$scope.state.selectedItemCount = 0; $scope.state.selectedItemCount = 0;
$scope.state.pagination_count = Pagination.getPaginationCount('services'); $scope.state.pagination_count = Pagination.getPaginationCount('services');
@ -114,7 +114,7 @@ function ($q, $scope, $stateParams, $state, Service, ServiceHelper, Messages, Pa
angular.forEach($scope.services, function (service) { angular.forEach($scope.services, function (service) {
if (service.Metadata) { if (service.Metadata) {
var serviceRC = service.Metadata.ResourceControl; var serviceRC = service.Metadata.ResourceControl;
if (serviceRC && serviceRC.OwnerId != $scope.user.ID) { if (serviceRC && serviceRC.OwnerId !== $scope.user.ID) {
angular.forEach(users, function (user) { angular.forEach(users, function (user) {
if (serviceRC.OwnerId === user.Id) { if (serviceRC.OwnerId === user.Id) {
service.Owner = user.Username; service.Owner = user.Username;
@ -137,6 +137,7 @@ function ($q, $scope, $stateParams, $state, Service, ServiceHelper, Messages, Pa
nodes: Node.query({}).$promise, nodes: Node.query({}).$promise,
}) })
.then(function success(data) { .then(function success(data) {
$scope.swarmManagerIP = NodeHelper.getManagerIP(data.nodes);
$scope.services = data.services.map(function (service) { $scope.services = data.services.map(function (service) {
var serviceTasks = data.tasks.filter(function (task) { var serviceTasks = data.tasks.filter(function (task) {
return task.ServiceID === service.ID; return task.ServiceID === service.ID;

View file

@ -31,8 +31,8 @@
<div class="col-sm-5"> <div class="col-sm-5">
<input type="text" name="container_name" class="form-control" ng-model="formValues.name" placeholder="e.g. web (optional)"> <input type="text" name="container_name" class="form-control" ng-model="formValues.name" placeholder="e.g. web (optional)">
</div> </div>
<label for="container_network" class="col-sm-1 control-label text-right">Network</label> <label for="container_network" class="col-sm-2 col-lg-1 control-label text-right">Network</label>
<div class="col-sm-5"> <div class="col-sm-4 col-lg-5">
<select class="form-control" ng-options="net.Name for net in availableNetworks" ng-model="formValues.network"> <select class="form-control" ng-options="net.Name for net in availableNetworks" ng-model="formValues.network">
<option disabled hidden value="">Select a network</option> <option disabled hidden value="">Select a network</option>
</select> </select>
@ -113,8 +113,8 @@
<!-- protocol-actions --> <!-- protocol-actions -->
<div class="input-group col-sm-3 input-group-sm"> <div class="input-group col-sm-3 input-group-sm">
<div class="btn-group btn-group-sm"> <div class="btn-group btn-group-sm">
<label class="btn btn-default" ng-model="portBinding.protocol" uib-btn-radio="'tcp'" ng-click="volume.name = ''">TCP</label> <label class="btn btn-primary" ng-model="portBinding.protocol" uib-btn-radio="'tcp'">TCP</label>
<label class="btn btn-default" ng-model="portBinding.protocol" uib-btn-radio="'udp'" ng-click="volume.name = ''">UDP</label> <label class="btn btn-primary" ng-model="portBinding.protocol" uib-btn-radio="'udp'">UDP</label>
</div> </div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removePortBinding($index)"> <button class="btn btn-sm btn-danger" type="button" ng-click="removePortBinding($index)">
<i class="fa fa-trash" aria-hidden="true"></i> <i class="fa fa-trash" aria-hidden="true"></i>
@ -183,8 +183,8 @@
<!-- read-only --> <!-- read-only -->
<div class="input-group input-group-sm col-sm-5" style="margin-left: 5px;"> <div class="input-group input-group-sm col-sm-5" style="margin-left: 5px;">
<div class="btn-group btn-group-sm"> <div class="btn-group btn-group-sm">
<label class="btn btn-default" ng-model="volume.readOnly" uib-btn-radio="false">Writable</label> <label class="btn btn-primary" ng-model="volume.readOnly" uib-btn-radio="false">Writable</label>
<label class="btn btn-default" ng-model="volume.readOnly" uib-btn-radio="true">Read-only</label> <label class="btn btn-primary" ng-model="volume.readOnly" uib-btn-radio="true">Read-only</label>
</div> </div>
</div> </div>
<!-- !read-only --> <!-- !read-only -->
@ -198,7 +198,7 @@
<!-- !advanced-options --> <!-- !advanced-options -->
<div class="form-group"> <div class="form-group">
<div class="col-sm-12"> <div class="col-sm-12">
<button type="button" class="btn btn-default btn-sm" ng-disabled="!formValues.network" ng-click="createTemplate()">Create</button> <button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.network" ng-click="createTemplate()">Create</button>
<i id="createContainerSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i> <i id="createContainerSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div> </div>
</div> </div>

View file

@ -45,14 +45,14 @@ function ($scope, $q, $state, $anchorScroll, Config, ContainerService, Container
volumeResourceControlQueries.push(ResourceControlService.setVolumeResourceControl(Authentication.getUserDetails().ID, volume.Name)); volumeResourceControlQueries.push(ResourceControlService.setVolumeResourceControl(Authentication.getUserDetails().ID, volume.Name));
}); });
} }
TemplateService.updateContainerConfigurationWithVolumes(templateConfiguration.container, template, data); TemplateService.updateContainerConfigurationWithVolumes(templateConfiguration, template, data);
return $q.all(volumeResourceControlQueries) return $q.all(volumeResourceControlQueries)
.then(function success() { .then(function success() {
return ImageService.pullImage(templateConfiguration.image); return ImageService.pullImage(template.Image, template.Registry);
}); });
}) })
.then(function success(data) { .then(function success(data) {
return ContainerService.createAndStartContainer(templateConfiguration.container); return ContainerService.createAndStartContainer(templateConfiguration);
}) })
.then(function success(data) { .then(function success(data) {
Messages.send('Container Started', data.Id); Messages.send('Container Started', data.Id);
@ -116,6 +116,7 @@ function ($scope, $q, $state, $anchorScroll, Config, ContainerService, Container
} else if (network.Name !== "bridge") { } else if (network.Name !== "bridge") {
containerMapping = 'BY_CONTAINER_NAME'; containerMapping = 'BY_CONTAINER_NAME';
} }
return containerMapping;
} }
function filterNetworksBasedOnProvider(networks) { function filterNetworksBasedOnProvider(networks) {

View file

@ -21,18 +21,14 @@
<button class="btn btn-xs btn-danger" ng-click="deleteUser()"><i class="fa fa-trash space-right" aria-hidden="true"></i>Delete this user</button> <button class="btn btn-xs btn-danger" ng-click="deleteUser()"><i class="fa fa-trash space-right" aria-hidden="true"></i>Delete this user</button>
</td> </td>
</tr> </tr>
<td>Permissions</td> <td colspan="2">
<td> <label for="permissions" class="control-label text-left">
<div class="btn-group btn-group-sm">
<label class="btn btn-default" ng-model="user.RoleId" uib-btn-radio="2" ng-change="updatePermissions()">
<i class="fa fa-user" aria-hidden="true"></i>
User
</label>
<label class="btn btn-default" ng-model="user.RoleId" uib-btn-radio="1" ng-change="updatePermissions()">
<i class="fa fa-user-circle-o" aria-hidden="true"></i>
Administrator Administrator
<portainer-tooltip position="bottom" message="Administrators have access to Portainer settings management as well as full control over all defined endpoints and their resources."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="formValues.Administrator" ng-change="updatePermissions()"><i></i>
</label> </label>
</div>
</td> </td>
</tr> </tr>
</tbody> </tbody>

View file

@ -8,7 +8,8 @@ function ($scope, $state, $stateParams, UserService, ModalService, Messages) {
$scope.formValues = { $scope.formValues = {
newPassword: '', newPassword: '',
confirmPassword: '' confirmPassword: '',
Administrator: false,
}; };
$scope.deleteUser = function() { $scope.deleteUser = function() {
@ -23,9 +24,10 @@ function ($scope, $state, $stateParams, UserService, ModalService, Messages) {
$scope.updatePermissions = function() { $scope.updatePermissions = function() {
$('#loadingViewSpinner').show(); $('#loadingViewSpinner').show();
UserService.updateUser($scope.user.Id, undefined, $scope.user.RoleId) var role = $scope.formValues.Administrator ? 1 : 2;
UserService.updateUser($scope.user.Id, undefined, role)
.then(function success(data) { .then(function success(data) {
var newRole = $scope.user.RoleId === 1 ? 'administrator' : 'user'; var newRole = role === 1 ? 'administrator' : 'user';
Messages.send('Permissions successfully updated', $scope.user.Username + ' is now ' + newRole); Messages.send('Permissions successfully updated', $scope.user.Username + ' is now ' + newRole);
$state.reload(); $state.reload();
}) })
@ -71,7 +73,9 @@ function ($scope, $state, $stateParams, UserService, ModalService, Messages) {
$('#loadingViewSpinner').show(); $('#loadingViewSpinner').show();
UserService.user($stateParams.id) UserService.user($stateParams.id)
.then(function success(data) { .then(function success(data) {
$scope.user = new UserViewModel(data); var user = new UserViewModel(data);
$scope.user = user;
$scope.formValues.Administrator = user.RoleId === 1 ? true : false;
}) })
.catch(function error(err) { .catch(function error(err) {
Messages.error("Failure", err, 'Unable to retrieve user information'); Messages.error("Failure", err, 'Unable to retrieve user information');

View file

@ -51,21 +51,22 @@
<!-- !confirm-password-input --> <!-- !confirm-password-input -->
<!-- role-checkbox --> <!-- role-checkbox -->
<div class="form-group"> <div class="form-group">
<label for="permissions" class="col-sm-2 control-label text-left">Permissions</label> <div class="col-sm-12">
<div class="col-sm-8"> <label for="permissions" class="control-label text-left">
<div class="btn-group btn-group-sm">
<label class="btn btn-default" ng-model="formValues.Role" uib-btn-radio="2">
<i class="fa fa-user" aria-hidden="true"></i>
User
</label>
<label class="btn btn-default" ng-model="formValues.Role" uib-btn-radio="1">
<i class="fa fa-user-circle-o" aria-hidden="true"></i>
Administrator Administrator
<portainer-tooltip position="bottom" message="Administrators have access to Portainer settings management as well as full control over all defined endpoints and their resources."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="formValues.Administrator"><i></i>
</label> </label>
</div>
</div> </div>
</div> </div>
<!-- !role-checkbox --> <!-- !role-checkbox -->
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">Note: non-administrator users do not have access to any endpoint by default. Head over the <a ui-sref="endpoints">endpoints view</a> to manage their accesses.</span>
</div>
</div>
<div class="form-group"> <div class="form-group">
<div class="col-sm-12"> <div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!state.validUsername || formValues.Username === '' || formValues.Password === '' || formValues.Password !== formValues.ConfirmPassword" ng-click="addUser()"><i class="fa fa-user-plus" aria-hidden="true"></i> Add user</button> <button type="button" class="btn btn-primary btn-sm" ng-disabled="!state.validUsername || formValues.Username === '' || formValues.Password === '' || formValues.Password !== formValues.ConfirmPassword" ng-click="addUser()"><i class="fa fa-user-plus" aria-hidden="true"></i> Add user</button>

View file

@ -14,7 +14,7 @@ function ($scope, $state, UserService, ModalService, Messages, Pagination) {
Username: '', Username: '',
Password: '', Password: '',
ConfirmPassword: '', ConfirmPassword: '',
Role: 2, Administrator: false,
}; };
$scope.order = function(sortType) { $scope.order = function(sortType) {
@ -59,7 +59,7 @@ function ($scope, $state, UserService, ModalService, Messages, Pagination) {
$scope.state.userCreationError = ''; $scope.state.userCreationError = '';
var username = $scope.formValues.Username; var username = $scope.formValues.Username;
var password = $scope.formValues.Password; var password = $scope.formValues.Password;
var role = $scope.formValues.Role; var role = $scope.formValues.Administrator ? 1 : 2;
UserService.createUser(username, password, role) UserService.createUser(username, password, role)
.then(function success(data) { .then(function success(data) {
Messages.send("User created", username); Messages.send("User created", username);

View file

@ -25,7 +25,7 @@
<rd-widget-taskbar classes="col-lg-12"> <rd-widget-taskbar classes="col-lg-12">
<div class="pull-left"> <div class="pull-left">
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button> <button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
<a class="btn btn-default" type="button" ui-sref="actions.create.volume">Add volume</a> <a class="btn btn-primary" type="button" ui-sref="actions.create.volume"><i class="fa fa-plus space-right" aria-hidden="true"></i>Add volume</a>
</div> </div>
<div class="pull-right"> <div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" /> <input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />

View file

@ -99,7 +99,7 @@ function ($scope, $state, Volume, Messages, Pagination, ModalService, Authentica
angular.forEach($scope.volumes, function (volume) { angular.forEach($scope.volumes, function (volume) {
if (volume.Metadata) { if (volume.Metadata) {
var volumeRC = volume.Metadata.ResourceControl; var volumeRC = volume.Metadata.ResourceControl;
if (volumeRC && volumeRC.OwnerId != $scope.user.ID) { if (volumeRC && volumeRC.OwnerId !== $scope.user.ID) {
angular.forEach(users, function (user) { angular.forEach(users, function (user) {
if (volumeRC.OwnerId === user.Id) { if (volumeRC.OwnerId === user.Id) {
volume.Owner = user.Username; volume.Owner = user.Username;

View file

@ -9,6 +9,16 @@ angular.module('portainer.helpers')
Labels: node.Spec.Labels, Labels: node.Spec.Labels,
Availability: node.Spec.Availability Availability: node.Spec.Availability
}; };
},
getManagerIP: function(nodes) {
var managerIp;
for (var n in nodes) {
if (undefined === nodes[n].ManagerStatus || nodes[n].ManagerStatus.Reachability !== "reachable") {
continue;
}
managerIp = nodes[n].ManagerStatus.Addr.split(":")[0];
}
return managerIp;
} }
}; };
}]); }]);

View file

@ -0,0 +1,19 @@
function ImageDetailsViewModel(data) {
this.Id = data.Id;
this.Tag = data.Tag;
this.Parent = data.Parent;
this.Repository = data.Repository;
this.Created = data.Created;
this.Checked = false;
this.RepoTags = data.RepoTags;
this.VirtualSize = data.VirtualSize;
this.DockerVersion = data.DockerVersion;
this.Os = data.Os;
this.Architecture = data.Architecture;
this.Author = data.Author;
this.Command = data.ContainerConfig.Cmd;
this.Entrypoint = data.ContainerConfig.Entrypoint ? data.ContainerConfig.Entrypoint : '';
this.ExposedPorts = data.ContainerConfig.ExposedPorts ? Object.keys(data.ContainerConfig.ExposedPorts) : [];
this.Volumes = data.ContainerConfig.Volumes ? Object.keys(data.ContainerConfig.Volumes) : [];
this.Env = data.ContainerConfig.Env ? data.ContainerConfig.Env : [];
}

View file

@ -2,6 +2,8 @@ function ServiceViewModel(data, runningTasks, nodes) {
this.Model = data; this.Model = data;
this.Id = data.ID; this.Id = data.ID;
this.Name = data.Spec.Name; this.Name = data.Spec.Name;
this.CreatedAt = data.CreatedAt;
this.UpdatedAt = data.UpdatedAt;
this.Image = data.Spec.TaskTemplate.ContainerSpec.Image; this.Image = data.Spec.TaskTemplate.ContainerSpec.Image;
this.Version = data.Version.Index; this.Version = data.Version.Index;
if (data.Spec.Mode.Replicated) { if (data.Spec.Mode.Replicated) {
@ -16,20 +18,52 @@ function ServiceViewModel(data, runningTasks, nodes) {
if (runningTasks) { if (runningTasks) {
this.Running = runningTasks.length; this.Running = runningTasks.length;
} }
if (data.Spec.TaskTemplate.Resources) {
if (data.Spec.TaskTemplate.Resources.Limits) {
this.LimitNanoCPUs = data.Spec.TaskTemplate.Resources.Limits.NanoCPUs;
this.LimitMemoryBytes = data.Spec.TaskTemplate.Resources.Limits.MemoryBytes;
}
if (data.Spec.TaskTemplate.Resources.Reservations) {
this.ReservationNanoCPUs = data.Spec.TaskTemplate.Resources.Reservations.NanoCPUs;
this.ReservationMemoryBytes = data.Spec.TaskTemplate.Resources.Reservations.MemoryBytes;
}
}
if (data.Spec.TaskTemplate.RestartPolicy) {
this.RestartCondition = data.Spec.TaskTemplate.RestartPolicy.Condition;
this.RestartDelay = data.Spec.TaskTemplate.RestartPolicy.Delay;
this.RestartMaxAttempts = data.Spec.TaskTemplate.RestartPolicy.MaxAttempts;
this.RestartWindow = data.Spec.TaskTemplate.RestartPolicy.Window;
} else {
this.RestartCondition = 'none';
this.RestartDelay = 0;
this.RestartMaxAttempts = 0;
this.RestartWindow = 0;
}
this.Constraints = data.Spec.TaskTemplate.Placement ? data.Spec.TaskTemplate.Placement.Constraints || [] : [];
this.Labels = data.Spec.Labels; this.Labels = data.Spec.Labels;
if (data.Spec.TaskTemplate.ContainerSpec) {
this.ContainerLabels = data.Spec.TaskTemplate.ContainerSpec.Labels; var containerSpec = data.Spec.TaskTemplate.ContainerSpec;
if (containerSpec) {
this.ContainerLabels = containerSpec.Labels;
this.Env = containerSpec.Env;
this.Mounts = containerSpec.Mounts || [];
this.User = containerSpec.User;
this.Dir = containerSpec.Dir;
this.Command = containerSpec.Command;
this.Secrets = containerSpec.Secrets;
} }
if (data.Spec.TaskTemplate.ContainerSpec.Env) { if (data.Spec.EndpointSpec) {
this.Env = data.Spec.TaskTemplate.ContainerSpec.Env; this.Ports = data.Spec.EndpointSpec.Ports;
} }
this.Mounts = []; this.Mounts = [];
if (data.Spec.TaskTemplate.ContainerSpec.Mounts) { if (data.Spec.TaskTemplate.ContainerSpec.Mounts) {
this.Mounts = data.Spec.TaskTemplate.ContainerSpec.Mounts; this.Mounts = data.Spec.TaskTemplate.ContainerSpec.Mounts;
} }
if (data.Endpoint.Ports) {
this.Ports = data.Endpoint.Ports; this.VirtualIPs = data.Endpoint ? data.Endpoint.VirtualIPs : [];
}
if (data.Spec.UpdateConfig) { if (data.Spec.UpdateConfig) {
this.UpdateParallelism = (typeof data.Spec.UpdateConfig.Parallelism !== undefined) ? data.Spec.UpdateConfig.Parallelism || 0 : 1; this.UpdateParallelism = (typeof data.Spec.UpdateConfig.Parallelism !== undefined) ? data.Spec.UpdateConfig.Parallelism || 0 : 1;
this.UpdateDelay = data.Spec.UpdateConfig.Delay || 0; this.UpdateDelay = data.Spec.UpdateConfig.Delay || 0;

View file

@ -1,10 +1,43 @@
angular.module('portainer.services') angular.module('portainer.services')
.factory('ImageService', ['$q', 'Image', function ImageServiceFactory($q, Image) { .factory('ImageService', ['$q', 'Image', 'ImageHelper', function ImageServiceFactory($q, Image, ImageHelper) {
'use strict'; 'use strict';
var service = {}; var service = {};
service.pullImage = function(imageConfiguration) { service.image = function(imageId) {
var deferred = $q.defer(); var deferred = $q.defer();
Image.get({id: imageId}).$promise
.then(function success(data) {
if (data.message) {
deferred.reject({ msg: data.message });
} else {
var image = new ImageDetailsViewModel(data);
deferred.resolve(image);
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve image details', err: err });
});
return deferred.promise;
};
service.images = function() {
var deferred = $q.defer();
Image.query({}).$promise
.then(function success(data) {
var images = data.map(function (item) {
return new ImageViewModel(item);
});
deferred.resolve(images);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve images', err: err });
});
return deferred.promise;
};
service.pullImage = function(image, registry) {
var deferred = $q.defer();
var imageConfiguration = ImageHelper.createImageConfigForContainer(image, registry);
Image.create(imageConfiguration).$promise Image.create(imageConfiguration).$promise
.then(function success(data) { .then(function success(data) {
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error'); var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
@ -20,5 +53,43 @@ angular.module('portainer.services')
}); });
return deferred.promise; return deferred.promise;
}; };
service.tagImage = function(id, image, registry) {
var imageConfig = ImageHelper.createImageConfigForCommit(image, registry);
return Image.tag({id: id, tag: imageConfig.tag, repo: imageConfig.repo}).$promise;
};
service.deleteImage = function(id, forceRemoval) {
var deferred = $q.defer();
Image.remove({id: id, force: forceRemoval}).$promise
.then(function success(data) {
if (data[0].message) {
deferred.reject({ msg: data[0].message });
} else {
deferred.resolve();
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to remove image', err: err });
});
return deferred.promise;
};
service.pushImage = function(tag) {
var deferred = $q.defer();
Image.push({tag: tag}).$promise
.then(function success(data) {
if (data[data.length - 1].error) {
deferred.reject({ msg: data[data.length - 1].error });
} else {
deferred.resolve();
}
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to push image tag', err: err });
});
return deferred.promise;
};
return service; return service;
}]); }]);

View file

@ -22,6 +22,8 @@ angular.module('portainer.services')
msg = e.message; msg = e.message;
} else if (e.data && e.data.length > 0 && e.data[0].message) { } else if (e.data && e.data.length > 0 && e.data[0].message) {
msg = e.data[0].message; msg = e.data[0].message;
} else if (e.msg) {
msg = e.msg;
} }
$.gritter.add({ $.gritter.add({
title: $sanitize(title), title: $sanitize(title),

View file

@ -21,17 +21,10 @@ angular.module('portainer.services')
}; };
service.createTemplateConfiguration = function(template, containerName, network, containerMapping) { service.createTemplateConfiguration = function(template, containerName, network, containerMapping) {
var imageConfiguration = service.createImageConfiguration(template); var imageConfiguration = ImageHelper.createImageConfigForContainer(template.Image, template.Registry);
var containerConfiguration = service.createContainerConfiguration(template, containerName, network, containerMapping); var containerConfiguration = service.createContainerConfiguration(template, containerName, network, containerMapping);
containerConfiguration.Image = imageConfiguration.fromImage + ':' + imageConfiguration.tag; containerConfiguration.Image = imageConfiguration.fromImage + ':' + imageConfiguration.tag;
return { return containerConfiguration;
container: containerConfiguration,
image: imageConfiguration
};
};
service.createImageConfiguration = function(template) {
return ImageHelper.createImageConfigForContainer(template.Image, template.Registry);
}; };
service.createContainerConfiguration = function(template, containerName, network, containerMapping) { service.createContainerConfiguration = function(template, containerName, network, containerMapping) {

View file

@ -57,6 +57,13 @@ html, body, #content-wrapper, .page-content, #view {
margin-left: 5px; margin-left: 5px;
} }
.form-section-title {
border-bottom: 1px solid #777;
margin-top: 5px;
margin-bottom: 15px;
color: #777;
}
.form-horizontal .control-label.text-left{ .form-horizontal .control-label.text-left{
text-align: left; text-align: left;
font-size: 0.9em; font-size: 0.9em;
@ -125,6 +132,10 @@ a[ng-click]{
padding: 0 !important; padding: 0 !important;
} }
.padding-top {
padding-top: 15px !important;
}
.terminal-container { .terminal-container {
width: 100%; width: 100%;
padding: 10px 5px; padding: 10px 5px;
@ -286,6 +297,10 @@ a[ng-click]{
margin: 0 auto; margin: 0 auto;
} }
ul.sidebar {
bottom: 40px;
}
ul.sidebar .sidebar-list a.active { ul.sidebar .sidebar-list a.active {
color: #fff; color: #fff;
text-indent: 22px; text-indent: 22px;
@ -293,6 +308,11 @@ ul.sidebar .sidebar-list a.active {
background: #2d3e63; background: #2d3e63;
} }
@media(min-width: 768px) and (max-width: 992px) {
.margin-sm-top {
margin-top: 5px;
}
}
@media (min-width: 768px) { @media (min-width: 768px) {
.pull-sm-left { .pull-sm-left {
float: left !important; float: left !important;
@ -329,3 +349,35 @@ ul.sidebar .sidebar-list a.active {
.pull-none { .pull-none {
float: none !important; float: none !important;
} }
.switch input {
display: none;
}
.switch i {
display: inline-block;
vertical-align: middle;
cursor: pointer;
padding-right: 24px;
transition: all ease 0.2s;
-webkit-transition: all ease 0.2s;
border-radius: 24px;
box-shadow: inset 0 0 1px 1px rgba(0,0,0,.5);
}
.switch i:before {
display: block;
content: '';
width: 24px;
height: 24px;
border-radius: 24px;
background: white;
box-shadow: 0 0 1px 1px rgba(0,0,0,.5);
}
.switch :checked + i {
padding-right: 0;
padding-left: 24px;
-webkit-box-shadow: inset 0 0 1px rgba(0,0,0,.5), inset 0 0 40px #337ab7;
box-shadow: inset 0 0 1px rgba(0,0,0,.5), inset 0 0 40px #337ab7;
}

View file

@ -1,6 +1,6 @@
{ {
"name": "portainer", "name": "portainer",
"version": "1.12.1", "version": "1.12.2",
"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

@ -11,7 +11,9 @@ mkdir -pv /tmp/portainer-builds
grunt release grunt release
docker build -t portainer/portainer:linux-amd64-${VERSION} -f build/linux/Dockerfile . docker build -t portainer/portainer:linux-amd64-${VERSION} -f build/linux/Dockerfile .
docker push portainer/portainer:linux-amd64-${VERSION}
docker build -t portainer/portainer:linux-amd64 -f build/linux/Dockerfile . docker build -t portainer/portainer:linux-amd64 -f build/linux/Dockerfile .
docker push portainer/portainer:linux-amd64
rm -rf /tmp/portainer-builds/unix && mkdir -pv /tmp/portainer-builds/unix/portainer rm -rf /tmp/portainer-builds/unix && mkdir -pv /tmp/portainer-builds/unix/portainer
mv dist/* /tmp/portainer-builds/unix/portainer mv dist/* /tmp/portainer-builds/unix/portainer
cd /tmp/portainer-builds/unix cd /tmp/portainer-builds/unix
@ -21,7 +23,9 @@ cd -
grunt release-arm grunt release-arm
docker build -t portainer/portainer:linux-arm-${VERSION} -f build/linux/Dockerfile . docker build -t portainer/portainer:linux-arm-${VERSION} -f build/linux/Dockerfile .
docker push portainer/portainer:linux-arm-${VERSION}
docker build -t portainer/portainer:linux-arm -f build/linux/Dockerfile . docker build -t portainer/portainer:linux-arm -f build/linux/Dockerfile .
docker push portainer/portainer:linux-arm
rm -rf /tmp/portainer-builds/arm && mkdir -pv /tmp/portainer-builds/arm/portainer rm -rf /tmp/portainer-builds/arm && mkdir -pv /tmp/portainer-builds/arm/portainer
mv dist/* /tmp/portainer-builds/arm/portainer mv dist/* /tmp/portainer-builds/arm/portainer
cd /tmp/portainer-builds/arm cd /tmp/portainer-builds/arm
@ -31,7 +35,9 @@ cd -
grunt release-arm64 grunt release-arm64
docker build -t portainer/portainer:linux-arm64-${VERSION} -f build/linux/Dockerfile . docker build -t portainer/portainer:linux-arm64-${VERSION} -f build/linux/Dockerfile .
docker push portainer/portainer:linux-arm64-${VERSION}
docker build -t portainer/portainer:linux-arm64 -f build/linux/Dockerfile . docker build -t portainer/portainer:linux-arm64 -f build/linux/Dockerfile .
docker push portainer/portainer:linux-arm64
rm -rf /tmp/portainer-builds/arm64 && mkdir -pv /tmp/portainer-builds/arm64/portainer rm -rf /tmp/portainer-builds/arm64 && mkdir -pv /tmp/portainer-builds/arm64/portainer
mv dist/* /tmp/portainer-builds/arm64/portainer mv dist/* /tmp/portainer-builds/arm64/portainer
cd /tmp/portainer-builds/arm64 cd /tmp/portainer-builds/arm64
@ -49,7 +55,7 @@ cd -
grunt release-win grunt release-win
rm -rf /tmp/portainer-builds/win && mkdir -pv /tmp/portainer-builds/win/portainer rm -rf /tmp/portainer-builds/win && mkdir -pv /tmp/portainer-builds/win/portainer
mv dist/* /tmp/portainer-builds/win/portainer cp -r dist/* /tmp/portainer-builds/win/portainer
cd /tmp/portainer-builds/win cd /tmp/portainer-builds/win
tar cvpfz portainer-${VERSION}-windows-amd64.tar.gz portainer tar cvpfz portainer-${VERSION}-windows-amd64.tar.gz portainer
mv portainer-${VERSION}-windows-amd64.tar.gz /tmp/portainer-builds/ mv portainer-${VERSION}-windows-amd64.tar.gz /tmp/portainer-builds/

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.12.1", "version": "1.12.2",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git@github.com:portainer/portainer.git" "url": "git@github.com:portainer/portainer.git"