Merge branch 'release/1.10.0'

pull/365/head 1.10.0
Anthony Lapenna 2016-11-11 15:29:16 +13:00
commit 8c3f7b3ec2
48 changed files with 869 additions and 453 deletions

30
LICENSE
View File

@ -1,22 +1,22 @@
Portainer: Copyright (c) 2016 Portainer.io Portainer: Copyright (c) 2016 Portainer.io
Permission is hereby granted, free of charge, to any person obtaining a copy This software is provided 'as-is', without any express or implied
of this software and associated documentation files (the "Software"), to deal warranty. In no event will the authors be held liable for any damages
in the Software without restriction, including without limitation the rights arising from the use of this software.
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all Permission is granted to anyone to use this software for any purpose,
copies or substantial portions of the Software. including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 1. The origin of this software must not be misrepresented; you must not
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, claim that you wrote the original software. If you use this software
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE in a product, an acknowledgment in the product documentation would be
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER appreciated but is not required.
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 2. Altered source versions must be plainly marked as such, and must not be
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE misrepresented as being the original software.
SOFTWARE. 3. This notice may not be removed or altered from any source distribution.
Portainer contains code which was originally under this license:
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (portainer.io) UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (portainer.io)

View File

@ -10,7 +10,7 @@
**_Portainer_** is a lightweight management UI which allows you to **easily** manage your Docker host or Swarm cluster. **_Portainer_** is a lightweight management UI which allows you to **easily** manage your Docker host or Swarm cluster.
**_Portainer_** is meant to be as **simple** to deploy as it is to use. It consists of a single container that can run on any Docker for Linux engine. A Docker for Windows version is on its way ! **_Portainer_** is meant to be as **simple** to deploy as it is to use. It consists of a single container that can run on any Docker engine (Docker for Linux and Docker for Windows are supported).
**_Portainer_** allows you to manage your Docker containers, images, volumes, networks and more ! It is compatible with the *standalone Docker* engine and with *Docker Swarm*. **_Portainer_** allows you to manage your Docker containers, images, volumes, networks and more ! It is compatible with the *standalone Docker* engine and with *Docker Swarm*.

View File

@ -33,6 +33,7 @@ type (
func (a *api) run(settings *Settings) { func (a *api) run(settings *Settings) {
handler := a.newHandler(settings) handler := a.newHandler(settings)
log.Printf("Starting portainer on %s", a.bindAddress)
if err := http.ListenAndServe(a.bindAddress, handler); err != nil { if err := http.ListenAndServe(a.bindAddress, handler); err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@ -17,7 +17,6 @@ func (a *api) newHandler(settings *Settings) http.Handler {
) )
handler := a.newAPIHandler() handler := a.newAPIHandler()
CSRFHandler := newCSRFHandler(a.dataPath)
mux.Handle("/", fileHandler) mux.Handle("/", fileHandler)
mux.Handle("/dockerapi/", http.StripPrefix("/dockerapi", handler)) mux.Handle("/dockerapi/", http.StripPrefix("/dockerapi", handler))
@ -28,7 +27,12 @@ func (a *api) newHandler(settings *Settings) http.Handler {
mux.HandleFunc("/templates", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/templates", func(w http.ResponseWriter, r *http.Request) {
templatesHandler(w, r, a.templatesURL) templatesHandler(w, r, a.templatesURL)
}) })
return CSRFHandler(newCSRFWrapper(mux)) // CSRF protection is disabled for the moment
// CSRFHandler := newCSRFHandler(a.dataPath)
// return CSRFHandler(newCSRFWrapper(mux))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mux.ServeHTTP(w, r)
})
} }
// newAPIHandler initializes a new http.Handler based on the URL scheme // newAPIHandler initializes a new http.Handler based on the URL scheme

View File

