Compare commits

..

22 Commits

Author SHA1 Message Date
Anthony Lapenna 0aa84422da chore(version): bump version number 2016-08-18 15:49:18 +12:00
Anthony Lapenna f744b4c234 fix(jshint): fix lint issue 2016-08-18 13:37:10 +12:00
Anthony Lapenna c2ffb73358 feat(image): use a dropdown box to list available registries when tagging an image 2016-08-18 08:51:35 +12:00
Anthony Lapenna ada649b05a Merge branch 'docs140-logo-flag' into internal 2016-08-17 21:53:36 +12:00
Anthony Lapenna 7e5a9313a0 docs(logo): add documentation about the --logo flag 2016-08-17 21:51:39 +12:00
Anthony Lapenna 62eda46bac Revert "refactor(container-creation): remove changes related to internal version"
This reverts commit 61d7b4f64c.
2016-08-17 19:35:41 +12:00
Anthony Lapenna 76c179948e Merge branch 'fix122-invalid-port-specification' into internal 2016-08-17 19:35:38 +12:00
Anthony Lapenna ff04044001 fix(container-creation): allow to specify an address in the host port binding 2016-08-17 19:30:21 +12:00
Anthony Lapenna a752e8bc54 Merge branch 'fix123-container-creation-missing-port-bindings' into internal 2016-08-17 19:06:17 +12:00
Anthony Lapenna f14cbc5967 fix(container-creation): fix unregistered ports bindings when creating a container 2016-08-17 18:57:38 +12:00
Anthony Lapenna 7ebf842601 Merge branch 'docs-reverse-proxy' into internal 2016-08-17 18:32:26 +12:00
Anthony Lapenna 8dcac532d6 docs(reverse-proxy): add reverse proxy instructions 2016-08-17 18:30:21 +12:00
Anthony Lapenna 39d3753acb Merge branch 'feat111-exec-protocol-awareness' into internal 2016-08-17 18:24:10 +12:00
Anthony Lapenna 1e13aa660e feat(console): add protocol awareness to switch between ws:// and wss:// 2016-08-17 18:22:31 +12:00
Anthony Lapenna 0bd599f98a fix(api): remove git conflict trace 2016-08-17 18:16:01 +12:00
Anthony Lapenna f6e05538d8 Revert "refactor(global): revert merge with internal (#133)"
This reverts commit eefa7ca138.
2016-08-17 18:09:55 +12:00
Anthony Lapenna 0acbc10095 Merge branch 'feat125-tag-image' into internal 2016-08-17 18:06:09 +12:00
Anthony Lapenna aa1017c260 feat(image): add the ability to tag an image 2016-08-17 18:04:29 +12:00
Anthony Lapenna 9cb7483e0a Merge branch 'refactor-remove-binary' into internal 2016-08-17 13:58:15 +12:00
Anthony Lapenna 7d4ae1a6fd refactor(api): remove the binary from versioning 2016-08-17 13:56:09 +12:00
Anthony Lapenna fa5c1d16b6 Merge branch 'refactor124-response-handlers' into internal 2016-08-17 13:53:57 +12:00
Anthony Lapenna c928af71a4 refactor(handlers): remove duplicated code 2016-08-17 13:42:33 +12:00
70 changed files with 1044 additions and 1388 deletions
+1 -1
View File
@@ -7,4 +7,4 @@ bower_components
*.iml
dist
dist/*
portainer-checksum.txt
ui-for-docker-checksum.txt
+1 -1
View File
@@ -1 +1 @@
portainer
dockerui
+2 -3
View File
@@ -1,9 +1,8 @@
FROM centurylink/ca-certs
FROM scratch
COPY dist /
VOLUME /data
EXPOSE 9000
ENTRYPOINT ["/portainer"]
ENTRYPOINT ["/ui-for-docker"]
+2 -2
View File
@@ -1,4 +1,4 @@
Portainer: Copyright (c) 2016 CloudInovasi
Cloudinovasi-ui: Copyright (c) 2016 Cloudinovasi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -16,7 +16,7 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
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
SOFTWARE.
SOFTWARE.
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (anthonylapenna at cloudinovasi dot id)
+1 -1
View File
@@ -1 +1 @@
web: portainer -p ":$PORT" -e "$DOCKER_ENDPOINT"
web: dockerui -p ":$PORT" -e "$DOCKER_ENDPOINT"
+47 -38
View File
@@ -1,42 +1,49 @@
# Portainer
# Cloudinovasi UI for Docker
Portainer is a web interface for the Docker remote API.
This UI is dedicated to CloudInovasi internal usage.
A fork of the amazing UI for Docker by Michael Crosby and Kevan Ahlquist (https://github.com/kevana/ui-for-docker) using the rdash-angular theme (https://github.com/rdash/rdash-angular).
![Dashboard](/dashboard.png)
UI For Docker is a web interface for the Docker Remote API. The goal is to provide a pure client side implementation so it is effortless to connect and manage docker.
## Goals
* Minimal dependencies - I really want to keep this project a pure html/js app.
* Consistency - The web UI should be consistent with the commands found on the docker CLI.
## Supported Docker versions
The following Docker versions are supported:
The current Docker version support policy is the following: `N` to `N-2` included where `N` is the latest version.
* full support for Docker 1.10, 1.11 and 1.12
* partial support for Docker 1.9 (some features won't be available)
At the moment, the following versions are supported: 1.9, 1.10 & 1.11.
## Run
### Quickstart
1. Run: `docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/portainer`
1. Run: `docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui`
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.
Bind mounting the Unix socket into the UI For Docker 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`.
By default UI For Docker 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
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H tcp://127.0.0.1:2375
```
```
# Connect to another unix socket:
$ docker run -d -p 9000:9000 cloudinovasi/portainer -H unix:///path/to/docker.sock
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H unix:///path/to/docker.sock
```
### Swarm support
@@ -47,17 +54,17 @@ You can access a specific view for you Swarm cluster by defining the `--swarm` f
```
# Connect to a tcp socket and enable Swarm:
$ docker run -d -p 9000:9000 cloudinovasi/portainer -H tcp://<SWARM_HOST>:<SWARM_PORT> --swarm
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -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.
### Change address/port Portainer is served on
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:
### Change address/port UI For Docker is served on
UI For Docker listens on port 9000 by default. If you run UI For Docker inside a container then you can bind the container's internal port to any external address and port:
```
# 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
# Expose UI For Docker 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/cloudinovasi-ui
```
### Access a Docker engine protected via TLS
@@ -67,13 +74,13 @@ Ensure that you have access to the CA, the cert and the public key used to acces
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
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -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
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -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.
@@ -85,10 +92,10 @@ 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"
$ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui --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.
The custom logo will replace the CloudInovasi logo in the UI.
### Hide containers with specific labels
@@ -103,60 +110,62 @@ $ docker run -d --label owner=acme nginx
You can hide it in the view by starting the ui with:
```
$ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/portainer -l owner=acme
$ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui -l owner=acme
```
### Custom Docker registries support
You can specify the support of others registries than DockerHub by using the `--registries` or `-r` options and specifying a registry using the format *REGISTRY_NAME=REGISTRY_ADDRESS*.
For example, if I want the registry 'myCustomRegistry' pointing to *myregistry.domain.com:5000* available in the UI:
```
$ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui -r myCustomRegistry=myregistry.domain.com:5000
```
### Reverse proxy configuration
Has been tested with Nginx 1.11.
Use the following configuration to host the UI at `myhost.mydomain.com/portainer`:
Use the following configuration to host the UI at `myhost.mydomain.com/dockerui`:
```nginx
upstream portainer {
upstream cloudinovasi-ui {
server ADDRESS:PORT;
}
server {
listen 80;
location /portainer/ {
location /dockerui/ {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://portainer/;
proxy_pass http://cloudinovasi-ui/;
}
location /portainer/ws/ {
location /dockerui/ws/ {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_pass http://portainer/ws/;
proxy_pass http://cloudinovasi-ui/ws/;
}
}
```
Replace `ADDRESS:PORT` with the Portainer container details.
### Host your own apps
You can specify an URL to your own templates (**Apps**) definitions using the `--templates` or `-t` flags.
By default, CloudInovasi templates will be used (https://raw.githubusercontent.com/cloud-inovasi/ui-templates/master/templates.json).
For more information about hosting your own template definition and the format, see: https://github.com/cloud-inovasi/ui-templates
Replace `ADDRESS:PORT` with the CloudInovasi UI container details.
### Available options
The following options are available for the `portainer` binary:
The following options are available for the `ui-for-docker` binary:
* `--host`, `-H`: Docker daemon endpoint (default: `"unix:///var/run/docker.sock"`)
* `--bind`, `-p`: Address and port to serve Portainer (default: `":9000"`)
* `--bind`, `-p`: Address and port to serve UI For Docker (default: `":9000"`)
* `--data`, `-d`: Path to the data folder (default: `"."`)
* `--assets`, `-a`: Path to the assets (default: `"."`)
* `--swarm`, `-s`: Swarm cluster support (default: `false`)
* `--registries`, `-r`: Available registries in the UI (format *REGISTRY_NAME=REGISTRY_ADDRESS*)
* `--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
+10 -13
View File
@@ -9,12 +9,11 @@ import (
type (
api struct {
endpoint *url.URL
bindAddress string
assetPath string
dataPath string
tlsConfig *tls.Config
templatesURL string
endpoint *url.URL
bindAddress string
assetPath string
dataPath string
tlsConfig *tls.Config
}
apiConfig struct {
@@ -27,7 +26,6 @@ type (
TLSCACertPath string
TLSCertPath string
TLSKeyPath string
TemplatesURL string
}
)
@@ -50,11 +48,10 @@ func newAPI(apiConfig apiConfig) *api {
}
return &api{
endpoint: endpointURL,
bindAddress: apiConfig.BindAddress,
assetPath: apiConfig.AssetPath,
dataPath: apiConfig.DataPath,
tlsConfig: tlsConfig,
templatesURL: apiConfig.TemplatesURL,
endpoint: endpointURL,
bindAddress: apiConfig.BindAddress,
assetPath: apiConfig.AssetPath,
dataPath: apiConfig.DataPath,
tlsConfig: tlsConfig,
}
}
-3
View File
@@ -25,9 +25,6 @@ func (a *api) newHandler(settings *Settings) http.Handler {
mux.HandleFunc("/settings", func(w http.ResponseWriter, r *http.Request) {
settingsHandler(w, r, settings)
})
mux.HandleFunc("/templates", func(w http.ResponseWriter, r *http.Request) {
templatesHandler(w, r, a.templatesURL)
})
return CSRFHandler(newCSRFWrapper(mux))
}
+14 -14
View File
@@ -1,4 +1,4 @@
package main // import "github.com/cloudinovasi/portainer"
package main // import "github.com/cloudinovasi/ui-for-docker"
import (
"gopkg.in/alecthomas/kingpin.v2"
@@ -6,20 +6,20 @@ import (
// main is the entry point of the program
func main() {
kingpin.Version("1.8.0")
kingpin.Version("1.7.0")
var (
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()
assets = kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String()
data = kingpin.Flag("data", "Path to the data").Default(".").Short('d').String()
tlsverify = kingpin.Flag("tlsverify", "TLS support").Default("false").Bool()
tlscacert = kingpin.Flag("tlscacert", "Path to the CA").Default("/certs/ca.pem").String()
tlscert = kingpin.Flag("tlscert", "Path to the TLS certificate file").Default("/certs/cert.pem").String()
tlskey = kingpin.Flag("tlskey", "Path to the TLS key").Default("/certs/key.pem").String()
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'))
endpoint = kingpin.Flag("host", "Dockerd endpoint").Default("unix:///var/run/docker.sock").Short('H').String()
addr = kingpin.Flag("bind", "Address and port to serve UI For Docker").Default(":9000").Short('p').String()
assets = kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String()
data = kingpin.Flag("data", "Path to the data").Default(".").Short('d').String()
tlsverify = kingpin.Flag("tlsverify", "TLS support").Default("false").Bool()
tlscacert = kingpin.Flag("tlscacert", "Path to the CA").Default("/certs/ca.pem").String()
tlscert = kingpin.Flag("tlscert", "Path to the TLS certificate file").Default("/certs/cert.pem").String()
tlskey = kingpin.Flag("tlskey", "Path to the TLS key").Default("/certs/key.pem").String()
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'))
registries = pairs(kingpin.Flag("registries", "Supported Docker registries").Short('r'))
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()
)
kingpin.Parse()
@@ -33,12 +33,12 @@ func main() {
TLSCACertPath: *tlscacert,
TLSCertPath: *tlscert,
TLSKeyPath: *tlskey,
TemplatesURL: *templates,
}
settings := &Settings{
Swarm: *swarm,
HiddenLabels: *labels,
Registries: *registries,
Logo: *logo,
}
+2 -1
View File
@@ -9,10 +9,11 @@ import (
type Settings struct {
Swarm bool `json:"swarm"`
HiddenLabels pairList `json:"hiddenLabels"`
Registries pairList `json:"registries"`
Logo string `json:"logo"`
}
// settingsHandler defines a handler function used to encode the configuration in JSON
// configurationHandler defines a handler function used to encode the configuration in JSON
func settingsHandler(w http.ResponseWriter, r *http.Request, s *Settings) {
json.NewEncoder(w).Encode(*s)
}
-27
View File
@@ -1,27 +0,0 @@
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
)
// templatesHandler defines a handler function used to retrieve the templates from a URL and put them in the response
func templatesHandler(w http.ResponseWriter, r *http.Request, templatesURL string) {
resp, err := http.Get(templatesURL)
if err != nil {
http.Error(w, fmt.Sprintf("Error making request to %s: %s", templatesURL, err.Error()), http.StatusInternalServerError)
log.Print(err)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
http.Error(w, "Error reading body from templates URL", http.StatusInternalServerError)
log.Print(err)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(body)
}
+6 -16
View File
@@ -1,13 +1,12 @@
angular.module('portainer', [
'portainer.templates',
angular.module('uifordocker', [
'uifordocker.templates',
'ui.bootstrap',
'ui.router',
'ui.select',
'ngCookies',
'ngSanitize',
'portainer.services',
'portainer.helpers',
'portainer.filters',
'uifordocker.services',
'uifordocker.filters',
'dashboard',
'container',
'containerConsole',
@@ -22,10 +21,7 @@ angular.module('portainer', [
'swarm',
'network',
'networks',
'createNetwork',
'templates',
'volumes',
'createVolume'])
'volumes'])
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', function ($stateProvider, $urlRouterProvider, $httpProvider) {
'use strict';
@@ -120,11 +116,6 @@ angular.module('portainer', [
templateUrl: 'app/components/network/network.html',
controller: 'NetworkController'
})
.state('templates', {
url: '/templates/',
templateUrl: 'app/components/templates/templates.html',
controller: 'TemplatesController'
})
.state('volumes', {
url: '/volumes/',
templateUrl: 'app/components/volumes/volumes.html',
@@ -163,5 +154,4 @@ angular.module('portainer', [
.constant('DOCKER_ENDPOINT', 'dockerapi')
.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('TEMPLATES_ENDPOINT', 'templates')
.constant('UI_VERSION', 'v1.8.0');
.constant('UI_VERSION', 'v1.7.0');
+138 -145
View File
@@ -7,19 +7,68 @@
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-6 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-body>
<div class="widget-icon grey pull-left">
<i class="fa fa-tasks"></i>
</div>
<div ng-if="!container.edit">
<div class="title">{{ container.Name|trimcontainername }}</div>
<div class="comment">
Name <a href="" ng-click="container.edit = true;"><i class="fa fa-edit"></i></a>
</div>
</div>
<div ng-if="container.edit">
<div class="title"><input type="text" class="containerNameInput" ng-model="container.newContainerName"></div>
<div class="comment">
Name
<a href="" ng-click="container.edit = false;"><i class="fa fa-times"></i></a>
<a href="" ng-click="renameContainer()"><i class="fa fa-check-square-o"></i></a>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-6 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-body>
<div ng-class="{true: 'widget-icon green pull-left', false: 'widget-icon red pull-left'}[container.State.Running]">
<i class="fa fa-heartbeat"></i>
</div>
<div class="title">{{ container.State|getstatetext }}</div>
<div class="comment">State</div>
</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-header icon="fa-cogs" title="Actions"></rd-widget-header>
<rd-widget-body classes="padding">
<div class="btn-group" role="group" aria-label="...">
<button class="btn btn-primary" ng-click="start()" ng-if="!container.State.Running"><i class="fa fa-play btn-ico" aria-hidden="true"></i>Start</button>
<button class="btn btn-danger" ng-click="stop()" ng-if="container.State.Running"><i class="fa fa-stop btn-ico" aria-hidden="true"></i>Stop</button>
<button class="btn btn-danger" ng-click="kill()" ng-if="container.State.Running"><i class="fa fa-bomb btn-ico" aria-hidden="true"></i>Kill</button>
<button class="btn btn-primary" ng-click="restart()" ng-if="container.State.Running"><i class="fa fa-refresh btn-ico" aria-hidden="true"></i>Restart</button>
<button class="btn btn-primary" ng-click="pause()" ng-if="container.State.Running && !container.State.Paused"><i class="fa fa-pause btn-ico" aria-hidden="true"></i>Pause</button>
<button class="btn btn-primary" ng-click="unpause()" ng-if="container.State.Paused"><i class="fa fa-play btn-ico" aria-hidden="true"></i>Resume</button>
<button class="btn btn-danger" ng-click="remove()" ng-disabled="container.State.Running"><i class="fa fa-trash btn-ico" aria-hidden="true"></i>Remove</button>
<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-primary" ng-click="commit()">Commit</button>
<button class="btn btn-primary" ng-click="start()" ng-disabled="container.State.Running">Start</button>
<button class="btn btn-primary" ng-click="stop()" ng-disabled="!container.State.Running">Stop</button>
<button class="btn btn-primary" ng-click="kill()" ng-disabled="!container.State.Running">Kill</button>
<button class="btn btn-primary" ng-click="restart()">Restart</button>
<button class="btn btn-primary" ng-click="pause()" ng-disabled="!container.State.Running && !container.State.Paused">Pause</button>
<button class="btn btn-primary" ng-click="unpause()" ng-disabled="!container.State.Paused">Unpause</button>
<button class="btn btn-danger" ng-click="remove()" ng-disabled="container.State.Running">Remove</button>
</div>
<div class="btn-group" role="group" aria-label="...">
<a class="btn btn-default" type="button" ui-sref="stats({id: container.Id})">Stats</a>
<a class="btn btn-default" type="button" ui-sref="logs({id: container.Id})">Logs</a>
<a class="btn btn-default" type="button" ui-sref="console({id: container.Id})">Console</a>
</div>
</div>
<div class="comment">
Actions
</div>
</rd-widget-body>
</rd-widget>
@@ -27,144 +76,44 @@
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<div class="col-lg-9">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Container status"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Name</td>
<td ng-if="!container.edit">
{{ container.Name|trimcontainername }}
<a href="" data-toggle="tooltip" title="Edit container name" ng-click="container.edit = true;"><i class="fa fa-edit"></i></a>
</td>
<td ng-if="container.edit">
<input type="text" class="containerNameInput" ng-model="container.newContainerName">
<a href="" ng-click="container.edit = false;"><i class="fa fa-times"></i></a>
<a href="" ng-click="renameContainer()"><i class="fa fa-check-square-o"></i></a>
</td>
</tr>
<tr ng-if="container.NetworkSettings.IPAddress">
<td>IP address</td>
<td>{{ container.NetworkSettings.IPAddress }}</td>
<td>Created</td>
<td>{{ container.Created|getisodate }}</td>
</tr>
<tr>
<td>Status</td>
<td>Path</td>
<td>{{ container.Path }}</td>
</tr>
<tr>
<td>Args</td>
<td>{{ container.Args.join(' ') || 'None' }}</td>
</tr>
<tr>
<td>Exposed Ports</td>
<td>
<i ng-class="{true: 'fa fa-heartbeat text-icon green-icon', false: 'fa fa-heartbeat text-icon red-icon'}[container.State.Running]"></i>
{{ container.State|getstatetext }} since {{ activityTime }}<span ng-if="!container.State.Running"> with exit code {{ container.State.ExitCode }}</span>
<ul>
<li ng-repeat="(k, v) in container.Config.ExposedPorts">{{ k }}</li>
</ul>
</td>
</tr>
<tr ng-if="container.State.Running">
<td>Start time</td>
<td>{{ container.State.StartedAt|getisodate }}</td>
</tr>
<tr ng-if="!container.State.Running">
<td>Finished</td>
<td>{{ container.State.FinishedAt|getisodate }}</td>
</tr>
<tr>
<td colspan="2">
<div class="btn-group" role="group" aria-label="...">
<a class="btn btn-outline-secondary" type="button" ui-sref="stats({id: container.Id})"><i class="fa fa-area-chart btn-ico" aria-hidden="true"></i>Stats</a>
<a class="btn btn-outline-secondary" type="button" ui-sref="logs({id: container.Id})"><i class="fa fa-exclamation-circle btn-ico" aria-hidden="true"></i>Logs</a>
<a class="btn btn-outline-secondary" type="button" ui-sref="console({id: container.Id})"><i class="fa fa-terminal btn-ico" aria-hidden="true"></i>Console</a>
</div>
</td>
</tr>
</tbody>
</table>
</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-header icon="fa-clone" title="Create image"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- tag-description -->
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">
You can create an image from this container, this allows you to backup important data or save
helpful configurations. You'll be able to spin up another container based on this image afterward.
</span>
</div>
</div>
<!-- !tag-description -->
<!-- name-and-registry-inputs -->
<div class="form-group">
<label for="image_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-7">
<input type="text" class="form-control" ng-model="config.Image" id="image_name" placeholder="e.g. myImage:myTag">
</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="config.Registry" id="image_registry" placeholder="optional">
</div>
</div>
<!-- !name-and-registry-inputs -->
<!-- tag-note -->
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">Note: if you don't specify the tag in the image name, <span class="label label-default">latest</span> will be used.</span>
</div>
</div>
<!-- !tag-note -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-default btn-sm" ng-disabled="!config.Image" ng-click="commit()">Create</button>
<i id="createImageSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
</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-header icon="fa-tasks" title="Container details"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Image</td>
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
</tr>
<tr ng-if="portBindings.length > 0">
<td>Port configuration</td>
<td>Environment</td>
<td>
<div ng-repeat="portMapping in portBindings">
{{ portMapping.container }} <i class="fa fa-long-arrow-right"></i> {{ portMapping.host }}
</div>
<ul>
<li ng-repeat="k in container.Config.Env">{{ k }}</li>
</ul>
</td>
</tr>
<tr>
<td>CMD</td>
<td><code>{{ container.Config.Cmd|command }}</code></td>
</tr>
<tr>
<td>ENV</td>
<td>
<table class="table table-bordered table-condensed">
<tr ng-repeat="var in container.Config.Env">
<td>{{ var|key: '=' }}</td>
<td>{{ var|value: '=' }}</td>
</tr>
</table>
</td>
</tr>
<tr ng-if="!(container.Config.Labels | emptyobject)">
<td>Labels</td>
<td>
<table class="table table-bordered table-condensed">
<table role="table" class="table">
<tr ng-repeat="(k, v) in container.Config.Labels">
<td>{{ k }}</td>
<td>{{ v }}</td>
@@ -172,33 +121,77 @@
</table>
</td>
</tr>
<tr>
<td>Publish all ports</td>
<td>{{ container.HostConfig.PublishAllPorts }}</td>
</tr>
<tr>
<td>Ports</td>
<td>
<ul>
<li ng-repeat="(containerport, hostports) in container.NetworkSettings.Ports">
{{ containerport }} =>
<span class="label label-default" style="margin-right: 5px;" ng-repeat="(k,v) in hostports">{{ v.HostIp }}:{{ v.HostPort }}</span>
</li>
</ul>
</td>
</tr>
<tr>
<td>Hostname</td>
<td>{{ container.Config.Hostname }}</td>
</tr>
<tr>
<td>IPAddress</td>
<td>{{ container.NetworkSettings.IPAddress }}</td>
</tr>
<tr>
<td>Cmd</td>
<td>{{ container.Config.Cmd }}</td>
</tr>
<tr>
<td>Entrypoint</td>
<td>{{ container.Config.Entrypoint.join(' ') }}</td>
</tr>
<tr>
<td>Bindings</td>
<td>
<ul>
<li ng-repeat="b in container.HostConfig.Binds">{{ b }}</li>
</ul>
</td>
</tr>
<tr>
<td>Volumes</td>
<td>{{ container.Volumes }}</td>
</tr>
<tr>
<td>SysInitpath</td>
<td>{{ container.SysInitPath }}</td>
</tr>
<tr>
<td>Image</td>
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="container.HostConfig.Binds.length > 0">
<div class="col-lg-12 col-md-12 col-xs-12">
<div class="col-lg-3">
<rd-widget>
<rd-widget-header icon="fa-cubes" title="Volumes"></rd-widget-header>
<rd-widget-header icon="fa-tasks" title="Container state details"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<thead>
<tr>
<th>Host</th>
<th>Container</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="vol in container.HostConfig.Binds">
<td>{{ vol|key: ':' }}</td>
<td>{{ vol|value: ':' }}</td>
<tr ng-repeat="(key, val) in container.State">
<td>{{key}}</td>
<td ng-if="key === 'StartedAt' || key === 'FinishedAt'">{{val|getisodate}}</td>
<td ng-if="key !== 'StartedAt' && key !== 'FinishedAt'">{{val}}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div>
+107 -56
View File
@@ -1,11 +1,13 @@
angular.module('container', [])
.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ImageHelper', 'Messages',
function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ImageHelper, Messages) {
$scope.activityTime = 0;
$scope.portBindings = [];
$scope.config = {
Image: '',
Registry: ''
.controller('ContainerController', ['$scope', '$stateParams', '$state', '$filter', 'Container', 'ContainerCommit', 'Image', 'Messages', '$timeout',
function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Image, Messages, $timeout) {
$scope.changes = [];
$scope.editEnv = false;
$scope.editPorts = false;
$scope.editBinds = false;
$scope.newCfg = {
Env: [],
Ports: {}
};
var update = function () {
@@ -15,38 +17,74 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
$scope.container.edit = false;
$scope.container.newContainerName = $filter('trimcontainername')(d.Name);
if (d.State.Running) {
$scope.activityTime = moment.duration(moment(d.State.StartedAt).utc().diff(moment().utc())).humanize();
} else {
$scope.activityTime = moment.duration(moment().utc().diff(moment(d.State.FinishedAt).utc())).humanize();
}
$scope.portBindings = [];
if (d.NetworkSettings.Ports) {
angular.forEach(Object.keys(d.NetworkSettings.Ports), function(portMapping) {
if (d.NetworkSettings.Ports[portMapping]) {
var mapping = {};
mapping.container = portMapping;
mapping.host = d.NetworkSettings.Ports[portMapping][0].HostIp + ':' + d.NetworkSettings.Ports[portMapping][0].HostPort;
$scope.portBindings.push(mapping);
}
// fill up env
if (d.Config.Env) {
$scope.newCfg.Env = d.Config.Env.map(function (entry) {
return {name: entry.split('=')[0], value: entry.split('=')[1]};
});
}
// fill up ports
$scope.newCfg.Ports = {};
angular.forEach(d.Config.ExposedPorts, function(i, port) {
if (d.HostConfig.PortBindings && port in d.HostConfig.PortBindings) {
$scope.newCfg.Ports[port] = d.HostConfig.PortBindings[port];
}
else {
$scope.newCfg.Ports[port] = [];
}
});
// fill up bindings
$scope.newCfg.Binds = [];
var defaultBinds = {};
angular.forEach(d.Config.Volumes, function(value, vol) {
defaultBinds[vol] = { ContPath: vol, HostPath: '', ReadOnly: false, DefaultBind: true };
});
angular.forEach(d.HostConfig.Binds, function(binding, i) {
var mountpoint = binding.split(':')[0];
var vol = binding.split(':')[1] || '';
var ro = binding.split(':').length > 2 && binding.split(':')[2] === 'ro';
var defaultBind = false;
if (vol === '') {
vol = mountpoint;
mountpoint = '';
}
if (vol in defaultBinds) {
delete defaultBinds[vol];
defaultBind = true;
}
$scope.newCfg.Binds.push({ ContPath: vol, HostPath: mountpoint, ReadOnly: ro, DefaultBind: defaultBind });
});
angular.forEach(defaultBinds, function(bind) {
$scope.newCfg.Binds.push(bind);
});
$('#loadingViewSpinner').hide();
}, function (e) {
if (e.status === 404) {
$('.detail').hide();
Messages.error("Not found", "Container not found.");
} else {
Messages.error("Failure", e.data);
}
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve container info");
});
};
$scope.start = function () {
$('#loadingViewSpinner').show();
Container.start({id: $scope.container.Id}, {}, function (d) {
Container.start({
id: $scope.container.Id,
HostConfig: $scope.container.HostConfig
}, function (d) {
update();
Messages.send("Container started", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to start container");
Messages.error("Failure", "Container failed to start." + e.data);
});
};
@@ -57,7 +95,7 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
Messages.send("Container stopped", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to stop container");
Messages.error("Failure", "Container failed to stop." + e.data);
});
};
@@ -68,26 +106,20 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
Messages.send("Container killed", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to kill container");
Messages.error("Failure", "Container failed to die." + e.data);
});
};
$scope.commit = function () {
$('#createImageSpinner').show();
var image = _.toLower($scope.config.Image);
var registry = _.toLower($scope.config.Registry);
var imageConfig = ImageHelper.createImageConfig(image, registry);
ContainerCommit.commit({id: $stateParams.id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) {
$('#createImageSpinner').hide();
$('#loadingViewSpinner').show();
ContainerCommit.commit({id: $stateParams.id, repo: $scope.container.Config.Image}, function (d) {
update();
Messages.send("Container commited", $stateParams.id);
}, function (e) {
$('#createImageSpinner').hide();
update();
Messages.error("Failure", e, "Unable to commit container");
Messages.error("Failure", "Container failed to commit." + e.data);
});
};
$scope.pause = function () {
$('#loadingViewSpinner').show();
Container.pause({id: $stateParams.id}, function (d) {
@@ -95,7 +127,7 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
Messages.send("Container paused", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to pause container");
Messages.error("Failure", "Container failed to pause." + e.data);
});
};
@@ -106,24 +138,19 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
Messages.send("Container unpaused", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to unpause container");
Messages.error("Failure", "Container failed to unpause." + e.data);
});
};
$scope.remove = function () {
$('#loadingViewSpinner').show();
Container.remove({id: $stateParams.id}, function (d) {
if (d.message) {
$('#loadingViewSpinner').hide();
Messages.send("Error", d.message);
}
else {
$state.go('containers', {}, {reload: true});
Messages.send("Container removed", $stateParams.id);
}
update();
$state.go('containers', {}, {reload: true});
Messages.send("Container removed", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to remove container");
Messages.error("Failure", "Container failed to remove." + e.data);
});
};
@@ -134,24 +161,48 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
Messages.send("Container restarted", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to restart container");
Messages.error("Failure", "Container failed to restart." + e.data);
});
};
$scope.hasContent = function (data) {
return data !== null && data !== undefined;
};
$scope.getChanges = function () {
$('#loadingViewSpinner').show();
Container.changes({id: $stateParams.id}, function (d) {
$scope.changes = d;
$('#loadingViewSpinner').hide();
});
};
$scope.renameContainer = function () {
Container.rename({id: $stateParams.id, 'name': $scope.container.newContainerName}, function (d) {
if (d.message) {
$scope.container.newContainerName = $scope.container.Name;
Messages.error("Unable to rename container", {}, d.message);
// #FIXME fix me later to handle http status to show the correct error message
Container.rename({id: $stateParams.id, 'name': $scope.container.newContainerName}, function (data) {
if (data.name) {
$scope.container.Name = data.name;
Messages.send("Container renamed", $stateParams.id);
} else {
$scope.container.Name = $scope.container.newContainerName;
Messages.send("Container successfully renamed", d.name);
$scope.container.newContainerName = $scope.container.Name;
Messages.error("Failure", "Container failed to rename.");
}
}, function (e) {
Messages.error("Failure", e, 'Unable to rename container');
});
$scope.container.edit = false;
};
$scope.addEntry = function (array, entry) {
array.push(entry);
};
$scope.rmEntry = function (array, entry) {
var idx = array.indexOf(entry);
array.splice(idx, 1);
};
$scope.toggleEdit = function() {
$scope.edit = !$scope.edit;
};
update();
$scope.getChanges();
}]);
@@ -1,6 +1,6 @@
angular.module('containerConsole', [])
.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Settings', 'Container', 'Exec', '$timeout', 'Messages',
function ($scope, $stateParams, Settings, Container, Exec, $timeout, Messages) {
.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Settings', 'Container', 'Exec', '$timeout', 'Messages', 'errorMsgFilter',
function ($scope, $stateParams, Settings, Container, Exec, $timeout, Messages, errorMsgFilter) {
$scope.state = {};
$scope.state.command = "bash";
$scope.connected = false;
@@ -33,10 +33,7 @@ function ($scope, $stateParams, Settings, Container, Exec, $timeout, Messages) {
};
Container.exec(execConfig, function(d) {
if (d.message) {
$('#loadConsoleSpinner').hide();
Messages.error("Error", {}, d.message);
} else {
if (d.Id) {
var execId = d.Id;
resizeTTY(execId, termHeight, termWidth);
var url = window.location.href.split('#')[0] + 'ws/exec?id=' + execId;
@@ -46,10 +43,13 @@ function ($scope, $stateParams, Settings, Container, Exec, $timeout, Messages) {
url = url.replace('http://', 'ws://');
}
initTerm(url, termHeight, termWidth);
} else {
$('#loadConsoleSpinner').hide();
Messages.error('Error', errorMsgFilter(d));
}
}, function (e) {
$('#loadConsoleSpinner').hide();
Messages.error("Failure", e, 'Unable to start an exec instance');
Messages.error("Failure", e.data);
});
};
@@ -66,11 +66,10 @@ function ($scope, $stateParams, Settings, Container, Exec, $timeout, Messages) {
function resizeTTY(execId, height, width) {
$timeout(function() {
Exec.resize({id: execId, height: height, width: width}, function (d) {
if (d.message) {
Messages.error('Error', {}, 'Unable to resize TTY');
var error = errorMsgFilter(d);
if (error) {
Messages.error('Error', 'Unable to resize TTY');
}
}, function (e) {
Messages.error("Failure", {}, 'Unable to resize TTY');
});
}, 2000);
@@ -13,8 +13,12 @@ function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container) {
$scope.container = d;
$('#loadingViewSpinner').hide();
}, function (e) {
if (e.status === 404) {
Messages.error("Not found", "Container not found.");
} else {
Messages.error("Failure", e.data);
}
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve container info");
});
function getLogs() {
+14 -19
View File
@@ -52,13 +52,6 @@
<span ng-show="sortType == 'Names' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="containers" 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 ng-if="state.displayIP">
<a ui-sref="containers" ng-click="order('IP')">
IP Address
@@ -68,16 +61,23 @@
</th>
<th ng-if="swarm">
<a ui-sref="containers" ng-click="order('Host')">
Host IP
Host
<span ng-show="sortType == 'Host' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Host' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="containers" ng-click="order('Ports')">
Exposed Ports
<span ng-show="sortType == 'Ports' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Ports' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
<a ui-sref="containers" 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="containers" ng-click="order('Command')">
Command
<span ng-show="sortType == 'Command' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Command' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
@@ -88,15 +88,10 @@
<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"><a ui-sref="container({id: container.Id})">{{ container|containername}}</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="swarm">{{ container.hostIP }}</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">
<i class="fa fa-external-link" aria-hidden="true"></i> {{ p.private }}
</a>
<span ng-if="container.Ports.length == 0" >-</span>
</td>
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
<td>{{ container.Command|truncate:60 }}</td>
</tr>
</tbody>
</table>
@@ -1,12 +1,12 @@
angular.module('containers', [])
.controller('ContainersController', ['$scope', 'Container', 'ContainerHelper', 'Info', 'Settings', 'Messages', 'Config',
function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config) {
.controller('ContainersController', ['$scope', 'Container', 'Info', 'Settings', 'Messages', 'Config', 'errorMsgFilter',
function ($scope, Container, Info, Settings, Messages, Config, errorMsgFilter) {
$scope.state = {};
$scope.state.displayAll = Settings.displayAll;
$scope.state.displayIP = false;
$scope.sortType = 'State';
$scope.sortReverse = false;
$scope.sortReverse = true;
$scope.state.selectedItemCount = 0;
$scope.order = function (sortType) {
@@ -14,13 +14,13 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config)
$scope.sortType = sortType;
};
var update = function (data, containersToHideLabels) {
var update = function (data) {
$('#loadContainersSpinner').show();
$scope.state.selectedItemCount = 0;
Container.query(data, function (d) {
var containers = d;
if (containersToHideLabels) {
containers = ContainerHelper.hideContainers(d, containersToHideLabels);
if (hiddenLabels) {
containers = hideContainers(d);
}
$scope.containers = containers.map(function (container) {
var model = new ContainerViewModel(container);
@@ -50,25 +50,37 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config)
if (c.Checked) {
counter = counter + 1;
if (action === Container.start) {
action({id: c.Id}, {}, function (d) {
Messages.send("Container " + msg, c.Id);
complete();
Container.get({id: c.Id}, function (d) {
c = d;
action({id: c.Id, HostConfig: c.HostConfig || {}}, function (d) {
Messages.send("Container " + msg, c.Id);
complete();
}, function (e) {
Messages.error("Failure", e.data);
complete();
});
}, function (e) {
Messages.error("Failure", e, "Unable to start container");
if (e.status === 404) {
$('.detail').hide();
Messages.error("Not found", "Container not found.");
} else {
Messages.error("Failure", e.data);
}
complete();
});
}
else if (action === Container.remove) {
action({id: c.Id}, function (d) {
if (d.message) {
Messages.send("Error", d.message);
var error = errorMsgFilter(d);
if (error) {
Messages.send("Error", "Unable to remove running container");
}
else {
Messages.send("Container " + msg, c.Id);
}
complete();
}, function (e) {
Messages.error("Failure", e, 'Unable to remove container');
Messages.error("Failure", e.data);
complete();
});
}
@@ -77,7 +89,7 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config)
Messages.send("Container " + msg, c.Id);
complete();
}, function (e) {
Messages.error("Failure", e, 'An error occured');
Messages.error("Failure", e.data);
complete();
});
@@ -130,6 +142,21 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config)
batch($scope.containers, Container.remove, "Removed");
};
var hideContainers = function (containers) {
return containers.filter(function (container) {
var filterContainer = false;
hiddenLabels.forEach(function(label, index) {
if (_.has(container.Labels, label.name) &&
container.Labels[label.name] === label.value) {
filterContainer = true;
}
});
if (!filterContainer) {
return container;
}
});
};
function retrieveSwarmHostsInfo(data) {
var swarm_hosts = {};
var systemStatus = data.SystemStatus;
@@ -147,15 +174,15 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config)
$scope.swarm = false;
Config.$promise.then(function (c) {
var containersToHideLabels = c.hiddenLabels;
hiddenLabels = c.hiddenLabels;
$scope.swarm = c.swarm;
if (c.swarm) {
Info.get({}, function (d) {
$scope.swarm_hosts = retrieveSwarmHostsInfo(d);
update({all: Settings.displayAll ? 1 : 0}, containersToHideLabels);
update({all: Settings.displayAll ? 1 : 0});
});
} else {
update({all: Settings.displayAll ? 1 : 0}, containersToHideLabels);
update({all: Settings.displayAll ? 1 : 0});
}
});
}]);
@@ -1,6 +1,6 @@
angular.module('createContainer', [])
.controller('CreateContainerController', ['$scope', '$state', 'Config', 'Container', 'Image', 'Volume', 'Network', 'Messages',
function ($scope, $state, Config, Container, Image, Volume, Network, Messages) {
.controller('CreateContainerController', ['$scope', '$state', 'Config', 'Container', 'Image', 'Volume', 'Network', 'Messages', 'errorMsgFilter',
function ($scope, $state, Config, Container, Image, Volume, Network, Messages, errorMsgFilter) {
$scope.state = {
alwaysPull: true
@@ -59,9 +59,14 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages) {
$scope.formValues.AvailableRegistries = c.registries;
Volume.query({}, function (d) {
$scope.availableVolumes = d.Volumes;
var persistedVolumes = d.Volumes.filter(function (volume) {
if (volume.Driver === 'local-persist') {
return volume;
}
});
$scope.availableVolumes = _.uniqBy(persistedVolumes, 'Name');
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve volumes");
Messages.error("Failure", e.data);
});
Network.query({}, function (d) {
@@ -79,51 +84,48 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages) {
}
$scope.availableNetworks = networks;
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve networks");
Messages.error("Failure", e.data);
});
});
// TODO: centralize, already present in templatesController
function createContainer(config) {
$('#createContainerSpinner').show();
Container.create(config, function (d) {
if (d.message) {
$('#createContainerSpinner').hide();
Messages.error('Error', {}, d.message);
} else {
Container.start({id: d.Id}, {}, function (cd) {
if (cd.message) {
$('#createContainerSpinner').hide();
Messages.error('Error', {}, cd.message);
} else {
$('#createContainerSpinner').hide();
Messages.send('Container Started', d.Id);
$state.go('containers', {}, {reload: true});
}
if (d.Id) {
var reqBody = config.HostConfig || {};
reqBody.id = d.Id;
Container.start(reqBody, function (cd) {
$('#createContainerSpinner').hide();
Messages.send('Container Started', d.Id);
$state.go('containers', {}, {reload: true});
}, function (e) {
$('#createContainerSpinner').hide();
Messages.error("Failure", e, 'Unable to start container');
Messages.error('Error', errorMsgFilter(e));
});
} else {
$('#createContainerSpinner').hide();
Messages.error('Error', errorMsgFilter(d));
}
}, function (e) {
$('#createContainerSpinner').hide();
Messages.error("Failure", e, 'Unable to create container');
Messages.error('Error', errorMsgFilter(e));
});
}
// TODO: centralize, already present in templatesController
function pullImageAndCreateContainer(config) {
$('#createContainerSpinner').show();
Image.create($scope.imageConfig, function (data) {
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
if (err) {
var detail = data[data.length - 1];
$('#createContainerSpinner').hide();
Messages.error('Error', {}, detail.error);
} else {
createContainer(config);
}
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
if (err) {
var detail = data[data.length - 1];
$('#createContainerSpinner').hide();
Messages.error('Error', detail.error);
} else {
createContainer(config);
}
}, function (e) {
$('#createContainerSpinner').hide();
Messages.error('Failure', e, 'Unable to pull image');
Messages.error('Error', 'Unable to pull image ' + image);
});
}
@@ -226,7 +228,7 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages) {
$scope.create = function () {
var config = prepareConfiguration();
$('#createContainerSpinner').show();
if ($scope.state.alwaysPull) {
pullImageAndCreateContainer(config);
} else {
@@ -26,7 +26,10 @@
</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">
<select class="selectpicker form-control" ng-model="formValues.Registry">
<option value="">Docker Hub</option>
<option ng-repeat="registry in formValues.AvailableRegistries" ng-value="registry.value">{{ registry.name }}</option>
</select>
</div>
<div class="col-sm-offset-1 col-sm-11">
<div class="checkbox">
@@ -107,6 +110,7 @@
<li class="clickable"><a data-target="#network" data-toggle="tab">Network</a></li>
<li class="clickable"><a data-target="#security" data-toggle="tab">Security/Host</a></li>
</ul>
<!-- tab-content -->
<div class="tab-content">
<!-- tab-command -->
@@ -211,28 +215,22 @@
<!-- volumes -->
<div class="form-group">
<label for="container_volumes" class="col-sm-1 control-label text-left">Volumes</label>
<div class="col-sm-11">
<div class="col-sm-11" ng-if="availableVolumes.length !== 0">
<span class="label label-default clickable" ng-click="addVolume()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> volume
</span>
</div>
<div class="col-sm-11" ng-if="availableVolumes.length === 0" style="margin-top: 5px;">
<span class="small text-muted">You don't have any persistent volumes. Head over the <a ui-sref="volumes">volumes view</a> to create one.</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.isPath" ng-click="resetVolumePath($index)">Path</span>
<select class="selectpicker form-control" ng-model="volume.name" ng-if="!volume.isPath">
<select class="selectpicker form-control" ng-model="volume.name">
<option selected disabled hidden value="">Select a volume</option>
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option>
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:50}}</option>
</select>
<input ng-if="volume.isPath" type="text" class="form-control" ng-model="volume.name" placeholder="e.g. /path/on/host">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">container</span>
@@ -1,74 +0,0 @@
angular.module('createNetwork', [])
.controller('CreateNetworkController', ['$scope', '$state', 'Messages', 'Network',
function ($scope, $state, Messages, Network) {
$scope.formValues = {
DriverOptions: [],
Subnet: '',
Gateway: ''
};
$scope.config = {
Driver: 'bridge',
CheckDuplicate: true,
Internal: false,
IPAM: {
Config: []
}
};
$scope.addDriverOption = function() {
$scope.formValues.DriverOptions.push({ name: '', value: '' });
};
$scope.removeDriverOption = function(index) {
$scope.formValues.DriverOptions.splice(index, 1);
};
function createNetwork(config) {
$('#createNetworkSpinner').show();
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');
});
}
function prepareIPAMConfiguration(config) {
if ($scope.formValues.Subnet) {
var ipamConfig = {};
ipamConfig.Subnet = $scope.formValues.Subnet;
if ($scope.formValues.Gateway) {
ipamConfig.Gateway = $scope.formValues.Gateway ;
}
config.IPAM.Config.push(ipamConfig);
}
}
function prepareDriverOptions(config) {
var options = {};
$scope.formValues.DriverOptions.forEach(function (option) {
options[option.name] = option.value;
});
config.Options = options;
}
function prepareConfiguration() {
var config = angular.copy($scope.config);
prepareIPAMConfiguration(config);
prepareDriverOptions(config);
return config;
}
$scope.create = function () {
var config = prepareConfiguration();
createNetwork(config);
};
}]);
@@ -1,95 +0,0 @@
<rd-header>
<rd-header-title title="Create network"></rd-header-title>
<rd-header-content>
Networks > Add network
</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="network_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="config.Name" id="network_name" placeholder="e.g. myNetwork">
</div>
</div>
<!-- !name-input -->
<!-- subnet-gateway-inputs -->
<div class="form-group">
<label for="network_subnet" class="col-sm-1 control-label text-left">Subnet</label>
<div class="col-sm-5">
<input type="text" class="form-control" ng-model="formValues.Subnet" id="network_subnet" placeholder="e.g. 172.20.0.0/16">
</div>
<label for="network_gateway" class="col-sm-1 control-label text-left">Gateway</label>
<div class="col-sm-5">
<input type="text" class="form-control" ng-model="formValues.Gateway" id="network_gateway" placeholder="e.g. 172.20.10.11">
</div>
</div>
<!-- !subnet-gateway-inputs -->
<!-- driver-input -->
<div class="form-group">
<label for="network_driver" class="col-sm-1 control-label text-left">Driver</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="config.Driver" id="network_driver" placeholder="e.g. driverName">
</div>
</div>
<!-- !driver-input -->
<!-- driver-options -->
<div class="form-group">
<label for="network_driveropts" class="col-sm-1 control-label text-left">Driver options</label>
<div class="col-sm-11">
<span class="label label-default clickable" ng-click="addDriverOption()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> driver option
</span>
</div>
<!-- driver-options-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
<div ng-repeat="option in formValues.DriverOptions" 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="option.name" placeholder="e.g. com.docker.network.bridge.enable_icc">
</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="option.value" placeholder="e.g. true">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeDriverOption($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !driver-options-input-list -->
</div>
<!-- !driver-options -->
<!-- internal -->
<div class="form-group">
<div class="col-sm-12">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="config.Internal"> Restrict external access to the network
</label>
</div>
</div>
</div>
<!-- !internal -->
</form>
</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="createNetworkSpinner" class="fa fa-cog fa-3x fa-spin" style="margin-bottom: 5px; display: none;"></i>
</div>
<button type="button" class="btn btn-default btn-lg" ng-click="create()">Create</button>
<a type="button" class="btn btn-default btn-lg" ui-sref="networks">Cancel</a>
</div>
</div>
@@ -1,56 +0,0 @@
angular.module('createVolume', [])
.controller('CreateVolumeController', ['$scope', '$state', 'Volume', 'Messages',
function ($scope, $state, Volume, Messages) {
$scope.formValues = {
DriverOptions: []
};
$scope.config = {
Driver: 'local'
};
$scope.addDriverOption = function() {
$scope.formValues.DriverOptions.push({ name: '', value: '' });
};
$scope.removeDriverOption = function(index) {
$scope.formValues.DriverOptions.splice(index, 1);
};
function createVolume(config) {
$('#createVolumeSpinner').show();
Volume.create(config, function (d) {
if (d.message) {
$('#createVolumeSpinner').hide();
Messages.error('Unable to create volume', {}, d.message);
} else {
Messages.send("Volume created", d.Name);
$('#createVolumeSpinner').hide();
$state.go('volumes', {}, {reload: true});
}
}, function (e) {
$('#createVolumeSpinner').hide();
Messages.error("Failure", e, 'Unable to create volume');
});
}
function prepareDriverOptions(config) {
var options = {};
$scope.formValues.DriverOptions.forEach(function (option) {
options[option.name] = option.value;
});
config.DriverOpts = options;
}
function prepareConfiguration() {
var config = angular.copy($scope.config);
prepareDriverOptions(config);
return config;
}
$scope.create = function () {
var config = prepareConfiguration();
createVolume(config);
};
}]);
@@ -1,72 +0,0 @@
<rd-header>
<rd-header-title title="Create volume"></rd-header-title>
<rd-header-content>
Volumes > Add volume
</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="volume_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="config.Name" id="volume_name" placeholder="e.g. myVolume">
</div>
</div>
<!-- !name-input -->
<!-- driver-input -->
<div class="form-group">
<label for="volume_driver" class="col-sm-1 control-label text-left">Driver</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="config.Driver" id="volume_driver" placeholder="e.g. driverName">
</div>
</div>
<!-- !driver-input -->
<!-- driver-options -->
<div class="form-group">
<label for="volume_driveropts" class="col-sm-1 control-label text-left">Driver options</label>
<div class="col-sm-11">
<span class="label label-default clickable" ng-click="addDriverOption()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> driver option
</span>
</div>
<!-- driver-options-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
<div ng-repeat="option in formValues.DriverOptions" 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="option.name" placeholder="e.g. mountpoint">
</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="option.value" placeholder="e.g. /path/on/host">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeDriverOption($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !driver-options-input-list -->
</div>
<!-- !driver-options -->
</form>
</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="createVolumeSpinner" class="fa fa-cog fa-3x fa-spin" style="margin-bottom: 5px; display: none;"></i>
</div>
<button type="button" class="btn btn-default btn-lg" ng-click="create()">Create</button>
<a type="button" class="btn btn-default btn-lg" ui-sref="volumes">Cancel</a>
</div>
</div>
@@ -1,6 +1,6 @@
angular.module('dashboard', [])
.controller('DashboardController', ['$scope', '$q', 'Config', 'Container', 'ContainerHelper', 'Image', 'Network', 'Volume', 'Info',
function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume, Info) {
.controller('DashboardController', ['$scope', '$q', 'Config', 'Container', 'Image', 'Network', 'Volume', 'Info',
function ($scope, $q, Config, Container, Image, Network, Volume, Info) {
$scope.containerData = {
total: 0
@@ -15,13 +15,13 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
total: 0
};
function prepareContainerData(d, containersToHideLabels) {
function prepareContainerData(d) {
var running = 0;
var stopped = 0;
var containers = d;
if (containersToHideLabels) {
containers = ContainerHelper.hideContainers(d, containersToHideLabels);
if (hiddenLabels) {
containers = hideContainers(d);
}
for (var i = 0; i < containers.length; i++) {
@@ -65,7 +65,7 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
$scope.infoData = info;
}
function fetchDashboardData(containersToHideLabels) {
function fetchDashboardData() {
$('#loadingViewSpinner').show();
$q.all([
Container.query({all: 1}).$promise,
@@ -74,7 +74,7 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
Network.query({}).$promise,
Info.get({}).$promise
]).then(function (d) {
prepareContainerData(d[0], containersToHideLabels);
prepareContainerData(d[0]);
prepareImageData(d[1]);
prepareVolumeData(d[2]);
prepareNetworkData(d[3]);
@@ -83,8 +83,24 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
});
}
var hideContainers = function (containers) {
return containers.filter(function (container) {
var filterContainer = false;
hiddenLabels.forEach(function(label, index) {
if (_.has(container.Labels, label.name) &&
container.Labels[label.name] === label.value) {
filterContainer = true;
}
});
if (!filterContainer) {
return container;
}
});
};
Config.$promise.then(function (c) {
$scope.swarm = c.swarm;
fetchDashboardData(c.hiddenLabels);
hiddenLabels = c.hiddenLabels;
fetchDashboardData();
});
}]);
+1 -1
View File
@@ -21,7 +21,7 @@ function ($scope, Settings, Messages, Events) {
$('#loadEventsSpinner').hide();
},
function (e) {
Messages.error("Unable to load events", e.data);
$('#loadEventsSpinner').hide();
Messages.error("Failure", e, "Unable to load events");
});
}]);
+6 -3
View File
@@ -48,7 +48,10 @@
</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="config.Registry" id="image_registry" placeholder="optional">
<select class="selectpicker form-control" ng-model="config.Registry">
<option value="">Docker Hub</option>
<option ng-repeat="registry in formValues.AvailableRegistries" ng-value="registry.value">{{ registry.name }}</option>
</select>
</div>
</div>
<!-- !name-and-registry-inputs -->
@@ -148,8 +151,8 @@
<td>
<table class="table table-bordered table-condensed">
<tr ng-repeat="var in image.ContainerConfig.Env">
<td>{{ var|key: '=' }}</td>
<td>{{ var|value: '=' }}</td>
<td>{{ var|key }}</td>
<td>{{ var|value }}</td>
</tr>
</table>
</td>
+47 -21
View File
@@ -1,12 +1,17 @@
angular.module('image', [])
.controller('ImageController', ['$scope', '$stateParams', '$state', 'Image', 'ImageHelper', 'Messages',
function ($scope, $stateParams, $state, Image, ImageHelper, Messages) {
.controller('ImageController', ['$scope', '$stateParams', '$state', 'Config', 'Image', 'Messages',
function ($scope, $stateParams, $state, Config, Image, Messages) {
$scope.RepoTags = [];
$scope.config = {
Image: '',
Registry: ''
};
$scope.formValues = {
AvailableRegistries: []
};
// Get RepoTags from the /images/query endpoint instead of /image/json,
// for backwards compatibility with Docker API versions older than 1.21
function getRepoTags(imageId) {
@@ -19,18 +24,31 @@ function ($scope, $stateParams, $state, Image, ImageHelper, Messages) {
});
}
function createImageConfig(imageName, registry) {
var imageNameAndTag = imageName.split(':');
var image = imageNameAndTag[0];
if (registry) {
image = registry + '/' + imageNameAndTag[0];
}
var imageConfig = {
repo: image,
tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest'
};
return imageConfig;
}
$scope.tagImage = function() {
$('#loadingViewSpinner').show();
var image = _.toLower($scope.config.Image);
var registry = _.toLower($scope.config.Registry);
var imageConfig = ImageHelper.createImageConfig(image, registry);
var imageConfig = createImageConfig(image, registry);
Image.tag({id: $stateParams.id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) {
Messages.send('Image successfully tagged');
$('#loadingViewSpinner').hide();
$state.go('image', {id: $stateParams.id}, {reload: true});
}, function(e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to tag image");
Messages.error("Unable to tag image", e.data);
});
};
@@ -38,14 +56,14 @@ function ($scope, $stateParams, $state, Image, ImageHelper, Messages) {
$('#loadingViewSpinner').show();
Image.push({tag: tag}, function (d) {
if (d[d.length-1].error) {
Messages.error("Unable to push image", {}, d[d.length-1].error);
Messages.error("Unable to push image", d[d.length-1].error);
} else {
Messages.send('Image successfully pushed');
}
$('#loadingViewSpinner').hide();
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to push image");
Messages.error("Unable to push image", e.data);
});
};
@@ -54,7 +72,7 @@ function ($scope, $stateParams, $state, Image, ImageHelper, Messages) {
Image.remove({id: id}, function (d) {
if (d[0].message) {
$('#loadingViewSpinner').hide();
Messages.error("Unable to remove image", {}, d[0].message);
Messages.error("Unable to remove image", d[0].message);
} else {
// If last message key is 'Deleted' or if it's 'Untagged' and there is only one tag associated to the image
// then assume the image is gone and send to images page
@@ -68,22 +86,30 @@ function ($scope, $stateParams, $state, Image, ImageHelper, Messages) {
}
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, 'Unable to remove image');
Messages.error("Unable to remove image", e.data);
});
};
$('#loadingViewSpinner').show();
Image.get({id: $stateParams.id}, function (d) {
$scope.image = d;
if (d.RepoTags) {
$scope.RepoTags = d.RepoTags;
} else {
getRepoTags(d.Id);
}
$('#loadingViewSpinner').hide();
$scope.exposedPorts = d.ContainerConfig.ExposedPorts ? Object.keys(d.ContainerConfig.ExposedPorts) : [];
$scope.volumes = d.ContainerConfig.Volumes ? Object.keys(d.ContainerConfig.Volumes) : [];
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve image info");
Config.$promise.then(function (c) {
$scope.formValues.AvailableRegistries = c.registries;
$('#loadingViewSpinner').show();
Image.get({id: $stateParams.id}, function (d) {
$scope.image = d;
if (d.RepoTags) {
$scope.RepoTags = d.RepoTags;
} else {
getRepoTags(d.Id);
}
$('#loadingViewSpinner').hide();
$scope.exposedPorts = d.ContainerConfig.ExposedPorts ? Object.keys(d.ContainerConfig.ExposedPorts) : [];
$scope.volumes = d.ContainerConfig.Volumes ? Object.keys(d.ContainerConfig.Volumes) : [];
}, function (e) {
if (e.status === 404) {
Messages.error("Unable to find image", $stateParams.id);
} else {
Messages.error("Unable to retrieve image info", e.data);
}
});
});
}]);
+7 -12
View File
@@ -22,7 +22,10 @@
</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="config.Registry" id="image_registry" placeholder="leave empty to use DockerHub">
<select class="selectpicker form-control" ng-model="config.Registry">
<option value="">Docker Hub</option>
<option ng-repeat="registry in availableRegistries" ng-value="registry.value">{{ registry.name }}</option>
</select>
</div>
</div>
<!-- !name-and-registry-inputs -->
@@ -88,13 +91,6 @@
<span ng-show="sortType == 'VirtualSize' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="images" ng-click="order('Created')">
Created
<span ng-show="sortType == 'Created' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Created' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
@@ -105,12 +101,11 @@
<span class="label label-primary image-tag" ng-repeat="tag in (image|repotags)">{{ tag }}</span>
</td>
<td>{{ image.VirtualSize|humansize }}</td>
<td>{{ image.Created|getisodatefromtimestamp }}</td>
</tr>
</tbody>
</table>
</div>
</rd-widget-body>
<rd-widget>
</div>
</div>
<rd-widget>
</div>
</div>
+7 -6
View File
@@ -40,21 +40,21 @@ function ($scope, $state, Config, Image, Messages) {
$scope.pullImage = function() {
$('#pullImageSpinner').show();
var image = _.toLower($scope.config.Image);
var registry = _.toLower($scope.config.Registry);
var registry = $scope.config.Registry;
var imageConfig = createImageConfig(image, registry);
Image.create(imageConfig, function (data) {
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
if (err) {
var detail = data[data.length - 1];
$('#pullImageSpinner').hide();
Messages.error('Error', {}, detail.error);
Messages.error('Error', detail.error);
} else {
$('#pullImageSpinner').hide();
$state.go('images', {}, {reload: true});
}
}, function (e) {
$('#pullImageSpinner').hide();
Messages.error("Failure", e, "Unable to pull image");
Messages.error('Error', 'Unable to pull image ' + image);
});
};
@@ -73,7 +73,7 @@ function ($scope, $state, Config, Image, Messages) {
Image.remove({id: i.Id}, function (d) {
if (d[0].message) {
$('#loadingViewSpinner').hide();
Messages.error("Unable to remove image", {}, d[0].message);
Messages.error("Unable to remove image", d[0].message);
} else {
Messages.send("Image deleted", i.Id);
var index = $scope.images.indexOf(i);
@@ -81,7 +81,7 @@ function ($scope, $state, Config, Image, Messages) {
}
complete();
}, function (e) {
Messages.error("Failure", e, 'Unable to remove image');
Messages.error("Unable to remove image", e.data);
complete();
});
}
@@ -95,8 +95,8 @@ function ($scope, $state, Config, Image, Messages) {
});
$('#loadImagesSpinner').hide();
}, function (e) {
Messages.error("Failure", e.data);
$('#loadImagesSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve images");
});
}
@@ -104,4 +104,5 @@ function ($scope, $state, Config, Image, Messages) {
$scope.availableRegistries = c.registries;
fetchImages();
});
}]);
+8 -13
View File
@@ -1,6 +1,6 @@
angular.module('network', [])
.controller('NetworkController', ['$scope', 'Network', 'Messages', '$state', '$stateParams',
function ($scope, Network, Messages, $state, $stateParams) {
.controller('NetworkController', ['$scope', 'Network', 'Messages', '$state', '$stateParams', 'errorMsgFilter',
function ($scope, Network, Messages, $state, $stateParams, errorMsgFilter) {
$scope.disconnect = function disconnect(networkId, containerId) {
$('#loadingViewSpinner').show();
@@ -10,24 +10,19 @@ function ($scope, Network, Messages, $state, $stateParams) {
$state.go('network', {id: $stateParams.id}, {reload: true});
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to disconnect container");
Messages.error("Failure", e.data);
});
};
$scope.remove = function remove(networkId) {
$('#loadingViewSpinner').show();
Network.remove({id: $stateParams.id}, function (d) {
if (d.message) {
$('#loadingViewSpinner').hide();
Messages.send("Error", {}, d.message);
} else {
$('#loadingViewSpinner').hide();
Messages.send("Network removed", $stateParams.id);
$state.go('networks', {});
}
$('#loadingViewSpinner').hide();
Messages.send("Network removed", "");
$state.go('networks', {});
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to remove network");
Messages.error("Failure", e.data);
});
};
@@ -36,7 +31,7 @@ function ($scope, Network, Messages, $state, $stateParams) {
$scope.network = d;
$('#loadingViewSpinner').hide();
}, function (e) {
Messages.error("Failure", e.data);
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve network info");
});
}]);
+60 -27
View File
@@ -7,6 +7,64 @@
<rd-header-content>Networks</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-plus" title="Add a shared network">
</rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- name-input -->
<div class="form-group">
<label for="network_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="config.Name" id="network_name" placeholder="e.g. myNetwork">
</div>
</div>
<!-- !name-input -->
<!-- advanced-settings-input -->
<div class="form-group">
<div class="col-sm-12">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="state.advancedSettings"> Show advanced settings
</label>
</div>
</div>
</div>
<!-- !advanced-settings-input -->
<!-- subnet-gateway-inputs -->
<div class="form-group" ng-if="state.advancedSettings">
<label for="network_subnet" class="col-sm-1 control-label text-left">Subnet</label>
<div class="col-sm-5">
<input type="text" class="form-control" ng-model="formValues.Subnet" id="network_subnet" placeholder="e.g. 172.20.0.0/16">
</div>
<label for="network_gateway" class="col-sm-1 control-label text-left">Gateway</label>
<div class="col-sm-5">
<input type="text" class="form-control" ng-model="formValues.Gateway" id="network_gateway" placeholder="e.g. 172.20.10.11">
</div>
</div>
<!-- !subnet-gateway-inputs -->
<!-- tag-note -->
<div class="form-group" ng-if="swarm">
<div class="col-sm-12">
<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>
</div>
</div>
<!-- !tag-note -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-default btn-sm" ng-disabled="!config.Name" ng-click="createNetwork()">Create</button>
<i id="createNetworkSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="col-lg-12">
<rd-widget>
<rd-widget-header icon="fa-sitemap" title="Networks">
@@ -17,7 +75,6 @@
<rd-widget-taskbar classes="col-lg-12">
<div class="pull-left">
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount">Remove</button>
<a class="btn btn-default" type="button" ui-sref="actions.create.network">Add network</a>
</div>
<div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
@@ -36,13 +93,6 @@
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="networks" ng-click="order('Id')">
Id
<span ng-show="sortType == 'Id' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Id' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="networks" ng-click="order('Scope')">
Scope
@@ -50,30 +100,16 @@
<span ng-show="sortType == 'Scope' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="networks" ng-click="order('Driver')">
Driver
<span ng-show="sortType == 'Driver' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</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
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
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>
@@ -84,10 +120,7 @@
<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>
+51 -9
View File
@@ -1,10 +1,10 @@
angular.module('networks', [])
.controller('NetworksController', ['$scope', '$state', 'Network', 'Config', 'Messages',
function ($scope, $state, Network, Config, Messages) {
.controller('NetworksController', ['$scope', '$state', 'Network', 'Config', 'Messages', 'errorMsgFilter',
function ($scope, $state, Network, Config, Messages, errorMsgFilter) {
$scope.state = {};
$scope.state.selectedItemCount = 0;
$scope.state.advancedSettings = false;
$scope.sortType = 'Name';
$scope.sortType = 'Scope';
$scope.sortReverse = false;
$scope.formValues = {
@@ -32,6 +32,44 @@ function ($scope, $state, Network, Config, Messages) {
}
};
function prepareIPAMConfiguration(config) {
if ($scope.formValues.Subnet) {
var ipamConfig = {};
ipamConfig.Subnet = $scope.formValues.Subnet;
if ($scope.formValues.Gateway) {
ipamConfig.Gateway = $scope.formValues.Gateway ;
}
config.IPAM.Config.push(ipamConfig);
}
}
function prepareNetworkConfiguration() {
var config = angular.copy($scope.config);
prepareIPAMConfiguration(config);
if ($scope.swarm) {
config.Driver = 'overlay';
}
return config;
}
$scope.createNetwork = function() {
$('#createNetworkSpinner').show();
var config = prepareNetworkConfiguration();
Network.create(config, function (d) {
if (d.Id) {
Messages.send("Network created", d.Id);
$('#createNetworkSpinner').hide();
$state.go('networks', {}, {reload: true});
} else {
$('#createNetworkSpinner').hide();
Messages.error('Unable to create network', errorMsgFilter(d));
}
}, function (e) {
$('#createNetworkSpinner').hide();
Messages.error('Unable to create network', e.data);
});
};
$scope.removeAction = function () {
$('#loadNetworksSpinner').show();
var counter = 0;
@@ -45,16 +83,17 @@ function ($scope, $state, Network, Config, Messages) {
if (network.Checked) {
counter = counter + 1;
Network.remove({id: network.Id}, function (d) {
if (d.message) {
Messages.send("Error", d.message);
var error = errorMsgFilter(d);
if (error) {
Messages.send("Error", "Unable to remove network with active endpoints");
} else {
Messages.send("Network removed", network.Id);
Messages.send("Network deleted", network.Id);
var index = $scope.networks.indexOf(network);
$scope.networks.splice(index, 1);
}
complete();
}, function (e) {
Messages.error("Failure", e, 'Unable to remove network');
Messages.error("Failure", e.data);
complete();
});
}
@@ -67,10 +106,13 @@ function ($scope, $state, Network, Config, Messages) {
$scope.networks = d;
$('#loadNetworksSpinner').hide();
}, function (e) {
Messages.error("Failure", e.data);
$('#loadNetworksSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve networks");
});
}
fetchNetworks();
Config.$promise.then(function (c) {
$scope.swarm = c.swarm;
fetchNetworks();
});
}]);
+3 -3
View File
@@ -112,7 +112,7 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
return d[key];
});
if (arr.join('').indexOf('no such id') !== -1) {
Messages.error('Unable to retrieve stats', {}, 'Is this container running?');
Messages.error('Unable to retrieve stats', 'Is this container running?');
return;
}
@@ -123,7 +123,7 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
updateNetworkChart(d);
timeout = $timeout(updateStats, 5000);
}, function () {
Messages.error('Unable to retrieve stats', {}, 'Is this container running?');
Messages.error('Unable to retrieve stats', 'Is this container running?');
timeout = $timeout(updateStats, 5000);
});
}
@@ -188,7 +188,7 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
Container.get({id: $stateParams.id}, function (d) {
$scope.container = d;
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve container info");
Messages.error("Failure", e.data);
});
$scope.getTop();
}]);
-81
View File
@@ -1,81 +0,0 @@
<rd-header>
<rd-header-title title="Application templates list">
<a data-toggle="tooltip" title="Refresh" ui-sref="templates" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>Templates</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-rocket" title="Available templates">
<div class="pull-right">
<i id="loadTemplatesSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
</div>
</rd-widget-header>
<rd-widget-body classes="padding">
<div class="template-list">
<div ng-repeat="tpl in templates" class="container-template hvr-grow" id="template_{{ $index }}" ng-click="selectTemplate($index)">
<img class="logo" ng-src="{{ tpl.logo }}" />
<div class="title">{{ tpl.title }}</div>
<div class="description">{{ tpl.description }}</div>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="selectedTemplate">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-cogs" title="Configuration"></rd-widget-header>
<rd-widget-body classes="padding">
<form class="form-horizontal">
<div class="form-group" ng-if="globalNetworkCount === 0">
<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>
</div>
</div>
<!-- name-and-network-inputs -->
<div class="form-group">
<label for="image_registry" class="col-sm-2 control-label text-left">Name</label>
<div class="col-sm-4">
<input type="text" class="form-control" ng-model="formValues.name" placeholder="e.g. web (optional)">
</div>
<label for="container_network" class="col-sm-2 control-label text-right">Network</label>
<div class="col-sm-4">
<select class="selectpicker form-control" ng-options="net.Name for net in availableNetworks" ng-model="formValues.network">
<option disabled hidden value="">Select a network</option>
</select>
</div>
</div>
<!-- !name-and-network-inputs -->
<div ng-repeat="var in selectedTemplate.env" ng-if="!var.set" class="form-group">
<label for="field_{{ $index }}" class="col-sm-2 control-label text-left">{{ var.label }}</label>
<div class="col-sm-10">
<select ng-if="!swarm && var.type === 'container'" ng-options="container|containername for container in runningContainers" class="selectpicker form-control" ng-model="var.value">
<option selected disabled hidden value="">Select a container</option>
</select>
<select ng-if="swarm && var.type === 'container'" ng-options="container|swarmcontainername for container in runningContainers" class="selectpicker form-control" ng-model="var.value">
<option selected disabled hidden value="">Select a container</option>
</select>
<input ng-if="!var.type || !var.type === 'container'" type="text" class="form-control" ng-model="var.value" id="field_{{ $index }}">
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="selectedTemplate">
<div class="col-lg-12 col-md-12 col-xs-12" style="text-align: center;">
<div>
<i id="createContainerSpinner" class="fa fa-cog fa-3x fa-spin" style="margin-bottom: 5px; display: none;"></i>
</div>
<button type="button" class="btn btn-default btn-lg" ng-disabled="!formValues.network" ng-click="createTemplate()">Create</button>
</div>
</div>
@@ -1,199 +0,0 @@
angular.module('templates', [])
.controller('TemplatesController', ['$scope', '$q', '$state', '$filter', 'Config', 'Container', 'ContainerHelper', 'Image', 'Volume', 'Network', 'Templates', 'Messages',
function ($scope, $q, $state, $filter, Config, Container, ContainerHelper, Image, Volume, Network, Templates, Messages) {
$scope.templates = [];
$scope.selectedTemplate = null;
$scope.formValues = {
network: "",
name: ""
};
var selectedItem = -1;
// TODO: centralize, already present in createContainerController
function createContainer(config) {
Container.create(config, function (d) {
if (d.message) {
$('#createContainerSpinner').hide();
Messages.error('Error', {}, d.message);
} else {
Container.start({id: d.Id}, {}, function (cd) {
if (cd.message) {
$('#createContainerSpinner').hide();
Messages.error('Error', {}, cd.message);
} else {
$('#createContainerSpinner').hide();
Messages.send('Container Started', d.Id);
$state.go('containers', {}, {reload: true});
}
}, function (e) {
$('#createContainerSpinner').hide();
Messages.error("Failure", e, 'Unable to start container');
});
}
}, function (e) {
$('#createContainerSpinner').hide();
Messages.error("Failure", e, 'Unable to create container');
});
}
// TODO: centralize, already present in createContainerController
function pullImageAndCreateContainer(imageConfig, containerConfig) {
Image.create(imageConfig, function (data) {
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
if (err) {
var detail = data[data.length - 1];
$('#createContainerSpinner').hide();
Messages.error("Error", {}, detail.error);
} else {
createContainer(containerConfig);
}
}, function (e) {
$('#createContainerSpinner').hide();
Messages.error("Failure", e, "Unable to pull image");
});
}
function getInitialConfiguration() {
return {
Env: [],
OpenStdin: false,
Tty: false,
ExposedPorts: {},
HostConfig: {
RestartPolicy: {
Name: 'no'
},
PortBindings: {},
Binds: [],
NetworkMode: $scope.formValues.network.Name,
Privileged: false
},
Volumes: {},
name: $scope.formValues.name
};
}
function createConfigFromTemplate(template) {
var containerConfig = getInitialConfiguration();
containerConfig.Image = template.image;
if (template.env) {
template.env.forEach(function (v) {
if (v.value || v.set) {
var val;
if (v.type && v.type === 'container') {
if ($scope.swarm && $scope.formValues.network.Scope === 'global') {
val = $filter('swarmcontainername')(v.value);
} else {
var container = v.value;
val = container.NetworkSettings.Networks[Object.keys(container.NetworkSettings.Networks)[0]].IPAddress;
}
} else {
val = v.set ? v.set : v.value;
}
containerConfig.Env.push(v.name + "=" + val);
}
});
}
if (template.ports) {
template.ports.forEach(function (p) {
containerConfig.ExposedPorts[p] = {};
containerConfig.HostConfig.PortBindings[p] = [{ HostPort: ""}];
});
}
return containerConfig;
}
function prepareVolumeQueries(template, containerConfig) {
var volumeQueries = [];
if (template.volumes) {
template.volumes.forEach(function (vol) {
volumeQueries.push(
Volume.create({}, function (d) {
if (d.message) {
Messages.error("Unable to create volume", {}, d.message);
} else {
Messages.send("Volume created", d.Name);
containerConfig.Volumes[vol] = {};
containerConfig.HostConfig.Binds.push(d.Name + ':' + vol);
}
}, function (e) {
Messages.error("Failure", e, "Unable to create volume");
}).$promise
);
});
}
return volumeQueries;
}
$scope.createTemplate = function() {
$('#createContainerSpinner').show();
var template = $scope.selectedTemplate;
var containerConfig = createConfigFromTemplate(template);
var imageConfig = {
fromImage: template.image.split(':')[0],
tag: template.image.split(':')[1] ? template.image.split(':')[1] : 'latest'
};
var createVolumeQueries = prepareVolumeQueries(template, containerConfig);
$q.all(createVolumeQueries).then(function (d) {
pullImageAndCreateContainer(imageConfig, containerConfig);
});
};
$scope.selectTemplate = function(id) {
$('#template_' + id).toggleClass("container-template--selected");
if (selectedItem === id) {
selectedItem = -1;
$scope.selectedTemplate = null;
} else {
$('#template_' + selectedItem).toggleClass("container-template--selected");
selectedItem = id;
$scope.selectedTemplate = $scope.templates[id];
}
};
function initTemplates() {
Templates.get(function (data) {
$scope.templates = data;
$('#loadTemplatesSpinner').hide();
}, function (e) {
$('#loadTemplatesSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve apps list");
});
}
Config.$promise.then(function (c) {
$scope.swarm = c.swarm;
var containersToHideLabels = c.hiddenLabels;
Network.query({}, function (d) {
var networks = d;
if ($scope.swarm) {
networks = d.filter(function (network) {
if (network.Scope === 'global') {
return network;
}
});
$scope.globalNetworkCount = networks.length;
networks.push({Scope: "local", Name: "bridge"});
networks.push({Scope: "local", Name: "host"});
networks.push({Scope: "local", Name: "none"});
} else {
$scope.formValues.network = _.find(networks, function(o) { return o.Name === "bridge"; });
}
$scope.availableNetworks = networks;
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve networks");
});
Container.query({all: 0}, function (d) {
var containers = d;
if (containersToHideLabels) {
containers = ContainerHelper.hideContainers(d, containersToHideLabels);
}
$scope.runningContainers = containers;
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve running containers");
});
initTemplates();
});
}]);
+36 -11
View File
@@ -7,6 +7,40 @@
<rd-header-content>Volumes</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-plus" title="Add a persistent volume">
</rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- name-input -->
<div class="form-group">
<label for="volume_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="config.Name" id="volume_name" placeholder="e.g. mysql-data">
</div>
</div>
<!-- !name-input -->
<!-- tag-note -->
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">Note: The volume will be created in our persisted storage and will be available across all the hosts of your cluster.</span>
</div>
</div>
<!-- !tag-note -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-default btn-sm" ng-disabled="!config.Name" ng-click="createVolume()">Create</button>
<i id="createVolumeSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="col-lg-12">
<rd-widget>
<rd-widget-header icon="fa-cubes" title="Volumes">
@@ -17,7 +51,6 @@
<rd-widget-taskbar classes="col-lg-12">
<div class="pull-left">
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount">Remove</button>
<a class="btn btn-default" type="button" ui-sref="actions.create.volume">Add volume</a>
</div>
<div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
@@ -43,13 +76,6 @@
<span ng-show="sortType == 'Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="volumes" ng-click="order('Mountpoint')">
Mountpoint
<span ng-show="sortType == 'Mountpoint' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Mountpoint' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
@@ -57,11 +83,10 @@
<td><input type="checkbox" ng-model="volume.Checked" ng-change="selectItem(volume)"/></td>
<td>{{ volume.Name|truncate:50 }}</td>
<td>{{ volume.Driver }}</td>
<td>{{ volume.Mountpoint }}</td>
</tr>
</tbody>
</table>
</div>
</rd-widget-body>
<rd-widget>
</div>
<rd-widget>
</div>
+32 -6
View File
@@ -1,9 +1,9 @@
angular.module('volumes', [])
.controller('VolumesController', ['$scope', '$state', 'Volume', 'Messages',
function ($scope, $state, Volume, Messages) {
.controller('VolumesController', ['$scope', '$state', 'Volume', 'Messages', 'errorMsgFilter',
function ($scope, $state, Volume, Messages, errorMsgFilter) {
$scope.state = {};
$scope.state.selectedItemCount = 0;
$scope.sortType = 'Name';
$scope.sortType = 'Driver';
$scope.sortReverse = true;
$scope.config = {
@@ -23,6 +23,32 @@ function ($scope, $state, Volume, Messages) {
}
};
function prepareVolumeConfiguration() {
var config = angular.copy($scope.config);
config.Driver = 'local-persist';
config.DriverOpts = {};
config.DriverOpts.mountpoint = '/volume/' + config.Name;
return config;
}
$scope.createVolume = function() {
$('#createVolumeSpinner').show();
var config = prepareVolumeConfiguration();
Volume.create(config, function (d) {
if (d.Name) {
Messages.send("Volume created", d.Name);
$('#createVolumeSpinner').hide();
$state.go('volumes', {}, {reload: true});
} else {
$('#createVolumeSpinner').hide();
Messages.error('Unable to create volume', errorMsgFilter(d));
}
}, function (e) {
$('#createVolumeSpinner').hide();
Messages.error('Unable to create volume', e.data);
});
};
$scope.removeAction = function () {
$('#loadVolumesSpinner').show();
var counter = 0;
@@ -41,7 +67,7 @@ function ($scope, $state, Volume, Messages) {
$scope.volumes.splice(index, 1);
complete();
}, function (e) {
Messages.error("Failure", e, "Unable to remove volume");
Messages.error("Failure", e.data);
complete();
});
}
@@ -51,11 +77,11 @@ function ($scope, $state, Volume, Messages) {
function fetchVolumes() {
$('#loadVolumesSpinner').show();
Volume.query({}, function (d) {
$scope.volumes = d.Volumes;
$scope.volumes = _.uniqBy(d.Volumes, 'Name');
$('#loadVolumesSpinner').hide();
}, function (e) {
Messages.error("Failure", e.data);
$('#loadVolumesSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve volumes");
});
}
fetchVolumes();
+1 -1
View File
@@ -1,5 +1,5 @@
angular
.module('portainer')
.module('uifordocker')
.directive('rdHeaderContent', function rdHeaderContent() {
var directive = {
requires: '^rdHeader',
+1 -1
View File
@@ -1,5 +1,5 @@
angular
.module('portainer')
.module('uifordocker')
.directive('rdHeaderTitle', function rdHeaderTitle() {
var directive = {
requires: '^rdHeader',
+1 -1
View File
@@ -1,5 +1,5 @@
angular
.module('portainer')
.module('uifordocker')
.directive('rdHeader', function rdHeader() {
var directive = {
scope: {
+1 -1
View File
@@ -1,5 +1,5 @@
angular
.module('portainer')
.module('uifordocker')
.directive('rdLoading', function rdLoading() {
var directive = {
restrict: 'AE',
+1 -1
View File
@@ -1,5 +1,5 @@
angular
.module('portainer')
.module('uifordocker')
.directive('rdWidgetBody', function rdWidgetBody() {
var directive = {
requires: '^rdWidget',
+1 -1
View File
@@ -1,5 +1,5 @@
angular
.module('portainer')
.module('uifordocker')
.directive('rdWidgetFooter', function rdWidgetFooter() {
var directive = {
requires: '^rdWidget',
+1 -1
View File
@@ -1,5 +1,5 @@
angular
.module('portainer')
.module('uifordocker')
.directive('rdWidgetHeader', function rdWidgetTitle() {
var directive = {
requires: '^rdWidget',
+1 -1
View File
@@ -1,5 +1,5 @@
angular
.module('portainer')
.module('uifordocker')
.directive('rdWidgetTaskbar', function rdWidgetTaskbar() {
var directive = {
requires: '^rdWidget',
+1 -1
View File
@@ -1,5 +1,5 @@
angular
.module('portainer')
.module('uifordocker')
.directive('rdWidget', function rdWidget() {
var directive = {
scope: {
+14 -9
View File
@@ -1,4 +1,4 @@
angular.module('portainer.filters', [])
angular.module('uifordocker.filters', [])
.filter('truncate', function () {
'use strict';
return function (text, length, end) {
@@ -176,19 +176,24 @@ angular.module('portainer.filters', [])
})
.filter('key', function () {
'use strict';
return function (pair, separator) {
return pair.slice(0, pair.indexOf(separator));
return function (pair) {
return pair.slice(0, pair.indexOf('='));
};
})
.filter('value', function () {
'use strict';
return function (pair, separator) {
return pair.slice(pair.indexOf(separator) + 1);
return function (pair) {
return pair.slice(pair.indexOf('=') + 1);
};
})
.filter('emptyobject', function () {
'use strict';
return function (obj) {
return _.isEmpty(obj);
.filter('errorMsg', function () {
return function (object) {
var idx = 0;
var msg = '';
while (object[idx] && typeof(object[idx]) === 'string') {
msg += object[idx];
idx++;
}
return msg;
};
});
-37
View File
@@ -1,37 +0,0 @@
angular.module('portainer.helpers', [])
.factory('ImageHelper', [function ImageHelperFactory() {
'use strict';
return {
createImageConfig: function(imageName, registry) {
var imageNameAndTag = imageName.split(':');
var image = imageNameAndTag[0];
if (registry) {
image = registry + '/' + imageNameAndTag[0];
}
var imageConfig = {
repo: image,
tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest'
};
return imageConfig;
}
};
}])
.factory('ContainerHelper', [function ContainerHelperFactory() {
'use strict';
return {
hideContainers: function(containers, containersToHideLabels) {
return containers.filter(function (container) {
var filterContainer = false;
containersToHideLabels.forEach(function(label, index) {
if (_.has(container.Labels, label.name) &&
container.Labels[label.name] === label.value) {
filterContainer = true;
}
});
if (!filterContainer) {
return container;
}
});
}
};
}]);
+7 -56
View File
@@ -1,18 +1,3 @@
function isJSONArray(jsonString) {
return Object.prototype.toString.call(jsonString) === '[object Array]';
}
function isJSON(jsonString) {
try {
var o = JSON.parse(jsonString);
if (o && typeof o === "object") {
return o;
}
}
catch (e) { }
return false;
}
// The Docker API often returns a list of JSON object.
// This handler wrap the JSON objects in an array.
// Used by the API in: Image push, Image create, Events query.
@@ -21,48 +6,14 @@ function jsonObjectsToArrayHandler(data) {
return angular.fromJson(str);
}
// The Docker API often returns an empty string or a valid JSON object on success (Docker 1.9 -> Docker 1.12).
// On error, it returns either an error message as a string (Docker < 1.12) or a JSON object with the field message
// container the error (Docker = 1.12)
// This handler ensure a valid JSON object is returned in any case.
// Used by the API in: container deletion, network deletion, network creation, volume creation,
// container exec, exec resize.
function genericHandler(data) {
var response = {};
// No data is returned when deletion is successful (Docker 1.9 -> 1.12)
if (!data) {
return response;
}
// A string is returned on failure (Docker < 1.12)
else if (!isJSON(data)) {
response.message = data;
}
// Docker 1.12 returns a valid JSON object when an error occurs
else {
response = angular.fromJson(data);
}
return response;
}
// Image delete API returns an array on success (Docker 1.9 -> Docker 1.12).
// On error, it returns either an error message as a string (Docker < 1.12) or a JSON object with the field message
// container the error (Docker = 1.12).
// This handler returns the original array on success or a newly created array containing
// only one JSON object with the field message filled with the error message on failure.
// Image delete API returns an array on success and an object on error.
// This handler creates an array from an object in case of error.
function deleteImageHandler(data) {
var response = [];
// A string is returned on failure (Docker < 1.12)
if (!isJSON(data)) {
response.push({message: data});
}
// A JSON object is returned on failure (Docker = 1.12)
else if (!isJSONArray) {
var json = angular.fromJson(data);
response.push(json);
}
// An array is returned on success (Docker 1.9 -> 1.12)
else {
response = angular.fromJson(data);
var response = angular.fromJson(data);
if (!Array.isArray(response)) {
var arr = [];
arr.push(response);
return arr;
}
return response;
}
+28 -45
View File
@@ -1,4 +1,4 @@
angular.module('portainer.services', ['ngResource', 'ngSanitize'])
angular.module('uifordocker.services', ['ngResource', 'ngSanitize'])
.factory('Container', ['$resource', 'Settings', function ContainerFactory($resource, Settings) {
'use strict';
// Resource for interacting with the docker containers
@@ -8,51 +8,46 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
}, {
query: {method: 'GET', params: {all: 0, action: 'json'}, isArray: true},
get: {method: 'GET', params: {action: 'json'}},
start: {method: 'POST', params: {id: '@id', action: 'start'}},
stop: {method: 'POST', params: {id: '@id', t: 5, action: 'stop'}},
restart: {method: 'POST', params: {id: '@id', t: 5, action: 'restart'}},
kill: {method: 'POST', params: {id: '@id', action: 'kill'}},
pause: {method: 'POST', params: {id: '@id', action: 'pause'}},
unpause: {method: 'POST', params: {id: '@id', action: 'unpause'}},
changes: {method: 'GET', params: {action: 'changes'}, isArray: true},
create: {method: 'POST', params: {action: 'create'}},
remove: {method: 'DELETE', params: {id: '@id', v: 0}},
rename: {method: 'POST', params: {id: '@id', action: 'rename'}, isArray: false},
stats: {method: 'GET', params: {id: '@id', stream: false, action: 'stats'}, timeout: 5000},
start: {
method: 'POST', params: {id: '@id', action: 'start'},
transformResponse: genericHandler
},
create: {
method: 'POST', params: {action: 'create'},
transformResponse: genericHandler
},
remove: {
method: 'DELETE', params: {id: '@id', v: 0},
transformResponse: genericHandler
},
rename: {
method: 'POST', params: {id: '@id', action: 'rename', name: '@name'},
transformResponse: genericHandler
},
exec: {
method: 'POST', params: {id: '@id', action: 'exec'},
transformResponse: genericHandler
}
exec: {method: 'POST', params: {id: '@id', action: 'exec'}}
});
}])
.factory('Exec', ['$resource', 'Settings', function ExecFactory($resource, Settings) {
'use strict';
// https://docs.docker.com/engine/reference/api/docker_remote_api_<%= remoteApiVersion %>/#/exec-resize
return $resource(Settings.url + '/exec/:id/:action', {}, {
resize: {
method: 'POST', params: {id: '@id', action: 'resize', h: '@height', w: '@width'},
transformResponse: genericHandler
}
resize: {method: 'POST', params: {id: '@id', action: 'resize', h: '@height', w: '@width'}}
});
}])
.factory('ContainerCommit', ['$resource', '$http', 'Settings', function ContainerCommitFactory($resource, $http, Settings) {
'use strict';
// http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#create-a-new-image-from-a-container-s-changes
return $resource(Settings.url + '/commit', {}, {
commit: {method: 'POST', params: {container: '@id', repo: '@repo', tag: '@tag'}}
});
return {
commit: function (params, callback) {
$http({
method: 'POST',
url: Settings.url + '/commit',
params: {
'container': params.id,
'tag': params.tag || null,
'repo': params.repo || null
},
data: params.config
}).success(callback).error(function (data, status, headers, config) {
console.log(error, data);
});
}
};
}])
.factory('ContainerLogs', ['$resource', '$http', 'Settings', function ContainerLogsFactory($resource, $http, Settings) {
'use strict';
@@ -152,8 +147,8 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
return $resource(Settings.url + '/networks/:id/:action', {id: '@id'}, {
query: {method: 'GET', isArray: true},
get: {method: 'GET'},
create: {method: 'POST', params: {action: 'create'}, transformResponse: genericHandler},
remove: { method: 'DELETE', transformResponse: genericHandler },
create: {method: 'POST', params: {action: 'create'}},
remove: {method: 'DELETE'},
connect: {method: 'POST', params: {action: 'connect'}},
disconnect: {method: 'POST', params: {action: 'disconnect'}}
});
@@ -164,18 +159,13 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
return $resource(Settings.url + '/volumes/:name/:action', {name: '@name'}, {
query: {method: 'GET'},
get: {method: 'GET'},
create: {method: 'POST', params: {action: 'create'}, transformResponse: genericHandler},
create: {method: 'POST', params: {action: 'create'}},
remove: {method: 'DELETE'}
});
}])
.factory('Config', ['$resource', 'CONFIG_ENDPOINT', function ConfigFactory($resource, CONFIG_ENDPOINT) {
return $resource(CONFIG_ENDPOINT).get();
}])
.factory('Templates', ['$resource', 'TEMPLATES_ENDPOINT', function TemplatesFactory($resource, TEMPLATES_ENDPOINT) {
return $resource(TEMPLATES_ENDPOINT, {}, {
get: {method: 'GET', isArray: true}
});
}])
.factory('Settings', ['DOCKER_ENDPOINT', 'DOCKER_PORT', 'UI_VERSION', function SettingsFactory(DOCKER_ENDPOINT, DOCKER_PORT, UI_VERSION) {
'use strict';
var url = DOCKER_ENDPOINT;
@@ -206,17 +196,10 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
}
});
},
error: function (title, e, fallbackText) {
console.log(JSON.stringify(e, null, 4));
var msg = fallbackText;
if (e.data && e.data.message) {
msg = e.data.message;
} else if (e.message) {
msg = e.message;
}
error: function (title, text) {
$.gritter.add({
title: $sanitize(title),
text: $sanitize(msg),
text: $sanitize(text),
time: 10000,
before_open: function () {
if ($('.gritter-item-wrapper').length === 4) {
-7
View File
@@ -19,13 +19,6 @@ function ContainerViewModel(data) {
this.Image = data.Image;
this.Command = data.Command;
this.Checked = false;
this.Ports = [];
for (var i = 0; i < data.Ports.length; ++i) {
var p = data.Ports[i];
if (p.PublicPort) {
this.Ports.push({ host: p.IP, private: p.PrivatePort, public: p.PublicPort });
}
}
}
function createEventDetails(event) {
-53
View File
@@ -202,56 +202,3 @@ input[type="radio"] {
.interactive {
cursor: pointer;
}
.action-group {
margin: 10px;
}
.btn-ico {
margin-right: 5px;
}
.template-list {
display: flex;
flex-wrap: wrap;
}
.container-template {
font-size: 1em;
width: 256px;
height: 128px;
margin: 10px;
padding: 10px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-radius: 10px;
cursor: pointer;
border: 2px solid #f6f6f6;
color: #30426a;
}
.container-template--selected {
background-color: #f6f6f6;
color: #2d3e63;
}
.container-template:hover {
background-color: #f6f6f6;
color: #2d3e63;
}
.container-template .logo {
max-width: 48px;
max-height: 48px;
}
.container-template .title {
text-align: center;
}
.container-template .description {
text-align: center;
font-size: 0.8em;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 180 KiB

+8 -9
View File
@@ -1,18 +1,18 @@
{
"name": "portainer",
"version": "1.8.0",
"homepage": "https://github.com/cloud-inovasi/portainer",
"name": "uifordocker",
"version": "1.7.0",
"homepage": "https://github.com/kevana/ui-for-docker",
"authors": [
"Michael Crosby <crosbymichael@gmail.com>",
"Kevan Ahlquist <ahlquistkd@gmail.com>",
"Anthony Lapenna <anthony.lapenna@cloudinovasi.id>"
],
"description": "A web interface for the Docker Remote API.",
"keywords": [
"docker",
"ui",
"api",
"portainer",
"uifordocker",
"dockerui"
"dockerui",
"docker",
"api"
],
"license": "MIT",
"ignore": [
@@ -34,7 +34,6 @@
"angular-ui-select": "~0.17.1",
"bootstrap": "~3.3.6",
"font-awesome": "~4.6.3",
"Hover": "2.0.2",
"jquery": "1.11.1",
"jquery.gritter": "1.7.4",
"lodash": "4.12.0",
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 55 KiB

+3 -3
View File
@@ -1,5 +1,5 @@
upstream portainer {
server portainer:9000;
upstream dockerui {
server dockerui:9000;
}
server {
@@ -12,6 +12,6 @@ server {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://portainer;
proxy_pass http://dockerui;
}
}
+3 -3
View File
@@ -1,10 +1,10 @@
portainer:
image: cloudinovasi/portainer
dockerui:
image: cloudinovasi/ui-for-docker
command: -e http://<SWARM_HOST>:<SWARM_PORT>
nginx:
build: .
links:
- portainer
- dockerui
ports:
- 80:80
+15 -16
View File
@@ -87,13 +87,12 @@ module.exports = function (grunt) {
'bower_components/font-awesome/css/font-awesome.min.css',
'bower_components/rdash-ui/dist/css/rdash.min.css',
'bower_components/angular-ui-select/dist/select.min.css',
'bower_components/xterm.js/src/xterm.css',
'bower_components/Hover/css/hover-min.css'
'bower_components/xterm.js/src/xterm.css'
]
},
clean: {
all: ['<%= distdir %>/*'],
app: ['<%= distdir %>/*', '!<%= distdir %>/portainer'],
app: ['<%= distdir %>/*', '!<%= distdir %>/ui-for-docker'],
tmpl: ['<%= distdir %>/templates']
},
copy: {
@@ -253,35 +252,35 @@ module.exports = function (grunt) {
},
shell: {
buildImage: {
command: 'docker build --rm -t portainer .'
command: 'docker build --rm -t ui-for-docker .'
},
buildBinary: {
command: [
'docker run --rm -v $(pwd)/api:/src centurylink/golang-builder',
'shasum api/portainer > portainer-checksum.txt',
'shasum api/ui-for-docker > ui-for-docker-checksum.txt',
'mkdir -p dist',
'mv api/portainer dist/'
'mv api/ui-for-docker dist/'
].join(' && ')
},
run: {
command: [
'docker stop portainer',
'docker rm portainer',
'docker run --privileged -d -p 9000:9000 -v /tmp/portainer:/data -v /var/run/docker.sock:/var/run/docker.sock --name portainer portainer -d /data'
'docker stop ui-for-docker',
'docker rm ui-for-docker',
'docker run --privileged -d -p 9000:9000 -v /tmp/docker-ui:/data -v /var/run/docker.sock:/var/run/docker.sock --name ui-for-docker ui-for-docker -d /data'
].join(';')
},
runSwarm: {
command: [
'docker stop portainer',
'docker rm portainer',
'docker run -d -p 9000:9000 -v /tmp/portainer:/data --name portainer portainer -H tcp://10.0.7.10:4000 --swarm -d /data'
'docker stop ui-for-docker',
'docker rm ui-for-docker',
'docker run -d -p 9000:9000 -v /tmp/docker-ui:/data --name ui-for-docker ui-for-docker -H tcp://10.0.7.10:4000 --swarm -d /data'
].join(';')
},
runSsl: {
command: [
'docker stop portainer',
'docker rm portainer',
'docker run -d -p 9000:9000 -v /tmp/portainer:/data -v /tmp/docker-ssl:/certs --name portainer portainer -H tcp://10.0.7.10:2376 -d /data --tlsverify'
'docker stop ui-for-docker',
'docker rm ui-for-docker',
'docker run -d -p 9000:9000 -v /tmp/docker-ui:/data -v /tmp/docker-ssl:/certs --name ui-for-docker ui-for-docker -H tcp://10.0.7.10:2376 -d /data --tlsverify'
].join(';')
},
cleanImages: {
@@ -291,7 +290,7 @@ module.exports = function (grunt) {
'if': {
binaryNotExist: {
options: {
executable: 'dist/portainer'
executable: 'dist/ui-for-docker'
},
ifFalse: ['shell:buildBinary']
}
+4 -7
View File
@@ -2,7 +2,7 @@
<html lang="en" ng-app="<%= pkg.name %>">
<head>
<meta charset="utf-8">
<title>Portainer</title>
<title>CloudInovasi UI for Docker</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="<%= pkg.author %>">
@@ -32,8 +32,8 @@
<ul class="sidebar">
<li class="sidebar-main">
<a ng-click="toggleSidebar()">
<img ng-if="config.logo" ng-src="{{ config.logo }}" class="img-responsive logo">
<img ng-if="!config.logo" src="images/logo.png" class="img-responsive logo" alt="Portainer">
<img ng-if="config.logo" ng-src="{{ config.logo }}" class="img-responsive logo" alt="CloudInovasi UI">
<img ng-if="!config.logo" src="images/logo.png" class="img-responsive logo" alt="CloudInovasi UI">
<span class="menu-icon glyphicon glyphicon-transfer"></span>
</a>
</li>
@@ -41,9 +41,6 @@
<li class="sidebar-list">
<a ui-sref="index">Dashboard <span class="menu-icon fa fa-tachometer"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="templates">App Templates <span class="menu-icon fa fa-rocket"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="containers">Containers <span class="menu-icon fa fa-server"></span></a>
</li>
@@ -68,7 +65,7 @@
</ul>
<div class="sidebar-footer">
<div class="col-xs-12">
<a href="https://github.com/cloud-inovasi/portainer" target="_blank">Portainer {{ uiVersion }}</a>
<a href="https://github.com/cloud-inovasi/cloudinovasi-ui" target="_blank">CloudInovasi UI {{ uiVersion }}</a>
</div>
</div>
</div>
+7 -7
View File
@@ -1,19 +1,19 @@
{
"author": "Cloud Inovasi",
"name": "portainer",
"homepage": "https://github.com/cloud-inovasi/portainer",
"version": "1.8.0",
"author": "Michael Crosby & Kevan Ahlquist",
"name": "uifordocker",
"homepage": "https://github.com/kevana/ui-for-docker",
"version": "1.7.0",
"repository": {
"type": "git",
"url": "git@github.com:cloud-inovasi/portainer.git"
"url": "git@github.com:kevana/ui-for-docker.git"
},
"bugs": {
"url": "https://github.com/cloud-inovasi/portainer/issues"
"url": "https://github.com/kevana/ui-for-docker/issues"
},
"licenses": [
{
"type": "MIT",
"url": "https://raw.githubusercontent.com/cloud-inovasi/portainer/develop/LICENSE"
"url": "https://raw.githubusercontent.com/kevana/ui-for-docker/master/LICENSE"
}
],
"engines": {
@@ -1,7 +1,7 @@
describe('ContainerController', function () {
var $scope, $httpBackend, mockContainer, $routeParams;
beforeEach(module('portainer'));
beforeEach(module('dockerui'));
beforeEach(inject(function ($rootScope, $controller, _$routeParams_) {
@@ -22,8 +22,8 @@ describe('ContainerController', function () {
$httpBackend.expectGET('dockerapi/containers/json').respond({
'Created': 1421817232,
'id': 'b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f',
'Image': 'portainer:latest',
'Name': '/portainer'
'Image': 'dockerui:latest',
'Name': '/dockerui'
});
}
@@ -33,8 +33,8 @@ describe('ContainerController', function () {
$scope.container = {
'Created': 1421817232,
'id': 'b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f',
'Image': 'portainer:latest',
'Name': '/portainer'
'Image': 'dockerui:latest',
'Name': '/dockerui'
};
$scope.container.newContainerName = "newName";
@@ -1,7 +1,7 @@
describe("ContainerTopController", function () {
var $scope, $httpBackend, $routeParams;
beforeEach(angular.mock.module('portainer'));
beforeEach(angular.mock.module('dockerui'));
beforeEach(inject(function (_$rootScope_, _$httpBackend_, $controller, _$routeParams_) {
$scope = _$rootScope_.$new();
@@ -1,7 +1,7 @@
describe('NetworkController', function () {
var $scope, $httpBackend, $routeParams;
beforeEach(module('portainer'));
beforeEach(module('dockerui'));
beforeEach(inject(function (_$httpBackend_, $controller, _$routeParams_) {
$scope = {};
$httpBackend = _$httpBackend_;
@@ -1,7 +1,7 @@
describe('NetworksController', function () {
var $scope, $httpBackend, $routeParams;
beforeEach(module('portainer'));
beforeEach(module('dockerui'));
beforeEach(inject(function (_$httpBackend_, $controller, _$routeParams_) {
$scope = {};
$httpBackend = _$httpBackend_;
@@ -1,7 +1,7 @@
describe('startContainerController', function () {
var scope, $location, createController, mockContainer, $httpBackend;
beforeEach(angular.mock.module('portainer'));
beforeEach(angular.mock.module('dockerui'));
beforeEach(inject(function ($rootScope, $controller, _$location_) {
$location = _$location_;
@@ -20,11 +20,11 @@ describe('startContainerController', function () {
}));
function expectGetContainers() {
$httpBackend.expectGET('dockerapi/containers/json?all=1').respond([{
'Command': './portainer -e /docker.sock',
'Command': './dockerui -e /docker.sock',
'Created': 1421817232,
'Id': 'b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f',
'Image': 'portainer:latest',
'Names': ['/portainer'],
'Image': 'dockerui:latest',
'Names': ['/dockerui'],
'Ports': [{
'IP': '0.0.0.0',
'PrivatePort': 9000,
@@ -1,7 +1,7 @@
describe("StatsController", function () {
var $scope, $httpBackend, $routeParams;
beforeEach(angular.mock.module('portainer'));
beforeEach(angular.mock.module('dockerui'));
beforeEach(inject(function (_$rootScope_, _$httpBackend_, $controller, _$routeParams_) {
$scope = _$rootScope_.$new();
@@ -1,7 +1,7 @@
describe('VolumesController', function () {
var $scope, $httpBackend, $routeParams;
beforeEach(module('portainer'));
beforeEach(module('dockerui'));
beforeEach(inject(function (_$httpBackend_, $controller, _$routeParams_) {
$scope = {};
$httpBackend = _$httpBackend_;
+200 -1
View File
@@ -1,5 +1,5 @@
describe('filters', function () {
beforeEach(module('portainer.filters'));
beforeEach(module('dockerui.filters'));
describe('truncate', function () {
it('should truncate the string to 10 characters ending in "..." by default', inject(function (truncateFilter) {
@@ -133,4 +133,203 @@ describe('filters', function () {
expect(repotagFilter(image)).toBe('ubuntu:latest');
}));
});
describe('errorMsgFilter', function () {
it('should convert the $resource object to a string message',
inject(function (errorMsgFilter) {
var response = {
'0': 'C',
'1': 'o',
'2': 'n',
'3': 'f',
'4': 'l',
'5': 'i',
'6': 'c',
'7': 't',
'8': ',',
'9': ' ',
'10': 'T',
'11': 'h',
'12': 'e',
'13': ' ',
'14': 'n',
'15': 'a',
'16': 'm',
'17': 'e',
'18': ' ',
'19': 'u',
'20': 'b',
'21': 'u',
'22': 'n',
'23': 't',
'24': 'u',
'25': '-',
'26': 's',
'27': 'l',
'28': 'e',
'29': 'e',
'30': 'p',
'31': '-',
'32': 'r',
'33': 'u',
'34': 'n',
'35': 't',
'36': 'i',
'37': 'm',
'38': 'e',
'39': ' ',
'40': 'i',
'41': 's',
'42': ' ',
'43': 'a',
'44': 'l',
'45': 'r',
'46': 'e',
'47': 'a',
'48': 'd',
'49': 'y',
'50': ' ',
'51': 'a',
'52': 's',
'53': 's',
'54': 'i',
'55': 'g',
'56': 'n',
'57': 'e',
'58': 'd',
'59': ' ',
'60': 't',
'61': 'o',
'62': ' ',
'63': 'b',
'64': '6',
'65': '9',
'66': 'e',
'67': '5',
'68': '3',
'69': 'a',
'70': '6',
'71': '2',
'72': '2',
'73': 'c',
'74': '8',
'75': '.',
'76': ' ',
'77': 'Y',
'78': 'o',
'79': 'u',
'80': ' ',
'81': 'h',
'82': 'a',
'83': 'v',
'84': 'e',
'85': ' ',
'86': 't',
'87': 'o',
'88': ' ',
'89': 'd',
'90': 'e',
'91': 'l',
'92': 'e',
'93': 't',
'94': 'e',
'95': ' ',
'96': '(',
'97': 'o',
'98': 'r',
'99': ' ',
'100': 'r',
'101': 'e',
'102': 'n',
'103': 'a',
'104': 'm',
'105': 'e',
'106': ')',
'107': ' ',
'108': 't',
'109': 'h',
'110': 'a',
'111': 't',
'112': ' ',
'113': 'c',
'114': 'o',
'115': 'n',
'116': 't',
'117': 'a',
'118': 'i',
'119': 'n',
'120': 'e',
'121': 'r',
'122': ' ',
'123': 't',
'124': 'o',
'125': ' ',
'126': 'b',
'127': 'e',
'128': ' ',
'129': 'a',
'130': 'b',
'131': 'l',
'132': 'e',
'133': ' ',
'134': 't',
'135': 'o',
'136': ' ',
'137': 'a',
'138': 's',
'139': 's',
'140': 'i',
'141': 'g',
'142': 'n',
'143': ' ',
'144': 'u',
'145': 'b',
'146': 'u',
'147': 'n',
'148': 't',
'149': 'u',
'150': '-',
'151': 's',
'152': 'l',
'153': 'e',
'154': 'e',
'155': 'p',
'156': '-',
'157': 'r',
'158': 'u',
'159': 'n',
'160': 't',
'161': 'i',
'162': 'm',
'163': 'e',
'164': ' ',
'165': 't',
'166': 'o',
'167': ' ',
'168': 'a',
'169': ' ',
'170': 'c',
'171': 'o',
'172': 'n',
'173': 't',
'174': 'a',
'175': 'i',
'176': 'n',
'177': 'e',
'178': 'r',
'179': ' ',
'180': 'a',
'181': 'g',
'182': 'a',
'183': 'i',
'184': 'n',
'185': '.',
'186': '\n',
'$promise': {},
'$resolved': true
};
var message = 'Conflict, The name ubuntu-sleep-runtime is already assigned to b69e53a622c8. You have to delete (or rename) that container to be able to assign ubuntu-sleep-runtime to a container again.\n';
expect(errorMsgFilter(response)).toBe(message);
}));
});
});
+1 -1
View File
@@ -7,7 +7,7 @@ files = [
JASMINE_ADAPTER,
'dist/angular.js',
'dist/vendor.js',
'dist/portainer.js',
'dist/dockerui.js',
'bower_components/angular-mocks/angular-mocks.js',
'test/unit/**/*.spec.js'
];