1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-27 17:29:39 +02:00

Merge branch 'release/1.9.0'

This commit is contained in:
Anthony Lapenna 2016-09-24 22:33:30 +12:00
commit c35d1b14ec
51 changed files with 1672 additions and 495 deletions

6
.gitignore vendored
View file

@ -1,10 +1,4 @@
logs/*
!.gitkeep
*.esproj/*
node_modules node_modules
bower_components bower_components
.idea
*.iml
dist dist
dist/*
portainer-checksum.txt portainer-checksum.txt

58
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,58 @@
# Contributing Guidelines
Some basic conventions for contributing to this project.
### General
Please make sure that there aren't existing pull requests attempting to address the issue mentioned. Likewise, please check for issues related to update, as someone else may be working on the issue in a branch or fork.
* Non-trivial changes should be discussed in an issue first
* Develop in a topic branch, not master
### Linting
Please check your code using `grunt lint` before submitting your pull requests.
### Commit Message Format
Each commit message should include a **type**, a **scope** and a **subject**:
```
<type>(<scope>): <subject>
```
Lines should not exceed 100 characters. This allows the message to be easier to read on github as well as in various git tools and produces a nice, neat commit log ie:
```
#271 feat(standard): add style config and refactor to match
#270 fix(config): only override publicPath when served by webpack
#269 feat(eslint-config-defaults): replace eslint-config-airbnb
#268 feat(config): allow user to configure webpack stats output
```
#### Type
Must be one of the following:
* **feat**: A new feature
* **fix**: A bug fix
* **docs**: Documentation only changes
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
semi-colons, etc)
* **refactor**: A code change that neither fixes a bug or adds a feature
* **test**: Adding missing tests
* **chore**: Changes to the build process or auxiliary tools and libraries such as documentation
generation
#### Scope
The scope could be anything specifying place of the commit change. For example `networks`,
`containers`, `images` etc...
#### Subject
The subject contains succinct description of the change:
* use the imperative, present tense: "change" not "changed" nor "changes"
* don't capitalize first letter
* no dot (.) at the end

View file

@ -1,4 +1,4 @@
Portainer: Copyright (c) 2016 CloudInovasi Portainer: Copyright (c) 2016 Portainer.io
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@ -18,7 +18,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (anthonylapenna at cloudinovasi dot id) UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (portainer.io)
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -1 +0,0 @@
web: portainer -p ":$PORT" -e "$DOCKER_ENDPOINT"

170
README.md
View file

@ -1,165 +1,81 @@
# Portainer # Portainer
[![Microbadger](https://images.microbadger.com/badges/image/cloudinovasi/portainer.svg)](http://microbadger.com/images/cloudinovasi/portainer "Image size") The easiest way to manage Docker.
[![Microbadger](https://images.microbadger.com/badges/image/portainer/portainer.svg)](http://microbadger.com/images/portainer/portainer "Image size")
[![Gitter](https://badges.gitter.im/portainer/Lobby.svg)](https://gitter.im/portainer/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Gitter](https://badges.gitter.im/portainer/Lobby.svg)](https://gitter.im/portainer/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
Portainer is a web interface for the Docker remote API. Portainer is a lightweight management UI which allows you to **easily** manage your Docker host or Swarm cluster.
![Dashboard](/dashboard.png) # Usage
## Supported Docker versions It's really simple to deploy it using Docker:
The following Docker versions are supported: ```shell
$ docker run -d -p 9000:9000 portainer/portainer -H tcp://<DOCKER_HOST>:<DOCKER_PORT>
* full support for Docker 1.10, 1.11 and 1.12
* partial support for Docker 1.9 (some features won't be available)
## Run
### Quickstart
1. Run: `docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/portainer`
2. Open your browser to `http://<dockerd host ip>:9000`
Bind mounting the Unix socket into the Portainer container is much more secure than exposing your docker daemon over TCP.
The `--privileged` flag is required for hosts using SELinux.
### Specify socket to connect to Docker daemon
By default Portainer connects to the Docker daemon with`/var/run/docker.sock`. For this to work you need to bind mount the unix socket into the container with `-v /var/run/docker.sock:/var/run/docker.sock`.
You can use the `--host`, `-H` flags to change this socket:
```
# Connect to a tcp socket:
$ docker run -d -p 9000:9000 cloudinovasi/portainer -H tcp://127.0.0.1:2375
``` ```
``` Just point it at your targeted Docker host and then access Portainer by hitting [http://localhost:9000](http://localhost:9000) with a web browser.
# Connect to another unix socket:
$ docker run -d -p 9000:9000 cloudinovasi/portainer -H unix:///path/to/docker.sock If your target is a Docker Swarm cluster or a Docker cluster using *swarm mode*, just add the flag `--swarm`:
```shell
$ docker run -d -p 9000:9000 portainer/portainer -H tcp://<SWARM_HOST>:<SWARM_PORT> --swarm
``` ```
### Swarm support If you don't specify any target, its default behaviour is to use a bind mount on the Docker socket so you can easily deploy it to manage your local Docker host:
**Supported Swarm version: 1.2.3** ```shell
$ docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock portainer/portainer
You can access a specific view for you Swarm cluster by defining the `--swarm` flag:
```
# Connect to a tcp socket and enable Swarm:
$ docker run -d -p 9000:9000 cloudinovasi/portainer -H tcp://<SWARM_HOST>:<SWARM_PORT> --swarm
``` ```
*NOTE*: Due to Swarm not exposing information in a machine readable way, the app is bound to a specific version of Swarm at the moment. Have a look at our [wiki](https://github.com/portainer/portainer/wiki/Deployment) for more deployment options.
### Change address/port Portainer is served on # Configuration
Portainer listens on port 9000 by default. If you run Portainer inside a container then you can bind the container's internal port to any external address and port:
``` Portainer is easy to tune using CLI flags.
# Expose Portainer on 10.20.30.1:80
$ docker run -d -p 10.20.30.1:80:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/portainer
```
### Access a Docker engine protected via TLS ## Hiding specific containers
Ensure that you have access to the CA, the cert and the public key used to access your Docker engine. Portainer allows you to hide container with a specific label by using the `-l` flag.
These files will need to be named `ca.pem`, `cert.pem` and `key.pem` respectively. Store them somewhere on your disk and mount a volume containing these files inside the UI container:
```
$ docker run -d -p 9000:9000 cloudinovasi/portainer -v /path/to/certs:/certs -H https://my-docker-host.domain:2376 --tlsverify
```
You can also use the `--tlscacert`, `--tlscert` and `--tlskey` flags if you want to change the default path to the CA, certificate and key file respectively:
```
$ docker run -d -p 9000:9000 cloudinovasi/portainer -v /path/to/certs:/certs -H https://my-docker-host.domain:2376 --tlsverify --tlscacert /certs/myCa.pem --tlscert /certs/myCert.pem --tlskey /certs/myKey.pem
```
*Note*: Replace `/path/to/certs` to the path to the certificate files on your disk.
### Use your own logo
You can use the `--logo` flag to specify an URL to your own logo.
For example, using the Docker logo:
```
$ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/portainer --logo "https://www.docker.com/sites/all/themes/docker/assets/images/brand-full.svg"
```
The custom logo will replace the Portainer logo in the UI.
### Hide containers with specific labels
You can hide specific containers in the containers view by using the `--hide-label` or `-l` options and specifying a label.
For example, take a container started with the label `owner=acme`: For example, take a container started with the label `owner=acme`:
```shell
```
$ docker run -d --label owner=acme nginx $ docker run -d --label owner=acme nginx
``` ```
You can hide it in the view by starting the ui with: Simply add the `-l owner=acme` option on the CLI when starting Portainer:
```shell
``` $ docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock portainer/portainer -l owner=acme
$ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/portainer -l owner=acme
``` ```
### Reverse proxy configuration ## Use your own templates
Has been tested with Nginx 1.11. Portainer allows you to rapidly deploy containers using `App Templates`.
Use the following configuration to host the UI at `myhost.mydomain.com/portainer`: By default [Portainer templates](https://raw.githubusercontent.com/portainer/templates/master/templates.json) will be used but you can also define your own templates.
```nginx Add the `--templates` flag and specify the external location of your templates when starting Portainer:
upstream portainer {
server ADDRESS:PORT;
}
server { ```shell
listen 80; $ docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock portainer/portainer --templates http://my-host.my-domain/templates.json
location /portainer/ {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://portainer/;
}
location /portainer/ws/ {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_pass http://portainer/ws/;
}
}
``` ```
Replace `ADDRESS:PORT` with the Portainer container details. For more information about hosting your own template definitions and the format, see: https://github.com/portainer/templates
### Host your own apps Have a look at our [wiki](https://github.com/portainer/portainer/wiki/Configuration) for more configuration options.
You can specify an URL to your own templates (**Apps**) definitions using the `--templates` or `-t` flags. # FAQ
By default, CloudInovasi templates will be used (https://raw.githubusercontent.com/cloud-inovasi/ui-templates/master/templates.json). Be sure to check our [FAQ](https://github.com/portainer/portainer/wiki/FAQ) if you are missing some information.
For more information about hosting your own template definition and the format, see: https://github.com/cloud-inovasi/ui-templates # Limitations
### Available options Portainer has full support for the following Docker versions:
The following options are available for the `portainer` binary: * Docker 1.10 to Docker 1.12 (including `swarm-mode`)
* Docker Swarm >= 1.2.3
* `--host`, `-H`: Docker daemon endpoint (default: `"unix:///var/run/docker.sock"`) Partial support for the following Docker versions (some features may not be available):
* `--bind`, `-p`: Address and port to serve Portainer (default: `":9000"`)
* `--data`, `-d`: Path to the data folder (default: `"."`) * Docker 1.9
* `--assets`, `-a`: Path to the assets (default: `"."`)
* `--swarm`, `-s`: Swarm cluster support (default: `false`)
* `--tlsverify`: TLS support (default: `false`)
* `--tlscacert`: Path to the CA (default `/certs/ca.pem`)
* `--tlscert`: Path to the TLS certificate file (default `/certs/cert.pem`)
* `--tlskey`: Path to the TLS key (default `/certs/key.pem`)
* `--hide-label`, `-l`: Hide containers with a specific label in the UI
* `--logo`: URL to a picture to be displayed as a logo in the UI
* `--templates`, `-t`: URL to templates (apps) definitions

View file

@ -1,4 +1,4 @@
package main // import "github.com/cloudinovasi/portainer" package main // import "github.com/portainer/portainer"
import ( import (
"gopkg.in/alecthomas/kingpin.v2" "gopkg.in/alecthomas/kingpin.v2"
@ -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.8.1") kingpin.Version("1.9.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()
@ -19,7 +19,7 @@ func main() {
swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool() swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool()
labels = pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')) labels = pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l'))
logo = kingpin.Flag("logo", "URL for the logo displayed in the UI").String() logo = kingpin.Flag("logo", "URL for the logo displayed in the UI").String()
templates = kingpin.Flag("templates", "URL to the templates (apps) definitions").Default("https://raw.githubusercontent.com/cloud-inovasi/ui-templates/master/templates.json").Short('t').String() templates = kingpin.Flag("templates", "URL to the templates (apps) definitions").Default("https://raw.githubusercontent.com/portainer/templates/master/templates.json").Short('t').String()
) )
kingpin.Parse() kingpin.Parse()

View file

@ -18,11 +18,15 @@ angular.module('portainer', [
'events', 'events',
'images', 'images',
'image', 'image',
'service',
'services',
'createService',
'stats', 'stats',
'swarm', 'swarm',
'network', 'network',
'networks', 'networks',
'createNetwork', 'createNetwork',
'task',
'templates', 'templates',
'volumes', 'volumes',
'createVolume']) 'createVolume'])
@ -80,16 +84,21 @@ angular.module('portainer', [
templateUrl: 'app/components/createContainer/createcontainer.html', templateUrl: 'app/components/createContainer/createcontainer.html',
controller: 'CreateContainerController' controller: 'CreateContainerController'
}) })
.state('actions.create.volume', {
url: "/volume",
templateUrl: 'app/components/createVolume/createvolume.html',
controller: 'CreateVolumeController'
})
.state('actions.create.network', { .state('actions.create.network', {
url: "/network", url: "/network",
templateUrl: 'app/components/createNetwork/createnetwork.html', templateUrl: 'app/components/createNetwork/createnetwork.html',
controller: 'CreateNetworkController' controller: 'CreateNetworkController'
}) })
.state('actions.create.service', {
url: "/service",
templateUrl: 'app/components/createService/createservice.html',
controller: 'CreateServiceController'
})
.state('actions.create.volume', {
url: "/volume",
templateUrl: 'app/components/createVolume/createvolume.html',
controller: 'CreateVolumeController'
})
.state('docker', { .state('docker', {
url: '/docker/', url: '/docker/',
templateUrl: 'app/components/docker/docker.html', templateUrl: 'app/components/docker/docker.html',
@ -120,6 +129,21 @@ angular.module('portainer', [
templateUrl: 'app/components/network/network.html', templateUrl: 'app/components/network/network.html',
controller: 'NetworkController' controller: 'NetworkController'
}) })
.state('services', {
url: '/services/',
templateUrl: 'app/components/services/services.html',
controller: 'ServicesController'
})
.state('service', {
url: '^/service/:id/',
templateUrl: 'app/components/service/service.html',
controller: 'ServiceController'
})
.state('task', {
url: '^/task/:id',
templateUrl: 'app/components/task/task.html',
controller: 'TaskController'
})
.state('templates', { .state('templates', {
url: '/templates/', url: '/templates/',
templateUrl: 'app/components/templates/templates.html', templateUrl: 'app/components/templates/templates.html',
@ -164,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.8.1'); .constant('UI_VERSION', 'v1.9.0');

View file

@ -29,7 +29,7 @@
<div class="row"> <div class="row">
<div class="col-lg-12 col-md-12 col-xs-12"> <div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget> <rd-widget>
<rd-widget-header icon="fa-tasks" title="Container status"></rd-widget-header> <rd-widget-header icon="fa-server" title="Container status"></rd-widget-header>
<rd-widget-body classes="no-padding"> <rd-widget-body classes="no-padding">
<table class="table"> <table class="table">
<tbody> <tbody>
@ -130,7 +130,7 @@
<div class="row"> <div class="row">
<div class="col-lg-12 col-md-12 col-xs-12"> <div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget> <rd-widget>
<rd-widget-header icon="fa-tasks" title="Container details"></rd-widget-header> <rd-widget-header icon="fa-server" title="Container details"></rd-widget-header>
<rd-widget-body classes="no-padding"> <rd-widget-body classes="no-padding">
<table class="table"> <table class="table">
<tbody> <tbody>

View file

@ -12,7 +12,7 @@
<rd-widget> <rd-widget>
<rd-widget-body> <rd-widget-body>
<div class="widget-icon grey pull-left"> <div class="widget-icon grey pull-left">
<i class="fa fa-tasks"></i> <i class="fa fa-server"></i>
</div> </div>
<div class="title">{{ container.Name|trimcontainername }}</div> <div class="title">{{ container.Name|trimcontainername }}</div>
<div class="comment">Name</div> <div class="comment">Name</div>

View file

@ -9,7 +9,7 @@
<div class="col-lg-12"> <div class="col-lg-12">
<rd-widget> <rd-widget>
<rd-widget-header icon="fa-tasks" title="Containers"> <rd-widget-header icon="fa-server" title="Containers">
<div class="pull-right"> <div class="pull-right">
<i id="loadContainersSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i> <i id="loadContainersSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
</div> </div>
@ -17,15 +17,15 @@
<rd-widget-taskbar classes="col-lg-12"> <rd-widget-taskbar classes="col-lg-12">
<div class="pull-left"> <div class="pull-left">
<div class="btn-group" role="group" aria-label="..."> <div class="btn-group" role="group" aria-label="...">
<button type="button" class="btn btn-primary" ng-click="startAction()" ng-disabled="!state.selectedItemCount">Start</button> <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" ng-click="stopAction()" ng-disabled="!state.selectedItemCount">Stop</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" ng-click="killAction()" ng-disabled="!state.selectedItemCount">Kill</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" ng-click="restartAction()" ng-disabled="!state.selectedItemCount">Restart</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" ng-click="pauseAction()" ng-disabled="!state.selectedItemCount">Pause</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" ng-click="unpauseAction()" ng-disabled="!state.selectedItemCount">Unpause</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-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount">Remove</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>
</div> </div>
<a class="btn btn-default" 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>
<div class="pull-right"> <div class="pull-right">
<input type="checkbox" ng-model="state.displayAll" id="displayAll" ng-change="toggleGetAll()" style="margin-top: -2px; margin-right: 5px;"/><label for="displayAll">Show all containers</label> <input type="checkbox" ng-model="state.displayAll" id="displayAll" ng-change="toggleGetAll()" style="margin-top: -2px; margin-right: 5px;"/><label for="displayAll">Show all containers</label>
@ -66,7 +66,7 @@
<span ng-show="sortType == 'IP' && sortReverse" class="glyphicon glyphicon-chevron-up"></span> <span ng-show="sortType == 'IP' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a> </a>
</th> </th>
<th ng-if="swarm"> <th ng-if="swarm && !swarm_mode">
<a ui-sref="containers" ng-click="order('Host')"> <a ui-sref="containers" ng-click="order('Host')">
Host IP Host IP
<span ng-show="sortType == 'Host' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span> <span ng-show="sortType == 'Host' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
@ -86,11 +86,11 @@
<tr ng-repeat="container in (state.filteredContainers = ( containers | filter:state.filter | orderBy:sortType:sortReverse))"> <tr ng-repeat="container in (state.filteredContainers = ( containers | filter:state.filter | orderBy:sortType:sortReverse))">
<td><input type="checkbox" ng-model="container.Checked" ng-change="selectItem(container)"/></td> <td><input type="checkbox" ng-model="container.Checked" ng-change="selectItem(container)"/></td>
<td><span class="label label-{{ container.Status|containerstatusbadge }}">{{ container.Status|containerstatus }}</span></td> <td><span class="label label-{{ container.Status|containerstatusbadge }}">{{ container.Status|containerstatus }}</span></td>
<td ng-if="swarm"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername}}</a></td> <td ng-if="swarm && !swarm_mode"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername}}</a></td>
<td ng-if="!swarm"><a ui-sref="container({id: container.Id})">{{ container|containername}}</a></td> <td ng-if="!swarm || swarm_mode"><a ui-sref="container({id: container.Id})">{{ container|containername}}</a></td>
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td> <td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
<td ng-if="state.displayIP">{{ container.IP ? container.IP : '-' }}</td> <td ng-if="state.displayIP">{{ container.IP ? container.IP : '-' }}</td>
<td ng-if="swarm">{{ container.hostIP }}</td> <td ng-if="swarm && !swarm_mode">{{ container.hostIP }}</td>
<td> <td>
<a ng-if="container.Ports.length > 0" ng-repeat="p in container.Ports" class="image-tag" ng-href="http://{{p.host}}:{{p.public}}" target="_blank"> <a ng-if="container.Ports.length > 0" ng-repeat="p in container.Ports" class="image-tag" ng-href="http://{{p.host}}:{{p.public}}" target="_blank">
<i class="fa fa-external-link" aria-hidden="true"></i> {{ p.private }} <i class="fa fa-external-link" aria-hidden="true"></i> {{ p.private }}

View file

@ -8,6 +8,7 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config)
$scope.sortType = 'State'; $scope.sortType = 'State';
$scope.sortReverse = false; $scope.sortReverse = false;
$scope.state.selectedItemCount = 0; $scope.state.selectedItemCount = 0;
$scope.swarm_mode = false;
$scope.order = function (sortType) { $scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
@ -27,7 +28,7 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config)
if (model.IP) { if (model.IP) {
$scope.state.displayIP = true; $scope.state.displayIP = true;
} }
if ($scope.swarm) { if ($scope.swarm && !$scope.swarm_mode) {
model.hostIP = $scope.swarm_hosts[_.split(container.Names[0], '/')[1]]; model.hostIP = $scope.swarm_hosts[_.split(container.Names[0], '/')[1]];
} }
return model; return model;
@ -151,7 +152,11 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config)
$scope.swarm = c.swarm; $scope.swarm = c.swarm;
if (c.swarm) { if (c.swarm) {
Info.get({}, function (d) { Info.get({}, function (d) {
$scope.swarm_hosts = retrieveSwarmHostsInfo(d); if (!_.startsWith(d.ServerVersion, 'swarm')) {
$scope.swarm_mode = true;
} else {
$scope.swarm_hosts = retrieveSwarmHostsInfo(d);
}
update({all: Settings.displayAll ? 1 : 0}); update({all: Settings.displayAll ? 1 : 0});
}); });
} else { } else {

View file

@ -1,6 +1,6 @@
angular.module('createContainer', []) angular.module('createContainer', [])
.controller('CreateContainerController', ['$scope', '$state', 'Config', 'Container', 'Image', 'Volume', 'Network', 'Messages', .controller('CreateContainerController', ['$scope', '$state', 'Config', 'Info', 'Container', 'Image', 'Volume', 'Network', 'Messages',
function ($scope, $state, Config, Container, Image, Volume, Network, Messages) { function ($scope, $state, Config, Info, Container, Image, Volume, Network, Messages) {
$scope.state = { $scope.state = {
alwaysPull: true alwaysPull: true
@ -55,6 +55,11 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages) {
Config.$promise.then(function (c) { Config.$promise.then(function (c) {
var swarm = c.swarm; var swarm = c.swarm;
Info.get({}, function(info) {
if (swarm && !_.startsWith(info.ServerVersion, 'swarm')) {
$scope.swarm_mode = true;
}
});
$scope.formValues.AvailableRegistries = c.registries; $scope.formValues.AvailableRegistries = c.registries;

View file

@ -254,7 +254,7 @@
<!-- tab-network --> <!-- tab-network -->
<div class="tab-pane" id="network"> <div class="tab-pane" id="network">
<form class="form-horizontal" style="margin-top: 15px;"> <form class="form-horizontal" style="margin-top: 15px;">
<div class="form-group" ng-if="globalNetworkCount === 0"> <div class="form-group" ng-if="globalNetworkCount === 0 && !swarm_mode">
<div class="col-sm-12"> <div class="col-sm-12">
<span class="small text-muted">You don't have any shared network. Head over the <a ui-sref="networks">networks view</a> to create one.</span> <span class="small text-muted">You don't have any shared network. Head over the <a ui-sref="networks">networks view</a> to create one.</span>
</div> </div>

View file

@ -11,7 +11,10 @@ function ($scope, $state, Messages, Network) {
Driver: 'bridge', Driver: 'bridge',
CheckDuplicate: true, CheckDuplicate: true,
Internal: false, Internal: false,
// Force IPAM Driver to 'default', should not be required.
// See: https://github.com/docker/docker/issues/25735
IPAM: { IPAM: {
Driver: 'default',
Config: [] Config: []
} }
}; };

View file

@ -0,0 +1,178 @@
angular.module('createService', [])
.controller('CreateServiceController', ['$scope', '$state', 'Service', 'Volume', 'Network', 'ImageHelper', 'Messages',
function ($scope, $state, Service, Volume, Network, ImageHelper, Messages) {
$scope.formValues = {
Name: '',
Image: '',
Registry: '',
Mode: 'replicated',
Replicas: 1,
Command: '',
WorkingDir: '',
User: '',
Env: [],
Volumes: [],
Network: '',
ExtraNetworks: [],
Ports: []
};
$scope.addPortBinding = function() {
$scope.formValues.Ports.push({ PublishedPort: '', TargetPort: '', Protocol: 'tcp' });
};
$scope.removePortBinding = function(index) {
$scope.formValues.Ports.splice(index, 1);
};
$scope.addExtraNetwork = function() {
$scope.formValues.ExtraNetworks.push({ Name: '' });
};
$scope.removeExtraNetwork = function(index) {
$scope.formValues.ExtraNetworks.splice(index, 1);
};
$scope.addVolume = function() {
$scope.formValues.Volumes.push({ name: '', containerPath: '' });
};
$scope.removeVolume = function(index) {
$scope.formValues.Volumes.splice(index, 1);
};
$scope.addEnvironmentVariable = function() {
$scope.formValues.Env.push({ name: '', value: ''});
};
$scope.removeEnvironmentVariable = function(index) {
$scope.formValues.Env.splice(index, 1);
};
function prepareImageConfig(config, input) {
var imageConfig = ImageHelper.createImageConfig(input.Image, input.Registry);
config.TaskTemplate.ContainerSpec.Image = imageConfig.repo + ':' + imageConfig.tag;
}
function preparePortsConfig(config, input) {
var ports = [];
input.Ports.forEach(function (binding) {
if (binding.PublishedPort && binding.TargetPort) {
ports.push({ PublishedPort: +binding.PublishedPort, TargetPort: +binding.TargetPort, Protocol: binding.Protocol });
}
});
config.EndpointSpec.Ports = ports;
}
function prepareSchedulingConfig(config, input) {
if (input.Mode === 'replicated') {
config.Mode.Replicated = {
Replicas: input.Replicas
};
} else {
config.Mode.Global = {};
}
}
function prepareCommandConfig(config, input) {
if (input.Command) {
config.TaskTemplate.ContainerSpec.Command = _.split(input.Command, ' ');
}
if (input.User) {
config.TaskTemplate.ContainerSpec.User = input.User;
}
if (input.WorkingDir) {
config.TaskTemplate.ContainerSpec.Dir = input.WorkingDir;
}
}
function prepareEnvConfig(config, input) {
var env = [];
input.Env.forEach(function (v) {
if (v.name && v.value) {
env.push(v.name + "=" + v.value);
}
});
config.TaskTemplate.ContainerSpec.Env = env;
}
function prepareVolumes(config, input) {
input.Volumes.forEach(function (volume) {
if (volume.Source && volume.Target) {
var mount = {};
mount.Type = volume.Bind ? 'bind' : 'volume';
mount.ReadOnly = volume.ReadOnly ? true : false;
mount.Source = volume.Source;
mount.Target = volume.Target;
config.TaskTemplate.ContainerSpec.Mounts.push(mount);
}
});
}
function prepareNetworks(config, input) {
var networks = [];
if (input.Network) {
networks.push({ Target: input.Network });
}
input.ExtraNetworks.forEach(function (network) {
networks.push({ Target: network.Name });
});
config.Networks = _.uniqWith(networks, _.isEqual);
}
function prepareConfiguration() {
var input = $scope.formValues;
var config = {
Name: input.Name,
TaskTemplate: {
ContainerSpec: {
Mounts: []
}
},
Mode: {},
EndpointSpec: {}
};
prepareSchedulingConfig(config, input);
prepareImageConfig(config, input);
preparePortsConfig(config, input);
prepareCommandConfig(config, input);
prepareEnvConfig(config, input);
prepareVolumes(config, input);
prepareNetworks(config, input);
return config;
}
function createNewService(config) {
Service.create(config, function (d) {
$('#createServiceSpinner').hide();
Messages.send('Service created', d.ID);
$state.go('services', {}, {reload: true});
}, function (e) {
$('#createServiceSpinner').hide();
Messages.error("Failure", e, 'Unable to create service');
});
}
$scope.create = function createService() {
$('#createServiceSpinner').show();
var config = prepareConfiguration();
createNewService(config);
};
Volume.query({}, function (d) {
$scope.availableVolumes = d.Volumes;
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve volumes");
});
Network.query({}, function (d) {
$scope.availableNetworks = d.filter(function (network) {
if (network.Scope === 'swarm') {
return network;
}
});
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve networks");
});
}]);

View file

@ -0,0 +1,272 @@
<rd-header>
<rd-header-title title="Create service"></rd-header-title>
<rd-header-content>
Services > Add service
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal">
<!-- name-input -->
<div class="form-group">
<label for="service_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="formValues.Name" id="service_name" placeholder="e.g. myService">
</div>
</div>
<!-- !name-input -->
<!-- image-and-registry-inputs -->
<div class="form-group">
<label for="service_image" class="col-sm-1 control-label text-left">Image</label>
<div class="col-sm-7">
<input type="text" class="form-control" ng-model="formValues.Image" id="service_image" placeholder="e.g. nginx:latest">
</div>
<label for="image_registry" class="col-sm-1 control-label text-left">Registry</label>
<div class="col-sm-3">
<input type="text" class="form-control" ng-model="formValues.Registry" id="image_registry" placeholder="leave empty to use DockerHub">
</div>
</div>
<!-- !image-and-registry-inputs -->
<!-- scheduling-mode -->
<div class="form-group">
<label class="col-sm-1 control-label text-left">Scheduling mode</label>
<div class="col-sm-11">
<label class="radio-inline">
<input type="radio" name="service_scheduling" ng-model="formValues.Mode" value="global">
Global
</label>
<label class="radio-inline">
<input type="radio" name="service_scheduling" ng-model="formValues.Mode" value="replicated">
Replicated
</label>
</div>
</div>
<div class="form-group" ng-if="formValues.Mode === 'replicated'">
<label for="replicas" class="col-sm-1 control-label text-left">Replicas</label>
<div class="col-sm-1">
<input type="number" class="form-control" ng-model="formValues.Replicas" id="replicas" placeholder="e.g. 3">
</div>
<div class="col-sm-10"></div>
</div>
<!-- !scheduling-mode -->
<!-- port-mapping -->
<div class="form-group">
<label for="container_ports" class="col-sm-1 control-label text-left">Port mapping</label>
<div class="col-sm-11">
<span class="label label-default clickable" ng-click="addPortBinding()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> map 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.PublishedPort" placeholder="e.g. 8080">
</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.TargetPort" 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 -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-body>
<ul class="nav nav-tabs">
<li class="active clickable"><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="clickable"><a data-target="#network" data-toggle="tab">Network</a></li>
</ul>
<!-- tab-content -->
<div class="tab-content">
<!-- tab-command -->
<div class="tab-pane active" id="command">
<form class="form-horizontal" style="margin-top: 15px;">
<!-- command-input -->
<div class="form-group">
<label for="service_command" class="col-sm-1 control-label text-left">Command</label>
<div class="col-sm-9">
<input type="text" class="form-control" ng-model="formValues.Command" id="service_command" placeholder="e.g. /usr/bin/nginx -t -c /mynginx.conf">
</div>
</div>
<!-- !command-input -->
<!-- workdir-user-input -->
<div class="form-group">
<label for="service_workingdir" class="col-sm-1 control-label text-left">Working Dir</label>
<div class="col-sm-4">
<input type="text" class="form-control" ng-model="formValues.WorkingDir" id="service_workingdir" placeholder="e.g. /myapp">
</div>
<label for="service_user" class="col-sm-1 control-label text-left">User</label>
<div class="col-sm-4">
<input type="text" class="form-control" ng-model="formValues.User" id="service_user" placeholder="e.g. nginx">
</div>
</div>
<!-- !workdir-user-input -->
<!-- environment-variables -->
<div class="form-group">
<label for="service_env" class="col-sm-1 control-label text-left">Environment variables</label>
<div class="col-sm-11">
<span class="label label-default clickable" ng-click="addEnvironmentVariable()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> environment variable
</span>
</div>
<!-- environment-variable-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
<div ng-repeat="variable in formValues.Env" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span>
<input type="text" class="form-control" ng-model="variable.name" placeholder="e.g. FOO">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="variable.value" placeholder="e.g. bar">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeEnvironmentVariable($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !environment-variable-input-list -->
</div>
<!-- !environment-variables -->
</form>
</div>
<!-- !tab-command -->
<!-- tab-volume -->
<div class="tab-pane" id="volumes">
<form class="form-horizontal" style="margin-top: 15px;">
<!-- volumes -->
<div class="form-group">
<label for="service_volumes" class="col-sm-1 control-label text-left">Volumes</label>
<div class="col-sm-11">
<span class="label label-default clickable" ng-click="addVolume()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> volume
</span>
</div>
<!-- volumes-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
<div ng-repeat="volume in formValues.Volumes" style="margin-top: 2px;">
<div class="input-group col-sm-1 input-group-sm">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="volume.ReadOnly"> Read-only
</label>
</div>
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon"><input type="checkbox" ng-model="volume.Bind">bind</span>
<select class="selectpicker form-control" ng-model="volume.Source" ng-if="!volume.Bind">
<option selected disabled hidden value="">Select a volume</option>
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option>
</select>
<input ng-if="volume.Bind" type="text" class="form-control" ng-model="volume.Source" placeholder="e.g. /path/on/host">
</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="volume.Target" placeholder="e.g. /path/in/container">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeVolume($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !volumes-input-list -->
</div>
<!-- !volumes -->
</form>
</div>
<!-- !tab-volume -->
<!-- tab-network -->
<div class="tab-pane" id="network">
<form class="form-horizontal" style="margin-top: 15px;">
<!-- network-input -->
<div class="form-group">
<label for="container_network" class="col-sm-1 control-label text-left">Network</label>
<div class="col-sm-9">
<select class="selectpicker form-control" ng-model="formValues.Network">
<option selected disabled hidden value="">Select a network</option>
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
</select>
</div>
<div class="col-sm-2"></div>
</div>
<!-- !network-input -->
<!-- extra-networks -->
<div class="form-group">
<label for="service_extra_networks" class="col-sm-1 control-label text-left">Extra networks</label>
<div class="col-sm-11">
<span class="label label-default clickable" ng-click="addExtraNetwork()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> network
</span>
</div>
<!-- network-input-list -->
<div style="margin-top: 10px;">
<div class="col-sm-12" ng-repeat="network in formValues.ExtraNetworks" style="margin-top: 5px;">
<div class="input-group col-sm-9 input-group-sm col-sm-offset-1">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeExtraNetwork($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
<select class="selectpicker form-control" ng-model="network.Name">
<option selected disabled hidden value="">Select a network</option>
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
</select>
</div>
<div class="col-sm-2"></div>
</div>
</div>
<!-- !network-input-list -->
</div>
<!-- !extra-networks -->
</form>
</div>
<!-- !tab-network -->
<!-- tab-security -->
<div class="tab-pane" id="security">
</div>
<!-- !tab-security -->
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12" style="text-align: center;">
<div>
<i id="createServiceSpinner" class="fa fa-cog fa-3x fa-spin" style="margin-bottom: 5px; display: none;"></i>
</div>
<button type="button" class="btn btn-default btn-lg" ng-click="create()">Create</button>
<a type="button" class="btn btn-default btn-lg" ui-sref="services">Cancel</a>
</div>
</div>

View file

@ -6,7 +6,7 @@
</rd-header> </rd-header>
<div class="row"> <div class="row">
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="!swarm"> <div class="col-lg-12 col-md-12 col-xs-12" ng-if="swarm_mode || !swarm">
<rd-widget> <rd-widget>
<rd-widget-header icon="fa-tachometer" title="Node info"></rd-widget-header> <rd-widget-header icon="fa-tachometer" title="Node info"></rd-widget-header>
<rd-widget-body classes="no-padding"> <rd-widget-body classes="no-padding">
@ -33,7 +33,7 @@
</rd-widget-body> </rd-widget-body>
</rd-widget> </rd-widget>
</div> </div>
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="swarm"> <div class="col-lg-12 col-md-12 col-xs-12" ng-if="swarm && !swarm_mode">
<rd-widget> <rd-widget>
<rd-widget-header icon="fa-tachometer" title="Cluster info"></rd-widget-header> <rd-widget-header icon="fa-tachometer" title="Cluster info"></rd-widget-header>
<rd-widget-body classes="no-padding"> <rd-widget-body classes="no-padding">
@ -60,6 +60,28 @@
</rd-widget-body> </rd-widget-body>
</rd-widget> </rd-widget>
</div> </div>
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="swarm && swarm_mode">
<rd-widget>
<rd-widget-header icon="fa-tachometer" title="Swarm info"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td colspan="2"><span class="small text-muted">This node is part of a Swarm cluster</span></td>
</tr>
<tr >
<td>Node role</td>
<td>{{ infoData.Swarm.ControlAvailable ? 'Manager' : 'Worker' }}</td>
</tr>
<tr ng-if="infoData.Swarm.ControlAvailable">
<td>Nodes in the cluster</td>
<td>{{ infoData.Swarm.Nodes }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div> </div>
<div class="row"> <div class="row">
@ -68,7 +90,7 @@
<rd-widget> <rd-widget>
<rd-widget-body> <rd-widget-body>
<div class="widget-icon blue pull-left"> <div class="widget-icon blue pull-left">
<i class="fa fa-tasks"></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 text-icon green-icon"></i>{{ containerData.running }} running</div>

View file

@ -14,6 +14,7 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
$scope.volumeData = { $scope.volumeData = {
total: 0 total: 0
}; };
$scope.swarm_mode = false;
function prepareContainerData(d, containersToHideLabels) { function prepareContainerData(d, containersToHideLabels) {
var running = 0; var running = 0;
@ -63,6 +64,9 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
function prepareInfoData(d) { function prepareInfoData(d) {
var info = d; var info = d;
$scope.infoData = info; $scope.infoData = info;
if ($scope.swarm && !_.startsWith(info.ServerVersion, 'swarm')) {
$scope.swarm_mode = true;
}
} }
function fetchDashboardData(containersToHideLabels) { function fetchDashboardData(containersToHideLabels) {

View file

@ -1,5 +1,6 @@
angular.module('dashboard') angular.module('dashboard')
.controller('MasterCtrl', ['$scope', '$cookieStore', 'Settings', 'Config', function ($scope, $cookieStore, Settings, Config) { .controller('MasterCtrl', ['$scope', '$cookieStore', 'Settings', 'Config', 'Info',
function ($scope, $cookieStore, Settings, Config, Info) {
/** /**
* Sidebar Toggle & Cookie Control * Sidebar Toggle & Cookie Control
*/ */
@ -9,7 +10,20 @@ angular.module('dashboard')
return window.innerWidth; return window.innerWidth;
}; };
$scope.config = Config; $scope.swarm_mode = false;
Config.$promise.then(function (c) {
$scope.swarm = c.swarm;
Info.get({}, function(d) {
if ($scope.swarm && !_.startsWith(d.ServerVersion, 'swarm')) {
$scope.swarm_mode = true;
$scope.swarm_manager = false;
if (d.Swarm.ControlAvailable) {
$scope.swarm_manager = true;
}
}
});
});
$scope.$watch($scope.getWidth, function(newValue, oldValue) { $scope.$watch($scope.getWidth, function(newValue, oldValue) {
if (newValue >= mobileView) { if (newValue >= mobileView) {

View file

@ -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)">Delete this image</button> <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>
</td> </td>
</tr> </tr>
<tr ng-if="image.Parent"> <tr ng-if="image.Parent">

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">Remove</button> <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>
</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

@ -72,7 +72,7 @@ function ($scope, $state, Config, Image, Messages) {
counter = counter + 1; counter = counter + 1;
Image.remove({id: i.Id}, function (d) { Image.remove({id: i.Id}, function (d) {
if (d[0].message) { if (d[0].message) {
$('#loadingViewSpinner').hide(); $('#loadImagesSpinner').hide();
Messages.error("Unable to remove image", {}, d[0].message); Messages.error("Unable to remove image", {}, d[0].message);
} else { } else {
Messages.send("Image deleted", i.Id); Messages.send("Image deleted", i.Id);

View file

@ -8,118 +8,56 @@
</rd-header> </rd-header>
<div class="row"> <div class="row">
<div class="col-lg-9 col-md-9 col-xs-9"> <div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-body>
<div class="widget-icon grey pull-left">
<i class="fa fa-sitemap"></i>
</div>
<div class="title">{{ network.Name }}</div>
<div class="comment">Name</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-3 col-md-3 col-xs-3">
<rd-widget>
<rd-widget-body>
<div class="widget-icon grey pull-left">
<i class="fa fa-cogs"></i>
</div>
<div class="title">
<div class="btn-group" role="group" aria-label="...">
<button class="btn btn-default" disabled>Connect container...</button>
<button class="btn btn-danger" ng-click="remove(id)">Remove</button>
</div>
</div>
<div class="comment">
Actions
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<rd-widget> <rd-widget>
<rd-widget-header icon="fa-sitemap" title="Network details"></rd-widget-header> <rd-widget-header icon="fa-sitemap" title="Network details"></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>Id</td> <td>Name</td>
<td>{{ network.Id }}</td> <td>{{ network.Name }}</td>
</tr> </tr>
<tr> <tr>
<td>Scope</td> <td>ID</td>
<td>{{ network.Scope }}</td> <td>
{{ 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>
</td>
</tr> </tr>
<tr> <tr>
<td>Driver</td> <td>Driver</td>
<td>{{ network.Driver }}</td> <td>{{ network.Driver }}</td>
</tr> </tr>
<tr> <tr>
<td>IPAM</td> <td>Scope</td>
<td> <td>{{ network.Scope }}</td>
<table class="table table-striped">
<tr>
<td>Driver</td>
<td>{{ network.IPAM.Driver }}</td>
</tr>
<tr>
<td>Subnet</td>
<td>{{ network.IPAM.Config[0].Subnet }}</td>
</tr>
<tr>
<td>Gateway</td>
<td>{{ network.IPAM.Config[0].Gateway }}</td>
</tr>
</table>
</td>
</tr> </tr>
<tr> <tr ng-if="network.IPAM.Config[0].Subnet">
<td>Containers</td> <td>Subnet</td>
<td> <td>{{ network.IPAM.Config[0].Subnet }}</td>
<table class="table table-striped" ng-repeat="(Id, container) in network.Containers">
<tr>
<td>Id</td>
<td><a ui-sref="container({id: Id})">{{ Id }}</a></td>
</tr>
<tr>
<td>EndpointID</td>
<td>{{ container.EndpointID}}</td>
</tr>
<tr>
<td>MacAddress</td>
<td>{{ container.MacAddress}}</td>
</tr>
<tr>
<td>IPv4Address</td>
<td>{{ container.IPv4Address}}</td>
</tr>
<tr>
<td>IPv6Address</td>
<td>{{ container.IPv6Address}}</td>
</tr>
<tr>
<td colspan="2">
<button ng-click="disconnect(network.Id, Id)" class="btn btn-danger">
Disconnect from network
</button>
</td>
</tr>
</table>
</td>
</tr> </tr>
<tr> <tr ng-if="network.IPAM.Config[0].Gateway">
<td>Options</td> <td>Gateway</td>
<td> <td>{{ network.IPAM.Config[0].Gateway }}</td>
<table role="table" class="table table-striped"> </tr>
<tr ng-repeat="(k, v) in network.Options"> </tbody>
<td>{{ k }}</td> </table>
<td>{{ v }}</td> </rd-widget-body>
</tr> </rd-widget>
</table> </div>
</td> </div>
<div class="row" ng-if="!(network.Options | emptyobject)">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-cogs" title="Network options"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr ng-repeat="(key, value) in network.Options">
<td>{{ key }}</td>
<td>{{ value }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View file

@ -1,20 +1,8 @@
angular.module('network', []) angular.module('network', [])
.controller('NetworkController', ['$scope', 'Network', 'Messages', '$state', '$stateParams', .controller('NetworkController', ['$scope', '$state', '$stateParams', 'Network', 'Messages',
function ($scope, Network, Messages, $state, $stateParams) { function ($scope, $state, $stateParams, Network, Messages) {
$scope.disconnect = function disconnect(networkId, containerId) { $scope.removeNetwork = function removeNetwork(networkId) {
$('#loadingViewSpinner').show();
Network.disconnect({id: $stateParams.id}, {Container: containerId}, function (d) {
$('#loadingViewSpinner').hide();
Messages.send("Container disconnected", containerId);
$state.go('network', {id: $stateParams.id}, {reload: true});
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to disconnect container");
});
};
$scope.remove = function remove(networkId) {
$('#loadingViewSpinner').show(); $('#loadingViewSpinner').show();
Network.remove({id: $stateParams.id}, function (d) { Network.remove({id: $stateParams.id}, function (d) {
if (d.message) { if (d.message) {

View file

@ -7,93 +7,134 @@
<rd-header-content>Networks</rd-header-content> <rd-header-content>Networks</rd-header-content>
</rd-header> </rd-header>
<div class="col-lg-12"> <div class="row">
<rd-widget> <div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget-header icon="fa-sitemap" title="Networks"> <rd-widget>
<div class="pull-right"> <rd-widget-header icon="fa-plus" title="Add a network">
<i id="loadNetworksSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i> </rd-widget-header>
</div> <rd-widget-body>
</rd-widget-header> <form class="form-horizontal">
<rd-widget-taskbar classes="col-lg-12"> <!-- name-input -->
<div class="pull-left"> <div class="form-group">
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount">Remove</button> <label for="network_name" class="col-sm-1 control-label text-left">Name</label>
<a class="btn btn-default" type="button" ui-sref="actions.create.network">Add network</a> <div class="col-sm-11">
</div> <input type="text" class="form-control" ng-model="config.Name" id="network_name" placeholder="e.g. myNetwork">
<div class="pull-right"> </div>
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" /> </div>
</div> <!-- !name-input -->
</rd-widget-taskbar> <!-- tag-note -->
<rd-widget-body classes="no-padding"> <div class="form-group" ng-if="swarm">
<div class="table-responsive"> <div class="col-sm-12">
<table class="table table-hover"> <span class="small text-muted">Note: The network will be created using the overlay driver and will allow containers to communicate across the hosts of your cluster.</span>
<thead> </div>
<tr> </div>
<th></th> <div class="form-group" ng-if="!swarm">
<th> <div class="col-sm-12">
<a ui-sref="networks" ng-click="order('Name')"> <span class="small text-muted">Note: The network will be created using the bridge driver.</span>
Name </div>
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span> </div>
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span> <!-- !tag-note -->
</a> <div class="form-group">
</th> <div class="col-sm-12">
<th> <button type="button" class="btn btn-default btn-sm" ng-disabled="!config.Name" ng-click="createNetwork()">Create</button>
<a ui-sref="networks" ng-click="order('Id')"> <button type="button" class="btn btn-default btn-sm" ui-sref="actions.create.network">Advanced settings...</button>
Id <i id="createNetworkSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
<span ng-show="sortType == 'Id' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span> </div>
<span ng-show="sortType == 'Id' && sortReverse" class="glyphicon glyphicon-chevron-up"></span> </div>
</a> </form>
</th> </rd-widget-body>
<th> </rd-widget>
<a ui-sref="networks" ng-click="order('Scope')"> </div>
Scope </div>
<span ng-show="sortType == 'Scope' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Scope' && sortReverse" class="glyphicon glyphicon-chevron-up"></span> <div class="row">
</a> <div class="col-lg-12 col-md-12 col-xs-12">
</th> <rd-widget>
<th> <rd-widget-header icon="fa-sitemap" title="Networks">
<a ui-sref="networks" ng-click="order('Driver')"> <div class="pull-right">
Driver <i id="loadNetworksSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
<span ng-show="sortType == 'Driver' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span> </div>
<span ng-show="sortType == 'Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span> </rd-widget-header>
</a> <rd-widget-taskbar classes="col-lg-12">
</th> <div class="pull-left">
<th> <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>
<a ui-sref="networks" ng-click="order('IPAM.Driver')"> </div>
IPAM Driver <div class="pull-right">
<span ng-show="sortType == 'IPAM.Driver' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span> <input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
<span ng-show="sortType == 'IPAM.Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span> </div>
</a> </rd-widget-taskbar>
</th> <rd-widget-body classes="no-padding">
<th> <div class="table-responsive">
<a ui-sref="networks" ng-click="order('IPAM.Config[0].Subnet')"> <table class="table table-hover">
IPAM Subnet <thead>
<span ng-show="sortType == 'IPAM.Config[0].Subnet' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span> <tr>
<span ng-show="sortType == 'IPAM.Config[0].Subnet' && sortReverse" class="glyphicon glyphicon-chevron-up"></span> <th></th>
</a> <th>
</th> <a ui-sref="networks" ng-click="order('Name')">
<th> Name
<a ui-sref="networks" ng-click="order('IPAM.Config[0].Gateway')"> <span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
IPAM Gateway <span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
<span ng-show="sortType == 'IPAM.Config[0].Gateway' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span> </a>
<span ng-show="sortType == 'IPAM.Config[0].Gateway' && sortReverse" class="glyphicon glyphicon-chevron-up"></span> </th>
</a> <th>
</th> <a ui-sref="networks" ng-click="order('Id')">
</tr> Id
</thead> <span ng-show="sortType == 'Id' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<tbody> <span ng-show="sortType == 'Id' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
<tr ng-repeat="network in ( state.filteredNetworks = (networks | filter:state.filter | orderBy:sortType:sortReverse))"> </a>
<td><input type="checkbox" ng-model="network.Checked" ng-change="selectItem(network)"/></td> </th>
<td><a ui-sref="network({id: network.Id})">{{ network.Name|truncate:40}}</a></td> <th>
<td>{{ network.Id }}</td> <a ui-sref="networks" ng-click="order('Scope')">
<td>{{ network.Scope }}</td> Scope
<td>{{ network.Driver }}</td> <span ng-show="sortType == 'Scope' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<td>{{ network.IPAM.Driver }}</td> <span ng-show="sortType == 'Scope' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
<td>{{ network.IPAM.Config[0].Subnet ? network.IPAM.Config[0].Subnet : '-' }}</td> </a>
<td>{{ network.IPAM.Config[0].Gateway ? network.IPAM.Config[0].Gateway : '-' }}</td> </th>
</tr> <th>
</tbody> <a ui-sref="networks" ng-click="order('Driver')">
</table> Driver
</div> <span ng-show="sortType == 'Driver' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
</rd-widget-body> <span ng-show="sortType == 'Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
<rd-widget> </a>
</th>
<th>
<a ui-sref="networks" ng-click="order('IPAM.Driver')">
IPAM Driver
<span ng-show="sortType == 'IPAM.Driver' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'IPAM.Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="networks" ng-click="order('IPAM.Config[0].Subnet')">
IPAM Subnet
<span ng-show="sortType == 'IPAM.Config[0].Subnet' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'IPAM.Config[0].Subnet' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="networks" ng-click="order('IPAM.Config[0].Gateway')">
IPAM Gateway
<span ng-show="sortType == 'IPAM.Config[0].Gateway' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'IPAM.Config[0].Gateway' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="network in ( state.filteredNetworks = (networks | filter:state.filter | orderBy:sortType:sortReverse))">
<td><input type="checkbox" ng-model="network.Checked" ng-change="selectItem(network)"/></td>
<td><a ui-sref="network({id: network.Id})">{{ network.Name|truncate:40}}</a></td>
<td>{{ network.Id }}</td>
<td>{{ network.Scope }}</td>
<td>{{ network.Driver }}</td>
<td>{{ network.IPAM.Driver }}</td>
<td>{{ network.IPAM.Config[0].Subnet ? network.IPAM.Config[0].Subnet : '-' }}</td>
<td>{{ network.IPAM.Config[0].Gateway ? network.IPAM.Config[0].Gateway : '-' }}</td>
</tr>
</tbody>
</table>
</div>
</rd-widget-body>
<rd-widget>
</div>
</div> </div>

View file

@ -7,16 +7,39 @@ function ($scope, $state, Network, Config, Messages) {
$scope.sortType = 'Name'; $scope.sortType = 'Name';
$scope.sortReverse = false; $scope.sortReverse = false;
$scope.formValues = { $scope.config = {
Subnet: '', Name: ''
Gateway: ''
}; };
$scope.config = { function prepareNetworkConfiguration() {
Name: '', var config = angular.copy($scope.config);
IPAM: { if ($scope.swarm) {
Config: [] config.Driver = 'overlay';
// Force IPAM Driver to 'default', should not be required.
// See: https://github.com/docker/docker/issues/25735
config.IPAM = {
Driver: 'default'
};
} }
return config;
}
$scope.createNetwork = function() {
$('#createNetworkSpinner').show();
var config = prepareNetworkConfiguration();
Network.create(config, function (d) {
if (d.message) {
$('#createNetworkSpinner').hide();
Messages.error('Unable to create network', {}, d.message);
} else {
Messages.send("Network created", d.Id);
$('#createNetworkSpinner').hide();
$state.go('networks', {}, {reload: true});
}
}, function (e) {
$('#createNetworkSpinner').hide();
Messages.error("Failure", e, 'Unable to create network');
});
}; };
$scope.order = function(sortType) { $scope.order = function(sortType) {
@ -72,5 +95,8 @@ function ($scope, $state, Network, Config, Messages) {
}); });
} }
fetchNetworks(); Config.$promise.then(function (c) {
$scope.swarm = c.swarm;
fetchNetworks();
});
}]); }]);

View file

@ -0,0 +1,150 @@
<rd-header>
<rd-header-title title="Service details">
<a data-toggle="tooltip" title="Refresh" ui-sref="service({id: service.Id})" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
Services > <a ui-sref="service({id: service.Id})">{{ service.Name }}</a>
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-list-alt" title="Service details"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Name</td>
<td ng-if="!service.EditName">
{{ service.Name }}
<a href="" data-toggle="tooltip" title="Edit service name" ng-click="service.EditName = true;"><i class="fa fa-edit"></i></a>
</td>
<td ng-if="service.EditName">
<input type="text" class="containerNameInput" ng-model="service.newServiceName">
<a class="interactive" ng-click="service.EditName = false;"><i class="fa fa-times"></i></a>
<a class="interactive" ng-click="renameService(service)"><i class="fa fa-check-square-o"></i></a>
</td>
</tr>
<tr>
<td>ID</td>
<td>
{{ 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>
</td>
</tr>
<tr>
<td>Scheduling mode</td>
<td>{{ service.Mode }}</td>
</tr>
<tr ng-if="service.Mode === 'replicated'">
<td>Replicas</td>
<td>
<span ng-if="service.Mode === 'replicated' && !service.Scale">
{{ 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>
</span>
<span ng-if="service.Mode === 'replicated' && service.Scale">
<input class="input-sm" type="number" ng-model="service.Replicas" />
<a class="interactive" ng-click="service.Scale = false;"><i class="fa fa-times"></i></a>
<a class="interactive" ng-click="scaleService(service)"><i class="fa fa-check-square-o"></i></a>
</span>
</td>
</tr>
<tr>
<td>Image</td>
<td>{{ service.Image }}</td>
</tr>
<tr ng-if="service.Ports">
<td>Published ports</td>
<td>
<div ng-repeat="mapping in service.Ports">
{{ mapping.TargetPort }} <i class="fa fa-long-arrow-right"></i> {{ mapping.PublishedPort }}
</div>
</td>
</tr>
<tr ng-if="service.Env">
<td>Env</td>
<td>
<table class="table table-bordered table-condensed">
<tr ng-repeat="var in service.Env">
<td>{{ var|key: '=' }}</td>
<td>{{ var|value: '=' }}</td>
</tr>
</table>
</td>
</tr>
<tr ng-if="service.Labels">
<td>Labels</td>
<td>
<table class="table table-bordered table-condensed">
<tr ng-repeat="(k, v) in service.Labels">
<td>{{ k }}</td>
<td>{{ v }}</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="tasks.length > 0">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Associated tasks"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>
<a ui-sref="service" ng-click="order('Status')">
Status
<span ng-show="sortType == 'Status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="service" ng-click="order('Slot')">
Slot
<span ng-show="sortType == 'Slot' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Slot' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-if="displayNode">
<a ui-sref="service" ng-click="order('Node')">
Node
<span ng-show="sortType == 'Node' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Node' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="service" ng-click="order('UpdatedAt')">
Last update
<span ng-show="sortType == 'UpdatedAt' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'UpdatedAt' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="task in (filteredTasks = ( tasks | orderBy:sortType:sortReverse))">
<td><a ui-sref="task({ id: task.Id })">{{ task.Id }}</a></td>
<td><span class="label label-{{ task.Status|taskstatusbadge }}">{{ task.Status }}</span></td>
<td>{{ task.Slot }}</td>
<td ng-if="displayNode">{{ task.Node }}</td>
<td>{{ task.Updated|getisodate }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>

View file

@ -0,0 +1,97 @@
angular.module('service', [])
.controller('ServiceController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Task', 'Node', 'Messages',
function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Messages) {
$scope.service = {};
$scope.tasks = [];
$scope.displayNode = false;
$scope.sortType = 'Status';
$scope.sortReverse = false;
$scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.renameService = function renameService(service) {
$('#loadServicesSpinner').show();
var serviceName = service.Name;
var config = ServiceHelper.serviceToConfig(service.Model);
config.Name = service.newServiceName;
Service.update({ id: service.Id, version: service.Version }, config, function (data) {
$('#loadServicesSpinner').hide();
Messages.send("Service successfully renamed", "New name: " + service.newServiceName);
$state.go('service', {id: service.Id}, {reload: true});
}, function (e) {
$('#loadServicesSpinner').hide();
service.EditName = false;
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() {
$('#loadingViewSpinner').show();
Service.remove({id: $stateParams.id}, function (d) {
if (d.message) {
$('#loadingViewSpinner').hide();
Messages.send("Error", {}, d.message);
} else {
$('#loadingViewSpinner').hide();
Messages.send("Service removed", $stateParams.id);
$state.go('services', {});
}
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to remove service");
});
};
function fetchServiceDetails() {
$('#loadingViewSpinner').show();
Service.get({id: $stateParams.id}, function (d) {
var service = new ServiceViewModel(d);
service.newServiceName = service.Name;
$scope.service = service;
Task.query({filters: {service: [service.Name]}}, function (tasks) {
Node.query({}, function (nodes) {
$scope.displayNode = true;
$scope.tasks = tasks.map(function (task) {
return new TaskViewModel(task, nodes);
});
$('#loadingViewSpinner').hide();
}, function (e) {
$('#loadingViewSpinner').hide();
$scope.tasks = tasks.map(function (task) {
return new TaskViewModel(task, null);
});
Messages.error("Failure", e, "Unable to retrieve node information");
});
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve tasks associated to the service");
});
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve service details");
});
}
fetchServiceDetails();
}]);

View file

@ -0,0 +1,78 @@
<rd-header>
<rd-header-title title="Service list">
<a data-toggle="tooltip" title="Refresh" ui-sref="services" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>Services</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-list-alt" title="Services">
<div class="pull-right">
<i id="loadServicesSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-lg-12 col-md-12 col-xs-12">
<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>
<a class="btn btn-default btn-responsive" type="button" ui-sref="actions.create.service">Add service</a>
</div>
<div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
</div>
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<th></th>
<th>
<a ui-sref="services" ng-click="order('Name')">
Name
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="services" ng-click="order('Image')">
Image
<span ng-show="sortType == 'Image' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Image' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="services" ng-click="order('Mode')">
Scheduling mode
<span ng-show="sortType == 'Mode' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Mode' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</thead>
<tbody>
<tr ng-repeat="service in (state.filteredServices = ( services | filter:state.filter | orderBy:sortType:sortReverse))">
<td><input type="checkbox" ng-model="service.Checked" ng-change="selectItem(service)"/></td>
<td><a ui-sref="service({id: service.Id})">{{ service.Name }}</a></td>
<td>{{ service.Image }}</td>
<td>
{{ service.Mode }}
<span ng-if="service.Mode === 'replicated' && !service.Scale">
<code data-toggle="tooltip" title="Replicas">{{ service.Replicas }}</code>
<a class="interactive" ng-click="service.Scale = true; service.ReplicaCount = service.Replicas;"><i class="fa fa-arrows-v" aria-hidden="true"></i> Scale</a>
</span>
<span ng-if="service.Mode === 'replicated' && service.Scale">
<input class="input-sm" type="number" ng-model="service.Replicas" />
<a class="interactive" ng-click="service.Scale = false;"><i class="fa fa-times"></i></a>
<a class="interactive" ng-click="scaleService(service)"><i class="fa fa-check-square-o"></i></a>
</span>
</td>
</tr>
</tbody>
</table>
</div>
</rd-widget-body>
<rd-widget>
</div>
</div>

View file

@ -0,0 +1,84 @@
angular.module('services', [])
.controller('ServicesController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Messages',
function ($scope, $stateParams, $state, Service, ServiceHelper, Messages) {
$scope.services = [];
$scope.state = {};
$scope.state.selectedItemCount = 0;
$scope.sortType = 'Name';
$scope.sortReverse = false;
$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('services', {}, {reload: true});
}, function (e) {
$('#loadServicesSpinner').hide();
service.Scale = false;
service.Replicas = service.ReplicaCount;
Messages.error("Failure", e, "Unable to scale service");
});
};
$scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
} else {
$scope.state.selectedItemCount--;
}
};
$scope.removeAction = function () {
$('#loadServicesSpinner').show();
var counter = 0;
var complete = function () {
counter = counter - 1;
if (counter === 0) {
$('#loadServicesSpinner').hide();
}
};
angular.forEach($scope.services, function (service) {
if (service.Checked) {
counter = counter + 1;
Service.remove({id: service.Id}, function (d) {
if (d.message) {
$('#loadServicesSpinner').hide();
Messages.error("Unable to remove service", {}, d[0].message);
} else {
Messages.send("Service deleted", service.Id);
var index = $scope.services.indexOf(service);
$scope.services.splice(index, 1);
}
complete();
}, function (e) {
Messages.error("Failure", e, 'Unable to remove service');
complete();
});
}
});
};
function fetchServices() {
$('#loadServicesSpinner').show();
Service.query({}, function (d) {
$scope.services = d.map(function (service) {
return new ServiceViewModel(service);
});
$('#loadServicesSpinner').hide();
}, function(e) {
$('#loadServicesSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve services");
});
}
fetchServices();
}]);

View file

@ -10,7 +10,7 @@
<rd-widget> <rd-widget>
<rd-widget-body> <rd-widget-body>
<div class="widget-icon grey pull-left"> <div class="widget-icon grey pull-left">
<i class="fa fa-tasks"></i> <i class="fa fa-server"></i>
</div> </div>
<div class="title">{{ container.Name|trimcontainername }}</div> <div class="title">{{ container.Name|trimcontainername }}</div>
<div class="comment"> <div class="comment">

View file

@ -16,13 +16,14 @@
<tbody> <tbody>
<tr> <tr>
<td>Nodes</td> <td>Nodes</td>
<td>{{ swarm.Nodes }}</td> <td ng-if="!swarm_mode">{{ swarm.Nodes }}</td>
<td ng-if="swarm_mode">{{ info.Swarm.Nodes }}</td>
</tr> </tr>
<tr> <tr ng-if="!swarm_mode">
<td>Images</td> <td>Images</td>
<td>{{ info.Images }}</td> <td>{{ info.Images }}</td>
</tr> </tr>
<tr> <tr ng-if="!swarm_mode">
<td>Swarm version</td> <td>Swarm version</td>
<td>{{ docker.Version|swarmversion }}</td> <td>{{ docker.Version|swarmversion }}</td>
</tr> </tr>
@ -30,27 +31,29 @@
<td>Docker API version</td> <td>Docker API version</td>
<td>{{ docker.ApiVersion }}</td> <td>{{ docker.ApiVersion }}</td>
</tr> </tr>
<tr> <tr ng-if="!swarm_mode">
<td>Strategy</td> <td>Strategy</td>
<td>{{ swarm.Strategy }}</td> <td>{{ swarm.Strategy }}</td>
</tr> </tr>
<tr> <tr>
<td>Total CPU</td> <td>Total CPU</td>
<td>{{ info.NCPU }}</td> <td ng-if="!swarm_mode">{{ info.NCPU }}</td>
<td ng-if="swarm_mode">{{ totalCPU }}</td>
</tr> </tr>
<tr> <tr>
<td>Total memory</td> <td>Total memory</td>
<td>{{ info.MemTotal|humansize }}</td> <td ng-if="!swarm_mode">{{ info.MemTotal|humansize }}</td>
<td ng-if="swarm_mode">{{ totalMemory|humansize }}</td>
</tr> </tr>
<tr> <tr ng-if="!swarm_mode">
<td>Operating system</td> <td>Operating system</td>
<td>{{ info.OperatingSystem }}</td> <td>{{ info.OperatingSystem }}</td>
</tr> </tr>
<tr> <tr ng-if="!swarm_mode">
<td>Kernel version</td> <td>Kernel version</td>
<td>{{ info.KernelVersion }}</td> <td>{{ info.KernelVersion }}</td>
</tr> </tr>
<tr> <tr ng-if="!swarm_mode">
<td>Go version</td> <td>Go version</td>
<td>{{ docker.GoVersion }}</td> <td>{{ docker.GoVersion }}</td>
</tr> </tr>
@ -60,8 +63,9 @@
</rd-widget> </rd-widget>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12"> <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" ng-if="!swarm_mode">
<rd-widget> <rd-widget>
<rd-widget-header icon="fa-hdd-o" title="Node status"></rd-widget-header> <rd-widget-header icon="fa-hdd-o" title="Node status"></rd-widget-header>
<rd-widget-body classes="no-padding"> <rd-widget-body classes="no-padding">
@ -126,4 +130,69 @@
</rd-widget-body> </rd-widget-body>
</rd-widget> </rd-widget>
</div> </div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" ng-if="swarm_mode">
<rd-widget>
<rd-widget-header icon="fa-hdd-o" title="Node status"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table table-striped">
<thead>
<tr>
<th>
<a ui-sref="swarm" ng-click="order('Name')">
Name
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('type')">
Role
<span ng-show="sortType == 'type' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'type' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('cpu')">
CPU
<span ng-show="sortType == 'cpu' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'cpu' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('memory')">
Memory
<span ng-show="sortType == 'memory' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'memory' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('Engine')">
Engine
<span ng-show="sortType == 'Engine' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Engine' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('Status')">
Status
<span ng-show="sortType == 'Status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="node in (state.filteredNodes = (nodes | filter:state.filter | orderBy:sortType:sortReverse))">
<td>{{ node.Description.Hostname }}</td>
<td>{{ node.Spec.Role }}</td>
<td>{{ node.Description.Resources.NanoCPUs / 1000000000 }}</td>
<td>{{ node.Description.Resources.MemoryBytes|humansize }}</td>
<td>{{ node.Description.Engine.EngineVersion }}</td>
<td><span class="label label-{{ node.Status.State|nodestatusbadge }}">{{ node.Status.State }}</span></td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div> </div>

View file

@ -1,62 +1,80 @@
angular.module('swarm', []) angular.module('swarm', [])
.controller('SwarmController', ['$scope', 'Info', 'Version', 'Settings', .controller('SwarmController', ['$scope', 'Info', 'Version', 'Node',
function ($scope, Info, Version, Settings) { function ($scope, Info, Version, Node) {
$scope.sortType = 'Name'; $scope.sortType = 'Name';
$scope.sortReverse = true; $scope.sortReverse = true;
$scope.info = {}; $scope.info = {};
$scope.docker = {}; $scope.docker = {};
$scope.swarm = {}; $scope.swarm = {};
$scope.swarm_mode = false;
$scope.totalCPU = 0;
$scope.totalMemory = 0;
$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;
}; };
Version.get({}, function (d) { Version.get({}, function (d) {
$scope.docker = d; $scope.docker = d;
}); });
Info.get({}, function (d) {
$scope.info = d; Info.get({}, function (d) {
extractSwarmInfo(d); $scope.info = d;
if (!_.startsWith(d.ServerVersion, 'swarm')) {
$scope.swarm_mode = true;
Node.query({}, function(d) {
$scope.nodes = d;
var CPU = 0, memory = 0;
angular.forEach(d, function(node) {
CPU += node.Description.Resources.NanoCPUs;
memory += node.Description.Resources.MemoryBytes;
});
$scope.totalCPU = CPU / 1000000000;
$scope.totalMemory = memory;
}); });
} else {
extractSwarmInfo(d);
}
});
function extractSwarmInfo(info) { function extractSwarmInfo(info) {
// Swarm info is available in SystemStatus object // Swarm info is available in SystemStatus object
var systemStatus = info.SystemStatus; var systemStatus = info.SystemStatus;
// Swarm strategy // Swarm strategy
$scope.swarm[systemStatus[1][0]] = systemStatus[1][1]; $scope.swarm[systemStatus[1][0]] = systemStatus[1][1];
// Swarm filters // Swarm filters
$scope.swarm[systemStatus[2][0]] = systemStatus[2][1]; $scope.swarm[systemStatus[2][0]] = systemStatus[2][1];
// Swarm node count // Swarm node count
var node_count = parseInt(systemStatus[3][1], 10); var node_count = parseInt(systemStatus[3][1], 10);
$scope.swarm[systemStatus[3][0]] = node_count; $scope.swarm[systemStatus[3][0]] = node_count;
$scope.swarm.Status = []; $scope.swarm.Status = [];
extractNodesInfo(systemStatus, node_count); extractNodesInfo(systemStatus, node_count);
} }
function extractNodesInfo(info, node_count) { function extractNodesInfo(info, node_count) {
// First information for node1 available at element #4 of SystemStatus // First information for node1 available at element #4 of SystemStatus
// The next 10 elements are information related to the node // The next 10 elements are information related to the node
var node_offset = 4; var node_offset = 4;
for (i = 0; i < node_count; i++) { for (i = 0; i < node_count; i++) {
extractNodeInfo(info, node_offset); extractNodeInfo(info, node_offset);
node_offset += 9; node_offset += 9;
} }
} }
function extractNodeInfo(info, offset) { function extractNodeInfo(info, offset) {
var node = {}; var node = {};
node.name = info[offset][0]; node.name = info[offset][0];
node.ip = info[offset][1]; node.ip = info[offset][1];
node.id = info[offset + 1][1]; node.id = info[offset + 1][1];
node.status = info[offset + 2][1]; node.status = info[offset + 2][1];
node.containers = info[offset + 3][1]; node.containers = info[offset + 3][1];
node.cpu = info[offset + 4][1].split('/')[1]; node.cpu = info[offset + 4][1].split('/')[1];
node.memory = info[offset + 5][1].split('/')[1]; node.memory = info[offset + 5][1].split('/')[1];
node.labels = info[offset + 6][1]; node.labels = info[offset + 6][1];
node.version = info[offset + 8][1]; node.version = info[offset + 8][1];
$scope.swarm.Status.push(node); $scope.swarm.Status.push(node);
} }
}]); }]);

View file

@ -0,0 +1,50 @@
<rd-header>
<rd-header-title title="Task details">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
Services > <a ui-sref="service({id: task.ServiceID})">{{ serviceName }}</a> > {{ task.ID }}
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Task status"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>ID</td>
<td>{{ task.ID }}</td>
</tr>
<tr>
<td>State</td>
<td><span class="label label-{{ task.Status.State|taskstatusbadge }}">{{ task.Status.State }}</span></td>
</tr>
<tr ng-if="task.Status.Err">
<td>Error message</td>
<td><code>{{ task.Status.Err }}</code></td>
</tr>
<tr>
<td>Image</td>
<td>{{ task.Spec.ContainerSpec.Image }}</td>
</tr>
<tr>
<td>Slot</td>
<td>{{ task.Slot }}</td>
</tr>
<tr>
<td>Created</td>
<td>{{ task.CreatedAt|getisodate }}</td>
</tr>
<tr ng-if="task.Status.ContainerStatus.ContainerID">
<td>Container ID</td>
<td>{{ task.Status.ContainerStatus.ContainerID }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>

View file

@ -0,0 +1,29 @@
angular.module('task', [])
.controller('TaskController', ['$scope', '$stateParams', '$state', 'Task', 'Service', 'Messages',
function ($scope, $stateParams, $state, Task, Service, Messages) {
$scope.task = {};
$scope.serviceName = 'service';
$scope.isTaskRunning = false;
function fetchTaskDetails() {
$('#loadingViewSpinner').show();
Task.get({id: $stateParams.id}, function (d) {
$scope.task = d;
fetchAssociatedServiceDetails(d.ServiceID);
$('#loadingViewSpinner').hide();
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve task details");
});
}
function fetchAssociatedServiceDetails(serviceId) {
Service.get({id: serviceId}, function (d) {
$scope.serviceName = d.Spec.Name;
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve associated service details");
});
}
fetchTaskDetails();
}]);

View file

@ -34,11 +34,17 @@
<rd-widget-header icon="fa-cogs" title="Configuration"></rd-widget-header> <rd-widget-header icon="fa-cogs" title="Configuration"></rd-widget-header>
<rd-widget-body classes="padding"> <rd-widget-body classes="padding">
<form class="form-horizontal"> <form class="form-horizontal">
<div class="form-group" ng-if="globalNetworkCount === 0"> <div class="form-group" ng-if="globalNetworkCount === 0 && !swarm_mode">
<div class="col-sm-12"> <div class="col-sm-12">
<span class="small text-muted">When using Swarm, we recommend deploying containers in a shared network. Looks like you don't have any shared network, head over the <a ui-sref="networks">networks view</a> to create one.</span> <span class="small text-muted">When using Swarm, we recommend deploying containers in a shared network. Looks like you don't have any shared network, head over the <a ui-sref="networks">networks view</a> to create one.</span>
</div> </div>
</div> </div>
<div class="form-group" ng-if="swarm_mode">
<div class="col-sm-12">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
<span class="small text-muted">App templates cannot be used with swarm-mode at the moment. You can still use them to quickly deploy containers to the Docker host.</span>
</div>
</div>
<!-- name-and-network-inputs --> <!-- name-and-network-inputs -->
<div class="form-group"> <div class="form-group">
<label for="image_registry" class="col-sm-2 control-label text-left">Name</label> <label for="image_registry" class="col-sm-2 control-label text-left">Name</label>

View file

@ -1,6 +1,6 @@
angular.module('templates', []) angular.module('templates', [])
.controller('TemplatesController', ['$scope', '$q', '$state', '$filter', 'Config', 'Container', 'ContainerHelper', 'Image', 'Volume', 'Network', 'Templates', 'Messages', .controller('TemplatesController', ['$scope', '$q', '$state', '$filter', 'Config', 'Info', 'Container', 'ContainerHelper', 'Image', 'Volume', 'Network', 'Templates', 'Messages',
function ($scope, $q, $state, $filter, Config, Container, ContainerHelper, Image, Volume, Network, Templates, Messages) { function ($scope, $q, $state, $filter, Config, Info, Container, ContainerHelper, Image, Volume, Network, Templates, Messages) {
$scope.templates = []; $scope.templates = [];
$scope.selectedTemplate = null; $scope.selectedTemplate = null;
$scope.formValues = { $scope.formValues = {
@ -165,6 +165,11 @@ function ($scope, $q, $state, $filter, Config, Container, ContainerHelper, Image
Config.$promise.then(function (c) { Config.$promise.then(function (c) {
$scope.swarm = c.swarm; $scope.swarm = c.swarm;
Info.get({}, function(info) {
if ($scope.swarm && !_.startsWith(info.ServerVersion, 'swarm')) {
$scope.swarm_mode = true;
}
});
var containersToHideLabels = c.hiddenLabels; var containersToHideLabels = c.hiddenLabels;
Network.query({}, function (d) { Network.query({}, function (d) {
var networks = d; var networks = d;

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">Remove</button> <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>
<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

@ -18,6 +18,24 @@ angular.module('portainer.filters', [])
} }
}; };
}) })
.filter('taskstatusbadge', function () {
'use strict';
return function (text) {
var status = _.toLower(text);
if (status.indexOf('new') !== -1 || status.indexOf('allocated') !== -1 ||
status.indexOf('assigned') !== -1 || status.indexOf('accepted') !== -1) {
return 'info';
} else if (status.indexOf('pending') !== -1) {
return 'warning';
} else if (status.indexOf('shutdown') !== -1 || status.indexOf('failed') !== -1 ||
status.indexOf('rejected') !== -1) {
return 'danger';
} else if (status.indexOf('complete') !== -1) {
return 'primary';
}
return 'success';
};
})
.filter('containerstatusbadge', function () { .filter('containerstatusbadge', function () {
'use strict'; 'use strict';
return function (text) { return function (text) {
@ -191,4 +209,10 @@ angular.module('portainer.filters', [])
return function (obj) { return function (obj) {
return _.isEmpty(obj); return _.isEmpty(obj);
}; };
})
.filter('ipaddress', function () {
'use strict';
return function (ip) {
return ip.slice(0, ip.indexOf('/'));
};
}); });

View file

@ -34,4 +34,18 @@ angular.module('portainer.helpers', [])
}); });
} }
}; };
}])
.factory('ServiceHelper', [function ServiceHelperFactory() {
'use strict';
return {
serviceToConfig: function(service) {
return {
Name: service.Spec.Name,
TaskTemplate: service.Spec.TaskTemplate,
Mode: service.Spec.Mode,
Networks: service.Spec.Networks,
EndpointSpec: service.Spec.EndpointSpec
};
}
};
}]); }]);

View file

@ -37,6 +37,25 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
} }
}); });
}]) }])
.factory('Service', ['$resource', 'Settings', function ServiceFactory($resource, Settings) {
'use strict';
// https://docs.docker.com/engine/reference/api/docker_remote_api_<%= remoteApiVersion %>/#/3-9-services
return $resource(Settings.url + '/services/:id/:action', {}, {
get: { method: 'GET', params: {id: '@id'} },
query: { method: 'GET', isArray: true },
create: { method: 'POST', params: {action: 'create'} },
update: { method: 'POST', params: {id: '@id', action: 'update', version: '@version'} },
remove: { method: 'DELETE', params: {id: '@id'} }
});
}])
.factory('Task', ['$resource', 'Settings', function TaskFactory($resource, Settings) {
'use strict';
// https://docs.docker.com/engine/reference/api/docker_remote_api_<%= remoteApiVersion %>/#/3-9-services
return $resource(Settings.url + '/tasks/:id', {}, {
get: { method: 'GET', params: {id: '@id'} },
query: { method: 'GET', isArray: true, params: {filters: '@filters'} }
});
}])
.factory('Exec', ['$resource', 'Settings', function ExecFactory($resource, Settings) { .factory('Exec', ['$resource', 'Settings', function ExecFactory($resource, Settings) {
'use strict'; 'use strict';
// https://docs.docker.com/engine/reference/api/docker_remote_api_<%= remoteApiVersion %>/#/exec-resize // https://docs.docker.com/engine/reference/api/docker_remote_api_<%= remoteApiVersion %>/#/exec-resize
@ -131,6 +150,22 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
get: {method: 'GET'} get: {method: 'GET'}
}); });
}]) }])
.factory('Node', ['$resource', 'Settings', function NodeFactory($resource, Settings) {
'use strict';
// https://docs.docker.com/engine/reference/api/docker_remote_api_<%= remoteApiVersion %>/#/3-7-nodes
return $resource(Settings.url + '/nodes', {}, {
query: {
method: 'GET', isArray: true
}
});
}])
.factory('Swarm', ['$resource', 'Settings', function SwarmFactory($resource, Settings) {
'use strict';
// https://docs.docker.com/engine/reference/api/docker_remote_api_<%= remoteApiVersion %>/#/3-8-swarm
return $resource(Settings.url + '/swarm', {}, {
get: {method: 'GET'}
});
}])
.factory('Auth', ['$resource', 'Settings', function AuthFactory($resource, Settings) { .factory('Auth', ['$resource', 'Settings', function AuthFactory($resource, Settings) {
'use strict'; 'use strict';
// http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#check-auth-configuration // http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#check-auth-configuration

View file

@ -8,6 +8,47 @@ function ImageViewModel(data) {
this.VirtualSize = data.VirtualSize; this.VirtualSize = data.VirtualSize;
} }
function TaskViewModel(data, node_data) {
this.Id = data.ID;
this.Created = data.CreatedAt;
this.Updated = data.UpdatedAt;
this.Slot = data.Slot;
this.Status = data.Status.State;
if (node_data) {
for (var i = 0; i < node_data.length; ++i) {
if (data.NodeID === node_data[i].ID) {
this.Node = node_data[i].Description.Hostname;
}
}
}
}
function ServiceViewModel(data) {
this.Model = data;
this.Id = data.ID;
this.Name = data.Spec.Name;
this.Image = data.Spec.TaskTemplate.ContainerSpec.Image;
this.Version = data.Version.Index;
if (data.Spec.Mode.Replicated) {
this.Mode = 'replicated' ;
this.Replicas = data.Spec.Mode.Replicated.Replicas;
} else {
this.Mode = 'global';
}
if (data.Spec.Labels) {
this.Labels = data.Spec.Labels;
}
if (data.Spec.TaskTemplate.ContainerSpec.Env) {
this.Env = data.Spec.TaskTemplate.ContainerSpec.Env;
}
if (data.Endpoint.Ports) {
this.Ports = data.Endpoint.Ports;
}
this.Checked = false;
this.Scale = false;
this.EditName = false;
}
function ContainerViewModel(data) { function ContainerViewModel(data) {
this.Id = data.Id; this.Id = data.Id;
this.Status = data.Status; this.Status = data.Status;

View file

@ -255,3 +255,19 @@ input[type="radio"] {
text-align: center; text-align: center;
font-size: 0.8em; font-size: 0.8em;
} }
.btn-responsive {
padding: 5px 10px;
font-size: 12px;
line-height: 1.5;
border-radius: 3px;
}
@media screen and (min-width: 1107px) {
.btn-responsive {
padding: 6px 12px;
font-size: 14px;
line-height: 1.42857143;
border-radius: 4px;
}
}

View file

@ -1,9 +1,9 @@
{ {
"name": "portainer", "name": "portainer",
"version": "1.8.1", "version": "1.9.0",
"homepage": "https://github.com/cloud-inovasi/portainer", "homepage": "https://github.com/portainer/portainer",
"authors": [ "authors": [
"Anthony Lapenna <anthony.lapenna@cloudinovasi.id>" "Anthony Lapenna <anthony.lapenna at gmail dot com>"
], ],
"description": "A web interface for the Docker Remote API.", "description": "A web interface for the Docker Remote API.",
"keywords": [ "keywords": [

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

View file

@ -1,4 +0,0 @@
FROM nginx:latest
COPY default.conf /etc/nginx/conf.d/default.conf
COPY users.htpasswd /etc/nginx/users.htpasswd

View file

@ -1,17 +0,0 @@
upstream portainer {
server portainer:9000;
}
server {
listen 80;
server_name localhost;
location / {
auth_basic "Docker UI";
auth_basic_user_file /etc/nginx/users.htpasswd;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://portainer;
}
}

View file

@ -1,10 +0,0 @@
portainer:
image: cloudinovasi/portainer
command: -e http://<SWARM_HOST>:<SWARM_PORT>
nginx:
build: .
links:
- portainer
ports:
- 80:80

View file

@ -1 +0,0 @@
user:{PLAIN}password

View file

@ -44,6 +44,9 @@
<li class="sidebar-list"> <li class="sidebar-list">
<a ui-sref="templates">App Templates <span class="menu-icon fa fa-rocket"></span></a> <a ui-sref="templates">App Templates <span class="menu-icon fa fa-rocket"></span></a>
</li> </li>
<li class="sidebar-list" ng-if="swarm_mode">
<a ui-sref="services">Services <span class="menu-icon fa fa-list-alt"></span></a>
</li>
<li class="sidebar-list"> <li class="sidebar-list">
<a ui-sref="containers">Containers <span class="menu-icon fa fa-server"></span></a> <a ui-sref="containers">Containers <span class="menu-icon fa fa-server"></span></a>
</li> </li>
@ -56,19 +59,22 @@
<li class="sidebar-list"> <li class="sidebar-list">
<a ui-sref="volumes">Volumes <span class="menu-icon fa fa-cubes"></span></a> <a ui-sref="volumes">Volumes <span class="menu-icon fa fa-cubes"></span></a>
</li> </li>
<li class="sidebar-list" ng-if="!config.swarm"> <li class="sidebar-list" ng-if="swarm_mode || !swarm">
<a ui-sref="events">Events <span class="menu-icon fa fa-history"></span></a> <a ui-sref="events">Events <span class="menu-icon fa fa-history"></span></a>
</li> </li>
<li class="sidebar-list" ng-if="config.swarm"> <li class="sidebar-list" ng-if="(swarm && !swarm_mode) || (swarm_mode && swarm_manager)">
<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="!config.swarm"> <li class="sidebar-list" ng-if="swarm_mode || !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-cogs"></span></a>
</li> </li>
</ul> </ul>
<div class="sidebar-footer"> <div class="sidebar-footer">
<div class="col-xs-12"> <div class="col-xs-12">
<a href="https://github.com/cloud-inovasi/portainer" target="_blank">Portainer {{ uiVersion }}</a> <a href="https://github.com/portainer/portainer" target="_blank">
<i class="fa fa-github" aria-hidden="true"></i>
Portainer {{ uiVersion }}
</a>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,19 +1,19 @@
{ {
"author": "Cloud Inovasi", "author": "Portainer.io",
"name": "portainer", "name": "portainer",
"homepage": "https://github.com/cloud-inovasi/portainer", "homepage": "http://portainer.io",
"version": "1.8.1", "version": "1.9.0",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git@github.com:cloud-inovasi/portainer.git" "url": "git@github.com:portainer/portainer.git"
}, },
"bugs": { "bugs": {
"url": "https://github.com/cloud-inovasi/portainer/issues" "url": "https://github.com/portainer/portainer/issues"
}, },
"licenses": [ "licenses": [
{ {
"type": "MIT", "type": "MIT",
"url": "https://raw.githubusercontent.com/cloud-inovasi/portainer/develop/LICENSE" "url": "https://raw.githubusercontent.com/portainer/portainer/develop/LICENSE"
} }
], ],
"engines": { "engines": {