@ -6,7 +6,7 @@ import (
// main is the entry point of the program // main is the entry point of the program
func main() { func main() {
kingpin.Version("1.9.3") kingpin.Version("1.10.0")
var ( var (
endpoint = kingpin.Flag("host", "Dockerd endpoint").Default("unix:///var/run/docker.sock").Short('H').String() endpoint = kingpin.Flag("host", "Dockerd endpoint").Default("unix:///var/run/docker.sock").Short('H').String()
addr = kingpin.Flag("bind", "Address and port to serve Portainer").Default(":9000").Short('p').String() addr = kingpin.Flag("bind", "Address and port to serve Portainer").Default(":9000").Short('p').String()

View File

@ -33,9 +33,6 @@ angular.module('portainer', [
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', function ($stateProvider, $urlRouterProvider, $httpProvider) { .config(['$stateProvider', '$urlRouterProvider', '$httpProvider', function ($stateProvider, $urlRouterProvider, $httpProvider) {
'use strict'; 'use strict';
$httpProvider.defaults.xsrfCookieName = 'csrfToken';
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token';
$urlRouterProvider.otherwise('/'); $urlRouterProvider.otherwise('/');
$stateProvider $stateProvider
@ -161,6 +158,8 @@ angular.module('portainer', [
}); });
// The Docker API likes to return plaintext errors, this catches them and disp // The Docker API likes to return plaintext errors, this catches them and disp
// $httpProvider.defaults.xsrfCookieName = 'csrfToken';
// $httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token';
$httpProvider.interceptors.push(function() { $httpProvider.interceptors.push(function() {
return { return {
'response': function(response) { 'response': function(response) {
@ -172,10 +171,11 @@ angular.module('portainer', [
time: 10000 time: 10000
}); });
} }
var csrfToken = response.headers('X-Csrf-Token'); // CSRF protection is disabled for the moment
if (csrfToken) { // var csrfToken = response.headers('X-Csrf-Token');
document.cookie = 'csrfToken=' + csrfToken; // if (csrfToken) {
} // document.cookie = 'csrfToken=' + csrfToken;
// }
return response; return response;
} }
}; };
@ -188,4 +188,4 @@ angular.module('portainer', [
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is requred. If you have a port, prefix it with a ':' i.e. :4243 .constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is requred. If you have a port, prefix it with a ':' i.e. :4243
.constant('CONFIG_ENDPOINT', 'settings') .constant('CONFIG_ENDPOINT', 'settings')
.constant('TEMPLATES_ENDPOINT', 'templates') .constant('TEMPLATES_ENDPOINT', 'templates')
.constant('UI_VERSION', 'v1.9.3'); .constant('UI_VERSION', 'v1.10.0');

View File

@ -3,7 +3,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i> <i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title> </rd-header-title>
<rd-header-content> <rd-header-content>
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> <a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a>
</rd-header-content> </rd-header-content>
</rd-header> </rd-header>
@ -13,13 +13,13 @@
<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 btn-ico" aria-hidden="true"></i>Start</button> <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-danger" ng-click="stop()" ng-if="container.State.Running"><i class="fa fa-stop btn-ico" aria-hidden="true"></i>Stop</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="kill()" ng-if="container.State.Running"><i class="fa fa-bomb btn-ico" aria-hidden="true"></i>Kill</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-primary" ng-click="restart()" ng-if="container.State.Running"><i class="fa fa-refresh btn-ico" aria-hidden="true"></i>Restart</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="pause()" ng-if="container.State.Running && !container.State.Paused"><i class="fa fa-pause btn-ico" aria-hidden="true"></i>Pause</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="unpause()" ng-if="container.State.Paused"><i class="fa fa-play btn-ico" aria-hidden="true"></i>Resume</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-danger" ng-click="remove()" ng-disabled="container.State.Running"><i class="fa fa-trash btn-ico" 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>
</rd-widget> </rd-widget>
@ -52,7 +52,7 @@
<tr> <tr>
<td>Status</td> <td>Status</td>
<td> <td>
<i ng-class="{true: 'fa fa-heartbeat text-icon green-icon', false: 'fa fa-heartbeat text-icon red-icon'}[container.State.Running]"></i> <i ng-class="{true: 'fa fa-heartbeat space-right green-icon', false: 'fa fa-heartbeat space-right red-icon'}[container.State.Running]"></i>
{{ container.State|getstatetext }} since {{ activityTime }}<span ng-if="!container.State.Running"> with exit code {{ container.State.ExitCode }}</span> {{ container.State|getstatetext }} since {{ activityTime }}<span ng-if="!container.State.Running"> with exit code {{ container.State.ExitCode }}</span>
</td> </td>
</tr> </tr>
@ -67,9 +67,9 @@
<tr> <tr>
<td colspan="2"> <td colspan="2">
<div class="btn-group" role="group" aria-label="..."> <div class="btn-group" role="group" aria-label="...">
<a class="btn btn-outline-secondary" type="button" ui-sref="stats({id: container.Id})"><i class="fa fa-area-chart btn-ico" aria-hidden="true"></i>Stats</a> <a class="btn btn-outline-secondary" type="button" ui-sref="stats({id: container.Id})"><i class="fa fa-area-chart space-right" aria-hidden="true"></i>Stats</a>
<a class="btn btn-outline-secondary" type="button" ui-sref="logs({id: container.Id})"><i class="fa fa-exclamation-circle btn-ico" aria-hidden="true"></i>Logs</a> <a class="btn btn-outline-secondary" type="button" ui-sref="logs({id: container.Id})"><i class="fa fa-exclamation-circle space-right" aria-hidden="true"></i>Logs</a>
<a class="btn btn-outline-secondary" type="button" ui-sref="console({id: container.Id})"><i class="fa fa-terminal btn-ico" aria-hidden="true"></i>Console</a> <a class="btn btn-outline-secondary" type="button" ui-sref="console({id: container.Id})"><i class="fa fa-terminal space-right" aria-hidden="true"></i>Console</a>
</div> </div>
</td> </td>
</tr> </tr>
@ -202,3 +202,34 @@
</rd-widget> </rd-widget>
</div> </div>
</div> </div>
<div class="row" ng-if="!(container.NetworkSettings.Networks | emptyobject)">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-sitemap" title="Connected networks"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<thead>
<th>Network Name</th>
<th>IP Address</th>
<th>Gateway</th>
<th>MacAddress</th>
<th>Actions</th>
</thead>
<tbody>
<tr ng-repeat="(key, value) in container.NetworkSettings.Networks">
<td><a ui-sref="network({id: value.NetworkID})">{{ key }}</a></td>
<td>{{ value.IPAddress || '-' }}</td>
<td>{{ value.Gateway || '-' }}</td>
<td>{{ value.MacAddress || '-' }}</td>
<td>
<button type="button" class="btn btn-xs btn-danger" ng-click="containerLeaveNetwork(container, value.NetworkID)"><i class="fa fa-trash space-right" aria-hidden="true"></i>Leave Network</button>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@ -1,6 +1,6 @@
angular.module('container', []) angular.module('container', [])
.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ImageHelper', 'Messages', .controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ImageHelper', 'Network', 'Messages',
function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ImageHelper, Messages) { function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ImageHelper, Network, Messages) {
$scope.activityTime = 0; $scope.activityTime = 0;
$scope.portBindings = []; $scope.portBindings = [];
$scope.config = { $scope.config = {
@ -153,5 +153,22 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
$scope.container.edit = false; $scope.container.edit = false;
}; };
$scope.containerLeaveNetwork = function containerLeaveNetwork(container, networkId) {
$('#loadingViewSpinner').show();
Network.disconnect({id: networkId}, { Container: $stateParams.id, Force: false }, function (d) {
if (d.message) {
$('#loadingViewSpinner').hide();
Messages.send("Error", {}, d.message);
} else {
$('#loadingViewSpinner').hide();
Messages.send("Container left network", $stateParams.id);
$state.go('container', {id: $stateParams.id}, {reload: true});
}
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to disconnect container from network");
});
};
update(); update();
}]); }]);

View File

@ -2,12 +2,12 @@
<rd-header-title title="Container console"> <rd-header-title title="Container console">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i> <i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title> </rd-header-title>
<rd-header-content> <rd-header-content ng-if="state.loaded">
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Console <a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Console
</rd-header-content> </rd-header-content>
</rd-header> </rd-header>
<div class="row"> <div class="row" ng-if="state.loaded">
<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-terminal" title="Console"> <rd-widget-header icon="fa-terminal" title="Console">
@ -16,18 +16,27 @@
</div> </div>
</rd-widget-header> </rd-widget-header>
<rd-widget-body> <rd-widget-body>
<form class="form-horizontal"> <form>
<!-- command-list --> <div class="row">
<div class="form-group"> <!-- command-list -->
<div class="col-sm-3"> <div class="col-sm-4">
<select class="selectpicker form-control" ng-model="state.command"> <div class="input-group">
<option value="bash">/bin/bash</option> <span class="input-group-addon">
<option value="sh">/bin/sh</option> <i class="fa fa-linux" aria-hidden="true" ng-if="imageOS == 'linux'"></i>
</select> <i class="fa fa-windows" aria-hidden="true" ng-if="imageOS == 'windows'"></i>
</span>
<select class="form-control" ng-model="state.command" id="command">
<option value="bash" ng-if="imageOS == 'linux'">/bin/bash</option>
<option value="sh" ng-if="imageOS == 'linux'">/bin/sh</option>
<option value="powershell" ng-if="imageOS == 'windows'">powershell</option>
<option value="cmd.exe" ng-if="imageOS == 'windows'">cmd.exe</option>
</select>
</div>
</div> </div>
<div class="col-sm-9 pull-left"> <!-- !command-list -->
<button type="button" class="btn btn-primary" ng-click="connect()" ng-disabled="connected">Connect</button> <div class="col-sm-8">
<button type="button" class="btn btn-default" ng-click="disconnect()" ng-disabled="!connected">Disconnect</button> <button type="button" class="btn btn-primary" ng-click="connect()" ng-disabled="state.connected">Connect</button>
<button type="button" class="btn btn-default" ng-click="disconnect()" ng-disabled="!state.connected">Disconnect</button>
</div> </div>
</div> </div>
</form> </form>

View File

@ -1,9 +1,9 @@
angular.module('containerConsole', []) angular.module('containerConsole', [])
.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Settings', 'Container', 'Exec', '$timeout', 'Messages', .controller('ContainerConsoleController', ['$scope', '$stateParams', 'Settings', 'Container', 'Image', 'Exec', '$timeout', 'Messages',
function ($scope, $stateParams, Settings, Container, Exec, $timeout, Messages) { function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, Messages) {
$scope.state = {}; $scope.state = {};
$scope.state.command = "bash"; $scope.state.loaded = false;
$scope.connected = false; $scope.state.connected = false;
var socket, term; var socket, term;
@ -16,6 +16,22 @@ function ($scope, $stateParams, Settings, Container, Exec, $timeout, Messages) {
Container.get({id: $stateParams.id}, function(d) { Container.get({id: $stateParams.id}, function(d) {
$scope.container = d; $scope.container = d;
if (d.message) {
Messages.error("Error", d, 'Unable to retrieve container details');
$('#loadingViewSpinner').hide();
} else {
Image.get({id: d.Image}, function(imgData) {
$scope.imageOS = imgData.Os;
$scope.state.command = imgData.Os === 'windows' ? 'powershell' : 'bash';
$scope.state.loaded = true;
$('#loadingViewSpinner').hide();
}, function (e) {
Messages.error("Failure", e, 'Unable to retrieve image details');
$('#loadingViewSpinner').hide();
});
}
}, function (e) {
Messages.error("Failure", e, 'Unable to retrieve container details');
$('#loadingViewSpinner').hide(); $('#loadingViewSpinner').hide();
}); });
@ -54,7 +70,7 @@ function ($scope, $stateParams, Settings, Container, Exec, $timeout, Messages) {
}; };
$scope.disconnect = function() { $scope.disconnect = function() {
$scope.connected = false; $scope.state.connected = false;
if (socket !== null) { if (socket !== null) {
socket.close(); socket.close();
} }
@ -79,7 +95,7 @@ function ($scope, $stateParams, Settings, Container, Exec, $timeout, Messages) {
function initTerm(url, height, width) { function initTerm(url, height, width) {
socket = new WebSocket(url); socket = new WebSocket(url);
$scope.connected = true; $scope.state.connected = true;
socket.onopen = function(evt) { socket.onopen = function(evt) {
$('#loadConsoleSpinner').hide(); $('#loadConsoleSpinner').hide();
term = new Terminal(); term = new Terminal();
@ -95,11 +111,10 @@ function ($scope, $stateParams, Settings, Container, Exec, $timeout, Messages) {
term.write(e.data); term.write(e.data);
}; };
socket.onerror = function (error) { socket.onerror = function (error) {
$scope.connected = false; $scope.state.connected = false;
}; };
socket.onclose = function(evt) { socket.onclose = function(evt) {
$scope.connected = false; $scope.state.connected = false;
}; };
}; };
} }

View File

@ -3,7 +3,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i> <i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title> </rd-header-title>
<rd-header-content> <rd-header-content>
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Logs <a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Logs
</rd-header-content> </rd-header-content>
</rd-header> </rd-header>

View File

@ -17,13 +17,13 @@
<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 btn-ico" aria-hidden="true"></i>Start</button> <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-primary btn-responsive" ng-click="stopAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-stop btn-ico" aria-hidden="true"></i>Stop</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-primary btn-responsive" ng-click="killAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-bomb btn-ico" aria-hidden="true"></i>Kill</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-primary btn-responsive" ng-click="restartAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-refresh btn-ico" 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 btn-ico" 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 btn-ico" 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 btn-ico" 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-default btn-responsive" type="button" ui-sref="actions.create.container">Add container</a>
</div> </div>

View File

@ -1,21 +1,18 @@
angular.module('createContainer', []) angular.module('createContainer', [])
.controller('CreateContainerController', ['$scope', '$state', 'Config', 'Info', 'Container', 'Image', 'Volume', 'Network', 'Messages', .controller('CreateContainerController', ['$scope', '$state', '$stateParams', 'Config', 'Info', 'Container', 'Image', 'Volume', 'Network', 'Messages',
function ($scope, $state, Config, Info, Container, Image, Volume, Network, Messages) { function ($scope, $state, $stateParams, Config, Info, Container, Image, Volume, Network, Messages) {
$scope.state = {
alwaysPull: true
};
$scope.formValues = { $scope.formValues = {
alwaysPull: true,
Console: 'none', Console: 'none',
Volumes: [], Volumes: [],
AvailableRegistries: [],
Registry: '' Registry: ''
}; };
$scope.imageConfig = {}; $scope.imageConfig = {};
$scope.config = { $scope.config = {
Image: '',
Env: [], Env: [],
ExposedPorts: {}, ExposedPorts: {},
HostConfig: { HostConfig: {
@ -61,8 +58,6 @@ function ($scope, $state, Config, Info, Container, Image, Volume, Network, Messa
} }
}); });
$scope.formValues.AvailableRegistries = c.registries;
Volume.query({}, function (d) { Volume.query({}, function (d) {
$scope.availableVolumes = d.Volumes; $scope.availableVolumes = d.Volumes;
}, function (e) { }, function (e) {
@ -83,6 +78,9 @@ function ($scope, $state, Config, Info, Container, Image, Volume, Network, Messa
networks.push({Name: "none"}); networks.push({Name: "none"});
} }
$scope.availableNetworks = networks; $scope.availableNetworks = networks;
if (!_.find(networks, {'Name': 'bridge'})) {
$scope.config.HostConfig.NetworkMode = 'nat';
}
}, function (e) { }, function (e) {
Messages.error("Failure", e, "Unable to retrieve networks"); Messages.error("Failure", e, "Unable to retrieve networks");
}); });
@ -232,11 +230,10 @@ function ($scope, $state, Config, Info, Container, Image, Volume, Network, Messa
$scope.create = function () { $scope.create = function () {
var config = prepareConfiguration(); var config = prepareConfiguration();
$('#createContainerSpinner').show(); $('#createContainerSpinner').show();
if ($scope.state.alwaysPull) { if ($scope.formValues.alwaysPull) {
pullImageAndCreateContainer(config); pullImageAndCreateContainer(config);
} else { } else {
createContainer(config); createContainer(config);
} }
}; };
}]); }]);

View File

@ -1,7 +1,7 @@
<rd-header> <rd-header>
<rd-header-title title="Create container"></rd-header-title> <rd-header-title title="Create container"></rd-header-title>
<rd-header-content> <rd-header-content>
Containers > Add container <a ui-sref="containers">Containers</a> > Add container
</rd-header-content> </rd-header-content>
</rd-header> </rd-header>
@ -31,7 +31,7 @@
<div class="col-sm-offset-1 col-sm-11"> <div class="col-sm-offset-1 col-sm-11">
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" ng-model="state.alwaysPull"> Always pull image before creating <input type="checkbox" ng-model="formValues.alwaysPull"> Always pull image before creating
</label> </label>
</div> </div>
</div> </div>
@ -53,6 +53,10 @@
<input type="radio" name="container_restart_policy" ng-model="config.HostConfig.RestartPolicy.Name" value="on-failure"> <input type="radio" name="container_restart_policy" ng-model="config.HostConfig.RestartPolicy.Name" value="on-failure">
<span class="radio-value">On failure</span> <span class="radio-value">On failure</span>
</label> </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>
</div> </div>
</div> </div>
<!-- !restart-policy --> <!-- !restart-policy -->
@ -60,7 +64,7 @@
<div class="form-group"> <div class="form-group">
<label for="container_ports" class="col-sm-1 control-label text-left">Port mapping</label> <label for="container_ports" class="col-sm-1 control-label text-left">Port mapping</label>
<div class="col-sm-11"> <div class="col-sm-11">
<span class="label label-default clickable" ng-click="addPortBinding()"> <span class="label label-default interactive" 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 port
</span> </span>
</div> </div>
@ -102,10 +106,10 @@
<rd-widget> <rd-widget>
<rd-widget-body> <rd-widget-body>
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
<li class="active clickable"><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="clickable"><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="clickable"><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="clickable"><a data-target="#security" data-toggle="tab">Security/Host</a></li> <li class="interactive"><a data-target="#security" data-toggle="tab">Security/Host</a></li>
</ul> </ul>
<!-- tab-content --> <!-- tab-content -->
<div class="tab-content"> <div class="tab-content">
@ -177,7 +181,7 @@
<div class="form-group"> <div class="form-group">
<label for="container_env" class="col-sm-1 control-label text-left">Environment variables</label> <label for="container_env" class="col-sm-1 control-label text-left">Environment variables</label>
<div class="col-sm-11"> <div class="col-sm-11">
<span class="label label-default clickable" ng-click="addEnvironmentVariable()"> <span class="label label-default interactive" 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> environment variable
</span> </span>
</div> </div>
@ -212,7 +216,7 @@
<div class="form-group"> <div class="form-group">
<label for="container_volumes" class="col-sm-1 control-label text-left">Volumes</label> <label for="container_volumes" class="col-sm-1 control-label text-left">Volumes</label>
<div class="col-sm-11"> <div class="col-sm-11">
<span class="label label-default clickable" ng-click="addVolume()"> <span class="label label-default interactive" ng-click="addVolume()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> volume <i class="fa fa-plus-circle" aria-hidden="true"></i> volume
</span> </span>
</div> </div>

View File

@ -1,7 +1,7 @@
<rd-header> <rd-header>
<rd-header-title title="Create network"></rd-header-title> <rd-header-title title="Create network"></rd-header-title>
<rd-header-content> <rd-header-content>
Networks > Add network <a ui-sref="networks">Networks</a> > Add network
</rd-header-content> </rd-header-content>
</rd-header> </rd-header>
@ -42,7 +42,7 @@
<div class="form-group"> <div class="form-group">
<label for="network_driveropts" class="col-sm-1 control-label text-left">Driver options</label> <label for="network_driveropts" class="col-sm-1 control-label text-left">Driver options</label>
<div class="col-sm-11"> <div class="col-sm-11">
<span class="label label-default clickable" ng-click="addDriverOption()"> <span class="label label-default interactive" ng-click="addDriverOption()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> driver option <i class="fa fa-plus-circle" aria-hidden="true"></i> driver option
</span> </span>
</div> </div>

View File

@ -12,6 +12,7 @@ function ($scope, $state, Service, Volume, Network, ImageHelper, Messages) {
WorkingDir: '', WorkingDir: '',
User: '', User: '',
Env: [], Env: [],
Labels: [],
Volumes: [], Volumes: [],
Network: '', Network: '',
ExtraNetworks: [], ExtraNetworks: [],
@ -50,6 +51,14 @@ function ($scope, $state, Service, Volume, Network, ImageHelper, Messages) {
$scope.formValues.Env.splice(index, 1); $scope.formValues.Env.splice(index, 1);
}; };
$scope.addLabel = function() {
$scope.formValues.Labels.push({ name: '', value: ''});
};
$scope.removeLabel = function(index) {
$scope.formValues.Labels.splice(index, 1);
};
function prepareImageConfig(config, input) { function prepareImageConfig(config, input) {
var imageConfig = ImageHelper.createImageConfig(input.Image, input.Registry); var imageConfig = ImageHelper.createImageConfig(input.Image, input.Registry);
config.TaskTemplate.ContainerSpec.Image = imageConfig.repo + ':' + imageConfig.tag; config.TaskTemplate.ContainerSpec.Image = imageConfig.repo + ':' + imageConfig.tag;
@ -97,6 +106,16 @@ function ($scope, $state, Service, Volume, Network, ImageHelper, Messages) {
config.TaskTemplate.ContainerSpec.Env = env; config.TaskTemplate.ContainerSpec.Env = env;
} }
function prepareLabelsConfig(config, input) {
var labels = {};
input.Labels.forEach(function (label) {
if (label.name && label.value) {
labels[label.name] = label.value;
}
});
config.TaskTemplate.ContainerSpec.Labels = labels;
}
function prepareVolumes(config, input) { function prepareVolumes(config, input) {
input.Volumes.forEach(function (volume) { input.Volumes.forEach(function (volume) {
if (volume.Source && volume.Target) { if (volume.Source && volume.Target) {
@ -138,6 +157,7 @@ function ($scope, $state, Service, Volume, Network, ImageHelper, Messages) {
preparePortsConfig(config, input); preparePortsConfig(config, input);
prepareCommandConfig(config, input); prepareCommandConfig(config, input);
prepareEnvConfig(config, input); prepareEnvConfig(config, input);
prepareLabelsConfig(config, input);
prepareVolumes(config, input); prepareVolumes(config, input);
prepareNetworks(config, input); prepareNetworks(config, input);
return config; return config;

View File

@ -1,7 +1,7 @@
<rd-header> <rd-header>
<rd-header-title title="Create service"></rd-header-title> <rd-header-title title="Create service"></rd-header-title>
<rd-header-content> <rd-header-content>
Services > Add service <a ui-sref="services">Services</a> > Add service
</rd-header-content> </rd-header-content>
</rd-header> </rd-header>
@ -56,7 +56,7 @@
<div class="form-group"> <div class="form-group">
<label for="container_ports" class="col-sm-1 control-label text-left">Port mapping</label> <label for="container_ports" class="col-sm-1 control-label text-left">Port mapping</label>
<div class="col-sm-11"> <div class="col-sm-11">
<span class="label label-default clickable" ng-click="addPortBinding()"> <span class="label label-default interactive" 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 port
</span> </span>
</div> </div>
@ -98,9 +98,10 @@
<rd-widget> <rd-widget>
<rd-widget-body> <rd-widget-body>
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
<li class="active clickable"><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="clickable"><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="clickable"><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="#labels" data-toggle="tab">Labels</a></li>
</ul> </ul>
<!-- tab-content --> <!-- tab-content -->
<div class="tab-content"> <div class="tab-content">
@ -131,7 +132,7 @@
<div class="form-group"> <div class="form-group">
<label for="service_env" class="col-sm-1 control-label text-left">Environment variables</label> <label for="service_env" class="col-sm-1 control-label text-left">Environment variables</label>
<div class="col-sm-11"> <div class="col-sm-11">
<span class="label label-default clickable" ng-click="addEnvironmentVariable()"> <span class="label label-default interactive" 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> environment variable
</span> </span>
</div> </div>
@ -166,7 +167,7 @@
<div class="form-group"> <div class="form-group">
<label for="service_volumes" class="col-sm-1 control-label text-left">Volumes</label> <label for="service_volumes" class="col-sm-1 control-label text-left">Volumes</label>
<div class="col-sm-11"> <div class="col-sm-11">
<span class="label label-default clickable" ng-click="addVolume()"> <span class="label label-default interactive" ng-click="addVolume()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> volume <i class="fa fa-plus-circle" aria-hidden="true"></i> volume
</span> </span>
</div> </div>
@ -224,7 +225,7 @@
<div class="form-group"> <div class="form-group">
<label for="service_extra_networks" class="col-sm-1 control-label text-left">Extra networks</label> <label for="service_extra_networks" class="col-sm-1 control-label text-left">Extra networks</label>
<div class="col-sm-11"> <div class="col-sm-11">
<span class="label label-default clickable" ng-click="addExtraNetwork()"> <span class="label label-default interactive" ng-click="addExtraNetwork()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> network <i class="fa fa-plus-circle" aria-hidden="true"></i> network
</span> </span>
</div> </div>
@ -251,6 +252,42 @@
</form> </form>
</div> </div>
<!-- !tab-network --> <!-- !tab-network -->
<!-- tab-labels -->
<div class="tab-pane" id="labels">
<form class="form-horizontal" style="margin-top: 15px;">
<!-- labels -->
<div class="form-group">
<label for="service_env" class="col-sm-1 control-label text-left">Labels</label>
<div class="col-sm-11">
<span class="label label-default interactive" ng-click="addLabel()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> label
</span>
</div>
<!-- labels-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
<div ng-repeat="label in formValues.Labels" 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="label.name" placeholder="e.g. com.example.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="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>
</div>
<!-- !labels-input-list -->
</div>
<!-- !labels-->
</form>
</div>
<!-- !tab-labels -->
<!-- tab-security --> <!-- tab-security -->
<div class="tab-pane" id="security"> <div class="tab-pane" id="security">
</div> </div>

View File

@ -1,7 +1,7 @@
<rd-header> <rd-header>
<rd-header-title title="Create volume"></rd-header-title> <rd-header-title title="Create volume"></rd-header-title>
<rd-header-content> <rd-header-content>
Volumes > Add volume <a ui-sref="volumes">Volumes</a> > Add volume
</rd-header-content> </rd-header-content>
</rd-header> </rd-header>
@ -30,7 +30,7 @@
<div class="form-group"> <div class="form-group">
<label for="volume_driveropts" class="col-sm-1 control-label text-left">Driver options</label> <label for="volume_driveropts" class="col-sm-1 control-label text-left">Driver options</label>
<div class="col-sm-11"> <div class="col-sm-11">
<span class="label label-default clickable" ng-click="addDriverOption()"> <span class="label label-default interactive" ng-click="addDriverOption()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> driver option <i class="fa fa-plus-circle" aria-hidden="true"></i> driver option
</span> </span>
</div> </div>

View File

@ -93,8 +93,8 @@
<i class="fa fa-server"></i> <i class="fa fa-server"></i>
</div> </div>
<div class="pull-right"> <div class="pull-right">
<div><i class="fa fa-heartbeat text-icon green-icon"></i>{{ containerData.running }} running</div> <div><i class="fa fa-heartbeat space-right green-icon"></i>{{ containerData.running }} running</div>
<div><i class="fa fa-heartbeat text-icon red-icon"></i>{{ containerData.stopped }} stopped</div> <div><i class="fa fa-heartbeat space-right red-icon"></i>{{ containerData.stopped }} stopped</div>
</div> </div>
<div class="title">{{ containerData.total }}</div> <div class="title">{{ containerData.total }}</div>
<div class="comment">Containers</div> <div class="comment">Containers</div>
@ -110,7 +110,7 @@
<i class="fa fa-clone"></i> <i class="fa fa-clone"></i>
</div> </div>
<div class="pull-right"> <div class="pull-right">
<div><i class="fa fa-pie-chart text-icon"></i>{{ imageData.size|humansize }}</div> <div><i class="fa fa-pie-chart space-right"></i>{{ imageData.size|humansize }}</div>
</div> </div>
<div class="title">{{ imageData.total }}</div> <div class="title">{{ imageData.total }}</div>
<div class="comment">Images</div> <div class="comment">Images</div>
@ -126,7 +126,7 @@
<i class="fa fa-cubes"></i> <i class="fa fa-cubes"></i>
</div> </div>
<div class="pull-right" ng-if="infoData.Driver"> <div class="pull-right" ng-if="infoData.Driver">
<div><i class="fa fa-hdd-o text-icon"></i>{{ infoData.Driver }} driver</div> <div><i class="fa fa-hdd-o space-right"></i>{{ infoData.Driver }} driver</div>
</div> </div>
<div class="title">{{ volumeData.total }}</div> <div class="title">{{ volumeData.total }}</div>
<div class="comment">Volumes</div> <div class="comment">Volumes</div>

View File

@ -13,6 +13,7 @@ function ($scope, $cookieStore, Settings, Config, Info) {
$scope.swarm_mode = false; $scope.swarm_mode = false;
Config.$promise.then(function (c) { Config.$promise.then(function (c) {
$scope.logo = c.logo;
$scope.swarm = c.swarm; $scope.swarm = c.swarm;
Info.get({}, function(d) { Info.get({}, function(d) {
if ($scope.swarm && !_.startsWith(d.ServerVersion, 'swarm')) { if ($scope.swarm && !_.startsWith(d.ServerVersion, 'swarm')) {

View File

@ -3,139 +3,113 @@
<a data-toggle="tooltip" title="Refresh" ui-sref="docker" ui-sref-opts="{reload: true}"> <a data-toggle="tooltip" title="Refresh" ui-sref="docker" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i> <i class="fa fa-refresh" aria-hidden="true"></i>
</a> </a>
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title> </rd-header-title>
<rd-header-content>Docker</rd-header-content> <rd-header-content>Docker</rd-header-content>
</rd-header> </rd-header>
<div class="row">
<div class="col-lg-3 col-md-6 col-xs-12">
<rd-widget>
<rd-widget-body>
<div class="widget-icon pull-left">
<i class="fa fa-code"></i>
</div>
<div class="title">{{ docker.Version }}</div>
<div class="comment">Docker version</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-3 col-md-6 col-xs-12">
<rd-widget>
<rd-widget-body>
<div class="widget-icon pull-left">
<i class="fa fa-code"></i>
</div>
<div class="title">{{ docker.ApiVersion }}</div>
<div class="comment">API version</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-3 col-md-6 col-xs-12">
<rd-widget>
<rd-widget-body>
<div class="widget-icon pull-left">
<i class="fa fa-code"></i>
</div>
<div class="title">{{ docker.GoVersion }}</div>
<div class="comment">Go version</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row"> <div class="row" ng-if="state.loaded">
<div class="col-lg-12"> <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<rd-widget> <rd-widget>
<rd-widget-header icon="fa-object-group" title="Engine status"></rd-widget-header> <rd-widget-header icon="fa-code" title="Engine version"></rd-widget-header>
<rd-widget-body classes="no-padding"> <rd-widget-body classes="no-padding">
<table class="table"> <table class="table">
<tbody> <tbody>
<tr> <tr>
<td>Containers</td> <td>Version</td>
<td>{{ info.Containers }}</td> <td>{{ version.Version }}</td>
</tr> </tr>
<tr> <tr>
<td>Images</td> <td>API version</td>
<td>{{ info.Images }}</td> <td>{{ version.ApiVersion }}</td>
</tr> </tr>
<tr> <tr>
<td>Debug</td> <td>Go version</td>
<td>{{ info.Debug }}</td> <td>{{ version.GoVersion }}</td>
</tr> </tr>
<tr> <tr>
<td>CPUs</td> <td>OS type</td>
<td>{{ info.NCPU }}</td> <td>{{ version.Os }}</td>
</tr> </tr>
<tr> <tr>
<td>Total Memory</td> <td>OS</td>
<td>{{ info.MemTotal|humansize }}</td>
</tr>
<tr>
<td>Operating System</td>
<td>{{ info.OperatingSystem }}</td> <td>{{ info.OperatingSystem }}</td>
</tr> </tr>
<tr> <tr>
<td>Kernel Version</td> <td>Architecture</td>
<td>{{ info.KernelVersion }}</td> <td>{{ version.Arch }}</td>
</tr> </tr>
<tr> <tr>
<td>ID</td> <td>Kernel version</td>
<td>{{ info.ID }}</td> <td>{{ version.KernelVersion }}</td>
</tr> </tr>
<tr> </tbody>
<td>Labels</td> </table>
<td>{{ info.Labels }}</td> </rd-widget-body>
</tr> </rd-widget>
<tr> </div>
<td>File Descriptors</td> </div>
<td>{{ info.NFd }}</td>
</tr> <div class="row" ng-if="state.loaded">
<tr> <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<td>Goroutines</td> <rd-widget>
<td>{{ info.NGoroutines }}</td> <rd-widget-header icon="fa-th" title="Engine status"></rd-widget-header>
</tr> <rd-widget-body classes="no-padding">
<tr> <table class="table">
<td>Storage Driver</td> <tbody>
<td>{{ info.Driver }}</td> <tr>
</tr> <td>Total CPU</td>
<tr> <td>{{ info.NCPU }}</td>
<td>Storage Driver Status</td> </tr>
<td> <tr>
<p ng-repeat="val in info.DriverStatus"> <td>Total memory</td>
{{ val[0] }}: {{ val[1] }} <td>{{ info.MemTotal|humansize }}</td>
</p> </tr>
</td> <tr>
</tr> <td>Docker root directory</td>
<tr> <td>{{ info.DockerRootDir }}</td>
<td>Execution Driver</td> </tr>
<td>{{ info.ExecutionDriver }}</td> <tr>
</tr> <td>Storage driver</td>
<tr> <td>{{ info.Driver }}</td>
<td>IPv4 Forwarding</td> </tr>
<td>{{ info.IPv4Forwarding }}</td> <tr>
</tr> <td>Logging driver</td>
<tr> <td>{{ info.LoggingDriver }}</td>
<td>Index Server Address</td> </tr>
<td>{{ info.IndexServerAddress }}</td> <tr ng-if="info.CgroupDriver">
</tr> <td>Cgroup driver</td>
<tr> <td>{{ info.CgroupDriver }}</td>
<td>Init Path</td> </tr>
<td>{{ info.InitPath }}</td> <tr ng-if="info.ExecutionDriver">
</tr> <td>Execution driver</td>
<tr> <td>{{ info.ExecutionDriver }}</td>
<td>Docker Root Directory</td> </tr>
<td>{{ info.DockerRootDir }}</td> </tbody>
</tr> </table>
<tr> </rd-widget-body>
<td>Init SHA1</td> </rd-widget>
<td>{{ info.InitSha1 }}</td> </div>
</tr> </div>
<tr>
<td>Memory Limit</td> <div class="row" ng-if="state.loaded && info.Plugins">
<td>{{ info.MemoryLimit }}</td> <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
</tr> <rd-widget>
<tr> <rd-widget-header icon="fa-plug" title="Engine plugins"></rd-widget-header>
<td>Swap Limit</td> <rd-widget-body classes="no-padding">
<td>{{ info.SwapLimit }}</td> <table class="table">
<tbody>
<tr ng-if="info.Plugins.Volume">
<td>Volume</td>
<td>{{ info.Plugins.Volume|arraytostr: ', '}}</td>
</tr>
<tr ng-if="info.Plugins.Network">
<td>Network</td>
<td>{{ info.Plugins.Network|arraytostr: ', '}}</td>
</tr>
<tr ng-if="info.Plugins.Authorization">
<td>Authorization</td>
<td>{{ info.Plugins.Authorization|arraytostr: ', '}}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -1,19 +1,24 @@
angular.module('docker', []) angular.module('docker', [])
.controller('DockerController', ['$scope', 'Info', 'Version', 'Settings', .controller('DockerController', ['$scope', 'Info', 'Version', 'Messages',
function ($scope, Info, Version, Settings) { function ($scope, Info, Version, Messages) {
$scope.state = {
$scope.info = {}; loaded: false
$scope.docker = {};
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
}; };
$scope.info = {};
$scope.version = {};
Version.get({}, function (d) { Info.get({}, function (infoData) {
$scope.docker = d; $scope.info = infoData;
}); Version.get({}, function (versionData) {
Info.get({}, function (d) { $scope.version = versionData;
$scope.info = d; $scope.state.loaded = true;
$('#loadingViewSpinner').hide();
}, function (e) {
Messages.error("Failure", e, 'Unable to retrieve engine details');
$('#loadingViewSpinner').hide();
});
}, function (e) {
Messages.error("Failure", e, 'Unable to retrieve engine information');
$('#loadingViewSpinner').hide();
}); });
}]); }]);

View File

@ -3,7 +3,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i> <i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title> </rd-header-title>
<rd-header-content> <rd-header-content>
Images > <a ui-sref="image({id: image.Id})">{{ image.Id }}</a> <a ui-sref="images">Images</a> > <a ui-sref="image({id: image.Id})">{{ image.Id }}</a>
</rd-header-content> </rd-header-content>
</rd-header> </rd-header>
@ -13,7 +13,7 @@
<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 classes="no-padding">
<div style="margin: 5px 10px;"> <div style="margin: 5px 10px;">
<span ng-repeat="tag in RepoTags" class="label label-primary image-tag"> <span ng-repeat="tag in RepoTags" class="label label-primary image-tag space-right">
<a data-toggle="tooltip" class="interactive" title="Push to registry" ng-click="pushImage(tag)"> <a data-toggle="tooltip" class="interactive" title="Push to registry" ng-click="pushImage(tag)">
<i class="fa fa-upload white-icon" aria-hidden="true"></i> <i class="fa fa-upload white-icon" aria-hidden="true"></i>
</a> </a>
@ -82,7 +82,7 @@
<td>ID</td> <td>ID</td>
<td> <td>
{{ image.Id }} {{ image.Id }}
<button class="btn btn-xs btn-danger" ng-click="removeImage(image.Id)"><i class="fa fa-trash btn-ico" aria-hidden="true"></i>Delete this image</button> <button class="btn btn-xs btn-danger" ng-click="removeImage(image.Id)"><i class="fa fa-trash space-right" aria-hidden="true"></i>Delete this image</button>
</td> </td>
</tr> </tr>
<tr ng-if="image.Parent"> <tr ng-if="image.Parent">
@ -130,7 +130,7 @@
<tr ng-if="image.ContainerConfig.ExposedPorts"> <tr ng-if="image.ContainerConfig.ExposedPorts">
<td>EXPOSE</td> <td>EXPOSE</td>
<td> <td>
<span class="label label-default tag" ng-repeat="port in exposedPorts"> <span class="label label-default space-right" ng-repeat="port in exposedPorts">
{{ port }} {{ port }}
</span> </span>
</td> </td>
@ -138,7 +138,7 @@
<tr ng-if="image.ContainerConfig.Volumes"> <tr ng-if="image.ContainerConfig.Volumes">
<td>VOLUME</td> <td>VOLUME</td>
<td> <td>
<span class="label label-default tag" ng-repeat="volume in volumes"> <span class="label label-default space-right" ng-repeat="volume in volumes">
{{ volume }} {{ volume }}
</span> </span>
</td> </td>

View File

@ -55,7 +55,7 @@
</rd-widget-header> </rd-widget-header>
<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 btn-ico" 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>
</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

@ -102,7 +102,6 @@ function ($scope, $state, Config, Image, Messages) {
} }
Config.$promise.then(function (c) { Config.$promise.then(function (c) {
$scope.availableRegistries = c.registries;
fetchImages(); fetchImages();
}); });
}]); }]);

View File

@ -3,7 +3,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i> <i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title> </rd-header-title>
<rd-header-content> <rd-header-content>
Networks > <a ui-sref="network({id: network.Id})">{{ network.Name }}</a> <a ui-sref="networks">Networks</a> > <a ui-sref="network({id: network.Id})">{{ network.Name }}</a>
</rd-header-content> </rd-header-content>
</rd-header> </rd-header>
@ -22,7 +22,7 @@
<td>ID</td> <td>ID</td>
<td> <td>
{{ network.Id }} {{ network.Id }}
<button class="btn btn-xs btn-danger" ng-click="removeNetwork(network.Id)"><i class="fa fa-trash btn-ico" aria-hidden="true"></i>Delete this network</button> <button class="btn btn-xs btn-danger" ng-click="removeNetwork(network.Id)"><i class="fa fa-trash space-right" aria-hidden="true"></i>Delete this network</button>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -57,7 +57,7 @@
</rd-widget-header> </rd-widget-header>
<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 btn-ico" 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>
</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

@ -6,7 +6,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i> <i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title> </rd-header-title>
<rd-header-content> <rd-header-content>
Services > <a ui-sref="service({id: service.Id})">{{ service.Name }}</a> <a ui-sref="services">Services</a> > <a ui-sref="service({id: service.Id})">{{ service.Name }}</a>
</rd-header-content> </rd-header-content>
</rd-header> </rd-header>
@ -33,7 +33,7 @@
<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 btn-ico" 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"></i>Delete this service</button>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -43,20 +43,29 @@
<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.Scale"> <span ng-if="service.Mode === 'replicated' && !service.EditReplicas">
{{ service.Replicas }} {{ service.Replicas }}
<a class="interactive" ng-click="service.Scale = true; service.ReplicaCount = service.Replicas;"><i class="fa fa-arrows-v" aria-hidden="true"></i> Scale</a> <a class="interactive" ng-click="service.EditReplicas = true;"><i class="fa fa-arrows-v" aria-hidden="true"></i> Scale</a>
</span> </span>
<span ng-if="service.Mode === 'replicated' && service.Scale"> <span ng-if="service.Mode === 'replicated' && service.EditReplicas">
<input class="input-sm" type="number" ng-model="service.Replicas" /> <input class="input-sm" type="number" ng-model="service.newServiceReplicas" />
<a class="interactive" ng-click="service.Scale = false;"><i class="fa fa-times"></i></a> <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> <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>{{ service.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>
<tr ng-if="service.Ports"> <tr ng-if="service.Ports">
<td>Published ports</td> <td>Published ports</td>
@ -66,31 +75,77 @@
</div> </div>
</td> </td>
</tr> </tr>
<tr ng-if="service.Env"> <tr ng-if="service.EnvironmentVariables">
<td>Env</td> <td>Environment variables</td>
<td> <td>
<table class="table table-bordered table-condensed"> <div class="form-group">
<tr ng-repeat="var in service.Env"> <div class="col-sm-11 nopadding">
<td>{{ var|key: '=' }}</td> <span class="label label-default interactive fit-text-size" ng-click="addEnvironmentVariable(service)">
<td>{{ var|value: '=' }}</td> <i class="fa fa-plus-circle" aria-hidden="true"></i> environment variable
</tr> </span>
</table> </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> </td>
</tr> </tr>
<tr ng-if="service.Labels"> <tr>
<td>Labels</td> <td>Labels</td>
<td> <td>
<table class="table table-bordered table-condensed"> <div class="form-group">
<tr ng-repeat="(k, v) in service.Labels"> <div class="col-sm-11 nopadding">
<td>{{ k }}</td> <span class="label label-default interactive fit-text-size" ng-click="addLabel(service)">
<td>{{ v }}</td> <i class="fa fa-plus-circle" aria-hidden="true"></i> label
</tr> </span>
</table> </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> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</rd-widget-body> </rd-widget-body>
<rd-widget-footer ng-if="service.hasChanges">
<div>
<button type="button" class="btn btn-primary" ng-click="updateService(service)">Save changes</button>
<button type="button" class="btn btn-default" ng-click="cancelChanges(service)">Reset</button>
</div>
</rd-widget-footer>
</rd-widget> </rd-widget>
</div> </div>
</div> </div>

View File

@ -8,43 +8,83 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess
$scope.sortType = 'Status'; $scope.sortType = 'Status';
$scope.sortReverse = false; $scope.sortReverse = false;
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;
$scope.sortType = sortType; $scope.sortType = sortType;
}; };
$scope.renameService = function renameService(service) { $scope.renameService = function renameService(service) {
updateServiceAttribute(service, 'Name', service.newServiceName || service.name);
service.EditName = false;
};
$scope.changeServiceImage = function changeServiceImage(service) {
updateServiceAttribute(service, 'Image', service.newServiceImage || service.image);
service.EditImage = false;
};
$scope.scaleService = function scaleService(service) {
updateServiceAttribute(service, 'Replicas', service.newServiceReplicas || service.Replicas);
service.EditReplicas = false;
};
$scope.addEnvironmentVariable = function addEnvironmentVariable(service) {
service.EnvironmentVariables.push({ key: '', value: '', originalValue: '' });
service.hasChanges = true;
};
$scope.removeEnvironmentVariable = function removeEnvironmentVariable(service, index) {
var removedElement = service.EnvironmentVariables.splice(index, 1);
service.hasChanges = service.hasChanges || removedElement !== null;
};
$scope.updateEnvironmentVariable = function updateEnvironmentVariable(service, variable) {
service.hasChanges = service.hasChanges || variable.value !== variable.originalValue;
};
$scope.addLabel = function addLabel(service) {
service.hasChanges = true;
service.ServiceLabels.push({ key: '', value: '', originalValue: '' });
};
$scope.removeLabel = function removeLabel(service, index) {
var removedElement = service.ServiceLabels.splice(index, 1);
service.hasChanges = service.hasChanges || removedElement !== null;
};
$scope.updateLabel = function updateLabel(service, label) {
service.hasChanges = service.hasChanges || label.value !== label.originalValue;
};
$scope.cancelChanges = function changeServiceImage(service) {
Object.keys(previousServiceValues).forEach(function(attribute) {
service[attribute] = previousServiceValues[attribute]; // reset service values
service['newService' + attribute] = previousServiceValues[attribute]; // reset edit fields
});
previousServiceValues = {}; // clear out all changes
// clear out environment variable changes
service.EnvironmentVariables = translateEnvironmentVariables(service.Env);
service.ServiceLabels = translateLabelsToServiceLabels(service.Labels);
service.hasChanges = false;
};
$scope.updateService = function updateService(service) {
$('#loadServicesSpinner').show(); $('#loadServicesSpinner').show();
var serviceName = service.Name;
var config = ServiceHelper.serviceToConfig(service.Model); var config = ServiceHelper.serviceToConfig(service.Model);
config.Name = service.newServiceName; config.Name = service.newServiceName;
config.TaskTemplate.ContainerSpec.Env = translateEnvironmentVariablesToEnv(service.EnvironmentVariables);
config.TaskTemplate.ContainerSpec.Labels = translateServiceLabelsToLabels(service.ServiceLabels);
config.TaskTemplate.ContainerSpec.Image = service.newServiceImage;
if (service.Mode === 'replicated') {
config.Mode.Replicated.Replicas = service.Replicas;
}
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 renamed", "New name: " + service.newServiceName); Messages.send("Service successfully updated", "Service updated");
$state.go('service', {id: service.Id}, {reload: true}); $state.go('service', {id: service.Id}, {reload: true});
}, function (e) { }, function (e) {
$('#loadServicesSpinner').hide(); $('#loadServicesSpinner').hide();
service.EditName = false; Messages.error("Failure", e, "Unable to update service");
service.Name = serviceName;
Messages.error("Failure", e, "Unable to rename service");
}); });
}; };
$scope.scaleService = function scaleService(service) {
$('#loadServicesSpinner').show();
var config = ServiceHelper.serviceToConfig(service.Model);
config.Mode.Replicated.Replicas = service.Replicas;
Service.update({ id: service.Id, version: service.Version }, config, function (data) {
$('#loadServicesSpinner').hide();
Messages.send("Service successfully scaled", "New replica count: " + service.Replicas);
$state.go('service', {id: service.Id}, {reload: true});
}, function (e) {
$('#loadServicesSpinner').hide();
service.Scale = false;
service.Replicas = service.ReplicaCount;
Messages.error("Failure", e, "Unable to scale service");
});
};
$scope.removeService = function removeService() { $scope.removeService = function removeService() {
$('#loadingViewSpinner').show(); $('#loadingViewSpinner').show();
@ -68,6 +108,11 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess
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; service.newServiceName = service.Name;
service.newServiceImage = service.Image;
service.newServiceReplicas = service.Replicas;
service.EnvironmentVariables = translateEnvironmentVariables(service.Env);
service.ServiceLabels = translateLabelsToServiceLabels(service.Labels);
$scope.service = service; $scope.service = 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) {
@ -93,5 +138,59 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess
}); });
} }
function updateServiceAttribute(service, name, newValue) {
// ensure we only capture the original previous value, in case we update the attribute multiple times
if (!previousServiceValues[name]) {
previousServiceValues[name] = service[name];
}
// update the attribute
service[name] = newValue;
service.hasChanges = true;
}
function translateEnvironmentVariables(env) {
if (env) {
var variables = [];
env.forEach(function(variable) {
var keyValue = variable.split('=');
var originalValue = (keyValue.length > 1) ? keyValue[1] : '';
variables.push({ key: keyValue[0], value: originalValue, originalValue: originalValue, added: true});
});
return variables;
}
return [];
}
function translateEnvironmentVariablesToEnv(env) {
if (env) {
var variables = [];
env.forEach(function(variable) {
if (variable.key && variable.key !== '' && variable.value && variable.value !== '') {
variables.push(variable.key + '=' + variable.value);
}
});
return variables;
}
return [];
}
function translateLabelsToServiceLabels(Labels) {
var labels = [];
if (Labels) {
Object.keys(Labels).forEach(function(key) {
labels.push({ key: key, value: Labels[key], originalValue: Labels[key], added: true});
});
}
return labels;
}
function translateServiceLabelsToLabels(labels) {
var Labels = {};
if (labels) {
labels.forEach(function(label) {
Labels[label.key] = label.value;
});
}
return Labels;
}
fetchServiceDetails(); fetchServiceDetails();
}]); }]);

View File

@ -17,7 +17,7 @@
</rd-widget-header> </rd-widget-header>
<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 btn-ico" 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-default btn-responsive" type="button" ui-sref="actions.create.service">Add service</a>
</div> </div>
<div class="pull-right"> <div class="pull-right">

View File

@ -1,7 +1,7 @@
<rd-header> <rd-header>
<rd-header-title title="Container stats"></rd-header-title> <rd-header-title title="Container stats"></rd-header-title>
<rd-header-content> <rd-header-content>
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Stats <a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Stats
</rd-header-content> </rd-header-content>
</rd-header> </rd-header>
@ -59,11 +59,17 @@
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th ng-repeat="title in containerTop.Titles">{{title}}</th> <th ng-repeat="title in containerTop.Titles">
<a ui-sref="stats({id: container.Id})" ng-click="order(title)">
{{title}}
<span ng-show="sortType == title && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == title && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr ng-repeat="processInfos in containerTop.Processes"> <tr ng-repeat="processInfos in state.filteredProcesses = (containerTop.Processes | orderBy:sortType:sortReverse)">
<td ng-repeat="processInfo in processInfos track by $index">{{processInfo}}</td> <td ng-repeat="processInfo in processInfos track by $index">{{processInfo}}</td>
</tr> </tr>
</tbody> </tbody>

View File

@ -4,6 +4,13 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
// TODO: Force scale to 0-100 for cpu, fix charts on dashboard, // TODO: Force scale to 0-100 for cpu, fix charts on dashboard,
// TODO: Force memory scale to 0 - max memory // TODO: Force memory scale to 0 - max memory
$scope.ps_args = ''; $scope.ps_args = '';
$scope.state = {};
$scope.sortType = 'CMD';
$scope.sortReverse = false;
$scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.getTop = function () { $scope.getTop = function () {
ContainerTop.get($stateParams.id, { ContainerTop.get($stateParams.id, {
ps_args: $scope.ps_args ps_args: $scope.ps_args

View File

@ -3,7 +3,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i> <i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title> </rd-header-title>
<rd-header-content> <rd-header-content>
Services > <a ui-sref="service({id: task.ServiceID})">{{ serviceName }}</a> > {{ task.ID }} <a ui-sref="services">Services</a> > <a ui-sref="service({id: task.ServiceID})">{{ serviceName }}</a> > {{ task.ID }}
</rd-header-content> </rd-header-content>
</rd-header> </rd-header>

View File

@ -7,10 +7,10 @@
<rd-header-content>Templates</rd-header-content> <rd-header-content>Templates</rd-header-content>
</rd-header> </rd-header>
<div class="row" ng-if="selectedTemplate"> <div class="row" ng-if="state.selectedTemplate">
<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-custom-header icon="selectedTemplate.logo" title="selectedTemplate.image"> <rd-widget-custom-header icon="state.selectedTemplate.logo" title="state.selectedTemplate.image">
</rd-widget-custom-header> </rd-widget-custom-header>
<rd-widget-body classes="padding"> <rd-widget-body classes="padding">
<form class="form-horizontal"> <form class="form-horizontal">
@ -39,7 +39,7 @@
</div> </div>
</div> </div>
<!-- !name-and-network-inputs --> <!-- !name-and-network-inputs -->
<div ng-repeat="var in selectedTemplate.env" ng-if="!var.set" class="form-group"> <div ng-repeat="var in state.selectedTemplate.env" ng-if="!var.set" class="form-group">
<label for="field_{{ $index }}" class="col-sm-2 control-label text-left">{{ var.label }}</label> <label for="field_{{ $index }}" class="col-sm-2 control-label text-left">{{ var.label }}</label>
<div class="col-sm-10"> <div class="col-sm-10">
<select ng-if="(!swarm || swarm && swarm_mode) && var.type === 'container'" ng-options="container|containername for container in runningContainers" class="selectpicker form-control" ng-model="var.value"> <select ng-if="(!swarm || swarm && swarm_mode) && var.type === 'container'" ng-options="container|containername for container in runningContainers" class="selectpicker form-control" ng-model="var.value">
@ -51,6 +51,50 @@
<input ng-if="!var.type || !var.type === 'container'" type="text" class="form-control" ng-model="var.value" id="field_{{ $index }}"> <input ng-if="!var.type || !var.type === 'container'" type="text" class="form-control" ng-model="var.value" id="field_{{ $index }}">
</div> </div>
</div> </div>
<div class="form-group">
<div class="col-sm-12">
<a class="small interactive" ng-if="!state.showAdvancedOptions" ng-click="state.showAdvancedOptions = true;">
<i class="fa fa-plus space-right" aria-hidden="true"></i> Show advanced options
</a>
<a class="small interactive" ng-if="state.showAdvancedOptions" ng-click="state.showAdvancedOptions = false;">
<i class="fa fa-minus space-right" aria-hidden="true"></i> Hide advanced options
</a>
</div>
</div>
<div class="form-group" ng-if="state.showAdvancedOptions">
<label for="container_ports" class="col-sm-1 control-label text-left">Port mapping</label>
<div class="col-sm-11" style="margin-top: 5px;">
<span class="label label-default interactive" ng-click="addPortBinding()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> map additional port
</span>
</div>
<!-- port-mapping-input-list -->
<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 class="input-group col-sm-5 input-group-sm">
<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)">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">container</span>
<input type="text" class="form-control" ng-model="portBinding.containerPort" placeholder="e.g. 80">
</div>
<div class="input-group col-sm-1 input-group-sm">
<select class="selectpicker form-control" ng-model="portBinding.protocol">
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removePortBinding($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !port-mapping-input-list -->
</div>
<!-- !port-mapping -->
<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-default btn-sm" ng-disabled="!formValues.network" ng-click="createTemplate()">Create</button>
@ -63,7 +107,7 @@
</div> </div>
</div> </div>
<div class="row" ng-if="selectedTemplate"> <div class="row" ng-if="state.selectedTemplate">
</div> </div>

View File

@ -1,14 +1,26 @@
angular.module('templates', []) angular.module('templates', [])
.controller('TemplatesController', ['$scope', '$q', '$state', '$filter', 'Config', 'Info', 'Container', 'ContainerHelper', 'Image', 'Volume', 'Network', 'Templates', 'Messages', .controller('TemplatesController', ['$scope', '$q', '$state', '$filter', 'Config', 'Info', 'Container', 'ContainerHelper', 'Image', 'Volume', 'Network', 'Templates', 'TemplateHelper', 'Messages',
function ($scope, $q, $state, $filter, Config, Info, Container, ContainerHelper, Image, Volume, Network, Templates, Messages) { function ($scope, $q, $state, $filter, Config, Info, Container, ContainerHelper, Image, Volume, Network, Templates, TemplateHelper, Messages) {
$scope.selectedTemplate = null; $scope.state = {
selectedTemplate: null,
showAdvancedOptions: false
};
$scope.formValues = { $scope.formValues = {
network: "", network: "",
name: "" name: "",
ports: []
}; };
var selectedItem = -1; var selectedItem = -1;
$scope.addPortBinding = function() {
$scope.formValues.ports.push({ hostPort: '', containerPort: '', protocol: 'tcp' });
};
$scope.removePortBinding = function(index) {
$scope.formValues.ports.splice(index, 1);
};
// TODO: centralize, already present in createContainerController // TODO: centralize, already present in createContainerController
function createContainer(config) { function createContainer(config) {
Container.create(config, function (d) { Container.create(config, function (d) {
@ -74,6 +86,26 @@ function ($scope, $q, $state, $filter, Config, Info, Container, ContainerHelper,
}; };
} }
function preparePortBindings(config, ports) {
var bindings = {};
ports.forEach(function (portBinding) {
if (portBinding.containerPort) {
var key = portBinding.containerPort + "/" + portBinding.protocol;
var binding = {};
if (portBinding.hostPort && portBinding.hostPort.indexOf(':') > -1) {
var hostAndPort = portBinding.hostPort.split(':');
binding.HostIp = hostAndPort[0];
binding.HostPort = hostAndPort[1];
} else {
binding.HostPort = portBinding.hostPort;
}
bindings[key] = [binding];
config.ExposedPorts[key] = {};
}
});
config.HostConfig.PortBindings = bindings;
}
function createConfigFromTemplate(template) { function createConfigFromTemplate(template) {
var containerConfig = getInitialConfiguration(); var containerConfig = getInitialConfiguration();
containerConfig.Image = template.image; containerConfig.Image = template.image;
@ -95,12 +127,7 @@ function ($scope, $q, $state, $filter, Config, Info, Container, ContainerHelper,
} }
}); });
} }
if (template.ports) { preparePortBindings(containerConfig, $scope.formValues.ports);
template.ports.forEach(function (p) {
containerConfig.ExposedPorts[p] = {};
containerConfig.HostConfig.PortBindings[p] = [{ HostPort: ""}];
});
}
return containerConfig; return containerConfig;
} }
@ -128,7 +155,7 @@ function ($scope, $q, $state, $filter, Config, Info, Container, ContainerHelper,
$scope.createTemplate = function() { $scope.createTemplate = function() {
$('#createContainerSpinner').show(); $('#createContainerSpinner').show();
var template = $scope.selectedTemplate; var template = $scope.state.selectedTemplate;
var containerConfig = createConfigFromTemplate(template); var containerConfig = createConfigFromTemplate(template);
var imageConfig = { var imageConfig = {
fromImage: template.image.split(':')[0], fromImage: template.image.split(':')[0],
@ -144,11 +171,13 @@ function ($scope, $q, $state, $filter, Config, Info, Container, ContainerHelper,
$('#template_' + id).toggleClass("container-template--selected"); $('#template_' + id).toggleClass("container-template--selected");
if (selectedItem === id) { if (selectedItem === id) {
selectedItem = -1; selectedItem = -1;
$scope.selectedTemplate = null; $scope.state.selectedTemplate = null;
} else { } else {
$('#template_' + selectedItem).toggleClass("container-template--selected"); $('#template_' + selectedItem).toggleClass("container-template--selected");
selectedItem = id; selectedItem = id;
$scope.selectedTemplate = $scope.templates[id]; var selectedTemplate = $scope.templates[id];
$scope.state.selectedTemplate = selectedTemplate;
$scope.formValues.ports = selectedTemplate.ports ? TemplateHelper.getPortBindings(selectedTemplate.ports) : [];
} }
}; };

View File

@ -16,7 +16,7 @@
</rd-widget-header> </rd-widget-header>
<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 btn-ico" 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-default" type="button" ui-sref="actions.create.volume">Add volume</a>
</div> </div>
<div class="pull-right"> <div class="pull-right">

View File

@ -213,4 +213,13 @@ angular.module('portainer.filters', [])
return function (ip) { return function (ip) {
return ip.slice(0, ip.indexOf('/')); return ip.slice(0, ip.indexOf('/'));
}; };
})
.filter('arraytostr', function () {
'use strict';
return function (arr, separator) {
if (arr) {
return _.join(arr, separator);
}
return '';
};
}); });

View File

@ -48,4 +48,43 @@ angular.module('portainer.helpers', [])
}; };
} }
}; };
}])
.factory('TemplateHelper', [function TemplateHelperFactory() {
'use strict';
return {
getPortBindings: function(ports) {
var bindings = [];
ports.forEach(function (port) {
var portAndProtocol = _.split(port, '/');
var binding = {
containerPort: portAndProtocol[0],
protocol: portAndProtocol[1]
};
bindings.push(binding);
});
return bindings;
},
//Not used atm, may prove useful later
getVolumeBindings: function(volumes) {
var bindings = [];
volumes.forEach(function (volume) {
bindings.push({ containerPath: volume });
});
return bindings;
},
//Not used atm, may prove useful later
getEnvBindings: function(env) {
var bindings = [];
env.forEach(function (envvar) {
var binding = {
name: envvar.name
};
if (envvar.set) {
binding.value = envvar.set;
}
bindings.push(binding);
});
return bindings;
}
};
}]); }]);

View File

@ -35,8 +35,8 @@ function ServiceViewModel(data) {
} else { } else {
this.Mode = 'global'; this.Mode = 'global';
} }
if (data.Spec.Labels) { if (data.Spec.TaskTemplate.ContainerSpec) {
this.Labels = data.Spec.Labels; this.Labels = data.Spec.TaskTemplate.ContainerSpec.Labels;
} }
if (data.Spec.TaskTemplate.ContainerSpec.Env) { if (data.Spec.TaskTemplate.ContainerSpec.Env) {
this.Env = data.Spec.TaskTemplate.ContainerSpec.Env; this.Env = data.Spec.TaskTemplate.ContainerSpec.Env;
@ -54,7 +54,7 @@ function ContainerViewModel(data) {
this.Status = data.Status; this.Status = data.Status;
this.Names = data.Names; this.Names = data.Names;
// Unavailable in Docker < 1.10 // Unavailable in Docker < 1.10
if (data.NetworkSettings) { if (data.NetworkSettings && !_.isEmpty(data.NetworkSettings.Networks)) {
this.IP = data.NetworkSettings.Networks[Object.keys(data.NetworkSettings.Networks)[0]].IPAddress; this.IP = data.NetworkSettings.Networks[Object.keys(data.NetworkSettings.Networks)[0]].IPAddress;
} }
this.Image = data.Image; this.Image = data.Image;

View File

@ -1,98 +1,7 @@
.container > hr {
margin: 60px 0;
}
.jumbotron {
margin: 80px 0;
text-align: center;
}
.jumbotron h1 {
font-size: 100px;
line-height: 1;
}
.jumbotron .lead {
font-size: 24px;
line-height: 1.25;
}
.jumbotron .btn {
padding: 14px 24px;
font-size: 21px;
}
.marketing {
margin: 60px 0;
}
.marketing p + h4 {
margin-top: 28px;
}
.masthead .nav {
margin: 0;
margin: 0 0 2em 0;
width: 100%;
}
.masthead .nav.well {
padding: 0;
}
.masthead .nav li {
display: table-cell;
float: none;
width: 1%;
}
.masthead .nav li a {
font-weight: bold;
text-align: center;
border-right: 1px solid rgba(0,0,0,.1);
border-left: 1px solid rgba(255,255,255,.75);
}
.masthead .nav li:first-child a {
border-left: 0;
border-radius: 3px 0 0 3px;
}
.masthead .nav li:last-child a {
border-right: 0;
border-radius: 0 3px 3px 0;
}
.btn-group button { .btn-group button {
margin: 3px; margin: 3px;
} }
.detail {
width: 80%;
margin: 0 auto;
}
.center {
width: 100%;
margin: 0 auto;
}
.btn-remove {
max-width: 70%;
margin: 0 auto;
}
.actions {
margin: 0 auto;
}
.container-bottom {
height: 50px;
}
.well {
padding: 10px 15px 0 15px;
}
.messages { .messages {
max-height: 50px; max-height: 50px;
overflow-x: hidden; overflow-x: hidden;
@ -106,14 +15,6 @@
border-width: 0 0 0 1em; border-width: 0 0 0 1em;
} }
.inline-four .form-control {
max-width: 25%;
}
.dropdown {
cursor: pointer;
}
.logo { .logo {
display: inline; display: inline;
width: 100%; width: 100%;
@ -162,11 +63,7 @@ input[type="radio"] {
vertical-align: middle; vertical-align: middle;
} }
.clickable { .space-right {
cursor: pointer;
}
.text-icon {
margin-right: 5px; margin-right: 5px;
} }
@ -182,16 +79,17 @@ input[type="radio"] {
color: white; color: white;
} }
.image-tag {
margin-right: 5px;
}
.label.tag {
margin-right: 5px;
}
.widget .widget-body table tbody .image-tag { .widget .widget-body table tbody .image-tag {
font-size: 90% !important; font-size: 90% !important;
margin-right: 5px;
}
.widget .widget-body table tbody .fit-text-size {
font-size: 90% !important;
}
.nopadding {
padding: 0 !important;
} }
.terminal-container { .terminal-container {
@ -203,14 +101,6 @@ input[type="radio"] {
cursor: pointer; cursor: pointer;
} }
.action-group {
margin: 10px;
}
.btn-ico {
margin-right: 5px;
}
.template-list { .template-list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View File

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

@ -8,9 +8,27 @@ if [[ $# -ne 1 ]] ; then
fi fi
grunt release grunt release
rm -rf /tmp/portainer-build && mkdir -pv /tmp/portainer-build/portainer rm -rf /tmp/portainer-build-unix && mkdir -pv /tmp/portainer-build-unix/portainer
mv dist/* /tmp/portainer-build/portainer mv dist/* /tmp/portainer-build-unix/portainer
cd /tmp/portainer-build cd /tmp/portainer-build-unix && tar cvpfz portainer-${VERSION}-linux-amd64.tar.gz portainer
tar cvpfz portainer-${VERSION}.tar.gz portainer cd -
grunt release-win
rm -rf /tmp/portainer-build-win && mkdir -pv /tmp/portainer-build-win/portainer
mv dist/* /tmp/portainer-build-win/portainer
cd /tmp/portainer-build-win
tar cvpfz portainer-${VERSION}-windows-amd64.tar.gz portainer
grunt release-arm
rm -rf /tmp/portainer-build-arm && mkdir -pv /tmp/portainer-build-arm/portainer
mv dist/* /tmp/portainer-build-arm/portainer
cd /tmp/portainer-build-arm
tar cvpfz portainer-${VERSION}-linux-arm.tar.gz portainer
grunt release-macos
rm -rf /tmp/portainer-build-darwin && mkdir -pv /tmp/portainer-build-darwin/portainer
mv dist/* /tmp/portainer-build-darwin/portainer
cd /tmp/portainer-build-darwin
tar cvpfz portainer-${VERSION}-darwin-amd64.tar.gz portainer
exit 0 exit 0

View File

@ -4,6 +4,8 @@ COPY dist /
VOLUME /data VOLUME /data
WORKDIR /
EXPOSE 9000 EXPOSE 9000
ENTRYPOINT ["/portainer"] ENTRYPOINT ["/portainer"]

View File

@ -0,0 +1,9 @@
FROM microsoft/windowsservercore
COPY dist /
WORKDIR /
EXPOSE 9000
ENTRYPOINT ["/portainer.exe"]

View File

@ -0,0 +1,9 @@
FROM microsoft/nanoserver
COPY dist /
WORKDIR /
EXPOSE 9000
ENTRYPOINT ["/portainer.exe"]

View File

@ -16,7 +16,7 @@ module.exports = function (grunt) {
grunt.registerTask('default', ['jshint', 'build', 'karma:unit']); grunt.registerTask('default', ['jshint', 'build', 'karma:unit']);
grunt.registerTask('build', [ grunt.registerTask('build', [
'clean:app', 'clean:app',
'if:binaryNotExist', 'if:unixBinaryNotExist',
'html2js', 'html2js',
'concat', 'concat',
'clean:tmpl', 'clean:tmpl',
@ -25,7 +25,43 @@ module.exports = function (grunt) {
]); ]);
grunt.registerTask('release', [ grunt.registerTask('release', [
'clean:all', 'clean:all',
'if:binaryNotExist', 'if:unixBinaryNotExist',
'html2js',
'uglify',
'clean:tmpl',
'jshint',
//'karma:unit',
'concat:index',
'recess:min',
'copy'
]);
grunt.registerTask('release-win', [
'clean:all',
'if:windowsBinaryNotExist',
'html2js',
'uglify',
'clean:tmpl',
'jshint',
//'karma:unit',
'concat:index',
'recess:min',
'copy'
]);
grunt.registerTask('release-arm', [
'clean:all',
'if:unixArmBinaryNotExist',
'html2js',
'uglify',
'clean:tmpl',
'jshint',
//'karma:unit',
'concat:index',
'recess:min',
'copy'
]);
grunt.registerTask('release-macos', [
'clean:all',
'if:darwinBinaryNotExist',
'html2js', 'html2js',
'uglify', 'uglify',
'clean:tmpl', 'clean:tmpl',
@ -37,10 +73,11 @@ module.exports = function (grunt) {
]); ]);
grunt.registerTask('lint', ['jshint']); grunt.registerTask('lint', ['jshint']);
grunt.registerTask('test-watch', ['karma:watch']); grunt.registerTask('test-watch', ['karma:watch']);
grunt.registerTask('run', ['if:binaryNotExist', 'build', 'shell:buildImage', 'shell:run']); grunt.registerTask('run', ['if:unixBinaryNotExist', 'build', 'shell:buildImage', 'shell:run']);
grunt.registerTask('run-swarm', ['if:binaryNotExist', 'build', 'shell:buildImage', 'shell:runSwarm', 'watch:buildSwarm']); grunt.registerTask('run-swarm', ['if:unixBinaryNotExist', 'build', 'shell:buildImage', 'shell:runSwarm', 'watch:buildSwarm']);
grunt.registerTask('run-dev', ['if:binaryNotExist', 'shell:buildImage', 'shell:run', 'watch:build']); grunt.registerTask('run-swarm-local', ['if:unixBinaryNotExist', 'build', 'shell:buildImage', 'shell:runSwarmLocal', 'watch:buildSwarm']);
grunt.registerTask('run-ssl', ['if:binaryNotExist', 'shell:buildImage', 'shell:runSsl', 'watch:buildSsl']); grunt.registerTask('run-dev', ['if:unixBinaryNotExist', 'shell:buildImage', 'shell:run', 'watch:build']);
grunt.registerTask('run-ssl', ['if:unixBinaryNotExist', 'shell:buildImage', 'shell:runSsl', 'watch:buildSsl']);
grunt.registerTask('clear', ['clean:app']); grunt.registerTask('clear', ['clean:app']);
// Print a timestamp (useful for when watching) // Print a timestamp (useful for when watching)
@ -253,7 +290,7 @@ module.exports = function (grunt) {
}, },
shell: { shell: {
buildImage: { buildImage: {
command: 'docker build --rm -t portainer .' command: 'docker build --rm -t portainer -f build/linux/Dockerfile .'
}, },
buildBinary: { buildBinary: {
command: [ command: [
@ -263,6 +300,30 @@ module.exports = function (grunt) {
'mv api/portainer dist/' 'mv api/portainer dist/'
].join(' && ') ].join(' && ')
}, },
buildUnixArmBinary: {
command: [
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="linux" -e BUILD_GOARCH="arm" centurylink/golang-builder-cross',
'shasum api/portainer-linux-arm > portainer-checksum.txt',
'mkdir -p dist',
'mv api/portainer-linux-arm dist/portainer'
].join(' && ')
},
buildDarwinBinary: {
command: [
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="darwin" -e BUILD_GOARCH="amd64" centurylink/golang-builder-cross',
'shasum api/portainer-darwin-amd64 > portainer-checksum.txt',
'mkdir -p dist',
'mv api/portainer-darwin-amd64 dist/portainer'
].join(' && ')
},
buildWindowsBinary: {
command: [
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="windows" -e BUILD_GOARCH="amd64" centurylink/golang-builder-cross',
'shasum api/portainer-windows-amd64 > portainer-checksum.txt',
'mkdir -p dist',
'mv api/portainer-windows-amd64 dist/portainer.exe'
].join(' && ')
},
run: { run: {
command: [ command: [
'docker stop portainer', 'docker stop portainer',
@ -277,6 +338,13 @@ module.exports = function (grunt) {
'docker run -d -p 9000:9000 -v /tmp/portainer:/data --name portainer portainer -H tcp://10.0.7.10:2375 --swarm -d /data' 'docker run -d -p 9000:9000 -v /tmp/portainer:/data --name portainer portainer -H tcp://10.0.7.10:2375 --swarm -d /data'
].join(';') ].join(';')
}, },
runSwarmLocal: {
command: [
'docker stop portainer',
'docker rm portainer',
'docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock --name portainer portainer --swarm'
].join(';')
},
runSsl: { runSsl: {
command: [ command: [
'docker stop portainer', 'docker stop portainer',
@ -289,11 +357,29 @@ module.exports = function (grunt) {
} }
}, },
'if': { 'if': {
binaryNotExist: { unixBinaryNotExist: {
options: { options: {
executable: 'dist/portainer' executable: 'dist/portainer'
}, },
ifFalse: ['shell:buildBinary'] ifFalse: ['shell:buildBinary']
},
unixArmBinaryNotExist: {
options: {
executable: 'dist/portainer'
},
ifFalse: ['shell:buildUnixArmBinary']
},
darwinBinaryNotExist: {
options: {
executable: 'dist/portainer'
},
ifFalse: ['shell:buildDarwinBinary']
},
windowsBinaryNotExist: {
options: {
executable: 'dist/portainer.exe'
},
ifFalse: ['shell:buildWindowsBinary']
} }
} }
}); });

View File

@ -32,8 +32,8 @@
<ul class="sidebar"> <ul class="sidebar">
<li class="sidebar-main"> <li class="sidebar-main">
<a ng-click="toggleSidebar()"> <a ng-click="toggleSidebar()">
<img ng-if="config.logo" ng-src="{{ config.logo }}" class="img-responsive logo"> <img ng-if="logo" ng-src="{{ logo }}" class="img-responsive logo">
<img ng-if="!config.logo" src="images/logo.png" class="img-responsive logo" alt="Portainer"> <img ng-if="!logo" src="images/logo.png" class="img-responsive logo" alt="Portainer">
<span class="menu-icon glyphicon glyphicon-transfer"></span> <span class="menu-icon glyphicon glyphicon-transfer"></span>
</a> </a>
</li> </li>
@ -66,7 +66,7 @@
<a ui-sref="swarm">Swarm <span class="menu-icon fa fa-object-group"></span></a> <a ui-sref="swarm">Swarm <span class="menu-icon fa fa-object-group"></span></a>
</li> </li>
<li class="sidebar-list" ng-if="!swarm"> <li class="sidebar-list" ng-if="!swarm">
<a ui-sref="docker">Docker <span class="menu-icon fa fa-cogs"></span></a> <a ui-sref="docker">Docker <span class="menu-icon fa fa-th"></span></a>
</li> </li>
</ul> </ul>
<div class="sidebar-footer"> <div class="sidebar-footer">

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