Compare commits
267 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cbbcb51162 | |||
| 954a6a11b7 | |||
| 0c5e98b47d | |||
| d941fef8d6 | |||
| 496de850c1 | |||
| 29fa33fb2b | |||
| 06fbb5ba34 | |||
| a32f6f343d | |||
| 61d7b4f64c | |||
| 1840ab4bba | |||
| ccb812cc33 | |||
| b098cd5638 | |||
| eefa7ca138 | |||
| b5dcdc8807 | |||
| 4b4e5d5ebd | |||
| 54fd9561f0 | |||
| fb67769928 | |||
| 9c8e632a09 | |||
| 0b6c2b032a | |||
| f0e4cdc13e | |||
| cfe31fbeac | |||
| 722dc0b3af | |||
| de3353feba | |||
| 145e45b4a8 | |||
| d0f57809d6 | |||
| 6c29377992 | |||
| 164902c0cb | |||
| 75466cb57f | |||
| 0e8fff7a51 | |||
| 7b72da857f | |||
| b89546a1e0 | |||
| 22122a27b5 | |||
| 24a9e9f61c | |||
| cf5378f604 | |||
| 1d8f51c141 | |||
| 87798cd1c8 | |||
| fd1496df93 | |||
| ea6e11000d | |||
| ef257f65cf | |||
| 01a707c8e7 | |||
| 9293b28ef4 | |||
| 30c0fda1b6 | |||
| 548a458b9a | |||
| 111cd4ac64 | |||
| 54ab81a7de | |||
| 21344280a9 | |||
| d3fa9736f4 | |||
| f1ec419e3a | |||
| de8c6b4ed8 | |||
| e661cef2fe | |||
| edf485bbe4 | |||
| 20eecffc40 | |||
| 232b180eef | |||
| 4a738ee362 | |||
| f4d90306b3 | |||
| 7801a91149 | |||
| 19d4e38d94 | |||
| bab57e0402 | |||
| d2b3360bff | |||
| 1aaa5acbef | |||
| 5878eed7ec | |||
| b0ebbdf68c | |||
| 0ec20d3093 | |||
| c5ddae12cf | |||
| b1e1850e9f | |||
| 06c2635e82 | |||
| 711ac742e1 | |||
| 201ab20131 | |||
| 716ba72217 | |||
| 95b16919a6 | |||
| d3d000a1d0 | |||
| 9499f78121 | |||
| fa36c9ee5c | |||
| c460eb4d7a | |||
| ab52270238 | |||
| 7c6fdebb3d | |||
| cf3cd76064 | |||
| 5ef6b536ac | |||
| adf5184a5d | |||
| ea596a8701 | |||
| 15a3cb7241 | |||
| f147da3017 | |||
| dfaf2eb6a9 | |||
| 97f6a32c78 | |||
| bcdd7498a1 | |||
| c45947b573 | |||
| 85140c7dcf | |||
| 30e9a604cd | |||
| 8c769148ad | |||
| 4cc08d7211 | |||
| 48b6b6340b | |||
| b857970236 | |||
| 1011fde9de | |||
| bee89720d5 | |||
| 23bff41304 | |||
| 8243326692 | |||
| 17ae122595 | |||
| b69d72fc8c | |||
| c52498993b | |||
| a4a82b4502 | |||
| 52d953a1c2 | |||
| e145d82947 | |||
| 25df1fe26c | |||
| 106718f416 | |||
| 8464faa2a1 | |||
| 9354b911bb | |||
| c8a5b82c89 | |||
| 1f884e9584 | |||
| 00b2c92e39 | |||
| 0796778d17 | |||
| 1eae1c03f0 | |||
| a9209da167 | |||
| 43c2f14289 | |||
| f378d56543 | |||
| 521d146d7b | |||
| dc721f5870 | |||
| 3b0d726c2a | |||
| f6226d19b8 | |||
| 71c091ae0d | |||
| 1fb008212a | |||
| cab34e4069 | |||
| e67e20ce18 | |||
| d253c0d494 | |||
| c74e8fc732 | |||
| 29358e5744 | |||
| b59c102098 | |||
| afaa1433ff | |||
| f923016052 | |||
| ca27e7f27a | |||
| 8fd9c2fce2 | |||
| d4ca060945 | |||
| d124c21d1b | |||
| d2fb2cb863 | |||
| 0350daca8d | |||
| 06f54e300c | |||
| 135b940897 | |||
| 7856276092 | |||
| bf14dcc3e8 | |||
| 21c1778822 | |||
| 337bfa74bb | |||
| 418b1ff544 | |||
| 092d866c73 | |||
| 50391c87e2 | |||
| fd6645d068 | |||
| 3a6e326e5e | |||
| b997b787c4 | |||
| d227bdfc75 | |||
| 4ba6286c97 | |||
| 56ef453203 | |||
| b573a8bafa | |||
| 59820e737e | |||
| 530eb20dfc | |||
| 446322dcbe | |||
| 2d311518a7 | |||
| 3bcd1bf665 | |||
| 88d5e22532 | |||
| 41a41cdf38 | |||
| e6e21e9f46 | |||
| f18aa8fe79 | |||
| 227e5883e9 | |||
| 87e835e873 | |||
| 965a099495 | |||
| 66ae15b4fb | |||
| 813c14d93c | |||
| 5d0af27a3f | |||
| aa3fda6de9 | |||
| 9e4f8c9fee | |||
| ce2e6f80fc | |||
| bf4622e4f5 | |||
| 9655f57698 | |||
| 808694d6b5 | |||
| cd12243b0f | |||
| abfa921b7a | |||
| 91f3b1f138 | |||
| 9468839bf9 | |||
| 9ca2aa9bbd | |||
| 54c82a3a5c | |||
| 9360693f8d | |||
| c54dd510ad | |||
| b940c7bfbd | |||
| 1460d69cd1 | |||
| a7619b06ba | |||
| f3a5251fd4 | |||
| 2ec247827d | |||
| e7a836a6b2 | |||
| 6d163ee1ef | |||
| a12c1916ec | |||
| 4e1a7077d7 | |||
| da453a6b7c | |||
| 0230c5bb59 | |||
| a471b77f8d | |||
| b1e4800605 | |||
| 7f5be16db8 | |||
| 791e069a4c | |||
| 20bfca97e0 | |||
| 3302c822f1 | |||
| 9a4115f086 | |||
| f77a9b4db5 | |||
| b3b3feac66 | |||
| 324f36513e | |||
| ebf045d2e0 | |||
| 3ec161ba56 | |||
| 12be43018e | |||
| 0f51cb66e0 | |||
| 1b206f223f | |||
| 02d4161ddd | |||
| 173c673d37 | |||
| 4fe6bcc5db | |||
| 7e1d5338cf | |||
| a67206dd8d | |||
| 5f3d856535 | |||
| 0244bc7317 | |||
| 7267516363 | |||
| 15d133324d | |||
| 5bf922325a | |||
| b2b814a65b | |||
| d2bc18b575 | |||
| 8cdb675abc | |||
| a25829bbd9 | |||
| 194aa9a750 | |||
| e21af77246 | |||
| bfe9038630 | |||
| a33123469a | |||
| f353dc2c41 | |||
| a2a367725b | |||
| db90a0eed7 | |||
| 93dba3f92f | |||
| 5a20b9fc04 | |||
| 9793c3f3ee | |||
| 6963a1ae8a | |||
| da1a5ead39 | |||
| f1b5037ee5 | |||
| cf18a3cd60 | |||
| cc1b67575c | |||
| 50d33a07df | |||
| fc0dedfda7 | |||
| 35dbacdfff | |||
| ad0d23d686 | |||
| 786b94b285 | |||
| 0ddf4a1828 | |||
| b244242cb2 | |||
| 49d4c5800f | |||
| a198382c06 | |||
| 316017c516 | |||
| f62cb483ce | |||
| 44c88f1ed5 | |||
| 9d1193c0b5 | |||
| 22d35eb32f | |||
| 2683ddb1ee | |||
| fe7646e939 | |||
| 00528edd7c | |||
| 06c2ac4d5c | |||
| a447d64d83 | |||
| 9260f23d41 | |||
| d7082c3959 | |||
| 437e171639 | |||
| 6de45cd422 | |||
| a065e0e259 | |||
| eede27e263 | |||
| 8a900254ae | |||
| 5f4641af67 | |||
| 8a4be8b93a | |||
| a712d5b77e | |||
| 5947e262fc | |||
| c3f22fe989 | |||
| 2f003a5bb5 | |||
| 75085b213a |
@@ -1,4 +1,2 @@
|
||||
node_modules
|
||||
bower_components
|
||||
.git
|
||||
Dockerfile
|
||||
*
|
||||
!dist
|
||||
|
||||
@@ -4,6 +4,7 @@ logs/*
|
||||
node_modules
|
||||
bower_components
|
||||
.idea
|
||||
dist
|
||||
dockerui
|
||||
*.iml
|
||||
dist
|
||||
dist/*
|
||||
ui-for-docker-checksum.txt
|
||||
|
||||
@@ -2,5 +2,7 @@ FROM scratch
|
||||
|
||||
COPY dist /
|
||||
|
||||
VOLUME /data
|
||||
|
||||
EXPOSE 9000
|
||||
ENTRYPOINT ["/dockerui"]
|
||||
ENTRYPOINT ["/ui-for-docker"]
|
||||
|
||||
@@ -1,7 +1,59 @@
|
||||
DockerUI: Copyright (c) 2013-2014 Michael Crosby. crosbymichael.com
|
||||
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 in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 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.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
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.
|
||||
|
||||
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (anthonylapenna at cloudinovasi dot id)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
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.
|
||||
|
||||
rdash-angular: Copyright (c) [2014] [Elliot Hesp]
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
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.
|
||||
|
||||
@@ -1,82 +1,159 @@
|
||||
## DockerUI
|
||||
# Cloudinovasi UI for Docker
|
||||
|
||||

|
||||
DockerUI 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. This project is not complete and is still under heavy development.
|
||||
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).
|
||||
|
||||

|
||||

|
||||
|
||||
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
|
||||
|
||||
### 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.
|
||||
|
||||
### Container Quickstart
|
||||
1. Run: `docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock dockerui/dockerui`
|
||||
## Supported Docker versions
|
||||
|
||||
The current Docker version support policy is the following: `N` to `N-2` included where `N` is the latest version.
|
||||
|
||||
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/cloudinovasi-ui`
|
||||
|
||||
2. Open your browser to `http://<dockerd host ip>:9000`
|
||||
|
||||
Bind mounting the Unix socket into the UI For Docker container is much more secure than exposing your docker daemon over TCP.
|
||||
|
||||
Bind mounting the Unix socket into the DockerUI container is much more secure than exposing your docker daemon over TCP. The `--privileged` flag is required for hosts using SELinux. You should still secure your DockerUI instance behind some type of auth. Directions for using Nginx auth are [here](https://github.com/crosbymichael/dockerui/wiki/Dockerui-with-Nginx-HTTP-Auth).
|
||||
The `--privileged` flag is required for hosts using SELinux.
|
||||
|
||||
### Specify socket to connect to Docker daemon
|
||||
|
||||
By default DockerUI 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 `-e` flag to change this socket:
|
||||
You can use the `--host`, `-H` flags to change this socket:
|
||||
|
||||
# Connect to a tcp socket:
|
||||
$ docker run -d -p 9000:9000 --privileged dockerui/dockerui -e http://127.0.0.1:2375
|
||||
```
|
||||
# Connect to a tcp socket:
|
||||
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H tcp://127.0.0.1:2375
|
||||
```
|
||||
|
||||
### Change address/port DockerUI is served on
|
||||
DockerUI listens on port 9000 by default. If you run DockerUI inside a container then you can bind the container's internal port to any external address and port:
|
||||
```
|
||||
# Connect to another unix socket:
|
||||
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H unix:///path/to/docker.sock
|
||||
```
|
||||
|
||||
# Expose DockerUI 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 dockerui/dockerui
|
||||
### Swarm support
|
||||
|
||||
### Check the [wiki](//github.com/crosbymichael/dockerui/wiki) for more info about using dockerui
|
||||
**Supported Swarm version: 1.2.3**
|
||||
|
||||
### Stack
|
||||
* [Angular.js](https://github.com/angular/angular.js)
|
||||
* [Bootstrap](http://getbootstrap.com/)
|
||||
* [Gritter](https://github.com/jboesch/Gritter)
|
||||
* [Spin.js](https://github.com/fgnass/spin.js/)
|
||||
* [Golang](https://golang.org/)
|
||||
* [Vis.js](http://visjs.org/)
|
||||
You can access a specific view for you Swarm cluster by defining the `--swarm` flag:
|
||||
|
||||
```
|
||||
# Connect to a tcp socket and enable Swarm:
|
||||
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H tcp://<SWARM_HOST>:<SWARM_PORT> --swarm
|
||||
```
|
||||
|
||||
### Todo:
|
||||
* Full repository support
|
||||
* Search
|
||||
* Push files to a container
|
||||
* Unit tests
|
||||
*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 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:
|
||||
|
||||
### License - MIT
|
||||
The DockerUI code is licensed under the MIT license.
|
||||
```
|
||||
# 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
|
||||
|
||||
**DockerUI:**
|
||||
Copyright (c) 2014 Michael Crosby. crosbymichael.com
|
||||
Ensure that you have access to the CA, the cert and the public key used to access your Docker engine.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use, copy,
|
||||
modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
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:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
```
|
||||
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -v /path/to/certs:/certs -H https://my-docker-host.domain:2376 --tlsverify
|
||||
```
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
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.
|
||||
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/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.
|
||||
|
||||
### Use your own logo
|
||||
|
||||
You can use the `--logo` flag to specify an URL to your own logo.
|
||||
|
||||
For example, using the Docker logo:
|
||||
|
||||
```
|
||||
$ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui --logo "https://www.docker.com/sites/all/themes/docker/assets/images/brand-full.svg"
|
||||
```
|
||||
|
||||
The custom logo will replace the CloudInovasi logo in the UI.
|
||||
|
||||
### Hide containers with specific labels
|
||||
|
||||
You can hide specific containers in the containers view by using the `--hide-label` or `-l` options and specifying a label.
|
||||
|
||||
For example, take a container started with the label `owner=acme`:
|
||||
|
||||
```
|
||||
$ 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/cloudinovasi-ui -l owner=acme
|
||||
```
|
||||
|
||||
### Reverse proxy configuration
|
||||
|
||||
Has been tested with Nginx 1.11.
|
||||
|
||||
Use the following configuration to host the UI at `myhost.mydomain.com/dockerui`:
|
||||
|
||||
```nginx
|
||||
upstream cloudinovasi-ui {
|
||||
server ADDRESS:PORT;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
location /dockerui/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_pass http://cloudinovasi-ui/;
|
||||
}
|
||||
location /dockerui/ws/ {
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_http_version 1.1;
|
||||
proxy_pass http://cloudinovasi-ui/ws/;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Replace `ADDRESS:PORT` with the CloudInovasi UI container details.
|
||||
|
||||
### Available options
|
||||
|
||||
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 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`)
|
||||
* `--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
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type (
|
||||
api struct {
|
||||
endpoint *url.URL
|
||||
bindAddress string
|
||||
assetPath string
|
||||
dataPath string
|
||||
tlsConfig *tls.Config
|
||||
}
|
||||
|
||||
apiConfig struct {
|
||||
Endpoint string
|
||||
BindAddress string
|
||||
AssetPath string
|
||||
DataPath string
|
||||
SwarmSupport bool
|
||||
TLSEnabled bool
|
||||
TLSCACertPath string
|
||||
TLSCertPath string
|
||||
TLSKeyPath string
|
||||
}
|
||||
)
|
||||
|
||||
func (a *api) run(settings *Settings) {
|
||||
handler := a.newHandler(settings)
|
||||
if err := http.ListenAndServe(a.bindAddress, handler); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func newAPI(apiConfig apiConfig) *api {
|
||||
endpointURL, err := url.Parse(apiConfig.Endpoint)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var tlsConfig *tls.Config
|
||||
if apiConfig.TLSEnabled {
|
||||
tlsConfig = newTLSConfig(apiConfig.TLSCACertPath, apiConfig.TLSCertPath, apiConfig.TLSKeyPath)
|
||||
}
|
||||
|
||||
return &api{
|
||||
endpoint: endpointURL,
|
||||
bindAddress: apiConfig.BindAddress,
|
||||
assetPath: apiConfig.AssetPath,
|
||||
dataPath: apiConfig.DataPath,
|
||||
tlsConfig: tlsConfig,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/gorilla/securecookie"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const keyFile = "authKey.dat"
|
||||
|
||||
// newAuthKey reuses an existing CSRF authkey if present or generates a new one
|
||||
func newAuthKey(path string) []byte {
|
||||
var authKey []byte
|
||||
authKeyPath := path + "/" + keyFile
|
||||
data, err := ioutil.ReadFile(authKeyPath)
|
||||
if err != nil {
|
||||
log.Print("Unable to find an existing CSRF auth key. Generating a new key.")
|
||||
authKey = securecookie.GenerateRandomKey(32)
|
||||
err := ioutil.WriteFile(authKeyPath, authKey, 0644)
|
||||
if err != nil {
|
||||
log.Fatal("Unable to persist CSRF auth key.")
|
||||
log.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
authKey = data
|
||||
}
|
||||
return authKey
|
||||
}
|
||||
|
||||
// newCSRF initializes a new CSRF handler
|
||||
func newCSRFHandler(keyPath string) func(h http.Handler) http.Handler {
|
||||
authKey := newAuthKey(keyPath)
|
||||
return csrf.Protect(
|
||||
authKey,
|
||||
csrf.HttpOnly(false),
|
||||
csrf.Secure(false),
|
||||
)
|
||||
}
|
||||
|
||||
// newCSRFWrapper wraps a http.Handler to add the CSRF token
|
||||
func newCSRFWrapper(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-CSRF-Token", csrf.Token(r))
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"golang.org/x/net/websocket"
|
||||
"log"
|
||||
)
|
||||
|
||||
// execContainer is used to create a websocket communication with an exec instance
|
||||
func (a *api) execContainer(ws *websocket.Conn) {
|
||||
qry := ws.Request().URL.Query()
|
||||
execID := qry.Get("id")
|
||||
|
||||
var host string
|
||||
if a.endpoint.Scheme == "tcp" {
|
||||
host = a.endpoint.Host
|
||||
} else if a.endpoint.Scheme == "unix" {
|
||||
host = a.endpoint.Path
|
||||
}
|
||||
|
||||
if err := hijack(host, a.endpoint.Scheme, "POST", "/exec/"+execID+"/start", a.tlsConfig, true, ws, ws, ws, nil, nil); err != nil {
|
||||
log.Fatalf("error during hijack: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// pair defines a key/value pair
|
||||
type pair struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// pairList defines an array of Label
|
||||
type pairList []pair
|
||||
|
||||
// Set implementation for Labels
|
||||
func (l *pairList) Set(value string) error {
|
||||
parts := strings.SplitN(value, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("expected NAME=VALUE got '%s'", value)
|
||||
}
|
||||
p := new(pair)
|
||||
p.Name = parts[0]
|
||||
p.Value = parts[1]
|
||||
*l = append(*l, *p)
|
||||
return nil
|
||||
}
|
||||
|
||||
// String implementation for Labels
|
||||
func (l *pairList) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// IsCumulative implementation for Labels
|
||||
func (l *pairList) IsCumulative() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// LabelParser defines a custom parser for Labels flags
|
||||
func pairs(s kingpin.Settings) (target *[]pair) {
|
||||
target = new([]pair)
|
||||
s.SetValue((*pairList)(target))
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"golang.org/x/net/websocket"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
)
|
||||
|
||||
// newHandler creates a new http.Handler with CSRF protection
|
||||
func (a *api) newHandler(settings *Settings) http.Handler {
|
||||
var (
|
||||
mux = http.NewServeMux()
|
||||
fileHandler = http.FileServer(http.Dir(a.assetPath))
|
||||
)
|
||||
|
||||
handler := a.newAPIHandler()
|
||||
CSRFHandler := newCSRFHandler(a.dataPath)
|
||||
|
||||
mux.Handle("/", fileHandler)
|
||||
mux.Handle("/dockerapi/", http.StripPrefix("/dockerapi", handler))
|
||||
mux.Handle("/ws/exec", websocket.Handler(a.execContainer))
|
||||
mux.HandleFunc("/settings", func(w http.ResponseWriter, r *http.Request) {
|
||||
settingsHandler(w, r, settings)
|
||||
})
|
||||
return CSRFHandler(newCSRFWrapper(mux))
|
||||
}
|
||||
|
||||
// newAPIHandler initializes a new http.Handler based on the URL scheme
|
||||
func (a *api) newAPIHandler() http.Handler {
|
||||
var handler http.Handler
|
||||
var endpoint = *a.endpoint
|
||||
if endpoint.Scheme == "tcp" {
|
||||
if a.tlsConfig != nil {
|
||||
handler = a.newTCPHandlerWithTLS(&endpoint)
|
||||
} else {
|
||||
handler = a.newTCPHandler(&endpoint)
|
||||
}
|
||||
} else if endpoint.Scheme == "unix" {
|
||||
socketPath := endpoint.Path
|
||||
if _, err := os.Stat(socketPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
log.Fatalf("Unix socket %s does not exist", socketPath)
|
||||
}
|
||||
log.Fatal(err)
|
||||
}
|
||||
handler = a.newUnixHandler(socketPath)
|
||||
} else {
|
||||
log.Fatalf("Bad Docker enpoint: %v. Only unix:// and tcp:// are supported.", &endpoint)
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
// newUnixHandler initializes a new UnixHandler
|
||||
func (a *api) newUnixHandler(e string) http.Handler {
|
||||
return &unixHandler{e}
|
||||
}
|
||||
|
||||
// newTCPHandler initializes a HTTP reverse proxy
|
||||
func (a *api) newTCPHandler(u *url.URL) http.Handler {
|
||||
u.Scheme = "http"
|
||||
return httputil.NewSingleHostReverseProxy(u)
|
||||
}
|
||||
|
||||
// newTCPHandlerWithL initializes a HTTPS reverse proxy with a TLS configuration
|
||||
func (a *api) newTCPHandlerWithTLS(u *url.URL) http.Handler {
|
||||
u.Scheme = "https"
|
||||
proxy := httputil.NewSingleHostReverseProxy(u)
|
||||
proxy.Transport = &http.Transport{
|
||||
TLSClientConfig: a.tlsConfig,
|
||||
}
|
||||
return proxy
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"time"
|
||||
)
|
||||
|
||||
type execConfig struct {
|
||||
Tty bool
|
||||
Detach bool
|
||||
}
|
||||
|
||||
// hijack allows to upgrade an HTTP connection to a TCP connection
|
||||
// It redirects IO streams for stdin, stdout and stderr to a websocket
|
||||
func hijack(addr, scheme, method, path string, tlsConfig *tls.Config, setRawTerminal bool, in io.ReadCloser, stdout, stderr io.Writer, started chan io.Closer, data interface{}) error {
|
||||
execConfig := &execConfig{
|
||||
Tty: true,
|
||||
Detach: false,
|
||||
}
|
||||
|
||||
buf, err := json.Marshal(execConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error marshaling exec config: %s", err)
|
||||
}
|
||||
|
||||
rdr := bytes.NewReader(buf)
|
||||
|
||||
req, err := http.NewRequest(method, path, rdr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error during hijack request: %s", err)
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "Docker-Client")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Connection", "Upgrade")
|
||||
req.Header.Set("Upgrade", "tcp")
|
||||
req.Host = addr
|
||||
|
||||
var (
|
||||
dial net.Conn
|
||||
dialErr error
|
||||
)
|
||||
|
||||
if tlsConfig == nil {
|
||||
dial, dialErr = net.Dial(scheme, addr)
|
||||
} else {
|
||||
dial, dialErr = tls.Dial(scheme, addr, tlsConfig)
|
||||
}
|
||||
|
||||
if dialErr != nil {
|
||||
return dialErr
|
||||
}
|
||||
|
||||
// When we set up a TCP connection for hijack, there could be long periods
|
||||
// of inactivity (a long running command with no output) that in certain
|
||||
// network setups may cause ECONNTIMEOUT, leaving the client in an unknown
|
||||
// state. Setting TCP KeepAlive on the socket connection will prohibit
|
||||
// ECONNTIMEOUT unless the socket connection truly is broken
|
||||
if tcpConn, ok := dial.(*net.TCPConn); ok {
|
||||
tcpConn.SetKeepAlive(true)
|
||||
tcpConn.SetKeepAlivePeriod(30 * time.Second)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clientconn := httputil.NewClientConn(dial, nil)
|
||||
defer clientconn.Close()
|
||||
|
||||
// Server hijacks the connection, error 'connection closed' expected
|
||||
clientconn.Do(req)
|
||||
|
||||
rwc, br := clientconn.Hijack()
|
||||
defer rwc.Close()
|
||||
|
||||
if started != nil {
|
||||
started <- rwc
|
||||
}
|
||||
|
||||
var receiveStdout chan error
|
||||
|
||||
if stdout != nil || stderr != nil {
|
||||
go func() (err error) {
|
||||
if setRawTerminal && stdout != nil {
|
||||
_, err = io.Copy(stdout, br)
|
||||
}
|
||||
return err
|
||||
}()
|
||||
}
|
||||
|
||||
go func() error {
|
||||
if in != nil {
|
||||
io.Copy(rwc, in)
|
||||
}
|
||||
|
||||
if conn, ok := rwc.(interface {
|
||||
CloseWrite() error
|
||||
}); ok {
|
||||
if err := conn.CloseWrite(); err != nil {
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
if stdout != nil || stderr != nil {
|
||||
if err := <-receiveStdout; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
fmt.Println(br)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package main // import "github.com/cloudinovasi/ui-for-docker"
|
||||
|
||||
import (
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
// main is the entry point of the program
|
||||
func main() {
|
||||
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 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'))
|
||||
logo = kingpin.Flag("logo", "URL for the logo displayed in the UI").String()
|
||||
)
|
||||
kingpin.Parse()
|
||||
|
||||
apiConfig := apiConfig{
|
||||
Endpoint: *endpoint,
|
||||
BindAddress: *addr,
|
||||
AssetPath: *assets,
|
||||
DataPath: *data,
|
||||
SwarmSupport: *swarm,
|
||||
TLSEnabled: *tlsverify,
|
||||
TLSCACertPath: *tlscacert,
|
||||
TLSCertPath: *tlscert,
|
||||
TLSKeyPath: *tlskey,
|
||||
}
|
||||
|
||||
settings := &Settings{
|
||||
Swarm: *swarm,
|
||||
HiddenLabels: *labels,
|
||||
Logo: *logo,
|
||||
}
|
||||
|
||||
api := newAPI(apiConfig)
|
||||
api.run(settings)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Settings defines the settings available under the /settings endpoint
|
||||
type Settings struct {
|
||||
Swarm bool `json:"swarm"`
|
||||
HiddenLabels pairList `json:"hiddenLabels"`
|
||||
Logo string `json:"logo"`
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
)
|
||||
|
||||
// newTLSConfig initializes a tls.Config using a CA certificate, a certificate and a key
|
||||
func newTLSConfig(caCertPath, certPath, keyPath string) *tls.Config {
|
||||
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
caCert, err := ioutil.ReadFile(caCertPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
RootCAs: caCertPool,
|
||||
}
|
||||
return tlsConfig
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
)
|
||||
|
||||
// unixHandler defines a handler holding the path to a socket under UNIX
|
||||
type unixHandler struct {
|
||||
path string
|
||||
}
|
||||
|
||||
// ServeHTTP implementation for unixHandler
|
||||
func (h *unixHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := net.Dial("unix", h.path)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
c := httputil.NewClientConn(conn, nil)
|
||||
defer c.Close()
|
||||
|
||||
res, err := c.Do(r)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
copyHeader(w.Header(), res.Header)
|
||||
if _, err := io.Copy(w, res.Body); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func copyHeader(dst, src http.Header) {
|
||||
for k, vv := range src {
|
||||
for _, v := range vv {
|
||||
dst.Add(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +1,159 @@
|
||||
angular.module('dockerui', ['dockerui.templates', 'ngRoute', 'dockerui.services', 'dockerui.filters', 'masthead', 'footer', 'dashboard', 'container', 'containers', 'containersNetwork', 'images', 'image', 'pullImage', 'startContainer', 'sidebar', 'info', 'builder', 'containerLogs', 'containerTop', 'events', 'stats'])
|
||||
.config(['$routeProvider', function ($routeProvider) {
|
||||
'use strict';
|
||||
$routeProvider.when('/', {
|
||||
templateUrl: 'app/components/dashboard/dashboard.html',
|
||||
controller: 'DashboardController'
|
||||
});
|
||||
$routeProvider.when('/containers/', {
|
||||
templateUrl: 'app/components/containers/containers.html',
|
||||
controller: 'ContainersController'
|
||||
});
|
||||
$routeProvider.when('/containers/:id/', {
|
||||
templateUrl: 'app/components/container/container.html',
|
||||
controller: 'ContainerController'
|
||||
});
|
||||
$routeProvider.when('/containers/:id/logs/', {
|
||||
templateUrl: 'app/components/containerLogs/containerlogs.html',
|
||||
controller: 'ContainerLogsController'
|
||||
});
|
||||
$routeProvider.when('/containers/:id/top', {
|
||||
templateUrl: 'app/components/containerTop/containerTop.html',
|
||||
controller: 'ContainerTopController'
|
||||
});
|
||||
$routeProvider.when('/containers/:id/stats', {
|
||||
templateUrl: 'app/components/stats/stats.html',
|
||||
controller: 'StatsController'
|
||||
});
|
||||
$routeProvider.when('/containers_network', {
|
||||
templateUrl: 'app/components/containersNetwork/containersNetwork.html',
|
||||
controller: 'ContainersNetworkController'
|
||||
});
|
||||
$routeProvider.when('/images/', {
|
||||
templateUrl: 'app/components/images/images.html',
|
||||
controller: 'ImagesController'
|
||||
});
|
||||
$routeProvider.when('/images/:id*/', {
|
||||
templateUrl: 'app/components/image/image.html',
|
||||
controller: 'ImageController'
|
||||
});
|
||||
$routeProvider.when('/info', {templateUrl: 'app/components/info/info.html', controller: 'InfoController'});
|
||||
$routeProvider.when('/events', {
|
||||
templateUrl: 'app/components/events/events.html',
|
||||
controller: 'EventsController'
|
||||
});
|
||||
$routeProvider.otherwise({redirectTo: '/'});
|
||||
}])
|
||||
// This is your docker url that the api will use to make requests
|
||||
// You need to set this to the api endpoint without the port i.e. http://192.168.1.9
|
||||
.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('UI_VERSION', 'v0.8.0')
|
||||
.constant('DOCKER_API_VERSION', 'v1.20');
|
||||
angular.module('uifordocker', [
|
||||
'uifordocker.templates',
|
||||
'ui.bootstrap',
|
||||
'ui.router',
|
||||
'ui.select',
|
||||
'ngCookies',
|
||||
'ngSanitize',
|
||||
'uifordocker.services',
|
||||
'uifordocker.filters',
|
||||
'dashboard',
|
||||
'container',
|
||||
'containerConsole',
|
||||
'containerLogs',
|
||||
'containers',
|
||||
'createContainer',
|
||||
'docker',
|
||||
'events',
|
||||
'images',
|
||||
'image',
|
||||
'stats',
|
||||
'swarm',
|
||||
'network',
|
||||
'networks',
|
||||
'createNetwork',
|
||||
'volumes',
|
||||
'createVolume'])
|
||||
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', function ($stateProvider, $urlRouterProvider, $httpProvider) {
|
||||
'use strict';
|
||||
|
||||
$httpProvider.defaults.xsrfCookieName = 'csrfToken';
|
||||
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token';
|
||||
|
||||
$urlRouterProvider.otherwise('/');
|
||||
|
||||
$stateProvider
|
||||
.state('index', {
|
||||
url: '/',
|
||||
templateUrl: 'app/components/dashboard/dashboard.html',
|
||||
controller: 'DashboardController'
|
||||
})
|
||||
.state('containers', {
|
||||
url: '/containers/',
|
||||
templateUrl: 'app/components/containers/containers.html',
|
||||
controller: 'ContainersController'
|
||||
})
|
||||
.state('container', {
|
||||
url: "^/containers/:id",
|
||||
templateUrl: 'app/components/container/container.html',
|
||||
controller: 'ContainerController'
|
||||
})
|
||||
.state('stats', {
|
||||
url: "^/containers/:id/stats",
|
||||
templateUrl: 'app/components/stats/stats.html',
|
||||
controller: 'StatsController'
|
||||
})
|
||||
.state('logs', {
|
||||
url: "^/containers/:id/logs",
|
||||
templateUrl: 'app/components/containerLogs/containerlogs.html',
|
||||
controller: 'ContainerLogsController'
|
||||
})
|
||||
.state('console', {
|
||||
url: "^/containers/:id/console",
|
||||
templateUrl: 'app/components/containerConsole/containerConsole.html',
|
||||
controller: 'ContainerConsoleController'
|
||||
})
|
||||
.state('actions', {
|
||||
abstract: true,
|
||||
url: "/actions",
|
||||
template: '<ui-view/>'
|
||||
})
|
||||
.state('actions.create', {
|
||||
abstract: true,
|
||||
url: "/create",
|
||||
template: '<ui-view/>'
|
||||
})
|
||||
.state('actions.create.container', {
|
||||
url: "/container",
|
||||
templateUrl: 'app/components/createContainer/createcontainer.html',
|
||||
controller: 'CreateContainerController'
|
||||
})
|
||||
.state('actions.create.volume', {
|
||||
url: "/volume",
|
||||
templateUrl: 'app/components/createVolume/createvolume.html',
|
||||
controller: 'CreateVolumeController'
|
||||
})
|
||||
.state('actions.create.network', {
|
||||
url: "/network",
|
||||
templateUrl: 'app/components/createNetwork/createnetwork.html',
|
||||
controller: 'CreateNetworkController'
|
||||
})
|
||||
.state('docker', {
|
||||
url: '/docker/',
|
||||
templateUrl: 'app/components/docker/docker.html',
|
||||
controller: 'DockerController'
|
||||
})
|
||||
.state('events', {
|
||||
url: '/events/',
|
||||
templateUrl: 'app/components/events/events.html',
|
||||
controller: 'EventsController'
|
||||
})
|
||||
.state('images', {
|
||||
url: '/images/',
|
||||
templateUrl: 'app/components/images/images.html',
|
||||
controller: 'ImagesController'
|
||||
})
|
||||
.state('image', {
|
||||
url: '^/images/:id/',
|
||||
templateUrl: 'app/components/image/image.html',
|
||||
controller: 'ImageController'
|
||||
})
|
||||
.state('networks', {
|
||||
url: '/networks/',
|
||||
templateUrl: 'app/components/networks/networks.html',
|
||||
controller: 'NetworksController'
|
||||
})
|
||||
.state('network', {
|
||||
url: '^/networks/:id/',
|
||||
templateUrl: 'app/components/network/network.html',
|
||||
controller: 'NetworkController'
|
||||
})
|
||||
.state('volumes', {
|
||||
url: '/volumes/',
|
||||
templateUrl: 'app/components/volumes/volumes.html',
|
||||
controller: 'VolumesController'
|
||||
})
|
||||
.state('swarm', {
|
||||
url: '/swarm/',
|
||||
templateUrl: 'app/components/swarm/swarm.html',
|
||||
controller: 'SwarmController'
|
||||
});
|
||||
|
||||
// The Docker API likes to return plaintext errors, this catches them and disp
|
||||
$httpProvider.interceptors.push(function() {
|
||||
return {
|
||||
'response': function(response) {
|
||||
if (typeof(response.data) === 'string' &&
|
||||
(response.data.startsWith('Conflict.') || response.data.startsWith('conflict:'))) {
|
||||
$.gritter.add({
|
||||
title: 'Error',
|
||||
text: $('<div>').text(response.data).html(),
|
||||
time: 10000
|
||||
});
|
||||
}
|
||||
var csrfToken = response.headers('X-Csrf-Token');
|
||||
if (csrfToken) {
|
||||
document.cookie = 'csrfToken=' + csrfToken;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
};
|
||||
});
|
||||
}])
|
||||
|
||||
// This is your docker url that the api will use to make requests
|
||||
// You need to set this to the api endpoint without the port i.e. http://192.168.1.9
|
||||
.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('UI_VERSION', 'v1.7.0');
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<div id="build-modal" class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h3>Build Image</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="editor"></div>
|
||||
<p>{{ messages }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="" class="btn btn-primary" ng-click="build()">Build</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,5 +0,0 @@
|
||||
angular.module('builder', [])
|
||||
.controller('BuilderController', ['$scope', 'Dockerfile', 'Messages',
|
||||
function ($scope, Dockerfile, Messages) {
|
||||
$scope.template = 'app/components/builder/builder.html';
|
||||
}]);
|
||||
@@ -1,196 +1,197 @@
|
||||
<div class="detail">
|
||||
<rd-header>
|
||||
<rd-header-title title="Container details">
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div ng-if="!container.edit">
|
||||
<h4>Container: {{ container.Name }}
|
||||
<button class="btn btn-primary"
|
||||
ng-click="container.edit = true;">Rename
|
||||
</button>
|
||||
</h4>
|
||||
</div>
|
||||
<div ng-if="container.edit">
|
||||
<h4>
|
||||
Container:
|
||||
<input type="text" ng-model="container.newContainerName">
|
||||
<button class="btn btn-success"
|
||||
ng-click="renameContainer()">Save
|
||||
</button>
|
||||
<button class="btn btn-danger"
|
||||
ng-click="container.edit = false;">×</button>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="btn-group detail">
|
||||
<button class="btn btn-success"
|
||||
ng-click="start()"
|
||||
ng-show="!container.State.Running">Start
|
||||
</button>
|
||||
<button class="btn btn-warning"
|
||||
ng-click="stop()"
|
||||
ng-show="container.State.Running && !container.State.Paused">Stop
|
||||
</button>
|
||||
<button class="btn btn-danger"
|
||||
ng-click="kill()"
|
||||
ng-show="container.State.Running && !container.State.Paused">Kill
|
||||
</button>
|
||||
<button class="btn btn-info"
|
||||
ng-click="pause()"
|
||||
ng-show="container.State.Running && !container.State.Paused">Pause
|
||||
</button>
|
||||
<button class="btn btn-success"
|
||||
ng-click="unpause()"
|
||||
ng-show="container.State.Running && container.State.Paused">Unpause
|
||||
</button>
|
||||
<button class="btn btn-success"
|
||||
ng-click="restart()"
|
||||
ng-show="container.State.Running && !container.State.Stopped">Restart
|
||||
</button>
|
||||
<button class="btn btn-primary"
|
||||
ng-click="commit()"
|
||||
ng-show="container.State.Running && !container.State.Paused">Commit
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table class="table table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Created:</td>
|
||||
<td>{{ container.Created | date: 'medium' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Path:</td>
|
||||
<td>{{ container.Path }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Args:</td>
|
||||
<td>
|
||||
<pre>{{ container.Args.join(' ') || 'None' }}</pre>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Exposed Ports:</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li ng-repeat="(k, v) in container.Config.ExposedPorts">{{ k }}</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Environment:</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li ng-repeat="k in container.Config.Env">{{ k }}</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Labels:</td>
|
||||
<td>
|
||||
<table role="table" class="table">
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
<tr ng-repeat="(k, v) in container.Config.Labels">
|
||||
<td>{{ k }}</td>
|
||||
<td>{{ v }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Publish All:</td>
|
||||
<td>{{ container.HostConfig.PublishAllPorts }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Ports:</td>
|
||||
<td>
|
||||
<ul style="display:inline-table">
|
||||
<li ng-repeat="(containerport, hostports) in container.HostConfig.PortBindings">
|
||||
{{ containerport }} => <span class="label label-default" 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>
|
||||
<pre>{{ container.Config.Entrypoint.join(' ') }}</pre>
|
||||
</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 href="#/images/{{ container.Image }}/">{{ container.Image }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>State:</td>
|
||||
<td>
|
||||
<accordion close-others="true">
|
||||
<accordion-group heading="{{ container.State|getstatetext }}">
|
||||
<ul>
|
||||
<li ng-repeat="(key, val) in container.State">{{key}} : {{ val }}</li>
|
||||
</ul>
|
||||
</accordion-group>
|
||||
</accordion>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Logs:</td>
|
||||
<td><a href="#/containers/{{ container.Id }}/logs">stdout/stderr</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Stats:</td>
|
||||
<td><a href="#/containers/{{ container.Id }}/stats">stats</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Top:</td>
|
||||
<td><a href="#/containers/{{ container.Id }}/top">Top</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="row-fluid">
|
||||
<div class="span1">
|
||||
Changes:
|
||||
<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 class="span5">
|
||||
<i class="icon-refresh" style="width:32px;height:32px;" ng-click="getChanges()"></i>
|
||||
<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>
|
||||
|
||||
<div class="well well-large">
|
||||
<ul>
|
||||
<li ng-repeat="change in changes | filter:hasContent">
|
||||
<strong>{{ change.Path }}</strong> {{ change.Kind }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
|
||||
<div class="btn-remove">
|
||||
<button class="btn btn-large btn-block btn-primary btn-danger" ng-click="remove()">Remove Container</button>
|
||||
</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-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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<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>Created</td>
|
||||
<td>{{ container.Created|getisodate }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
<ul>
|
||||
<li ng-repeat="(k, v) in container.Config.ExposedPorts">{{ k }}</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Environment</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li ng-repeat="k in container.Config.Env">{{ k }}</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Labels</td>
|
||||
<td>
|
||||
<table role="table" class="table">
|
||||
<tr ng-repeat="(k, v) in container.Config.Labels">
|
||||
<td>{{ k }}</td>
|
||||
<td>{{ v }}</td>
|
||||
</tr>
|
||||
</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 class="col-lg-3">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Container state details"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<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>
|
||||
|
||||
@@ -1,143 +1,208 @@
|
||||
angular.module('container', [])
|
||||
.controller('ContainerController', ['$scope', '$routeParams', '$location', 'Container', 'ContainerCommit', 'Messages', 'ViewSpinner',
|
||||
function ($scope, $routeParams, $location, Container, ContainerCommit, Messages, ViewSpinner) {
|
||||
$scope.changes = [];
|
||||
$scope.edit = false;
|
||||
.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 () {
|
||||
ViewSpinner.spin();
|
||||
Container.get({id: $routeParams.id}, function (d) {
|
||||
$scope.container = d;
|
||||
$scope.container.edit = false;
|
||||
$scope.container.newContainerName = d.Name;
|
||||
ViewSpinner.stop();
|
||||
}, function (e) {
|
||||
if (e.status === 404) {
|
||||
$('.detail').hide();
|
||||
Messages.error("Not found", "Container not found.");
|
||||
} else {
|
||||
Messages.error("Failure", e.data);
|
||||
}
|
||||
ViewSpinner.stop();
|
||||
});
|
||||
};
|
||||
var update = function () {
|
||||
$('#loadingViewSpinner').show();
|
||||
Container.get({id: $stateParams.id}, function (d) {
|
||||
$scope.container = d;
|
||||
$scope.container.edit = false;
|
||||
$scope.container.newContainerName = $filter('trimcontainername')(d.Name);
|
||||
|
||||
$scope.start = function () {
|
||||
ViewSpinner.spin();
|
||||
Container.start({
|
||||
id: $scope.container.Id,
|
||||
HostConfig: $scope.container.HostConfig
|
||||
}, function (d) {
|
||||
update();
|
||||
Messages.send("Container started", $routeParams.id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", "Container failed to start." + e.data);
|
||||
});
|
||||
};
|
||||
// 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]};
|
||||
});
|
||||
}
|
||||
|
||||
$scope.stop = function () {
|
||||
ViewSpinner.spin();
|
||||
Container.stop({id: $routeParams.id}, function (d) {
|
||||
update();
|
||||
Messages.send("Container stopped", $routeParams.id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", "Container failed to stop." + e.data);
|
||||
});
|
||||
};
|
||||
// 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] = [];
|
||||
}
|
||||
});
|
||||
|
||||
$scope.kill = function () {
|
||||
ViewSpinner.spin();
|
||||
Container.kill({id: $routeParams.id}, function (d) {
|
||||
update();
|
||||
Messages.send("Container killed", $routeParams.id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", "Container failed to die." + e.data);
|
||||
});
|
||||
};
|
||||
// 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 = '';
|
||||
}
|
||||
|
||||
$scope.commit = function () {
|
||||
ViewSpinner.spin();
|
||||
ContainerCommit.commit({id: $routeParams.id, repo: $scope.container.Config.Image}, function (d) {
|
||||
update();
|
||||
Messages.send("Container commited", $routeParams.id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", "Container failed to commit." + e.data);
|
||||
});
|
||||
};
|
||||
$scope.pause = function () {
|
||||
ViewSpinner.spin();
|
||||
Container.pause({id: $routeParams.id}, function (d) {
|
||||
update();
|
||||
Messages.send("Container paused", $routeParams.id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", "Container failed to pause." + e.data);
|
||||
});
|
||||
};
|
||||
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);
|
||||
});
|
||||
|
||||
$scope.unpause = function () {
|
||||
ViewSpinner.spin();
|
||||
Container.unpause({id: $routeParams.id}, function (d) {
|
||||
update();
|
||||
Messages.send("Container unpaused", $routeParams.id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", "Container failed to unpause." + e.data);
|
||||
});
|
||||
};
|
||||
$('#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();
|
||||
});
|
||||
|
||||
$scope.remove = function () {
|
||||
ViewSpinner.spin();
|
||||
Container.remove({id: $routeParams.id}, function (d) {
|
||||
update();
|
||||
Messages.send("Container removed", $routeParams.id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", "Container failed to remove." + e.data);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
$scope.restart = function () {
|
||||
ViewSpinner.spin();
|
||||
Container.restart({id: $routeParams.id}, function (d) {
|
||||
update();
|
||||
Messages.send("Container restarted", $routeParams.id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", "Container failed to restart." + e.data);
|
||||
});
|
||||
};
|
||||
$scope.start = function () {
|
||||
$('#loadingViewSpinner').show();
|
||||
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", "Container failed to start." + e.data);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.hasContent = function (data) {
|
||||
return data !== null && data !== undefined;
|
||||
};
|
||||
$scope.stop = function () {
|
||||
$('#loadingViewSpinner').show();
|
||||
Container.stop({id: $stateParams.id}, function (d) {
|
||||
update();
|
||||
Messages.send("Container stopped", $stateParams.id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", "Container failed to stop." + e.data);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.getChanges = function () {
|
||||
ViewSpinner.spin();
|
||||
Container.changes({id: $routeParams.id}, function (d) {
|
||||
$scope.changes = d;
|
||||
ViewSpinner.stop();
|
||||
});
|
||||
};
|
||||
$scope.kill = function () {
|
||||
$('#loadingViewSpinner').show();
|
||||
Container.kill({id: $stateParams.id}, function (d) {
|
||||
update();
|
||||
Messages.send("Container killed", $stateParams.id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", "Container failed to die." + e.data);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.renameContainer = function () {
|
||||
// #FIXME fix me later to handle http status to show the correct error message
|
||||
Container.rename({id: $routeParams.id, 'name': $scope.container.newContainerName}, function (data) {
|
||||
if (data.name) {
|
||||
$scope.container.Name = data.name;
|
||||
Messages.send("Container renamed", $routeParams.id);
|
||||
} else {
|
||||
$scope.container.newContainerName = $scope.container.Name;
|
||||
Messages.error("Failure", "Container failed to rename.");
|
||||
}
|
||||
});
|
||||
$scope.container.edit = false;
|
||||
};
|
||||
$scope.commit = function () {
|
||||
$('#loadingViewSpinner').show();
|
||||
ContainerCommit.commit({id: $stateParams.id, repo: $scope.container.Config.Image}, function (d) {
|
||||
update();
|
||||
Messages.send("Container commited", $stateParams.id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", "Container failed to commit." + e.data);
|
||||
});
|
||||
};
|
||||
$scope.pause = function () {
|
||||
$('#loadingViewSpinner').show();
|
||||
Container.pause({id: $stateParams.id}, function (d) {
|
||||
update();
|
||||
Messages.send("Container paused", $stateParams.id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", "Container failed to pause." + e.data);
|
||||
});
|
||||
};
|
||||
|
||||
update();
|
||||
$scope.getChanges();
|
||||
}]);
|
||||
$scope.unpause = function () {
|
||||
$('#loadingViewSpinner').show();
|
||||
Container.unpause({id: $stateParams.id}, function (d) {
|
||||
update();
|
||||
Messages.send("Container unpaused", $stateParams.id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", "Container failed to unpause." + e.data);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.remove = function () {
|
||||
$('#loadingViewSpinner').show();
|
||||
Container.remove({id: $stateParams.id}, function (d) {
|
||||
update();
|
||||
$state.go('containers', {}, {reload: true});
|
||||
Messages.send("Container removed", $stateParams.id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", "Container failed to remove." + e.data);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.restart = function () {
|
||||
$('#loadingViewSpinner').show();
|
||||
Container.restart({id: $stateParams.id}, function (d) {
|
||||
update();
|
||||
Messages.send("Container restarted", $stateParams.id);
|
||||
}, function (e) {
|
||||
update();
|
||||
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 () {
|
||||
// #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.newContainerName = $scope.container.Name;
|
||||
Messages.error("Failure", "Container failed to rename.");
|
||||
}
|
||||
});
|
||||
$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();
|
||||
}]);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Container console">
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Console
|
||||
</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-terminal" title="Console">
|
||||
<div class="pull-right">
|
||||
<i id="loadConsoleSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px; display: none;"></i>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<!-- command-list -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-3">
|
||||
<select class="selectpicker form-control" ng-model="state.command">
|
||||
<option value="bash">/bin/bash</option>
|
||||
<option value="sh">/bin/sh</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-9 pull-left">
|
||||
<button type="button" class="btn btn-primary" ng-click="connect()" ng-disabled="connected">Connect</button>
|
||||
<button type="button" class="btn btn-default" ng-click="disconnect()" ng-disabled="!connected">Disconnect</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<div id="terminal-container" class="terminal-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,109 @@
|
||||
angular.module('containerConsole', [])
|
||||
.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;
|
||||
|
||||
var socket, term;
|
||||
|
||||
// Ensure the socket is closed before leaving the view
|
||||
$scope.$on('$stateChangeStart', function (event, next, current) {
|
||||
if (socket !== null) {
|
||||
socket.close();
|
||||
}
|
||||
});
|
||||
|
||||
Container.get({id: $stateParams.id}, function(d) {
|
||||
$scope.container = d;
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
|
||||
$scope.connect = function() {
|
||||
$('#loadConsoleSpinner').show();
|
||||
var termWidth = Math.round($('#terminal-container').width() / 8.2);
|
||||
var termHeight = 30;
|
||||
var execConfig = {
|
||||
id: $stateParams.id,
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Tty: true,
|
||||
Cmd: $scope.state.command.replace(" ", ",").split(",")
|
||||
};
|
||||
|
||||
Container.exec(execConfig, function(d) {
|
||||
if (d.Id) {
|
||||
var execId = d.Id;
|
||||
resizeTTY(execId, termHeight, termWidth);
|
||||
var url = window.location.href.split('#')[0] + 'ws/exec?id=' + execId;
|
||||
if (url.indexOf('https') > -1) {
|
||||
url = url.replace('https://', 'wss://');
|
||||
} else {
|
||||
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.data);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.disconnect = function() {
|
||||
$scope.connected = false;
|
||||
if (socket !== null) {
|
||||
socket.close();
|
||||
}
|
||||
if (term !== null) {
|
||||
term.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
function resizeTTY(execId, height, width) {
|
||||
$timeout(function() {
|
||||
Exec.resize({id: execId, height: height, width: width}, function (d) {
|
||||
var error = errorMsgFilter(d);
|
||||
if (error) {
|
||||
Messages.error('Error', 'Unable to resize TTY');
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
|
||||
}
|
||||
|
||||
function initTerm(url, height, width) {
|
||||
socket = new WebSocket(url);
|
||||
|
||||
$scope.connected = true;
|
||||
socket.onopen = function(evt) {
|
||||
$('#loadConsoleSpinner').hide();
|
||||
term = new Terminal({
|
||||
cols: width,
|
||||
rows: height,
|
||||
cursorBlink: true
|
||||
});
|
||||
|
||||
term.on('data', function (data) {
|
||||
socket.send(data);
|
||||
});
|
||||
term.open(document.getElementById('terminal-container'));
|
||||
|
||||
socket.onmessage = function (e) {
|
||||
term.write(e.data);
|
||||
};
|
||||
socket.onerror = function (error) {
|
||||
$scope.connected = false;
|
||||
|
||||
};
|
||||
socket.onclose = function(evt) {
|
||||
$scope.connected = false;
|
||||
// term.write("Session terminated");
|
||||
// term.destroy();
|
||||
};
|
||||
};
|
||||
}
|
||||
}]);
|
||||
@@ -1,76 +1,79 @@
|
||||
angular.module('containerLogs', [])
|
||||
.controller('ContainerLogsController', ['$scope', '$routeParams', '$location', '$anchorScroll', 'ContainerLogs', 'Container', 'ViewSpinner',
|
||||
function ($scope, $routeParams, $location, $anchorScroll, ContainerLogs, Container, ViewSpinner) {
|
||||
$scope.stdout = '';
|
||||
$scope.stderr = '';
|
||||
$scope.showTimestamps = false;
|
||||
$scope.tailLines = 2000;
|
||||
.controller('ContainerLogsController', ['$scope', '$stateParams', '$anchorScroll', 'ContainerLogs', 'Container',
|
||||
function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container) {
|
||||
$scope.state = {};
|
||||
$scope.state.displayTimestampsOut = false;
|
||||
$scope.state.displayTimestampsErr = false;
|
||||
$scope.stdout = '';
|
||||
$scope.stderr = '';
|
||||
$scope.tailLines = 2000;
|
||||
|
||||
ViewSpinner.spin();
|
||||
Container.get({id: $routeParams.id}, function (d) {
|
||||
$scope.container = d;
|
||||
ViewSpinner.stop();
|
||||
}, function (e) {
|
||||
if (e.status === 404) {
|
||||
Messages.error("Not found", "Container not found.");
|
||||
} else {
|
||||
Messages.error("Failure", e.data);
|
||||
}
|
||||
ViewSpinner.stop();
|
||||
});
|
||||
$('#loadingViewSpinner').show();
|
||||
Container.get({id: $stateParams.id}, function (d) {
|
||||
$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();
|
||||
});
|
||||
|
||||
function getLogs() {
|
||||
ViewSpinner.spin();
|
||||
ContainerLogs.get($routeParams.id, {
|
||||
stdout: 1,
|
||||
stderr: 0,
|
||||
timestamps: $scope.showTimestamps,
|
||||
tail: $scope.tailLines
|
||||
}, function (data, status, headers, config) {
|
||||
// Replace carriage returns with newlines to clean up output
|
||||
data = data.replace(/[\r]/g, '\n');
|
||||
// Strip 8 byte header from each line of output
|
||||
data = data.substring(8);
|
||||
data = data.replace(/\n(.{8})/g, '\n');
|
||||
$scope.stdout = data;
|
||||
ViewSpinner.stop();
|
||||
});
|
||||
function getLogs() {
|
||||
$('#loadingViewSpinner').show();
|
||||
getLogsStdout();
|
||||
getLogsStderr();
|
||||
$('#loadingViewSpinner').hide();
|
||||
}
|
||||
|
||||
ContainerLogs.get($routeParams.id, {
|
||||
stdout: 0,
|
||||
stderr: 1,
|
||||
timestamps: $scope.showTimestamps,
|
||||
tail: $scope.tailLines
|
||||
}, function (data, status, headers, config) {
|
||||
// Replace carriage returns with newlines to clean up output
|
||||
data = data.replace(/[\r]/g, '\n');
|
||||
// Strip 8 byte header from each line of output
|
||||
data = data.substring(8);
|
||||
data = data.replace(/\n(.{8})/g, '\n');
|
||||
$scope.stderr = data;
|
||||
ViewSpinner.stop();
|
||||
});
|
||||
}
|
||||
function getLogsStderr() {
|
||||
ContainerLogs.get($stateParams.id, {
|
||||
stdout: 0,
|
||||
stderr: 1,
|
||||
timestamps: $scope.state.displayTimestampsErr,
|
||||
tail: $scope.tailLines
|
||||
}, function (data, status, headers, config) {
|
||||
// Replace carriage returns with newlines to clean up output
|
||||
data = data.replace(/[\r]/g, '\n');
|
||||
// Strip 8 byte header from each line of output
|
||||
data = data.substring(8);
|
||||
data = data.replace(/\n(.{8})/g, '\n');
|
||||
$scope.stderr = data;
|
||||
});
|
||||
}
|
||||
|
||||
// initial call
|
||||
getLogs();
|
||||
var logIntervalId = window.setInterval(getLogs, 5000);
|
||||
function getLogsStdout() {
|
||||
ContainerLogs.get($stateParams.id, {
|
||||
stdout: 1,
|
||||
stderr: 0,
|
||||
timestamps: $scope.state.displayTimestampsOut,
|
||||
tail: $scope.tailLines
|
||||
}, function (data, status, headers, config) {
|
||||
// Replace carriage returns with newlines to clean up output
|
||||
data = data.replace(/[\r]/g, '\n');
|
||||
// Strip 8 byte header from each line of output
|
||||
data = data.substring(8);
|
||||
data = data.replace(/\n(.{8})/g, '\n');
|
||||
$scope.stdout = data;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.$on("$destroy", function () {
|
||||
// clearing interval when view changes
|
||||
clearInterval(logIntervalId);
|
||||
});
|
||||
// initial call
|
||||
getLogs();
|
||||
var logIntervalId = window.setInterval(getLogs, 5000);
|
||||
|
||||
$scope.scrollTo = function (id) {
|
||||
$location.hash(id);
|
||||
$anchorScroll();
|
||||
};
|
||||
$scope.$on("$destroy", function () {
|
||||
// clearing interval when view changes
|
||||
clearInterval(logIntervalId);
|
||||
});
|
||||
|
||||
$scope.toggleTimestamps = function () {
|
||||
getLogs();
|
||||
};
|
||||
$scope.toggleTimestampsOut = function () {
|
||||
getLogsStdout();
|
||||
};
|
||||
|
||||
$scope.toggleTail = function () {
|
||||
getLogs();
|
||||
};
|
||||
}]);
|
||||
$scope.toggleTimestampsErr = function () {
|
||||
getLogsStderr();
|
||||
};
|
||||
}]);
|
||||
|
||||
@@ -1,43 +1,56 @@
|
||||
<div class="row logs">
|
||||
<div class="col-xs-12">
|
||||
<h4>Logs for container: <a href="#/containers/{{ container.Id }}/">{{ container.Name }}</a></td></h4>
|
||||
<rd-header>
|
||||
<rd-header-title title="Container logs">
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Logs
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="btn-group detail">
|
||||
<button class="btn btn-info" ng-click="scrollTo('stdout')">stdout</button>
|
||||
<button class="btn btn-warning" ng-click="scrollTo('stderr')">stderr</button>
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon grey pull-left">
|
||||
<i class="fa fa-tasks"></i>
|
||||
</div>
|
||||
<div class="pull-right col-xs-6">
|
||||
<div class="col-xs-6">
|
||||
<a class="btn btn-primary" ng-click="toggleTail()" role="button">Reload logs</a>
|
||||
<input id="tailLines" type="number" ng-style="{width: '45px'}"
|
||||
ng-model="tailLines" ng-keypress="($event.which === 13)? toggleTail() : 0"/>
|
||||
<label for="tailLines">lines</label>
|
||||
</div>
|
||||
<div class="col-xs-4">
|
||||
<input id="timestampToggle" type="checkbox" ng-model="showTimestamps"
|
||||
ng-change="toggleTimestamps()"/> <label for="timestampToggle">Timestamps</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 id="stdout" class="panel-title">STDOUT</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<pre id="stdoutLog" class="pre-scrollable pre-x-scrollable">{{stdout}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 id="stderr" class="panel-title">STDERR</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<pre id="stderrLog" class="pre-scrollable pre-x-scrollable">{{stderr}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="title">{{ container.Name|trimcontainername }}</div>
|
||||
<div class="comment">Name</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-info-circle" title="Stdout logs"></rd-widget-header>
|
||||
<rd-widget-taskbar>
|
||||
<input type="checkbox" ng-model="state.displayTimestampsOut" id="displayAllTsOut" ng-change="toggleTimestampsOut()"/>
|
||||
<label for="displayAllTsOut">Display timestamps</label>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="panel-body">
|
||||
<pre id="stdoutLog" class="pre-scrollable pre-x-scrollable">{{stdout}}</pre>
|
||||
</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-exclamation-triangle" title="Stderr logs"></rd-widget-header>
|
||||
<rd-widget-taskbar>
|
||||
<input type="checkbox" ng-model="state.displayTimestampsErr" id="displayAllTsErr" ng-change="toggleTimestampsErr()"/>
|
||||
<label for="displayAllTsErr">Display timestamps</label>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="panel-body">
|
||||
<pre id="stderrLog" class="pre-scrollable pre-x-scrollable">{{stderr}}</pre>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<div class="containerTop">
|
||||
<div class="form-group col-xs-2">
|
||||
<input type="text" class="form-control" placeholder="[options] (aux)" ng-model="ps_args">
|
||||
</div>
|
||||
<button type="button" class="btn btn-default" ng-click="getTop()">Submit</button>
|
||||
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th ng-repeat="title in containerTop.Titles">{{title}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="processInfos in containerTop.Processes">
|
||||
<td ng-repeat="processInfo in processInfos track by $index">{{processInfo}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1,19 +0,0 @@
|
||||
angular.module('containerTop', [])
|
||||
.controller('ContainerTopController', ['$scope', '$routeParams', 'ContainerTop', 'ViewSpinner', function ($scope, $routeParams, ContainerTop, ViewSpinner) {
|
||||
$scope.ps_args = '';
|
||||
|
||||
/**
|
||||
* Get container processes
|
||||
*/
|
||||
$scope.getTop = function () {
|
||||
ViewSpinner.spin();
|
||||
ContainerTop.get($routeParams.id, {
|
||||
ps_args: $scope.ps_args
|
||||
}, function (data) {
|
||||
$scope.containerTop = data;
|
||||
ViewSpinner.stop();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.getTop();
|
||||
}]);
|
||||
@@ -1,46 +1,101 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Container list">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="containers" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Containers</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<h2>Containers:</h2>
|
||||
|
||||
<div>
|
||||
<ul class="nav nav-pills pull-left">
|
||||
<li class="dropdown">
|
||||
<a class="dropdown-toggle" id="drop4" role="button" data-toggle="dropdown" data-target="#">Actions <b class="caret"></b></a>
|
||||
<ul id="menu1" class="dropdown-menu" role="menu" aria-labelledby="drop4">
|
||||
<li><a tabindex="-1" href="" ng-click="startAction()">Start</a></li>
|
||||
<li><a tabindex="-1" href="" ng-click="stopAction()">Stop</a></li>
|
||||
<li><a tabindex="-1" href="" ng-click="restartAction()">Restart</a></li>
|
||||
<li><a tabindex="-1" href="" ng-click="killAction()">Kill</a></li>
|
||||
<li><a tabindex="-1" href="" ng-click="pauseAction()">Pause</a></li>
|
||||
<li><a tabindex="-1" href="" ng-click="unpauseAction()">Unpause</a></li>
|
||||
<li><a tabindex="-1" href="" ng-click="removeAction()">Remove</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="pull-right form-inline">
|
||||
<input type="checkbox" ng-model="displayAll" id="displayAll" ng-change="toggleGetAll()"/> <label for="displayAll">Display All</label>
|
||||
<input type="text" class="form-control" style="vertical-align: center" id="filter" placeholder="Filter" ng-model="filter"/> <label class="sr-only" for="filter">Filter</label>
|
||||
</div>
|
||||
<div class="col-lg-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Containers">
|
||||
<div class="pull-right">
|
||||
<i id="loadContainersSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-lg-12">
|
||||
<div class="pull-left">
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<button type="button" class="btn btn-primary" ng-click="startAction()" ng-disabled="!state.selectedItemCount">Start</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="stopAction()" ng-disabled="!state.selectedItemCount">Stop</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="killAction()" ng-disabled="!state.selectedItemCount">Kill</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="restartAction()" ng-disabled="!state.selectedItemCount">Restart</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="pauseAction()" ng-disabled="!state.selectedItemCount">Pause</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="unpauseAction()" ng-disabled="!state.selectedItemCount">Unpause</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount">Remove</button>
|
||||
</div>
|
||||
<a class="btn btn-default" type="button" ui-sref="actions.create.container">Add container</a>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<input type="checkbox" ng-model="state.displayAll" id="displayAll" ng-change="toggleGetAll()" style="margin-top: -2px; margin-right: 5px;"/><label for="displayAll">Show all containers</label>
|
||||
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
|
||||
</div>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>
|
||||
<a ui-sref="containers" ng-click="order('Status')">
|
||||
State
|
||||
<span ng-show="sortType == 'State' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'State' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="containers" ng-click="order('Names')">
|
||||
Name
|
||||
<span ng-show="sortType == 'Names' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Names' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th ng-if="state.displayIP">
|
||||
<a ui-sref="containers" ng-click="order('IP')">
|
||||
IP Address
|
||||
<span ng-show="sortType == 'IP' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'IP' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th ng-if="swarm">
|
||||
<a ui-sref="containers" ng-click="order('Host')">
|
||||
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('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>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="container in (state.filteredContainers = ( containers | filter:state.filter | orderBy:sortType:sortReverse))">
|
||||
<td><input type="checkbox" ng-model="container.Checked" ng-change="selectItem(container)"/></td>
|
||||
<td><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 ng-if="state.displayIP">{{ container.IP ? container.IP : '-' }}</td>
|
||||
<td ng-if="swarm">{{ container.hostIP }}</td>
|
||||
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
|
||||
<td>{{ container.Command|truncate:60 }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
<rd-widget>
|
||||
</div>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" ng-model="toggle" ng-change="toggleSelectAll()" /> Action</th>
|
||||
<th>Name</th>
|
||||
<th>Image</th>
|
||||
<th>Command</th>
|
||||
<th>Created</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="container in containers | filter:filter | orderBy:predicate">
|
||||
<td><input type="checkbox" ng-model="container.Checked" /></td>
|
||||
<td><a href="#/containers/{{ container.Id }}/">{{ container|containername}}</a></td>
|
||||
<td><a href="#/images/{{ container.Image }}/">{{ container.Image }}</a></td>
|
||||
<td>{{ container.Command|truncate:40 }}</td>
|
||||
<td>{{ container.Created|getdate }}</td>
|
||||
<td><span class="label label-{{ container.Status|statusbadge }}">{{ container.Status }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,112 +1,188 @@
|
||||
angular.module('containers', [])
|
||||
.controller('ContainersController', ['$scope', 'Container', 'Settings', 'Messages', 'ViewSpinner',
|
||||
function ($scope, Container, Settings, Messages, ViewSpinner) {
|
||||
$scope.predicate = '-Created';
|
||||
$scope.toggle = false;
|
||||
$scope.displayAll = Settings.displayAll;
|
||||
.controller('ContainersController', ['$scope', 'Container', 'Info', 'Settings', 'Messages', 'Config', 'errorMsgFilter',
|
||||
function ($scope, Container, Info, Settings, Messages, Config, errorMsgFilter) {
|
||||
|
||||
var update = function (data) {
|
||||
ViewSpinner.spin();
|
||||
Container.query(data, function (d) {
|
||||
$scope.containers = d.map(function (item) {
|
||||
return new ContainerViewModel(item);
|
||||
});
|
||||
ViewSpinner.stop();
|
||||
});
|
||||
};
|
||||
$scope.state = {};
|
||||
$scope.state.displayAll = Settings.displayAll;
|
||||
$scope.state.displayIP = false;
|
||||
$scope.sortType = 'State';
|
||||
$scope.sortReverse = true;
|
||||
$scope.state.selectedItemCount = 0;
|
||||
|
||||
var batch = function (items, action, msg) {
|
||||
ViewSpinner.spin();
|
||||
var counter = 0;
|
||||
var complete = function () {
|
||||
counter = counter - 1;
|
||||
if (counter === 0) {
|
||||
ViewSpinner.stop();
|
||||
update({all: Settings.displayAll ? 1 : 0});
|
||||
}
|
||||
};
|
||||
angular.forEach(items, function (c) {
|
||||
if (c.Checked) {
|
||||
if (action === Container.start) {
|
||||
Container.get({id: c.Id}, function (d) {
|
||||
c = d;
|
||||
counter = counter + 1;
|
||||
action({id: c.Id, HostConfig: c.HostConfig || {}}, function (d) {
|
||||
Messages.send("Container " + msg, c.Id);
|
||||
var index = $scope.containers.indexOf(c);
|
||||
complete();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e.data);
|
||||
complete();
|
||||
});
|
||||
}, function (e) {
|
||||
if (e.status === 404) {
|
||||
$('.detail').hide();
|
||||
Messages.error("Not found", "Container not found.");
|
||||
} else {
|
||||
Messages.error("Failure", e.data);
|
||||
}
|
||||
complete();
|
||||
});
|
||||
}
|
||||
else {
|
||||
counter = counter + 1;
|
||||
action({id: c.Id}, function (d) {
|
||||
Messages.send("Container " + msg, c.Id);
|
||||
var index = $scope.containers.indexOf(c);
|
||||
complete();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e.data);
|
||||
complete();
|
||||
});
|
||||
$scope.order = function (sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
}
|
||||
var update = function (data) {
|
||||
$('#loadContainersSpinner').show();
|
||||
$scope.state.selectedItemCount = 0;
|
||||
Container.query(data, function (d) {
|
||||
var containers = d;
|
||||
if (hiddenLabels) {
|
||||
containers = hideContainers(d);
|
||||
}
|
||||
$scope.containers = containers.map(function (container) {
|
||||
var model = new ContainerViewModel(container);
|
||||
if (model.IP) {
|
||||
$scope.state.displayIP = true;
|
||||
}
|
||||
if ($scope.swarm) {
|
||||
model.hostIP = $scope.swarm_hosts[_.split(container.Names[0], '/')[1]];
|
||||
}
|
||||
return model;
|
||||
});
|
||||
$('#loadContainersSpinner').hide();
|
||||
});
|
||||
};
|
||||
|
||||
}
|
||||
});
|
||||
if (counter === 0) {
|
||||
ViewSpinner.stop();
|
||||
}
|
||||
};
|
||||
var batch = function (items, action, msg) {
|
||||
$('#loadContainersSpinner').show();
|
||||
var counter = 0;
|
||||
var complete = function () {
|
||||
counter = counter - 1;
|
||||
if (counter === 0) {
|
||||
$('#loadContainersSpinner').hide();
|
||||
update({all: Settings.displayAll ? 1 : 0});
|
||||
}
|
||||
};
|
||||
angular.forEach(items, function (c) {
|
||||
if (c.Checked) {
|
||||
counter = counter + 1;
|
||||
if (action === Container.start) {
|
||||
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) {
|
||||
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) {
|
||||
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.data);
|
||||
complete();
|
||||
});
|
||||
}
|
||||
else {
|
||||
action({id: c.Id}, function (d) {
|
||||
Messages.send("Container " + msg, c.Id);
|
||||
complete();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e.data);
|
||||
complete();
|
||||
});
|
||||
|
||||
$scope.toggleSelectAll = function () {
|
||||
angular.forEach($scope.containers, function (i) {
|
||||
i.Checked = $scope.toggle;
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
if (counter === 0) {
|
||||
$('#loadContainersSpinner').hide();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.toggleGetAll = function () {
|
||||
Settings.displayAll = $scope.displayAll;
|
||||
update({all: Settings.displayAll ? 1 : 0});
|
||||
};
|
||||
$scope.selectItem = function (item) {
|
||||
if (item.Checked) {
|
||||
$scope.state.selectedItemCount++;
|
||||
} else {
|
||||
$scope.state.selectedItemCount--;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.startAction = function () {
|
||||
batch($scope.containers, Container.start, "Started");
|
||||
};
|
||||
$scope.toggleGetAll = function () {
|
||||
Settings.displayAll = $scope.state.displayAll;
|
||||
update({all: Settings.displayAll ? 1 : 0});
|
||||
};
|
||||
|
||||
$scope.stopAction = function () {
|
||||
batch($scope.containers, Container.stop, "Stopped");
|
||||
};
|
||||
$scope.startAction = function () {
|
||||
batch($scope.containers, Container.start, "Started");
|
||||
};
|
||||
|
||||
$scope.restartAction = function () {
|
||||
batch($scope.containers, Container.restart, "Restarted");
|
||||
};
|
||||
$scope.stopAction = function () {
|
||||
batch($scope.containers, Container.stop, "Stopped");
|
||||
};
|
||||
|
||||
$scope.killAction = function () {
|
||||
batch($scope.containers, Container.kill, "Killed");
|
||||
};
|
||||
$scope.restartAction = function () {
|
||||
batch($scope.containers, Container.restart, "Restarted");
|
||||
};
|
||||
|
||||
$scope.pauseAction = function () {
|
||||
batch($scope.containers, Container.pause, "Paused");
|
||||
};
|
||||
$scope.killAction = function () {
|
||||
batch($scope.containers, Container.kill, "Killed");
|
||||
};
|
||||
|
||||
$scope.unpauseAction = function () {
|
||||
batch($scope.containers, Container.unpause, "Unpaused");
|
||||
};
|
||||
$scope.pauseAction = function () {
|
||||
batch($scope.containers, Container.pause, "Paused");
|
||||
};
|
||||
|
||||
$scope.removeAction = function () {
|
||||
batch($scope.containers, Container.remove, "Removed");
|
||||
};
|
||||
$scope.unpauseAction = function () {
|
||||
batch($scope.containers, Container.unpause, "Unpaused");
|
||||
};
|
||||
|
||||
update({all: Settings.displayAll ? 1 : 0});
|
||||
}]);
|
||||
$scope.removeAction = function () {
|
||||
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;
|
||||
var node_count = parseInt(systemStatus[3][1], 10);
|
||||
var node_offset = 4;
|
||||
for (i = 0; i < node_count; i++) {
|
||||
var host = {};
|
||||
host.name = _.trim(systemStatus[node_offset][0]);
|
||||
host.ip = _.split(systemStatus[node_offset][1], ':')[0];
|
||||
swarm_hosts[host.name] = host.ip;
|
||||
node_offset += 9;
|
||||
}
|
||||
return swarm_hosts;
|
||||
}
|
||||
|
||||
$scope.swarm = false;
|
||||
Config.$promise.then(function (c) {
|
||||
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});
|
||||
});
|
||||
} else {
|
||||
update({all: Settings.displayAll ? 1 : 0});
|
||||
}
|
||||
});
|
||||
}]);
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
<div class="detail">
|
||||
<h2>Containers Network</h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="input-group">
|
||||
<input type="text" ng-model="query" autofocus="true" class="form-control"
|
||||
placeholder="Search" ng-change="network.selectContainers(query)"/>
|
||||
<span class="input-group-addon"><span class="glyphicon glyphicon-search"/></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-warning" ng-click="network.hideSelected()">Hide Selected</button>
|
||||
<button class="btn btn-info" ng-click="network.showSelectedDownstream()">Show Selected Downstream</button>
|
||||
<button class="btn btn-info" ng-click="network.showSelectedUpstream()">Show Selected Upstream</button>
|
||||
<button class="btn btn-success" ng-click="network.showAll()">Show All</button>
|
||||
</div>
|
||||
<input type="checkbox" ng-model="includeStopped" id="includeStopped" ng-change="toggleIncludeStopped()"/> <label
|
||||
for="includeStopped">Include stopped containers</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<vis-network data="network.data" options="network.options" events="network.events"
|
||||
component="network.component"/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,271 +0,0 @@
|
||||
angular.module('containersNetwork', ['ngVis'])
|
||||
.controller('ContainersNetworkController', ['$scope', '$location', 'Container', 'Messages', 'VisDataSet', function ($scope, $location, Container, Messages, VisDataSet) {
|
||||
|
||||
function ContainerNode(data) {
|
||||
this.Id = data.Id;
|
||||
// names have the following format: /Name
|
||||
this.Name = data.Name.substring(1);
|
||||
this.Image = data.Config.Image;
|
||||
this.Running = data.State.Running;
|
||||
var dataLinks = data.HostConfig.Links;
|
||||
if (dataLinks != null) {
|
||||
this.Links = {};
|
||||
for (var i = 0; i < dataLinks.length; i++) {
|
||||
// links have the following format: /TargetContainerName:/SourceContainerName/LinkAlias
|
||||
var link = dataLinks[i].split(":");
|
||||
var target = link[0].substring(1);
|
||||
var alias = link[1].substring(link[1].lastIndexOf("/") + 1);
|
||||
// only keep shortest alias
|
||||
if (this.Links[target] == null || alias.length < this.Links[target].length) {
|
||||
this.Links[target] = alias;
|
||||
}
|
||||
}
|
||||
}
|
||||
var dataVolumes = data.HostConfig.VolumesFrom;
|
||||
//converting array into properties for simpler and faster access
|
||||
if (dataVolumes != null) {
|
||||
this.VolumesFrom = {};
|
||||
for (var j = 0; j < dataVolumes.length; j++) {
|
||||
this.VolumesFrom[dataVolumes[j]] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ContainersNetworkData() {
|
||||
this.nodes = new VisDataSet();
|
||||
this.edges = new VisDataSet();
|
||||
|
||||
this.addContainerNode = function (container) {
|
||||
this.nodes.add({
|
||||
id: container.Id,
|
||||
label: container.Name,
|
||||
title: "<ul style=\"list-style-type:none; padding: 0px; margin: 0px\">" +
|
||||
"<li><strong>ID:</strong> " + container.Id + "</li>" +
|
||||
"<li><strong>Image:</strong> " + container.Image + "</li>" +
|
||||
"</ul>",
|
||||
color: (container.Running ? "#8888ff" : "#cccccc")
|
||||
});
|
||||
};
|
||||
|
||||
this.hasEdge = function (from, to) {
|
||||
return this.edges.getIds({
|
||||
filter: function (item) {
|
||||
return item.from === from.Id && item.to === to.Id;
|
||||
}
|
||||
}).length > 0;
|
||||
};
|
||||
|
||||
this.addLinkEdgeIfExists = function (from, to) {
|
||||
if (from.Links != null && from.Links[to.Name] != null && !this.hasEdge(from, to)) {
|
||||
this.edges.add({
|
||||
from: from.Id,
|
||||
to: to.Id,
|
||||
label: from.Links[to.Name]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.addVolumeEdgeIfExists = function (from, to) {
|
||||
if (from.VolumesFrom != null && (from.VolumesFrom[to.Id] != null || from.VolumesFrom[to.Name] != null) && !this.hasEdge(from, to)) {
|
||||
this.edges.add({
|
||||
from: from.Id,
|
||||
to: to.Id,
|
||||
color: {color: '#A0A0A0', highlight: '#A0A0A0', hover: '#848484'}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.removeContainersNodes = function (containersIds) {
|
||||
this.nodes.remove(containersIds);
|
||||
};
|
||||
}
|
||||
|
||||
function ContainersNetwork() {
|
||||
this.data = new ContainersNetworkData();
|
||||
this.containers = {};
|
||||
this.selectedContainersIds = [];
|
||||
this.shownContainersIds = [];
|
||||
this.events = {
|
||||
select: function (event) {
|
||||
$scope.network.selectedContainersIds = event.nodes;
|
||||
$scope.$apply(function () {
|
||||
$scope.query = '';
|
||||
});
|
||||
},
|
||||
doubleClick: function (event) {
|
||||
$scope.$apply(function () {
|
||||
$location.path('/containers/' + event.nodes[0]);
|
||||
});
|
||||
}
|
||||
};
|
||||
this.options = {
|
||||
navigation: true,
|
||||
keyboard: true,
|
||||
height: '500px', width: '700px',
|
||||
nodes: {
|
||||
shape: 'box'
|
||||
},
|
||||
edges: {
|
||||
style: 'arrow'
|
||||
},
|
||||
physics: {
|
||||
barnesHut: {
|
||||
springLength: 200
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.addContainer = function (data) {
|
||||
var container = new ContainerNode(data);
|
||||
this.containers[container.Id] = container;
|
||||
this.shownContainersIds.push(container.Id);
|
||||
this.data.addContainerNode(container);
|
||||
for (var otherContainerId in this.containers) {
|
||||
var otherContainer = this.containers[otherContainerId];
|
||||
this.data.addLinkEdgeIfExists(container, otherContainer);
|
||||
this.data.addLinkEdgeIfExists(otherContainer, container);
|
||||
this.data.addVolumeEdgeIfExists(container, otherContainer);
|
||||
this.data.addVolumeEdgeIfExists(otherContainer, container);
|
||||
}
|
||||
};
|
||||
|
||||
this.selectContainers = function (query) {
|
||||
if (this.component != null) {
|
||||
this.selectedContainersIds = this.searchContainers(query);
|
||||
this.component.selectNodes(this.selectedContainersIds);
|
||||
}
|
||||
};
|
||||
|
||||
this.searchContainers = function (query) {
|
||||
if (query.trim() === "") {
|
||||
return [];
|
||||
}
|
||||
var selectedContainersIds = [];
|
||||
for (var i = 0; i < this.shownContainersIds.length; i++) {
|
||||
var container = this.containers[this.shownContainersIds[i]];
|
||||
if (container.Name.indexOf(query) > -1 ||
|
||||
container.Image.indexOf(query) > -1 ||
|
||||
container.Id.indexOf(query) > -1) {
|
||||
selectedContainersIds.push(container.Id);
|
||||
}
|
||||
}
|
||||
return selectedContainersIds;
|
||||
};
|
||||
|
||||
this.hideSelected = function () {
|
||||
var i = 0;
|
||||
while (i < this.shownContainersIds.length) {
|
||||
if (this.selectedContainersIds.indexOf(this.shownContainersIds[i]) > -1) {
|
||||
this.shownContainersIds.splice(i, 1);
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
this.data.removeContainersNodes(this.selectedContainersIds);
|
||||
$scope.query = '';
|
||||
this.selectedContainersIds = [];
|
||||
};
|
||||
|
||||
this.searchDownstream = function (containerId, downstreamContainersIds) {
|
||||
if (downstreamContainersIds.indexOf(containerId) > -1) {
|
||||
return;
|
||||
}
|
||||
downstreamContainersIds.push(containerId);
|
||||
var container = this.containers[containerId];
|
||||
if (container.Links == null && container.VolumesFrom == null) {
|
||||
return;
|
||||
}
|
||||
for (var otherContainerId in this.containers) {
|
||||
var otherContainer = this.containers[otherContainerId];
|
||||
if (container.Links != null && container.Links[otherContainer.Name] != null) {
|
||||
this.searchDownstream(otherContainer.Id, downstreamContainersIds);
|
||||
} else if (container.VolumesFrom != null &&
|
||||
container.VolumesFrom[otherContainer.Id] != null) {
|
||||
this.searchDownstream(otherContainer.Id, downstreamContainersIds);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.updateShownContainers = function (newShownContainersIds) {
|
||||
for (var containerId in this.containers) {
|
||||
if (newShownContainersIds.indexOf(containerId) > -1 &&
|
||||
this.shownContainersIds.indexOf(containerId) === -1) {
|
||||
this.data.addContainerNode(this.containers[containerId]);
|
||||
} else if (newShownContainersIds.indexOf(containerId) === -1 &&
|
||||
this.shownContainersIds.indexOf(containerId) > -1) {
|
||||
this.data.removeContainersNodes(containerId);
|
||||
}
|
||||
}
|
||||
this.shownContainersIds = newShownContainersIds;
|
||||
};
|
||||
|
||||
this.showSelectedDownstream = function () {
|
||||
var downstreamContainersIds = [];
|
||||
for (var i = 0; i < this.selectedContainersIds.length; i++) {
|
||||
this.searchDownstream(this.selectedContainersIds[i], downstreamContainersIds);
|
||||
}
|
||||
this.updateShownContainers(downstreamContainersIds);
|
||||
};
|
||||
|
||||
this.searchUpstream = function (containerId, upstreamContainersIds) {
|
||||
if (upstreamContainersIds.indexOf(containerId) > -1) {
|
||||
return;
|
||||
}
|
||||
upstreamContainersIds.push(containerId);
|
||||
var container = this.containers[containerId];
|
||||
for (var otherContainerId in this.containers) {
|
||||
var otherContainer = this.containers[otherContainerId];
|
||||
if (otherContainer.Links != null && otherContainer.Links[container.Name] != null) {
|
||||
this.searchUpstream(otherContainer.Id, upstreamContainersIds);
|
||||
} else if (otherContainer.VolumesFrom != null &&
|
||||
otherContainer.VolumesFrom[container.Id] != null) {
|
||||
this.searchUpstream(otherContainer.Id, upstreamContainersIds);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.showSelectedUpstream = function () {
|
||||
var upstreamContainersIds = [];
|
||||
for (var i = 0; i < this.selectedContainersIds.length; i++) {
|
||||
this.searchUpstream(this.selectedContainersIds[i], upstreamContainersIds);
|
||||
}
|
||||
this.updateShownContainers(upstreamContainersIds);
|
||||
};
|
||||
|
||||
this.showAll = function () {
|
||||
for (var containerId in this.containers) {
|
||||
if (this.shownContainersIds.indexOf(containerId) === -1) {
|
||||
this.data.addContainerNode(this.containers[containerId]);
|
||||
this.shownContainersIds.push(containerId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
$scope.network = new ContainersNetwork();
|
||||
|
||||
var showFailure = function (event) {
|
||||
Messages.error('Failure', e.data);
|
||||
};
|
||||
|
||||
var addContainer = function (container) {
|
||||
$scope.network.addContainer(container);
|
||||
};
|
||||
|
||||
var update = function (data) {
|
||||
Container.query(data, function (d) {
|
||||
for (var i = 0; i < d.length; i++) {
|
||||
Container.get({id: d[i].Id}, addContainer, showFailure);
|
||||
}
|
||||
});
|
||||
};
|
||||
update({all: 0});
|
||||
|
||||
$scope.includeStopped = false;
|
||||
$scope.toggleIncludeStopped = function () {
|
||||
$scope.network.updateShownContainers([]);
|
||||
update({all: $scope.includeStopped ? 1 : 0});
|
||||
};
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,234 @@
|
||||
angular.module('createContainer', [])
|
||||
.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
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
Console: 'none',
|
||||
Volumes: [],
|
||||
AvailableRegistries: [],
|
||||
Registry: ''
|
||||
};
|
||||
|
||||
$scope.imageConfig = {};
|
||||
|
||||
$scope.config = {
|
||||
Env: [],
|
||||
ExposedPorts: {},
|
||||
HostConfig: {
|
||||
RestartPolicy: {
|
||||
Name: 'no'
|
||||
},
|
||||
PortBindings: [],
|
||||
Binds: [],
|
||||
NetworkMode: 'bridge',
|
||||
Privileged: false
|
||||
}
|
||||
};
|
||||
|
||||
$scope.addVolume = function() {
|
||||
$scope.formValues.Volumes.push({ name: '', containerPath: '' });
|
||||
};
|
||||
|
||||
$scope.removeVolume = function(index) {
|
||||
$scope.formValues.Volumes.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addEnvironmentVariable = function() {
|
||||
$scope.config.Env.push({ name: '', value: ''});
|
||||
};
|
||||
|
||||
$scope.removeEnvironmentVariable = function(index) {
|
||||
$scope.config.Env.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addPortBinding = function() {
|
||||
$scope.config.HostConfig.PortBindings.push({ hostPort: '', containerPort: '', protocol: 'tcp' });
|
||||
};
|
||||
|
||||
$scope.removePortBinding = function(index) {
|
||||
$scope.config.HostConfig.PortBindings.splice(index, 1);
|
||||
};
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
var swarm = c.swarm;
|
||||
|
||||
$scope.formValues.AvailableRegistries = c.registries;
|
||||
|
||||
Volume.query({}, function (d) {
|
||||
$scope.availableVolumes = d.Volumes;
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e.data);
|
||||
});
|
||||
|
||||
Network.query({}, function (d) {
|
||||
var networks = d;
|
||||
if (swarm) {
|
||||
networks = d.filter(function (network) {
|
||||
if (network.Scope === 'global') {
|
||||
return network;
|
||||
}
|
||||
});
|
||||
$scope.globalNetworkCount = networks.length;
|
||||
networks.push({Name: "bridge"});
|
||||
networks.push({Name: "host"});
|
||||
networks.push({Name: "none"});
|
||||
}
|
||||
$scope.availableNetworks = networks;
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e.data);
|
||||
});
|
||||
});
|
||||
|
||||
function createContainer(config) {
|
||||
$('#createContainerSpinner').show();
|
||||
Container.create(config, function (d) {
|
||||
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('Error', errorMsgFilter(e));
|
||||
});
|
||||
} else {
|
||||
$('#createContainerSpinner').hide();
|
||||
Messages.error('Error', errorMsgFilter(d));
|
||||
}
|
||||
}, function (e) {
|
||||
$('#createContainerSpinner').hide();
|
||||
Messages.error('Error', errorMsgFilter(e));
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}, function (e) {
|
||||
$('#createContainerSpinner').hide();
|
||||
Messages.error('Error', 'Unable to pull image ' + image);
|
||||
});
|
||||
}
|
||||
|
||||
function createImageConfig(imageName, registry) {
|
||||
var imageNameAndTag = imageName.split(':');
|
||||
var image = imageNameAndTag[0];
|
||||
if (registry) {
|
||||
image = registry + '/' + imageNameAndTag[0];
|
||||
}
|
||||
var imageConfig = {
|
||||
fromImage: image,
|
||||
tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest'
|
||||
};
|
||||
return imageConfig;
|
||||
}
|
||||
|
||||
function prepareImageConfig(config) {
|
||||
var image = _.toLower(config.Image);
|
||||
var registry = $scope.formValues.Registry;
|
||||
var imageConfig = createImageConfig(image, registry);
|
||||
config.Image = imageConfig.fromImage + ':' + imageConfig.tag;
|
||||
$scope.imageConfig = imageConfig;
|
||||
}
|
||||
|
||||
function preparePortBindings(config) {
|
||||
var bindings = {};
|
||||
config.HostConfig.PortBindings.forEach(function (portBinding) {
|
||||
if (portBinding.hostPort && portBinding.containerPort) {
|
||||
var key = portBinding.containerPort + "/" + portBinding.protocol;
|
||||
var binding = {};
|
||||
if (portBinding.hostPort.indexOf(':') > -1) {
|
||||
var hostAndPort = portBinding.hostPort.split(':');
|
||||
binding.HostIp = hostAndPort[0];
|
||||
binding.HostPort = hostAndPort[1];
|
||||
} else {
|
||||
binding.HostPort = portBinding.hostPort;
|
||||
}
|
||||
bindings[key] = [binding];
|
||||
config.ExposedPorts[key] = {};
|
||||
}
|
||||
});
|
||||
config.HostConfig.PortBindings = bindings;
|
||||
}
|
||||
|
||||
function prepareConsole(config) {
|
||||
var value = $scope.formValues.Console;
|
||||
var openStdin = true;
|
||||
var tty = true;
|
||||
if (value === 'tty') {
|
||||
openStdin = false;
|
||||
} else if (value === 'interactive') {
|
||||
tty = false;
|
||||
} else if (value === 'none') {
|
||||
openStdin = false;
|
||||
tty = false;
|
||||
}
|
||||
config.OpenStdin = openStdin;
|
||||
config.Tty = tty;
|
||||
}
|
||||
|
||||
function prepareEnvironmentVariables(config) {
|
||||
var env = [];
|
||||
config.Env.forEach(function (v) {
|
||||
if (v.name && v.value) {
|
||||
env.push(v.name + "=" + v.value);
|
||||
}
|
||||
});
|
||||
config.Env = env;
|
||||
}
|
||||
|
||||
function prepareVolumes(config) {
|
||||
var binds = [];
|
||||
var volumes = {};
|
||||
|
||||
$scope.formValues.Volumes.forEach(function (volume) {
|
||||
var name = volume.name;
|
||||
var containerPath = volume.containerPath;
|
||||
if (name && containerPath) {
|
||||
var bind = name + ':' + containerPath;
|
||||
volumes[containerPath] = {};
|
||||
if (volume.readOnly) {
|
||||
bind += ':ro';
|
||||
}
|
||||
binds.push(bind);
|
||||
}
|
||||
});
|
||||
config.HostConfig.Binds = binds;
|
||||
config.Volumes = volumes;
|
||||
}
|
||||
|
||||
function prepareConfiguration() {
|
||||
var config = angular.copy($scope.config);
|
||||
prepareImageConfig(config);
|
||||
preparePortBindings(config);
|
||||
prepareConsole(config);
|
||||
prepareEnvironmentVariables(config);
|
||||
prepareVolumes(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
$scope.create = function () {
|
||||
var config = prepareConfiguration();
|
||||
|
||||
if ($scope.state.alwaysPull) {
|
||||
pullImageAndCreateContainer(config);
|
||||
} else {
|
||||
createContainer(config);
|
||||
}
|
||||
};
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,323 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Create container"></rd-header-title>
|
||||
<rd-header-content>
|
||||
Containers > Add container
|
||||
</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="container_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="container_name" placeholder="e.g. myContainer">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- image-and-registry-inputs -->
|
||||
<div class="form-group">
|
||||
<label for="container_image" class="col-sm-1 control-label text-left">Image</label>
|
||||
<div class="col-sm-7">
|
||||
<input type="text" class="form-control" ng-model="config.Image" id="container_image" placeholder="e.g. ubuntu:trusty">
|
||||
</div>
|
||||
<label for="image_registry" class="col-sm-1 control-label text-left">Registry</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" ng-model="formValues.Registry" id="image_registry" placeholder="leave empty to use DockerHub">
|
||||
</div>
|
||||
<div class="col-sm-offset-1 col-sm-11">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="state.alwaysPull"> Always pull image before creating
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !image-and-registry-inputs -->
|
||||
<!-- restart-policy -->
|
||||
<div class="form-group">
|
||||
<label class="col-sm-1 control-label text-left">Restart policy</label>
|
||||
<div class="col-sm-11">
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="container_restart_policy" ng-model="config.HostConfig.RestartPolicy.Name" value="no">
|
||||
Never
|
||||
</label>
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="container_restart_policy" ng-model="config.HostConfig.RestartPolicy.Name" value="always">
|
||||
Always
|
||||
</label>
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="container_restart_policy" ng-model="config.HostConfig.RestartPolicy.Name" value="on-failure">
|
||||
<span class="radio-value">On failure</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !restart-policy -->
|
||||
<!-- port-mapping -->
|
||||
<div class="form-group">
|
||||
<label for="container_ports" class="col-sm-1 control-label text-left">Port mapping</label>
|
||||
<div class="col-sm-11">
|
||||
<span class="label label-default clickable" ng-click="addPortBinding()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> map port
|
||||
</span>
|
||||
</div>
|
||||
<!-- port-mapping-input-list -->
|
||||
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="portBinding in config.HostConfig.PortBindings" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">host</span>
|
||||
<input type="text" class="form-control" ng-model="portBinding.hostPort" placeholder="e.g. 80 or 1.2.3.4:80">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">container</span>
|
||||
<input type="text" class="form-control" ng-model="portBinding.containerPort" placeholder="e.g. 80">
|
||||
</div>
|
||||
<div class="input-group col-sm-1 input-group-sm">
|
||||
<select class="selectpicker form-control" ng-model="portBinding.protocol">
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-click="removePortBinding($index)">
|
||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !port-mapping-input-list -->
|
||||
</div>
|
||||
<!-- !port-mapping -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active clickable"><a data-target="#command" data-toggle="tab">Command</a></li>
|
||||
<li class="clickable"><a data-target="#volumes" data-toggle="tab">Volumes</a></li>
|
||||
<li class="clickable"><a data-target="#network" data-toggle="tab">Network</a></li>
|
||||
<li class="clickable"><a data-target="#security" data-toggle="tab">Security/Host</a></li>
|
||||
</ul>
|
||||
<!-- tab-content -->
|
||||
<div class="tab-content">
|
||||
<!-- tab-command -->
|
||||
<div class="tab-pane active" id="command">
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<!-- command-input -->
|
||||
<div class="form-group">
|
||||
<label for="container_command" class="col-sm-1 control-label text-left">Command</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" ng-model="config.Cmd" id="container_command" placeholder="e.g. /usr/bin/nginx -t -c /mynginx.conf">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !command-input -->
|
||||
<!-- entrypoint-input -->
|
||||
<div class="form-group">
|
||||
<label for="container_entrypoint" class="col-sm-1 control-label text-left">Entry Point</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" ng-model="config.Entrypoint" id="container_entrypoint" placeholder="e.g. /bin/sh -c">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !entrypoint-input -->
|
||||
<!-- workdir-user-input -->
|
||||
<div class="form-group">
|
||||
<label for="container_workingdir" class="col-sm-1 control-label text-left">Working Dir</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" class="form-control" ng-model="config.WorkingDir" id="container_workingdir" placeholder="e.g. /myapp">
|
||||
</div>
|
||||
<label for="container_user" class="col-sm-1 control-label text-left">User</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" class="form-control" ng-model="config.User" id="container_user" placeholder="e.g. nginx">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !workdir-user-input -->
|
||||
<!-- console -->
|
||||
<div class="form-group">
|
||||
<label for="container_console" class="col-sm-1 control-label text-left">Console</label>
|
||||
<div class="col-sm-11">
|
||||
<div class="col-sm-4">
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="container_console" ng-model="formValues.Console" value="both">
|
||||
Interactive & TTY <span class="small text-muted">(-i -t)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="container_console" ng-model="formValues.Console" value="interactive">
|
||||
Interactive <span class="small text-muted">(-i)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-offset-1 col-sm-11">
|
||||
<div class="col-sm-4">
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="container_console" ng-model="formValues.Console" value="tty">
|
||||
TTY <span class="small text-muted">(-t)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="container_console" ng-model="formValues.Console" value="none">
|
||||
None
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !console -->
|
||||
<!-- environment-variables -->
|
||||
<div class="form-group">
|
||||
<label for="container_env" class="col-sm-1 control-label text-left">Environment variables</label>
|
||||
<div class="col-sm-11">
|
||||
<span class="label label-default clickable" ng-click="addEnvironmentVariable()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> environment variable
|
||||
</span>
|
||||
</div>
|
||||
<!-- environment-variable-input-list -->
|
||||
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="variable in config.Env" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input type="text" class="form-control" ng-model="variable.name" placeholder="e.g. FOO">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="variable.value" placeholder="e.g. bar">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-click="removeEnvironmentVariable($index)">
|
||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !environment-variable-input-list -->
|
||||
</div>
|
||||
<!-- !environment-variables -->
|
||||
</form>
|
||||
</div>
|
||||
<!-- !tab-command -->
|
||||
<!-- tab-volume -->
|
||||
<div class="tab-pane" id="volumes">
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<!-- volumes -->
|
||||
<div class="form-group">
|
||||
<label for="container_volumes" class="col-sm-1 control-label text-left">Volumes</label>
|
||||
<div class="col-sm-11">
|
||||
<span class="label label-default clickable" ng-click="addVolume()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> volume
|
||||
</span>
|
||||
</div>
|
||||
<!-- volumes-input-list -->
|
||||
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="volume in formValues.Volumes" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-1 input-group-sm">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="volume.readOnly"> Read-only
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon"><input type="checkbox" ng-model="volume.isPath" ng-click="resetVolumePath($index)">Path</span>
|
||||
<select class="selectpicker form-control" ng-model="volume.name" ng-if="!volume.isPath">
|
||||
<option selected disabled hidden value="">Select a volume</option>
|
||||
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option>
|
||||
</select>
|
||||
<input ng-if="volume.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>
|
||||
<input type="text" class="form-control" ng-model="volume.containerPath" placeholder="e.g. /path/in/container">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-click="removeVolume($index)">
|
||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !volumes-input-list -->
|
||||
</div>
|
||||
</form>
|
||||
<!-- !volumes -->
|
||||
</div>
|
||||
<!-- !tab-volume -->
|
||||
<!-- tab-network -->
|
||||
<div class="tab-pane" id="network">
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<div class="form-group" ng-if="globalNetworkCount === 0">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">You don't have any shared network. Head over the <a ui-sref="networks">networks view</a> to create one.</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- network-input -->
|
||||
<div class="form-group">
|
||||
<label for="container_network" class="col-sm-1 control-label text-left">Network</label>
|
||||
<div class="col-sm-9">
|
||||
<select class="selectpicker form-control" ng-model="config.HostConfig.NetworkMode">
|
||||
<option selected disabled hidden value="">Select a network</option>
|
||||
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !network-input -->
|
||||
<!-- hostname-input -->
|
||||
<div class="form-group">
|
||||
<label for="container_hostname" class="col-sm-1 control-label text-left">Hostname</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" ng-model="config.Hostname" id="container_hostname" placeholder="e.g. web01">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !hostname-input -->
|
||||
<!-- domainname-input -->
|
||||
<div class="form-group">
|
||||
<label for="container_domainname" class="col-sm-1 control-label text-left">Domain Name</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" ng-model="config.Domainname" id="container_domainname" placeholder="e.g. example.com">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !domainname -->
|
||||
</form>
|
||||
</div>
|
||||
<!-- !tab-network -->
|
||||
<!-- tab-security -->
|
||||
<div class="tab-pane" id="security">
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<!-- privileged-mode -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="config.HostConfig.Privileged"> Privileged mode
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !privileged-mode -->
|
||||
</form>
|
||||
</div>
|
||||
<!-- !tab-security -->
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" style="text-align: center;">
|
||||
<div>
|
||||
<i id="createContainerSpinner" class="fa fa-cog fa-3x fa-spin" style="margin-bottom: 5px; display: none;"></i>
|
||||
</div>
|
||||
<button type="button" class="btn btn-default btn-lg" ng-click="create()">Create</button>
|
||||
<a type="button" class="btn btn-default btn-lg" ui-sref="containers">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,74 @@
|
||||
angular.module('createNetwork', [])
|
||||
.controller('CreateNetworkController', ['$scope', '$state', 'Messages', 'Network', 'errorMsgFilter',
|
||||
function ($scope, $state, Messages, Network, errorMsgFilter) {
|
||||
$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.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);
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
}]);
|
||||
@@ -0,0 +1,95 @@
|
||||
<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>
|
||||
@@ -0,0 +1,56 @@
|
||||
angular.module('createVolume', [])
|
||||
.controller('CreateVolumeController', ['$scope', '$state', 'Volume', 'Messages', 'errorMsgFilter',
|
||||
function ($scope, $state, Volume, Messages, errorMsgFilter) {
|
||||
|
||||
$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.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);
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
}]);
|
||||
@@ -0,0 +1,72 @@
|
||||
<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,52 +1,131 @@
|
||||
<div class="col-xs-offset-1">
|
||||
<!--<div class="sidebar span4">
|
||||
<div ng-include="template" ng-controller="SideBarController"></div>
|
||||
</div>-->
|
||||
<div class="row">
|
||||
<div class="col-xs-10" id="masthead" style="display:none">
|
||||
<div class="jumbotron">
|
||||
<h1>DockerUI</h1>
|
||||
<rd-header>
|
||||
<rd-header-title title="Home">
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Dashboard</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<p class="lead">The Linux container engine</p>
|
||||
<a class="btn btn-large btn-success" href="http://docker.io">Learn more.</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-10">
|
||||
<div class="col-xs-5">
|
||||
<h3>Running Containers</h3>
|
||||
<ul>
|
||||
<li ng-repeat="container in containers|orderBy:predicate">
|
||||
<a href="#/containers/{{ container.Id }}/">{{ container|containername }}</a>
|
||||
<span class="label label-{{ container.Status|statusbadge }}">{{ container.Status }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-xs-5 text-right">
|
||||
<h3>Status</h3>
|
||||
<canvas id="containers-chart" class="pull-right">
|
||||
<p class="browserupgrade">You are using an <strong>outdated</strong> browser. Please <a
|
||||
href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
|
||||
</canvas>
|
||||
<div id="chart-legend"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-10" id="stats">
|
||||
<h4>Containers created</h4>
|
||||
<canvas id="containers-started-chart" width="700">
|
||||
<p class="browserupgrade">You are using an <strong>outdated</strong> browser. Please <a
|
||||
href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
|
||||
</canvas>
|
||||
<h4>Images created</h4>
|
||||
<canvas id="images-created-chart" width="700">
|
||||
<p class="browserupgrade">You are using an <strong>outdated</strong> browser. Please <a
|
||||
href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
|
||||
</canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="!swarm">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tachometer" title="Node info"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ infoData.Name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Docker version</td>
|
||||
<td>{{ infoData.ServerVersion }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CPU</td>
|
||||
<td>{{ infoData.NCPU }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Memory</td>
|
||||
<td>{{ infoData.MemTotal|humansize }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="swarm">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tachometer" title="Cluster info"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Nodes</td>
|
||||
<td>{{ infoData.SystemStatus[3][1] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Swarm version</td>
|
||||
<td>{{ infoData.ServerVersion|swarmversion }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total CPU</td>
|
||||
<td>{{ infoData.NCPU }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total memory</td>
|
||||
<td>{{ infoData.MemTotal|humansize }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
|
||||
<a ui-sref="containers">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon blue pull-left">
|
||||
<i class="fa fa-tasks"></i>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<div><i class="fa fa-heartbeat text-icon green-icon"></i>{{ containerData.running }} running</div>
|
||||
<div><i class="fa fa-heartbeat text-icon red-icon"></i>{{ containerData.stopped }} stopped</div>
|
||||
</div>
|
||||
<div class="title">{{ containerData.total }}</div>
|
||||
<div class="comment">Containers</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
|
||||
<a ui-sref="images">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon blue pull-left">
|
||||
<i class="fa fa-clone"></i>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<div><i class="fa fa-pie-chart text-icon"></i>{{ imageData.size|humansize }}</div>
|
||||
</div>
|
||||
<div class="title">{{ imageData.total }}</div>
|
||||
<div class="comment">Images</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
|
||||
<a ui-sref="volumes">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon blue pull-left">
|
||||
<i class="fa fa-cubes"></i>
|
||||
</div>
|
||||
<div class="pull-right" ng-if="infoData.Driver">
|
||||
<div><i class="fa fa-hdd-o text-icon"></i>{{ infoData.Driver }} driver</div>
|
||||
</div>
|
||||
<div class="title">{{ volumeData.total }}</div>
|
||||
<div class="comment">Volumes</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
|
||||
<a ui-sref="networks">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon blue pull-left">
|
||||
<i class="fa fa-sitemap"></i>
|
||||
</div>
|
||||
<div class="title">{{ networkData.total }}</div>
|
||||
<div class="comment">Networks</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
</div>
|
||||
|
||||
@@ -1,74 +1,106 @@
|
||||
angular.module('dashboard', [])
|
||||
.controller('DashboardController', ['$scope', 'Container', 'Image', 'Settings', 'LineChart', function ($scope, Container, Image, Settings, LineChart) {
|
||||
$scope.predicate = '-Created';
|
||||
$scope.containers = [];
|
||||
.controller('DashboardController', ['$scope', '$q', 'Config', 'Container', 'Image', 'Network', 'Volume', 'Info',
|
||||
function ($scope, $q, Config, Container, Image, Network, Volume, Info) {
|
||||
|
||||
var getStarted = function (data) {
|
||||
$scope.totalContainers = data.length;
|
||||
LineChart.build('#containers-started-chart', data, function (c) {
|
||||
return new Date(c.Created * 1000).toLocaleDateString();
|
||||
});
|
||||
var s = $scope;
|
||||
Image.query({}, function (d) {
|
||||
s.totalImages = d.length;
|
||||
LineChart.build('#images-created-chart', d, function (c) {
|
||||
return new Date(c.Created * 1000).toLocaleDateString();
|
||||
});
|
||||
});
|
||||
};
|
||||
$scope.containerData = {
|
||||
total: 0
|
||||
};
|
||||
$scope.imageData = {
|
||||
total: 0
|
||||
};
|
||||
$scope.networkData = {
|
||||
total: 0
|
||||
};
|
||||
$scope.volumeData = {
|
||||
total: 0
|
||||
};
|
||||
|
||||
var opts = {animation: false};
|
||||
if (Settings.firstLoad) {
|
||||
opts.animation = true;
|
||||
Settings.firstLoad = false;
|
||||
$('#masthead').show();
|
||||
function prepareContainerData(d) {
|
||||
var running = 0;
|
||||
var stopped = 0;
|
||||
|
||||
setTimeout(function () {
|
||||
$('#masthead').slideUp('slow');
|
||||
}, 5000);
|
||||
var containers = d;
|
||||
if (hiddenLabels) {
|
||||
containers = hideContainers(d);
|
||||
}
|
||||
|
||||
for (var i = 0; i < containers.length; i++) {
|
||||
var item = containers[i];
|
||||
if (item.Status.indexOf('Up') !== -1) {
|
||||
running += 1;
|
||||
} else if (item.Status.indexOf('Exit') !== -1) {
|
||||
stopped += 1;
|
||||
}
|
||||
}
|
||||
$scope.containerData.running = running;
|
||||
$scope.containerData.stopped = stopped;
|
||||
$scope.containerData.total = containers.length;
|
||||
}
|
||||
|
||||
function prepareImageData(d) {
|
||||
var images = d;
|
||||
var totalImageSize = 0;
|
||||
for (var i = 0; i < images.length; i++) {
|
||||
var item = images[i];
|
||||
totalImageSize += item.VirtualSize;
|
||||
}
|
||||
$scope.imageData.total = images.length;
|
||||
$scope.imageData.size = totalImageSize;
|
||||
}
|
||||
|
||||
function prepareVolumeData(d) {
|
||||
var volumes = d.Volumes;
|
||||
if (volumes) {
|
||||
$scope.volumeData.total = volumes.length;
|
||||
}
|
||||
}
|
||||
|
||||
function prepareNetworkData(d) {
|
||||
var networks = d;
|
||||
$scope.networkData.total = networks.length;
|
||||
}
|
||||
|
||||
function prepareInfoData(d) {
|
||||
var info = d;
|
||||
$scope.infoData = info;
|
||||
}
|
||||
|
||||
function fetchDashboardData() {
|
||||
$('#loadingViewSpinner').show();
|
||||
$q.all([
|
||||
Container.query({all: 1}).$promise,
|
||||
Image.query({}).$promise,
|
||||
Volume.query({}).$promise,
|
||||
Network.query({}).$promise,
|
||||
Info.get({}).$promise
|
||||
]).then(function (d) {
|
||||
prepareContainerData(d[0]);
|
||||
prepareImageData(d[1]);
|
||||
prepareVolumeData(d[2]);
|
||||
prepareNetworkData(d[3]);
|
||||
prepareInfoData(d[4]);
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Container.query({all: 1}, function (d) {
|
||||
var running = 0;
|
||||
var ghost = 0;
|
||||
var stopped = 0;
|
||||
|
||||
for (var i = 0; i < d.length; i++) {
|
||||
var item = d[i];
|
||||
|
||||
if (item.Status === "Ghost") {
|
||||
ghost += 1;
|
||||
} else if (item.Status.indexOf('Exit') !== -1) {
|
||||
stopped += 1;
|
||||
} else {
|
||||
running += 1;
|
||||
$scope.containers.push(new ContainerViewModel(item));
|
||||
}
|
||||
}
|
||||
|
||||
getStarted(d);
|
||||
|
||||
var c = new Chart($('#containers-chart').get(0).getContext("2d"));
|
||||
var data = [
|
||||
{
|
||||
value: running,
|
||||
color: '#5bb75b',
|
||||
title: 'Running'
|
||||
}, // running
|
||||
{
|
||||
value: stopped,
|
||||
color: '#C7604C',
|
||||
title: 'Stopped'
|
||||
}, // stopped
|
||||
{
|
||||
value: ghost,
|
||||
color: '#E2EAE9',
|
||||
title: 'Ghost'
|
||||
} // ghost
|
||||
];
|
||||
|
||||
c.Doughnut(data, opts);
|
||||
var lgd = $('#chart-legend').get(0);
|
||||
legend(lgd, data);
|
||||
});
|
||||
}]);
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.swarm = c.swarm;
|
||||
hiddenLabels = c.hiddenLabels;
|
||||
fetchDashboardData();
|
||||
});
|
||||
}]);
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
angular.module('dashboard')
|
||||
.controller('MasterCtrl', ['$scope', '$cookieStore', 'Settings', 'Config', function ($scope, $cookieStore, Settings, Config) {
|
||||
/**
|
||||
* Sidebar Toggle & Cookie Control
|
||||
*/
|
||||
var mobileView = 992;
|
||||
|
||||
$scope.getWidth = function() {
|
||||
return window.innerWidth;
|
||||
};
|
||||
|
||||
$scope.config = Config;
|
||||
|
||||
$scope.$watch($scope.getWidth, function(newValue, oldValue) {
|
||||
if (newValue >= mobileView) {
|
||||
if (angular.isDefined($cookieStore.get('toggle'))) {
|
||||
$scope.toggle = ! $cookieStore.get('toggle') ? false : true;
|
||||
} else {
|
||||
$scope.toggle = true;
|
||||
}
|
||||
} else {
|
||||
$scope.toggle = false;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
$scope.toggleSidebar = function() {
|
||||
$scope.toggle = !$scope.toggle;
|
||||
$cookieStore.put('toggle', $scope.toggle);
|
||||
};
|
||||
|
||||
window.onresize = function() {
|
||||
$scope.$apply();
|
||||
};
|
||||
|
||||
$scope.uiVersion = Settings.uiVersion;
|
||||
}]);
|
||||
@@ -0,0 +1,145 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Engine overview">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="docker" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Docker</rd-header-content>
|
||||
</rd-header>
|
||||
<div class="row">
|
||||
<div class="col-lg-3 col-md-6 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon pull-left">
|
||||
<i class="fa fa-code"></i>
|
||||
</div>
|
||||
<div class="title">{{ docker.Version }}</div>
|
||||
<div class="comment">Docker version</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon pull-left">
|
||||
<i class="fa fa-code"></i>
|
||||
</div>
|
||||
<div class="title">{{ docker.ApiVersion }}</div>
|
||||
<div class="comment">API version</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon pull-left">
|
||||
<i class="fa fa-code"></i>
|
||||
</div>
|
||||
<div class="title">{{ docker.GoVersion }}</div>
|
||||
<div class="comment">Go version</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-object-group" title="Engine status"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Containers</td>
|
||||
<td>{{ info.Containers }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Images</td>
|
||||
<td>{{ info.Images }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Debug</td>
|
||||
<td>{{ info.Debug }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CPUs</td>
|
||||
<td>{{ info.NCPU }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total Memory</td>
|
||||
<td>{{ info.MemTotal|humansize }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Operating System</td>
|
||||
<td>{{ info.OperatingSystem }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kernel Version</td>
|
||||
<td>{{ info.KernelVersion }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ID</td>
|
||||
<td>{{ info.ID }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Labels</td>
|
||||
<td>{{ info.Labels }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>File Descriptors</td>
|
||||
<td>{{ info.NFd }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Goroutines</td>
|
||||
<td>{{ info.NGoroutines }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Storage Driver</td>
|
||||
<td>{{ info.Driver }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Storage Driver Status</td>
|
||||
<td>
|
||||
<p ng-repeat="val in info.DriverStatus">
|
||||
{{ val[0] }}: {{ val[1] }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Execution Driver</td>
|
||||
<td>{{ info.ExecutionDriver }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IPv4 Forwarding</td>
|
||||
<td>{{ info.IPv4Forwarding }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Index Server Address</td>
|
||||
<td>{{ info.IndexServerAddress }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Init Path</td>
|
||||
<td>{{ info.InitPath }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Docker Root Directory</td>
|
||||
<td>{{ info.DockerRootDir }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Init SHA1</td>
|
||||
<td>{{ info.InitSha1 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Memory Limit</td>
|
||||
<td>{{ info.MemoryLimit }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Swap Limit</td>
|
||||
<td>{{ info.SwapLimit }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,19 @@
|
||||
angular.module('docker', [])
|
||||
.controller('DockerController', ['$scope', 'Info', 'Version', 'Settings',
|
||||
function ($scope, Info, Version, Settings) {
|
||||
|
||||
$scope.info = {};
|
||||
$scope.docker = {};
|
||||
|
||||
$scope.order = function(sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
Version.get({}, function (d) {
|
||||
$scope.docker = d;
|
||||
});
|
||||
Info.get({}, function (d) {
|
||||
$scope.info = d;
|
||||
});
|
||||
}]);
|
||||
@@ -1,34 +1,63 @@
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<h2>Events</h2>
|
||||
<rd-header>
|
||||
<rd-header-title title="Event list">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="events" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Events</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<form class="form-inline">
|
||||
<div class="form-group">
|
||||
<label for="since">Since:</label>
|
||||
<input id="since" type="datetime-local" ng-model="model.since" class="form-control" step="any"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="until">Until:</label>
|
||||
<input id="until" type="datetime-local" ng-model="model.until" class="form-control" step="any"/>
|
||||
</div>
|
||||
<button ng-click="updateEvents()" class="btn btn-primary">Update</button>
|
||||
</form>
|
||||
<br>
|
||||
<table class="table">
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-history" title="Events">
|
||||
<div class="pull-right">
|
||||
<i id="loadEventsSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-lg-12">
|
||||
<div class="pull-right">
|
||||
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
|
||||
</div>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<a ui-sref="events" ng-click="order('Time')">
|
||||
Date
|
||||
<span ng-show="sortType == 'Time' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Time' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="events" ng-click="order('Type')">
|
||||
Category
|
||||
<span ng-show="sortType == 'Type' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Type' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="events" ng-click="order('Details')">
|
||||
Details
|
||||
<span ng-show="sortType == 'Details' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Details' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Event</th>
|
||||
<th>From</th>
|
||||
<th>ID</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
<tr ng-repeat="event in dockerEvents">
|
||||
<td ng-bind="event.status"/>
|
||||
<td ng-bind="event.from"/>
|
||||
<td ng-bind="event.id"/>
|
||||
<td ng-bind="event.time * 1000 | date:'medium'"/>
|
||||
</tr>
|
||||
<tr ng-repeat="event in (events | filter:state.filter | orderBy:sortType:sortReverse)">
|
||||
<td>{{ event.Time|getisodatefromtimestamp }}</td>
|
||||
<td>{{ event.Type }}</td>
|
||||
<td>{{ event.Details }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</table>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
<rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,42 +1,27 @@
|
||||
angular.module('events', ['ngOboe'])
|
||||
.controller('EventsController', ['Settings', '$scope', 'Oboe', 'Messages', '$timeout', function (Settings, $scope, oboe, Messages, $timeout) {
|
||||
$scope.updateEvents = function () {
|
||||
$scope.dockerEvents = [];
|
||||
angular.module('events', [])
|
||||
.controller('EventsController', ['$scope', 'Settings', 'Messages', 'Events',
|
||||
function ($scope, Settings, Messages, Events) {
|
||||
$scope.state = {};
|
||||
$scope.sortType = 'Time';
|
||||
$scope.sortReverse = true;
|
||||
|
||||
// TODO: Clean up URL building
|
||||
var url = Settings.url + '/events?';
|
||||
$scope.order = function(sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
if ($scope.model.since) {
|
||||
var sinceSecs = Math.floor($scope.model.since.getTime() / 1000);
|
||||
url += 'since=' + sinceSecs + '&';
|
||||
}
|
||||
if ($scope.model.until) {
|
||||
var untilSecs = Math.floor($scope.model.until.getTime() / 1000);
|
||||
url += 'until=' + untilSecs;
|
||||
}
|
||||
var from = moment().subtract(24, 'hour').unix();
|
||||
var to = moment().unix();
|
||||
|
||||
oboe({
|
||||
url: url,
|
||||
pattern: '{id status time}'
|
||||
})
|
||||
.then(function (node) {
|
||||
// finished loading
|
||||
$timeout(function () {
|
||||
$scope.$apply();
|
||||
});
|
||||
}, function (error) {
|
||||
// handle errors
|
||||
Messages.error("Failure", error.data);
|
||||
}, function (node) {
|
||||
// node received
|
||||
$scope.dockerEvents.push(node);
|
||||
});
|
||||
};
|
||||
|
||||
// Init
|
||||
$scope.model = {};
|
||||
$scope.model.since = new Date(Date.now() - 86400000); // 24 hours in the past
|
||||
$scope.model.until = new Date();
|
||||
$scope.updateEvents();
|
||||
|
||||
}]);
|
||||
Events.query({since: from, until: to},
|
||||
function(d) {
|
||||
$scope.events = d.map(function (item) {
|
||||
return new EventViewModel(item);
|
||||
});
|
||||
$('#loadEventsSpinner').hide();
|
||||
},
|
||||
function (e) {
|
||||
Messages.error("Unable to load events", e.data);
|
||||
$('#loadEventsSpinner').hide();
|
||||
});
|
||||
}]);
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
angular.module('footer', [])
|
||||
.controller('FooterController', ['$scope', 'Settings', 'Docker', function ($scope, Settings, Docker) {
|
||||
$scope.template = 'app/components/footer/statusbar.html';
|
||||
|
||||
$scope.uiVersion = Settings.uiVersion;
|
||||
Docker.get({}, function (d) {
|
||||
$scope.apiVersion = d.ApiVersion;
|
||||
});
|
||||
}]);
|
||||
@@ -1,6 +0,0 @@
|
||||
<footer class="center well">
|
||||
<p>
|
||||
<small>Docker API Version: <strong>{{ apiVersion }}</strong> UI Version: <strong>{{ uiVersion }}</strong> <a
|
||||
class="pull-right" href="https://github.com/crosbymichael/dockerui">dockerui</a></small>
|
||||
</p>
|
||||
</footer>
|
||||
@@ -1,120 +1,162 @@
|
||||
<div ng-include="template" ng-controller="StartContainerController"></div>
|
||||
<rd-header>
|
||||
<rd-header-title title="Image details">
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
Images > <a ui-sref="image({id: image.Id})">{{ image.Id }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="alert alert-error" id="error-message" style="display:none">
|
||||
{{ error }}
|
||||
<div class="row" ng-if="RepoTags.length > 0">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa fa-tags" title="Image tags"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div style="margin: 5px 10px;">
|
||||
<span ng-repeat="tag in RepoTags" class="label label-primary image-tag">
|
||||
<a data-toggle="tooltip" class="interactive" title="Push to registry" ng-click="pushImage(tag)">
|
||||
<i class="fa fa-upload white-icon" aria-hidden="true"></i>
|
||||
</a>
|
||||
{{ tag }}
|
||||
<a data-toggle="tooltip" class="interactive" title="Remove tag" ng-click="removeImage(tag)">
|
||||
<i class="fa fa-trash-o white-icon" aria-hidden="true"></i>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div style="margin: 5px 10px;">
|
||||
<span class="small text-muted">
|
||||
Note: you can click on the upload icon to push an image
|
||||
and on the trash icon to delete a tag
|
||||
</span>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail">
|
||||
|
||||
<h4>Image: {{ id }}</h4>
|
||||
|
||||
<div class="btn-group detail">
|
||||
<button class="btn btn-success" data-toggle="modal" data-target="#create-modal">Start Container</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4>Containers created:</h4>
|
||||
<canvas id="containers-started-chart" width="750">
|
||||
<p class="browserupgrade">You are using an <strong>outdated</strong> browser. Please <a
|
||||
href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
|
||||
</canvas>
|
||||
</div>
|
||||
|
||||
<table class="table table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Tags:</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li ng-repeat="tag in RepoTags">{{ tag }}
|
||||
<button ng-click="removeImage(tag)" class="btn btn-sm btn-danger">Remove tag</button>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Created:</td>
|
||||
<td>{{ image.Created | date: 'medium'}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Parent:</td>
|
||||
<td><a href="#/images/{{ image.Parent }}/">{{ image.Parent }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Size (Virtual Size):</td>
|
||||
<td>{{ image.Size|humansize }} ({{ image.VirtualSize|humansize }})</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Hostname:</td>
|
||||
<td>{{ image.ContainerConfig.Hostname }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>User:</td>
|
||||
<td>{{ image.ContainerConfig.User }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cmd:</td>
|
||||
<td>{{ image.ContainerConfig.Cmd }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Volumes:</td>
|
||||
<td>{{ image.ContainerConfig.Volumes }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Volumes from:</td>
|
||||
<td>{{ image.ContainerConfig.VolumesFrom }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Built with:</td>
|
||||
<td>Docker {{ image.DockerVersion }} on {{ image.Os}}, {{ image.Architecture }}</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="row-fluid">
|
||||
<div class="span1">
|
||||
History:
|
||||
</div>
|
||||
<div class="span5">
|
||||
<i class="icon-refresh" style="width:32px;height:32px;" ng-click="getHistory()"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="well well-large">
|
||||
<ul>
|
||||
<li ng-repeat="change in history">
|
||||
<strong>{{ change.Id }}</strong>: Created: {{ change.Created|getdate }} Created by: {{ change.CreatedBy
|
||||
}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
|
||||
<div class="row-fluid">
|
||||
<form class="form-inline" role="form">
|
||||
<fieldset>
|
||||
<legend>Tag image</legend>
|
||||
<div class="form-group">
|
||||
<label>Tag:</label>
|
||||
<input type="text" placeholder="repo" ng-model="tagInfo.repo" class="form-control">
|
||||
<input type="text" placeholder="version" ng-model="tagInfo.version" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" ng-model="tagInfo.force" class="form-control"/> Force?
|
||||
</label>
|
||||
</div>
|
||||
<input type="button" ng-click="addTag()" value="Add Tag" class="btn btn-primary"/>
|
||||
</fieldset>
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tag" title="Tag the image"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<!-- 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="tagImage()">Tag</button>
|
||||
<i id="pullImageSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
|
||||
<div class="btn-remove">
|
||||
<button class="btn btn-large btn-block btn-primary btn-danger" ng-click="removeImage(id)">Remove Image</button>
|
||||
</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-clone" title="Image details"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>ID</td>
|
||||
<td>
|
||||
{{ image.Id }}
|
||||
<button class="btn btn-xs btn-danger" ng-click="removeImage(image.Id)">Delete this image</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="image.Parent">
|
||||
<td>Parent</td>
|
||||
<td><a ui-sref="image({id: image.Parent})">{{ image.Parent }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Size</td>
|
||||
<td>{{ image.VirtualSize|humansize }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Created</td>
|
||||
<td>{{ image.Created|getisodate }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Build</td>
|
||||
<td>Docker {{ image.DockerVersion }} on {{ image.Os}}, {{ image.Architecture }}</td>
|
||||
</tr>
|
||||
<tr ng-if="image.Author">
|
||||
<td>Author</td>
|
||||
<td>{{ image.Author }}</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="Dockerfile details"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>CMD</td>
|
||||
<td><code>{{ image.ContainerConfig.Cmd|command }}</code></td>
|
||||
</tr>
|
||||
<tr ng-if="image.ContainerConfig.Entrypoint">
|
||||
<td>ENTRYPOINT</td>
|
||||
<td><code>{{ image.ContainerConfig.Entrypoint|command }}</code></td>
|
||||
</tr>
|
||||
<tr ng-if="image.ContainerConfig.ExposedPorts">
|
||||
<td>EXPOSE</td>
|
||||
<td>
|
||||
<span class="label label-default tag" ng-repeat="port in exposedPorts">
|
||||
{{ port }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="image.ContainerConfig.Volumes">
|
||||
<td>VOLUME</td>
|
||||
<td>
|
||||
<span class="label label-default tag" ng-repeat="volume in volumes">
|
||||
{{ volume }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ENV</td>
|
||||
<td>
|
||||
<table class="table table-bordered table-condensed">
|
||||
<tr ng-repeat="var in image.ContainerConfig.Env">
|
||||
<td>{{ var|key }}</td>
|
||||
<td>{{ var|value }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,87 +1,107 @@
|
||||
angular.module('image', [])
|
||||
.controller('ImageController', ['$scope', '$q', '$routeParams', '$location', 'Image', 'Container', 'Messages', 'LineChart',
|
||||
function ($scope, $q, $routeParams, $location, Image, Container, Messages, LineChart) {
|
||||
$scope.history = [];
|
||||
$scope.tagInfo = {repo: '', version: '', force: false};
|
||||
$scope.id = '';
|
||||
$scope.repoTags = [];
|
||||
.controller('ImageController', ['$scope', '$stateParams', '$state', 'Image', 'Messages',
|
||||
function ($scope, $stateParams, $state, Image, Messages) {
|
||||
$scope.RepoTags = [];
|
||||
|
||||
$scope.removeImage = function (id) {
|
||||
Image.remove({id: id}, function (d) {
|
||||
d.forEach(function(msg){
|
||||
var key = Object.keys(msg)[0];
|
||||
Messages.send(key, msg[key]);
|
||||
});
|
||||
// If last message key is 'Deleted' then assume the image is gone and send to images page
|
||||
if (d[d.length-1].Deleted) {
|
||||
$location.path('/images');
|
||||
} else {
|
||||
$location.path('/images/' + $scope.id); // Refresh the current page.
|
||||
}
|
||||
}, function (e) {
|
||||
$scope.error = e.data;
|
||||
$('#error-message').show();
|
||||
});
|
||||
};
|
||||
$scope.config = {
|
||||
Image: '',
|
||||
Registry: ''
|
||||
};
|
||||
|
||||
$scope.getHistory = function () {
|
||||
Image.history({id: $routeParams.id}, function (d) {
|
||||
$scope.history = d;
|
||||
});
|
||||
};
|
||||
// Get RepoTags from the /images/query endpoint instead of /image/json,
|
||||
// for backwards compatibility with Docker API versions older than 1.21
|
||||
function getRepoTags(imageId) {
|
||||
Image.query({}, function (d) {
|
||||
d.forEach(function(image) {
|
||||
if (image.Id === imageId && image.RepoTags[0] !== '<none>:<none>') {
|
||||
$scope.RepoTags = image.RepoTags;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$scope.addTag = function () {
|
||||
var tag = $scope.tagInfo;
|
||||
Image.tag({
|
||||
id: $routeParams.id,
|
||||
repo: tag.repo,
|
||||
tag: tag.version,
|
||||
force: tag.force ? 1 : 0
|
||||
}, function (d) {
|
||||
Messages.send("Tag Added", $routeParams.id);
|
||||
$location.path('/images/' + $scope.id);
|
||||
}, function (e) {
|
||||
$scope.error = e.data;
|
||||
$('#error-message').show();
|
||||
});
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
function getContainersFromImage($q, Container, imageId) {
|
||||
var defer = $q.defer();
|
||||
$scope.tagImage = function() {
|
||||
$('#loadingViewSpinner').show();
|
||||
var image = _.toLower($scope.config.Image);
|
||||
var registry = _.toLower($scope.config.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("Unable to tag image", e.data);
|
||||
});
|
||||
};
|
||||
|
||||
Container.query({all: 1, notruc: 1}, function (d) {
|
||||
var containers = [];
|
||||
for (var i = 0; i < d.length; i++) {
|
||||
var c = d[i];
|
||||
if (c.ImageID === imageId) {
|
||||
containers.push(new ContainerViewModel(c));
|
||||
}
|
||||
}
|
||||
defer.resolve(containers);
|
||||
});
|
||||
$scope.pushImage = function(tag) {
|
||||
$('#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);
|
||||
} else {
|
||||
Messages.send('Image successfully pushed');
|
||||
}
|
||||
$('#loadingViewSpinner').hide();
|
||||
}, function (e) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Unable to push image", e.data);
|
||||
});
|
||||
};
|
||||
|
||||
return defer.promise;
|
||||
}
|
||||
$scope.removeImage = function (id) {
|
||||
$('#loadingViewSpinner').show();
|
||||
Image.remove({id: id}, function (d) {
|
||||
if (d[0].message) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Unable to remove image", d[0].message);
|
||||
} else {
|
||||
// If last message key is 'Deleted' or if it's 'Untagged' and there is only one tag associated to the image
|
||||
// then assume the image is gone and send to images page
|
||||
if (d[d.length-1].Deleted || (d[d.length-1].Untagged && $scope.RepoTags.length === 1)) {
|
||||
Messages.send('Image successfully deleted');
|
||||
$state.go('images', {}, {reload: true});
|
||||
} else {
|
||||
Messages.send('Tag successfully deleted');
|
||||
$state.go('image', {id: $stateParams.id}, {reload: true});
|
||||
}
|
||||
}
|
||||
}, function (e) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Unable to remove image", e.data);
|
||||
});
|
||||
};
|
||||
|
||||
Image.get({id: $routeParams.id}, function (d) {
|
||||
$scope.image = d;
|
||||
$scope.id = d.Id;
|
||||
$scope.RepoTags = d.RepoTags;
|
||||
|
||||
getContainersFromImage($q, Container, $scope.id).then(function (containers) {
|
||||
LineChart.build('#containers-started-chart', containers, function (c) {
|
||||
return new Date(c.Created * 1000).toLocaleDateString();
|
||||
});
|
||||
});
|
||||
}, function (e) {
|
||||
if (e.status === 404) {
|
||||
$('.detail').hide();
|
||||
$scope.error = "Image not found.<br />" + $routeParams.id;
|
||||
} else {
|
||||
$scope.error = e.data;
|
||||
}
|
||||
$('#error-message').show();
|
||||
});
|
||||
|
||||
$scope.getHistory();
|
||||
}]);
|
||||
$('#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);
|
||||
}
|
||||
});
|
||||
}]);
|
||||
|
||||
@@ -1,40 +1,116 @@
|
||||
<div ng-include="template" ng-controller="BuilderController"></div>
|
||||
<div ng-include="template" ng-controller="PullImageController"></div>
|
||||
<rd-header>
|
||||
<rd-header-title title="Image list">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="images" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Images</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<h2>Images:</h2>
|
||||
|
||||
<div>
|
||||
<ul class="nav nav-pills pull-left">
|
||||
<li class="dropdown">
|
||||
<a class="dropdown-toggle" id="drop4" role="button" data-toggle="dropdown" data-target="#">Actions <b class="caret"></b></a>
|
||||
<ul id="menu1" class="dropdown-menu" role="menu" aria-labelledby="drop4">
|
||||
<li><a tabindex="-1" href="" ng-click="removeAction()">Remove</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a data-toggle="modal" data-target="#pull-modal" href="">Pull</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="pull-right form-inline">
|
||||
<input type="text" class="form-control" id="filter" placeholder="Filter" ng-model="filter"/> <label class="sr-only" for="filter">Filter</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-download" title="Pull image ">
|
||||
</rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<!-- 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. ubuntu:trusty">
|
||||
</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">
|
||||
</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="pullImage()">Pull</button>
|
||||
<i id="pullImageSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" ng-model="toggle" ng-change="toggleSelectAll()" /> Action</th>
|
||||
<th>Id</th>
|
||||
<th>Repository</th>
|
||||
<th>VirtualSize</th>
|
||||
<th>Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="image in images | filter:filter | orderBy:predicate">
|
||||
<td><input type="checkbox" ng-model="image.Checked" /></td>
|
||||
<td><a href="#/images/{{ image.Id }}/?tag={{ image|repotag }}">{{ image.Id|truncate:20}}</a></td>
|
||||
<td>{{ image|repotag }}</td>
|
||||
<td>{{ image.VirtualSize|humansize }}</td>
|
||||
<td>{{ image.Created|getdate }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-clone" title="Images">
|
||||
<div class="pull-right">
|
||||
<i id="loadImagesSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<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>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
|
||||
</div>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>
|
||||
<a ui-sref="images" 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="images" ng-click="order('RepoTags')">
|
||||
Tags
|
||||
<span ng-show="sortType == 'RepoTags' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'RepoTags' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="images" ng-click="order('VirtualSize')">
|
||||
Size
|
||||
<span ng-show="sortType == 'VirtualSize' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<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>
|
||||
<tr ng-repeat="image in (state.filteredImages = (images | filter:state.filter | orderBy:sortType:sortReverse))">
|
||||
<td><input type="checkbox" ng-model="image.Checked" ng-change="selectItem(image)" /></td>
|
||||
<td><a ui-sref="image({id: image.Id})">{{ image.Id|truncate:20}}</a></td>
|
||||
<td>
|
||||
<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>
|
||||
|
||||
@@ -1,54 +1,108 @@
|
||||
angular.module('images', [])
|
||||
.controller('ImagesController', ['$scope', 'Image', 'ViewSpinner', 'Messages',
|
||||
function ($scope, Image, ViewSpinner, Messages) {
|
||||
$scope.toggle = false;
|
||||
$scope.predicate = '-Created';
|
||||
.controller('ImagesController', ['$scope', '$state', 'Config', 'Image', 'Messages',
|
||||
function ($scope, $state, Config, Image, Messages) {
|
||||
$scope.state = {};
|
||||
$scope.sortType = 'RepoTags';
|
||||
$scope.sortReverse = true;
|
||||
$scope.state.selectedItemCount = 0;
|
||||
|
||||
$scope.showBuilder = function () {
|
||||
$('#build-modal').modal('show');
|
||||
};
|
||||
$scope.config = {
|
||||
Image: '',
|
||||
Registry: ''
|
||||
};
|
||||
|
||||
$scope.removeAction = function () {
|
||||
ViewSpinner.spin();
|
||||
var counter = 0;
|
||||
var complete = function () {
|
||||
counter = counter - 1;
|
||||
if (counter === 0) {
|
||||
ViewSpinner.stop();
|
||||
}
|
||||
};
|
||||
angular.forEach($scope.images, function (i) {
|
||||
if (i.Checked) {
|
||||
counter = counter + 1;
|
||||
Image.remove({id: i.Id}, function (d) {
|
||||
angular.forEach(d, function (resource) {
|
||||
Messages.send("Image deleted", resource.Deleted);
|
||||
});
|
||||
var index = $scope.images.indexOf(i);
|
||||
$scope.images.splice(index, 1);
|
||||
complete();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e.data);
|
||||
complete();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
$scope.order = function(sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
$scope.toggleSelectAll = function () {
|
||||
angular.forEach($scope.images, function (i) {
|
||||
i.Checked = $scope.toggle;
|
||||
});
|
||||
};
|
||||
$scope.selectItem = function (item) {
|
||||
if (item.Checked) {
|
||||
$scope.state.selectedItemCount++;
|
||||
} else {
|
||||
$scope.state.selectedItemCount--;
|
||||
}
|
||||
};
|
||||
|
||||
ViewSpinner.spin();
|
||||
Image.query({}, function (d) {
|
||||
$scope.images = d.map(function (item) {
|
||||
return new ImageViewModel(item);
|
||||
});
|
||||
ViewSpinner.stop();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e.data);
|
||||
ViewSpinner.stop();
|
||||
});
|
||||
}]);
|
||||
function createImageConfig(imageName, registry) {
|
||||
var imageNameAndTag = imageName.split(':');
|
||||
var image = imageNameAndTag[0];
|
||||
if (registry) {
|
||||
image = registry + '/' + imageNameAndTag[0];
|
||||
}
|
||||
var imageConfig = {
|
||||
fromImage: image,
|
||||
tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest'
|
||||
};
|
||||
return imageConfig;
|
||||
}
|
||||
|
||||
$scope.pullImage = function() {
|
||||
$('#pullImageSpinner').show();
|
||||
var image = _.toLower($scope.config.Image);
|
||||
var registry = _.toLower($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);
|
||||
} else {
|
||||
$('#pullImageSpinner').hide();
|
||||
$state.go('images', {}, {reload: true});
|
||||
}
|
||||
}, function (e) {
|
||||
$('#pullImageSpinner').hide();
|
||||
Messages.error('Error', 'Unable to pull image ' + image);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeAction = function () {
|
||||
$('#loadImagesSpinner').show();
|
||||
var counter = 0;
|
||||
var complete = function () {
|
||||
counter = counter - 1;
|
||||
if (counter === 0) {
|
||||
$('#loadImagesSpinner').hide();
|
||||
}
|
||||
};
|
||||
angular.forEach($scope.images, function (i) {
|
||||
if (i.Checked) {
|
||||
counter = counter + 1;
|
||||
Image.remove({id: i.Id}, function (d) {
|
||||
if (d[0].message) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Unable to remove image", d[0].message);
|
||||
} else {
|
||||
Messages.send("Image deleted", i.Id);
|
||||
var index = $scope.images.indexOf(i);
|
||||
$scope.images.splice(index, 1);
|
||||
}
|
||||
complete();
|
||||
}, function (e) {
|
||||
Messages.error("Unable to remove image", e.data);
|
||||
complete();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function fetchImages() {
|
||||
Image.query({}, function (d) {
|
||||
$scope.images = d.map(function (item) {
|
||||
return new ImageViewModel(item);
|
||||
});
|
||||
$('#loadImagesSpinner').hide();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e.data);
|
||||
$('#loadImagesSpinner').hide();
|
||||
});
|
||||
}
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.availableRegistries = c.registries;
|
||||
fetchImages();
|
||||
});
|
||||
|
||||
}]);
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
<div class="detail">
|
||||
<h2>Docker Information</h2>
|
||||
|
||||
<div>
|
||||
<p class="lead">
|
||||
<strong>API Endpoint: </strong>{{ endpoint }}<br/>
|
||||
<strong>API Version: </strong>{{ docker.ApiVersion }}<br/>
|
||||
<strong>Docker version: </strong>{{ docker.Version }}<br/>
|
||||
<strong>Git Commit: </strong>{{ docker.GitCommit }}<br/>
|
||||
<strong>Go Version: </strong>{{ docker.GoVersion }}<br/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<table class="table table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Containers:</td>
|
||||
<td>{{ info.Containers }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Images:</td>
|
||||
<td>{{ info.Images }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Debug:</td>
|
||||
<td>{{ info.Debug }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CPUs:</td>
|
||||
<td>{{ info.NCPU }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total Memory:</td>
|
||||
<td>{{ info.MemTotal|humansize }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Operating System:</td>
|
||||
<td>{{ info.OperatingSystem }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kernel Version:</td>
|
||||
<td>{{ info.KernelVersion }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ID:</td>
|
||||
<td>{{ info.ID }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Labels:</td>
|
||||
<td>{{ info.Labels }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>File Descriptors:</td>
|
||||
<td>{{ info.NFd }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Goroutines:</td>
|
||||
<td>{{ info.NGoroutines }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Storage Driver:</td>
|
||||
<td>{{ info.Driver }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Storage Driver Status:</td>
|
||||
<td>
|
||||
<p ng-repeat="val in info.DriverStatus">
|
||||
{{ val[0] }}: {{ val[1] }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Execution Driver:</td>
|
||||
<td>{{ info.ExecutionDriver }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Events:</td>
|
||||
<td><a href="#/events">Events</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IPv4 Forwarding:</td>
|
||||
<td>{{ info.IPv4Forwarding }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Index Server Address:</td>
|
||||
<td>{{ info.IndexServerAddress }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Init Path:</td>
|
||||
<td>{{ info.InitPath }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Docker Root Directory:</td>
|
||||
<td>{{ info.DockerRootDir }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Init SHA1</td>
|
||||
<td>{{ info.InitSha1 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Memory Limit:</td>
|
||||
<td>{{ info.MemoryLimit }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Swap Limit:</td>
|
||||
<td>{{ info.SwapLimit }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1,15 +0,0 @@
|
||||
angular.module('info', [])
|
||||
.controller('InfoController', ['$scope', 'System', 'Docker', 'Settings', 'Messages',
|
||||
function ($scope, System, Docker, Settings, Messages) {
|
||||
$scope.info = {};
|
||||
$scope.docker = {};
|
||||
$scope.endpoint = Settings.endpoint;
|
||||
$scope.apiVersion = Settings.version;
|
||||
|
||||
Docker.get({}, function (d) {
|
||||
$scope.docker = d;
|
||||
});
|
||||
System.get({}, function (d) {
|
||||
$scope.info = d;
|
||||
});
|
||||
}]);
|
||||
@@ -1,10 +0,0 @@
|
||||
<div class="masthead">
|
||||
<h3 class="text-muted">DockerUI</h3>
|
||||
<ul class="nav well">
|
||||
<li><a href="#/">Dashboard</a></li>
|
||||
<li><a href="#/containers/">Containers</a></li>
|
||||
<li><a href="#/containers_network/">Containers Network</a></li>
|
||||
<li><a href="#/images/">Images</a></li>
|
||||
<li><a href="#/info/">Info</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -1,4 +0,0 @@
|
||||
angular.module('masthead', [])
|
||||
.controller('MastheadController', ['$scope', function ($scope) {
|
||||
$scope.template = 'app/components/masthead/masthead.html';
|
||||
}]);
|
||||
@@ -0,0 +1,129 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Network details">
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
Networks > <a ui-sref="network({id: network.Id})">{{ network.Name }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-9 col-md-9 col-xs-9">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon grey pull-left">
|
||||
<i class="fa fa-sitemap"></i>
|
||||
</div>
|
||||
<div class="title">{{ network.Name }}</div>
|
||||
<div class="comment">Name</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-3 col-xs-3">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon grey pull-left">
|
||||
<i class="fa fa-cogs"></i>
|
||||
</div>
|
||||
<div class="title">
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<button class="btn btn-default" disabled>Connect container...</button>
|
||||
<button class="btn btn-danger" ng-click="remove(id)">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment">
|
||||
Actions
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-sitemap" title="Network details"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Id</td>
|
||||
<td>{{ network.Id }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Scope</td>
|
||||
<td>{{ network.Scope }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Driver</td>
|
||||
<td>{{ network.Driver }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IPAM</td>
|
||||
<td>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<td>Driver</td>
|
||||
<td>{{ network.IPAM.Driver }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Subnet</td>
|
||||
<td>{{ network.IPAM.Config[0].Subnet }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Gateway</td>
|
||||
<td>{{ network.IPAM.Config[0].Gateway }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Containers</td>
|
||||
<td>
|
||||
<table class="table table-striped" ng-repeat="(Id, container) in network.Containers">
|
||||
<tr>
|
||||
<td>Id</td>
|
||||
<td><a ui-sref="container({id: Id})">{{ Id }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>EndpointID</td>
|
||||
<td>{{ container.EndpointID}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MacAddress</td>
|
||||
<td>{{ container.MacAddress}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IPv4Address</td>
|
||||
<td>{{ container.IPv4Address}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IPv6Address</td>
|
||||
<td>{{ container.IPv6Address}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<button ng-click="disconnect(network.Id, Id)" class="btn btn-danger">
|
||||
Disconnect from network
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Options</td>
|
||||
<td>
|
||||
<table role="table" class="table table-striped">
|
||||
<tr ng-repeat="(k, v) in network.Options">
|
||||
<td>{{ k }}</td>
|
||||
<td>{{ v }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,37 @@
|
||||
angular.module('network', [])
|
||||
.controller('NetworkController', ['$scope', 'Network', 'Messages', '$state', '$stateParams', 'errorMsgFilter',
|
||||
function ($scope, Network, Messages, $state, $stateParams, errorMsgFilter) {
|
||||
|
||||
$scope.disconnect = function disconnect(networkId, containerId) {
|
||||
$('#loadingViewSpinner').show();
|
||||
Network.disconnect({id: $stateParams.id}, {Container: containerId}, function (d) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.send("Container disconnected", containerId);
|
||||
$state.go('network', {id: $stateParams.id}, {reload: true});
|
||||
}, function (e) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Failure", e.data);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.remove = function remove(networkId) {
|
||||
$('#loadingViewSpinner').show();
|
||||
Network.remove({id: $stateParams.id}, function (d) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.send("Network removed", "");
|
||||
$state.go('networks', {});
|
||||
}, function (e) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Failure", e.data);
|
||||
});
|
||||
};
|
||||
|
||||
$('#loadingViewSpinner').show();
|
||||
Network.get({id: $stateParams.id}, function (d) {
|
||||
$scope.network = d;
|
||||
$('#loadingViewSpinner').hide();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e.data);
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
}]);
|
||||
@@ -0,0 +1,99 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Network list">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="networks" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Networks</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="col-lg-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-sitemap" title="Networks">
|
||||
<div class="pull-right">
|
||||
<i id="loadNetworksSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<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" />
|
||||
</div>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('Name')">
|
||||
Name
|
||||
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="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
|
||||
<span ng-show="sortType == 'Scope' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<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
|
||||
<span ng-show="sortType == 'IPAM.Config[0].Subnet' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'IPAM.Config[0].Subnet' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('IPAM.Config[0].Gateway')">
|
||||
IPAM Gateway
|
||||
<span ng-show="sortType == 'IPAM.Config[0].Gateway' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'IPAM.Config[0].Gateway' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="network in ( state.filteredNetworks = (networks | filter:state.filter | orderBy:sortType:sortReverse))">
|
||||
<td><input type="checkbox" ng-model="network.Checked" ng-change="selectItem(network)"/></td>
|
||||
<td><a ui-sref="network({id: network.Id})">{{ network.Name|truncate:40}}</a></td>
|
||||
<td>{{ network.Id }}</td>
|
||||
<td>{{ network.Scope }}</td>
|
||||
<td>{{ network.Driver }}</td>
|
||||
<td>{{ network.IPAM.Driver }}</td>
|
||||
<td>{{ network.IPAM.Config[0].Subnet ? network.IPAM.Config[0].Subnet : '-' }}</td>
|
||||
<td>{{ network.IPAM.Config[0].Gateway ? network.IPAM.Config[0].Gateway : '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
<rd-widget>
|
||||
</div>
|
||||
@@ -0,0 +1,77 @@
|
||||
angular.module('networks', [])
|
||||
.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.sortReverse = false;
|
||||
|
||||
$scope.formValues = {
|
||||
Subnet: '',
|
||||
Gateway: ''
|
||||
};
|
||||
|
||||
$scope.config = {
|
||||
Name: '',
|
||||
IPAM: {
|
||||
Config: []
|
||||
}
|
||||
};
|
||||
|
||||
$scope.order = function(sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
$scope.selectItem = function (item) {
|
||||
if (item.Checked) {
|
||||
$scope.state.selectedItemCount++;
|
||||
} else {
|
||||
$scope.state.selectedItemCount--;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.removeAction = function () {
|
||||
$('#loadNetworksSpinner').show();
|
||||
var counter = 0;
|
||||
var complete = function () {
|
||||
counter = counter - 1;
|
||||
if (counter === 0) {
|
||||
$('#loadNetworksSpinner').hide();
|
||||
}
|
||||
};
|
||||
angular.forEach($scope.networks, function (network) {
|
||||
if (network.Checked) {
|
||||
counter = counter + 1;
|
||||
Network.remove({id: network.Id}, function (d) {
|
||||
var error = errorMsgFilter(d);
|
||||
if (error) {
|
||||
Messages.send("Error", "Unable to remove network with active endpoints");
|
||||
} else {
|
||||
Messages.send("Network deleted", network.Id);
|
||||
var index = $scope.networks.indexOf(network);
|
||||
$scope.networks.splice(index, 1);
|
||||
}
|
||||
complete();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e.data);
|
||||
complete();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function fetchNetworks() {
|
||||
$('#loadNetworksSpinner').show();
|
||||
Network.query({}, function (d) {
|
||||
$scope.networks = d;
|
||||
$('#loadNetworksSpinner').hide();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e.data);
|
||||
$('#loadNetworksSpinner').hide();
|
||||
});
|
||||
}
|
||||
|
||||
fetchNetworks();
|
||||
}]);
|
||||
@@ -1,44 +0,0 @@
|
||||
<div id="pull-modal" class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h3>Pull Image</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form novalidate role="form" name="pullForm">
|
||||
<!--<div class="input-group">
|
||||
<span class="input-group-addon" id="basic-addon1">Image name</span>
|
||||
<input type="text" class="form-control" placeholder="imageName" aria-describedby="basic-addon1">
|
||||
</div>-->
|
||||
<div class="form-group">
|
||||
<label>Registry:</label>
|
||||
<input type="text" ng-model="config.registry" class="form-control"
|
||||
placeholder="Registry. Leave empty to user docker hub"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Repo:</label>
|
||||
<input type="text" ng-model="config.repo" class="form-control"
|
||||
placeholder="Repository - usually your username."/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Image Name:</label>
|
||||
<input type="text" ng-model="config.fromImage" class="form-control" placeholder="Image name"
|
||||
required/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Tag Name:</label>
|
||||
<input type="text" ng-model="config.tag" class="form-control"
|
||||
placeholder="Tag name. If empty it will download ALL tags."/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="alert alert-error" id="error-message" style="display:none">
|
||||
{{ error }}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="" class="btn btn-primary" ng-click="pull()">Pull</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,56 +0,0 @@
|
||||
angular.module('pullImage', [])
|
||||
.controller('PullImageController', ['$scope', '$log', 'Dockerfile', 'Messages', 'Image', 'ViewSpinner',
|
||||
function ($scope, $log, Dockerfile, Messages, Image, ViewSpinner) {
|
||||
$scope.template = 'app/components/pullImage/pullImage.html';
|
||||
|
||||
$scope.init = function () {
|
||||
$scope.config = {
|
||||
registry: '',
|
||||
repo: '',
|
||||
fromImage: '',
|
||||
tag: 'latest'
|
||||
};
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
|
||||
function failedRequestHandler(e, Messages) {
|
||||
Messages.error('Error', errorMsgFilter(e));
|
||||
}
|
||||
|
||||
$scope.pull = function () {
|
||||
$('#error-message').hide();
|
||||
var config = angular.copy($scope.config);
|
||||
var imageName = (config.registry ? config.registry + '/' : '' ) +
|
||||
(config.repo ? config.repo + '/' : '') +
|
||||
(config.fromImage) +
|
||||
(config.tag ? ':' + config.tag : '');
|
||||
|
||||
ViewSpinner.spin();
|
||||
$('#pull-modal').modal('hide');
|
||||
Image.create(config, function (data) {
|
||||
ViewSpinner.stop();
|
||||
if (data.constructor === Array) {
|
||||
var f = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
|
||||
//check for error
|
||||
if (f) {
|
||||
var d = data[data.length - 1];
|
||||
$scope.error = "Cannot pull image " + imageName + " Reason: " + d.error;
|
||||
$('#pull-modal').modal('show');
|
||||
$('#error-message').show();
|
||||
} else {
|
||||
Messages.send("Image Added", imageName);
|
||||
$scope.init();
|
||||
}
|
||||
} else {
|
||||
Messages.send("Image Added", imageName);
|
||||
$scope.init();
|
||||
}
|
||||
}, function (e) {
|
||||
ViewSpinner.stop();
|
||||
$scope.error = "Cannot pull image " + imageName + " Reason: " + e.data;
|
||||
$('#pull-modal').modal('show');
|
||||
$('#error-message').show();
|
||||
});
|
||||
};
|
||||
}]);
|
||||
@@ -1,11 +0,0 @@
|
||||
<div class="well">
|
||||
<strong>Running containers:</strong>
|
||||
<br/>
|
||||
<strong>Endpoint: </strong>{{ endpoint }}
|
||||
<ul>
|
||||
<li ng-repeat="container in containers">
|
||||
<a href="#/containers/{{ container.Id }}/">{{ container.Id|truncate:20 }}</a>
|
||||
<span class="pull-right label label-{{ container.Status|statusbadge }}">{{ container.Status }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -1,11 +0,0 @@
|
||||
angular.module('sidebar', [])
|
||||
.controller('SideBarController', ['$scope', 'Container', 'Settings',
|
||||
function ($scope, Container, Settings) {
|
||||
$scope.template = 'partials/sidebar.html';
|
||||
$scope.containers = [];
|
||||
$scope.endpoint = Settings.endpoint;
|
||||
|
||||
Container.query({all: 0}, function (d) {
|
||||
$scope.containers = d;
|
||||
});
|
||||
}]);
|
||||
@@ -1,159 +0,0 @@
|
||||
angular.module('startContainer', ['ui.bootstrap'])
|
||||
.controller('StartContainerController', ['$scope', '$routeParams', '$location', 'Container', 'Messages', 'containernameFilter', 'errorMsgFilter',
|
||||
function ($scope, $routeParams, $location, Container, Messages, containernameFilter, errorMsgFilter) {
|
||||
$scope.template = 'app/components/startContainer/startcontainer.html';
|
||||
|
||||
Container.query({all: 1}, function (d) {
|
||||
$scope.containerNames = d.map(function (container) {
|
||||
return containernameFilter(container);
|
||||
});
|
||||
});
|
||||
|
||||
$scope.config = {
|
||||
Env: [],
|
||||
Labels: [],
|
||||
Volumes: [],
|
||||
SecurityOpts: [],
|
||||
HostConfig: {
|
||||
PortBindings: [],
|
||||
Binds: [],
|
||||
Links: [],
|
||||
Dns: [],
|
||||
DnsSearch: [],
|
||||
VolumesFrom: [],
|
||||
CapAdd: [],
|
||||
CapDrop: [],
|
||||
Devices: [],
|
||||
LxcConf: [],
|
||||
ExtraHosts: []
|
||||
}
|
||||
};
|
||||
|
||||
$scope.menuStatus = {
|
||||
containerOpen: true,
|
||||
hostConfigOpen: false
|
||||
};
|
||||
|
||||
function failedRequestHandler(e, Messages) {
|
||||
Messages.error('Error', errorMsgFilter(e));
|
||||
}
|
||||
|
||||
function rmEmptyKeys(col) {
|
||||
for (var key in col) {
|
||||
if (col[key] === null || col[key] === undefined || col[key] === '' || $.isEmptyObject(col[key]) || col[key].length === 0) {
|
||||
delete col[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getNames(arr) {
|
||||
return arr.map(function (item) {
|
||||
return item.name;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.create = function () {
|
||||
// Copy the config before transforming fields to the remote API format
|
||||
var config = angular.copy($scope.config);
|
||||
|
||||
config.Image = $routeParams.id;
|
||||
|
||||
if (config.Cmd && config.Cmd[0] === "[") {
|
||||
config.Cmd = angular.fromJson(config.Cmd);
|
||||
} else if (config.Cmd) {
|
||||
config.Cmd = config.Cmd.split(' ');
|
||||
}
|
||||
|
||||
config.Env = config.Env.map(function (envar) {
|
||||
return envar.name + '=' + envar.value;
|
||||
});
|
||||
var labels = {};
|
||||
config.Labels = config.Labels.forEach(function(label) {
|
||||
labels[label.key] = label.value;
|
||||
});
|
||||
config.Labels = labels;
|
||||
|
||||
config.Volumes = getNames(config.Volumes);
|
||||
config.SecurityOpts = getNames(config.SecurityOpts);
|
||||
|
||||
config.HostConfig.VolumesFrom = getNames(config.HostConfig.VolumesFrom);
|
||||
config.HostConfig.Binds = getNames(config.HostConfig.Binds);
|
||||
config.HostConfig.Links = getNames(config.HostConfig.Links);
|
||||
config.HostConfig.Dns = getNames(config.HostConfig.Dns);
|
||||
config.HostConfig.DnsSearch = getNames(config.HostConfig.DnsSearch);
|
||||
config.HostConfig.CapAdd = getNames(config.HostConfig.CapAdd);
|
||||
config.HostConfig.CapDrop = getNames(config.HostConfig.CapDrop);
|
||||
config.HostConfig.LxcConf = config.HostConfig.LxcConf.reduce(function (prev, cur, idx) {
|
||||
prev[cur.name] = cur.value;
|
||||
return prev;
|
||||
}, {});
|
||||
config.HostConfig.ExtraHosts = config.HostConfig.ExtraHosts.map(function (entry) {
|
||||
return entry.host + ':' + entry.ip;
|
||||
});
|
||||
|
||||
var ExposedPorts = {};
|
||||
var PortBindings = {};
|
||||
config.HostConfig.PortBindings.forEach(function (portBinding) {
|
||||
var intPort = portBinding.intPort + "/tcp";
|
||||
if (portBinding.protocol === "udp") {
|
||||
intPort = portBinding.intPort + "/udp";
|
||||
}
|
||||
var binding = {
|
||||
HostIp: portBinding.ip,
|
||||
HostPort: portBinding.extPort
|
||||
};
|
||||
if (portBinding.intPort) {
|
||||
ExposedPorts[intPort] = {};
|
||||
if (intPort in PortBindings) {
|
||||
PortBindings[intPort].push(binding);
|
||||
} else {
|
||||
PortBindings[intPort] = [binding];
|
||||
}
|
||||
} else {
|
||||
Messages.send('Warning', 'Internal port must be specified for PortBindings');
|
||||
}
|
||||
});
|
||||
config.ExposedPorts = ExposedPorts;
|
||||
config.HostConfig.PortBindings = PortBindings;
|
||||
|
||||
// Remove empty fields from the request to avoid overriding defaults
|
||||
rmEmptyKeys(config.HostConfig);
|
||||
rmEmptyKeys(config);
|
||||
|
||||
var ctor = Container;
|
||||
var loc = $location;
|
||||
var s = $scope;
|
||||
Container.create(config, function (d) {
|
||||
if (d.Id) {
|
||||
var reqBody = config.HostConfig || {};
|
||||
reqBody.id = d.Id;
|
||||
ctor.start(reqBody, function (cd) {
|
||||
if (cd.id) {
|
||||
Messages.send('Container Started', d.Id);
|
||||
$('#create-modal').modal('hide');
|
||||
loc.path('/containers/' + d.Id + '/');
|
||||
} else {
|
||||
failedRequestHandler(cd, Messages);
|
||||
ctor.remove({id: d.Id}, function () {
|
||||
Messages.send('Container Removed', d.Id);
|
||||
});
|
||||
}
|
||||
}, function (e) {
|
||||
failedRequestHandler(e, Messages);
|
||||
});
|
||||
} else {
|
||||
failedRequestHandler(d, Messages);
|
||||
}
|
||||
}, function (e) {
|
||||
failedRequestHandler(e, Messages);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.addEntry = function (array, entry) {
|
||||
array.push(entry);
|
||||
};
|
||||
$scope.rmEntry = function (array, entry) {
|
||||
var idx = array.indexOf(entry);
|
||||
array.splice(idx, 1);
|
||||
};
|
||||
}]);
|
||||
@@ -1,450 +0,0 @@
|
||||
<div id="create-modal" class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h3>Create And Start Container From Image</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form role="form">
|
||||
<accordion close-others="true">
|
||||
<accordion-group heading="Container options" is-open="menuStatus.containerOpen">
|
||||
<fieldset>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<div class="form-group">
|
||||
<label>Cmd:</label>
|
||||
<input type="text" placeholder='["/bin/echo", "Hello world"]'
|
||||
ng-model="config.Cmd" class="form-control"/>
|
||||
<small>Input commands as a raw string or JSON array</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Entrypoint:</label>
|
||||
<input type="text" ng-model="config.Entrypoint" class="form-control"
|
||||
placeholder="./entrypoint.sh"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Name:</label>
|
||||
<input type="text" ng-model="config.name" class="form-control"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Hostname:</label>
|
||||
<input type="text" ng-model="config.Hostname" class="form-control"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Domainname:</label>
|
||||
<input type="text" ng-model="config.Domainname" class="form-control"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>User:</label>
|
||||
<input type="text" ng-model="config.User" class="form-control"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Memory:</label>
|
||||
<input type="number" ng-model="config.Memory" class="form-control"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Volumes:</label>
|
||||
|
||||
<div ng-repeat="volume in config.Volumes">
|
||||
<div class="form-group form-inline">
|
||||
<input type="text" ng-model="volume.name" class="form-control"
|
||||
placeholder="/var/data"/>
|
||||
<button type="button" class="btn btn-danger btn-sm"
|
||||
ng-click="rmEntry(config.Volumes, volume)">Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-success btn-sm"
|
||||
ng-click="addEntry(config.Volumes, {name: ''})">Add Volume
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<div class="form-group">
|
||||
<label>MemorySwap:</label>
|
||||
<input type="number" ng-model="config.MemorySwap" class="form-control"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>CpuShares:</label>
|
||||
<input type="number" ng-model="config.CpuShares" class="form-control"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Cpuset:</label>
|
||||
<input type="text" ng-model="config.Cpuset" class="form-control"
|
||||
placeholder="1,2"/>
|
||||
<small>Input as comma-separated list of numbers</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>WorkingDir:</label>
|
||||
<input type="text" ng-model="config.WorkingDir" class="form-control"
|
||||
placeholder="/app"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>MacAddress:</label>
|
||||
<input type="text" ng-model="config.MacAddress" class="form-control"
|
||||
placeholder="12:34:56:78:9a:bc"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="networkDisabled">NetworkDisabled:</label>
|
||||
<input id="networkDisabled" type="checkbox"
|
||||
ng-model="config.NetworkDisabled"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="tty">Tty:</label>
|
||||
<input id="tty" type="checkbox" ng-model="config.Tty"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="openStdin">OpenStdin:</label>
|
||||
<input id="openStdin" type="checkbox" ng-model="config.OpenStdin"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="stdinOnce">StdinOnce:</label>
|
||||
<input id="stdinOnce" type="checkbox" ng-model="config.StdinOnce"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>SecurityOpts:</label>
|
||||
|
||||
<div ng-repeat="opt in config.SecurityOpts">
|
||||
<div class="form-group form-inline">
|
||||
<input type="text" ng-model="opt.name" class="form-control"
|
||||
placeholder="label:type:svirt_apache"/>
|
||||
<button type="button" class="btn btn-danger btn-sm"
|
||||
ng-click="rmEntry(config.SecurityOpts, opt)">Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-success btn-sm"
|
||||
ng-click="addEntry(config.SecurityOpts, {name: ''})">Add Option
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-group">
|
||||
<label>Env:</label>
|
||||
|
||||
<div ng-repeat="envar in config.Env">
|
||||
<div class="form-group form-inline">
|
||||
<div class="form-group">
|
||||
<label class="sr-only">Variable Name:</label>
|
||||
<input type="text" ng-model="envar.name" class="form-control"
|
||||
placeholder="NAME"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="sr-only">Variable Value:</label>
|
||||
<input type="text" ng-model="envar.value" class="form-control"
|
||||
placeholder="value"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-danger btn-xs form-control"
|
||||
ng-click="rmEntry(config.Env, envar)">Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-success btn-sm"
|
||||
ng-click="addEntry(config.Env, {name: '', value: ''})">Add environment
|
||||
variable
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Labels:</label>
|
||||
|
||||
<div ng-repeat="label in config.Labels">
|
||||
<div class="form-group form-inline">
|
||||
<div class="form-group">
|
||||
<label class="sr-only">Key:</label>
|
||||
<input type="text" ng-model="label.key" class="form-control"
|
||||
placeholder="key"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="sr-only">Value:</label>
|
||||
<input type="text" ng-model="label.value" class="form-control"
|
||||
placeholder="value"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-danger btn-xs form-control"
|
||||
ng-click="rmEntry(config.Labels, label)">Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-success btn-sm"
|
||||
ng-click="addEntry(config.Labels, {key: '', value: ''})">Add Label
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</accordion-group>
|
||||
<accordion-group heading="HostConfig options" is-open="menuStatus.hostConfigOpen">
|
||||
<fieldset>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<div class="form-group">
|
||||
<label>Binds:</label>
|
||||
|
||||
<div ng-repeat="bind in config.HostConfig.Binds">
|
||||
<div class="form-group form-inline">
|
||||
<input type="text" ng-model="bind.name" class="form-control"
|
||||
placeholder="/host:/container"/>
|
||||
<button type="button" class="btn btn-danger btn-sm"
|
||||
ng-click="rmEntry(config.HostConfig.Binds, bind)">Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-success btn-sm"
|
||||
ng-click="addEntry(config.HostConfig.Binds, {name: ''})">Add Bind
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Links:</label>
|
||||
|
||||
<div ng-repeat="link in config.HostConfig.Links">
|
||||
<div class="form-group form-inline">
|
||||
<input type="text" ng-model="link.name" class="form-control"
|
||||
placeholder="web:db">
|
||||
<button type="button" class="btn btn-danger btn-sm"
|
||||
ng-click="rmEntry(config.HostConfig.Links, link)">Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-success btn-sm"
|
||||
ng-click="addEntry(config.HostConfig.Links, {name: ''})">Add Link
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Dns:</label>
|
||||
|
||||
<div ng-repeat="entry in config.HostConfig.Dns">
|
||||
<div class="form-group form-inline">
|
||||
<input type="text" ng-model="entry.name" class="form-control"
|
||||
placeholder="8.8.8.8"/>
|
||||
<button type="button" class="btn btn-danger btn-sm"
|
||||
ng-click="rmEntry(config.HostConfig.Dns, entry)">Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-success btn-sm"
|
||||
ng-click="addEntry(config.HostConfig.Dns, {name: ''})">Add entry
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>DnsSearch:</label>
|
||||
|
||||
<div ng-repeat="entry in config.HostConfig.DnsSearch">
|
||||
<div class="form-group form-inline">
|
||||
<input type="text" ng-model="entry.name" class="form-control"
|
||||
placeholder="example.com"/>
|
||||
<button type="button" class="btn btn-danger btn-sm"
|
||||
ng-click="rmEntry(config.HostConfig.DnsSearch, entry)">
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-success btn-sm"
|
||||
ng-click="addEntry(config.HostConfig.DnsSearch, {name: ''})">Add
|
||||
entry
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>CapAdd:</label>
|
||||
|
||||
<div ng-repeat="entry in config.HostConfig.CapAdd">
|
||||
<div class="form-group form-inline">
|
||||
<input type="text" ng-model="entry.name" class="form-control"
|
||||
placeholder="cap_sys_admin"/>
|
||||
<button type="button" class="btn btn-danger btn-sm"
|
||||
ng-click="rmEntry(config.HostConfig.CapAdd, entry)">Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-success btn-sm"
|
||||
ng-click="addEntry(config.HostConfig.CapAdd, {name: ''})">Add entry
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>CapDrop:</label>
|
||||
|
||||
<div ng-repeat="entry in config.HostConfig.CapDrop">
|
||||
<div class="form-group form-inline">
|
||||
<input type="text" ng-model="entry.name" class="form-control"
|
||||
placeholder="cap_sys_admin"/>
|
||||
<button type="button" class="btn btn-danger btn-sm"
|
||||
ng-click="rmEntry(config.HostConfig.CapDrop, entry)">Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-success btn-sm"
|
||||
ng-click="addEntry(config.HostConfig.CapDrop, {name: ''})">Add entry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<div class="form-group">
|
||||
<label>NetworkMode:</label>
|
||||
<input type="text" ng-model="config.HostConfig.NetworkMode"
|
||||
class="form-control" placeholder="bridge"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="publishAllPorts">PublishAllPorts:</label>
|
||||
<input id="publishAllPorts" type="checkbox"
|
||||
ng-model="config.HostConfig.PublishAllPorts"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="privileged">Privileged:</label>
|
||||
<input id="privileged" type="checkbox"
|
||||
ng-model="config.HostConfig.Privileged"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>VolumesFrom:</label>
|
||||
|
||||
<div ng-repeat="volume in config.HostConfig.VolumesFrom">
|
||||
<div class="form-group form-inline">
|
||||
<select ng-model="volume.name"
|
||||
ng-options="name for name in containerNames track by name"
|
||||
class="form-control">
|
||||
</select>
|
||||
<button class="btn btn-danger btn-xs form-control"
|
||||
ng-click="rmEntry(config.HostConfig.VolumesFrom, volume)">
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-success btn-sm"
|
||||
ng-click="addEntry(config.HostConfig.VolumesFrom, {name: ''})">Add
|
||||
volume
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>RestartPolicy:</label>
|
||||
<select ng-model="config.HostConfig.RestartPolicy.name">
|
||||
<option value="">disabled</option>
|
||||
<option value="always">always</option>
|
||||
<option value="on-failure">on-failure</option>
|
||||
</select>
|
||||
<label>MaximumRetryCount:</label>
|
||||
<input type="number"
|
||||
ng-model="config.HostConfig.RestartPolicy.MaximumRetryCount"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-group">
|
||||
<label>ExtraHosts:</label>
|
||||
|
||||
<div ng-repeat="entry in config.HostConfig.ExtraHosts">
|
||||
<div class="form-group form-inline">
|
||||
<div class="form-group">
|
||||
<label class="sr-only">Hostname:</label>
|
||||
<input type="text" ng-model="entry.host" class="form-control"
|
||||
placeholder="hostname"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="sr-only">IP Address:</label>
|
||||
<input type="text" ng-model="entry.ip" class="form-control"
|
||||
placeholder="127.0.0.1"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-danger btn-xs form-control"
|
||||
ng-click="rmEntry(config.HostConfig.ExtraHosts, entry)">Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-success btn-sm"
|
||||
ng-click="addEntry(config.HostConfig.ExtraHosts, {host: '', ip: ''})">Add
|
||||
extra host
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>LxcConf:</label>
|
||||
|
||||
<div ng-repeat="entry in config.HostConfig.LxcConf">
|
||||
<div class="form-group form-inline">
|
||||
<div class="form-group">
|
||||
<label class="sr-only">Name:</label>
|
||||
<input type="text" ng-model="entry.name" class="form-control"
|
||||
placeholder="lxc.utsname"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="sr-only">Value:</label>
|
||||
<input type="text" ng-model="entry.value" class="form-control"
|
||||
placeholder="docker"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-danger btn-xs form-control"
|
||||
ng-click="rmEntry(config.HostConfig.LxcConf, entry)">Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-success btn-sm"
|
||||
ng-click="addEntry(config.HostConfig.LxcConf, {name: '', value: ''})">Add
|
||||
Entry
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Devices:</label>
|
||||
|
||||
<div ng-repeat="device in config.HostConfig.Devices">
|
||||
<div class="form-group form-inline inline-four">
|
||||
<label class="sr-only">PathOnHost:</label>
|
||||
<input type="text" ng-model="device.PathOnHost" class="form-control"
|
||||
placeholder="PathOnHost"/>
|
||||
<label class="sr-only">PathInContainer:</label>
|
||||
<input type="text" ng-model="device.PathInContainer" class="form-control"
|
||||
placeholder="PathInContainer"/>
|
||||
<label class="sr-only">CgroupPermissions:</label>
|
||||
<input type="text" ng-model="device.CgroupPermissions" class="form-control"
|
||||
placeholder="CgroupPermissions"/>
|
||||
<button class="btn btn-danger btn-xs form-control"
|
||||
ng-click="rmEntry(config.HostConfig.Devices, device)">Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-success btn-sm"
|
||||
ng-click="addEntry(config.HostConfig.Devices, { PathOnHost: '', PathInContainer: '', CgroupPermissions: ''})">
|
||||
Add Device
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>PortBindings:</label>
|
||||
|
||||
<div ng-repeat="portBinding in config.HostConfig.PortBindings">
|
||||
<div class="form-group form-inline inline-four">
|
||||
<label class="sr-only">Host IP:</label>
|
||||
<input type="text" ng-model="portBinding.ip" class="form-control"
|
||||
placeholder="Host IP Address"/>
|
||||
<label class="sr-only">Host Port:</label>
|
||||
<input type="text" ng-model="portBinding.extPort" class="form-control"
|
||||
placeholder="Host Port"/>
|
||||
<label class="sr-only">Container port:</label>
|
||||
<input type="text" ng-model="portBinding.intPort" class="form-control"
|
||||
placeholder="Container Port"/>
|
||||
<select ng-model="portBinding.protocol">
|
||||
<option value="">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
<button class="btn btn-danger btn-xs form-control"
|
||||
ng-click="rmEntry(config.HostConfig.PortBindings, portBinding)">
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-success btn-sm"
|
||||
ng-click="addEntry(config.HostConfig.PortBindings, {ip: '', extPort: '', intPort: ''})">
|
||||
Add Port Binding
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</accordion-group>
|
||||
</accordion>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="" class="btn btn-primary btn-lg" ng-click="create()">Create</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,67 +1,74 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Container stats"></rd-header-title>
|
||||
<rd-header-content>
|
||||
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Stats
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<h1>Stats</h1>
|
||||
|
||||
<h2>CPU</h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-7">
|
||||
<canvas id="cpu-stats-chart" width="650" height="300"></canvas>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon grey pull-left">
|
||||
<i class="fa fa-tasks"></i>
|
||||
</div>
|
||||
|
||||
<h2>Memory</h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-7">
|
||||
<canvas id="memory-stats-chart" width="650" height="300"></canvas>
|
||||
</div>
|
||||
<div class="col-sm-offset-1 col-sm-4">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<td>Max usage</td>
|
||||
<td>{{ data.memory_stats.max_usage | humansize }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Limit</td>
|
||||
<td>{{ data.memory_stats.limit | humansize }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Fail count</td>
|
||||
<td>{{ data.memory_stats.failcnt }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<accordion>
|
||||
<accordion-group heading="Other stats">
|
||||
<table class="table">
|
||||
<tr ng-repeat="(key, value) in data.memory_stats.stats">
|
||||
<td>{{ key }}</td>
|
||||
<td>{{ value }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</accordion-group>
|
||||
</accordion>
|
||||
</div>
|
||||
<div class="title">{{ container.Name|trimcontainername }}</div>
|
||||
<div class="comment">
|
||||
Name
|
||||
</div>
|
||||
|
||||
<h1>Network</h1>
|
||||
<div class="row">
|
||||
<div class="col-sm-7">
|
||||
<canvas id="network-stats-chart" width="650" height="300"></canvas>
|
||||
</div>
|
||||
<div class="col-sm-offset-1 col-sm-4">
|
||||
<div id="network-legend" style="margin-bottom: 20px;"></div>
|
||||
<accordion>
|
||||
<accordion-group heading="Other stats">
|
||||
<table class="table">
|
||||
<tr ng-repeat="(key, value) in data.network">
|
||||
<td>{{ key }}</td>
|
||||
<td>{{ value }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</accordion-group>
|
||||
</accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-area-chart" title="CPU usage"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<canvas id="cpu-stats-chart" width="770" height="230"></canvas>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-area-chart" title="Memory usage"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<canvas id="memory-stats-chart" width="770" height="230"></canvas>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-area-chart" title="Network usage"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<canvas id="network-stats-chart" width="770" height="230"></canvas>
|
||||
<div class="comment">
|
||||
<div id="network-legend" style="margin-bottom: 20px;"></div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Processes"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th ng-repeat="title in containerTop.Titles">{{title}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="processInfos in containerTop.Processes">
|
||||
<td ng-repeat="processInfo in processInfos track by $index">{{processInfo}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,187 +1,194 @@
|
||||
angular.module('stats', [])
|
||||
.controller('StatsController', ['Settings', '$scope', 'Messages', '$timeout', 'Container', '$routeParams', 'humansizeFilter', '$sce', function (Settings, $scope, Messages, $timeout, Container, $routeParams, humansizeFilter, $sce) {
|
||||
// TODO: Implement memory chart, force scale to 0-100 for cpu, 0 to limit for memory, fix charts on dashboard,
|
||||
// TODO: Force memory scale to 0 - max memory
|
||||
//var initialStats = {}; // Used to set scale of memory graph.
|
||||
//
|
||||
//Container.stats({id: $routeParams.id}, function (d) {
|
||||
// var arr = Object.keys(d).map(function (key) {
|
||||
// return d[key];
|
||||
// });
|
||||
// if (arr.join('').indexOf('no such id') !== -1) {
|
||||
// Messages.error('Unable to retrieve stats', 'Is this container running?');
|
||||
// return;
|
||||
// }
|
||||
// initialStats = d;
|
||||
//}, function () {
|
||||
// Messages.error('Unable to retrieve stats', 'Is this container running?');
|
||||
//});
|
||||
.controller('StatsController', ['Settings', '$scope', 'Messages', '$timeout', 'Container', 'ContainerTop', '$stateParams', 'humansizeFilter', '$sce', '$document',
|
||||
function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateParams, humansizeFilter, $sce, $document) {
|
||||
// TODO: Force scale to 0-100 for cpu, fix charts on dashboard,
|
||||
// TODO: Force memory scale to 0 - max memory
|
||||
$scope.ps_args = '';
|
||||
$scope.getTop = function () {
|
||||
ContainerTop.get($stateParams.id, {
|
||||
ps_args: $scope.ps_args
|
||||
}, function (data) {
|
||||
$scope.containerTop = data;
|
||||
});
|
||||
};
|
||||
$document.ready(function(){
|
||||
var cpuLabels = [];
|
||||
var cpuData = [];
|
||||
var memoryLabels = [];
|
||||
var memoryData = [];
|
||||
var networkLabels = [];
|
||||
var networkTxData = [];
|
||||
var networkRxData = [];
|
||||
for (var i = 0; i < 20; i++) {
|
||||
cpuLabels.push('');
|
||||
cpuData.push(0);
|
||||
memoryLabels.push('');
|
||||
memoryData.push(0);
|
||||
networkLabels.push('');
|
||||
networkTxData.push(0);
|
||||
networkRxData.push(0);
|
||||
}
|
||||
var cpuDataset = { // CPU Usage
|
||||
fillColor: "rgba(151,187,205,0.5)",
|
||||
strokeColor: "rgba(151,187,205,1)",
|
||||
pointColor: "rgba(151,187,205,1)",
|
||||
pointStrokeColor: "#fff",
|
||||
data: cpuData
|
||||
};
|
||||
var memoryDataset = {
|
||||
fillColor: "rgba(151,187,205,0.5)",
|
||||
strokeColor: "rgba(151,187,205,1)",
|
||||
pointColor: "rgba(151,187,205,1)",
|
||||
pointStrokeColor: "#fff",
|
||||
data: memoryData
|
||||
};
|
||||
var networkRxDataset = {
|
||||
label: "Rx Bytes",
|
||||
fillColor: "rgba(151,187,205,0.5)",
|
||||
strokeColor: "rgba(151,187,205,1)",
|
||||
pointColor: "rgba(151,187,205,1)",
|
||||
pointStrokeColor: "#fff",
|
||||
data: networkRxData
|
||||
};
|
||||
var networkTxDataset = {
|
||||
label: "Tx Bytes",
|
||||
fillColor: "rgba(255,180,174,0.5)",
|
||||
strokeColor: "rgba(255,180,174,1)",
|
||||
pointColor: "rgba(255,180,174,1)",
|
||||
pointStrokeColor: "#fff",
|
||||
data: networkTxData
|
||||
};
|
||||
var networkLegendData = [
|
||||
{
|
||||
//value: '',
|
||||
color: 'rgba(151,187,205,0.5)',
|
||||
title: 'Rx Data'
|
||||
},
|
||||
{
|
||||
//value: '',
|
||||
color: 'rgba(255,180,174,0.5)',
|
||||
title: 'Tx Data'
|
||||
}
|
||||
];
|
||||
|
||||
var cpuLabels = [];
|
||||
var cpuData = [];
|
||||
var memoryLabels = [];
|
||||
var memoryData = [];
|
||||
var networkLabels = [];
|
||||
var networkTxData = [];
|
||||
var networkRxData = [];
|
||||
for (var i = 0; i < 20; i++) {
|
||||
cpuLabels.push('');
|
||||
cpuData.push(0);
|
||||
memoryLabels.push('');
|
||||
memoryData.push(0);
|
||||
networkLabels.push('');
|
||||
networkTxData.push(0);
|
||||
networkRxData.push(0);
|
||||
}
|
||||
var cpuDataset = { // CPU Usage
|
||||
fillColor: "rgba(151,187,205,0.5)",
|
||||
strokeColor: "rgba(151,187,205,1)",
|
||||
pointColor: "rgba(151,187,205,1)",
|
||||
pointStrokeColor: "#fff",
|
||||
data: cpuData
|
||||
};
|
||||
var memoryDataset = {
|
||||
fillColor: "rgba(151,187,205,0.5)",
|
||||
strokeColor: "rgba(151,187,205,1)",
|
||||
pointColor: "rgba(151,187,205,1)",
|
||||
pointStrokeColor: "#fff",
|
||||
data: memoryData
|
||||
};
|
||||
var networkRxDataset = {
|
||||
label: "Rx Bytes",
|
||||
fillColor: "rgba(151,187,205,0.5)",
|
||||
strokeColor: "rgba(151,187,205,1)",
|
||||
pointColor: "rgba(151,187,205,1)",
|
||||
pointStrokeColor: "#fff",
|
||||
data: networkRxData
|
||||
};
|
||||
var networkTxDataset = {
|
||||
label: "Tx Bytes",
|
||||
fillColor: "rgba(255,180,174,0.5)",
|
||||
strokeColor: "rgba(255,180,174,1)",
|
||||
pointColor: "rgba(255,180,174,1)",
|
||||
pointStrokeColor: "#fff",
|
||||
data: networkTxData
|
||||
};
|
||||
var networkLegendData = [
|
||||
{
|
||||
//value: '',
|
||||
color: 'rgba(151,187,205,0.5)',
|
||||
title: 'Rx Data'
|
||||
},
|
||||
{
|
||||
//value: '',
|
||||
color: 'rgba(255,180,174,0.5)',
|
||||
title: 'Rx Data'
|
||||
}];
|
||||
legend($('#network-legend').get(0), networkLegendData);
|
||||
legend($('#network-legend').get(0), networkLegendData);
|
||||
|
||||
Chart.defaults.global.animationSteps = 30; // Lower from 60 to ease CPU load.
|
||||
var cpuChart = new Chart($('#cpu-stats-chart').get(0).getContext("2d")).Line({
|
||||
labels: cpuLabels,
|
||||
datasets: [cpuDataset]
|
||||
}, {
|
||||
responsive: true
|
||||
Chart.defaults.global.animationSteps = 30; // Lower from 60 to ease CPU load.
|
||||
var cpuChart = new Chart($('#cpu-stats-chart').get(0).getContext("2d")).Line({
|
||||
labels: cpuLabels,
|
||||
datasets: [cpuDataset]
|
||||
}, {
|
||||
responsive: true
|
||||
});
|
||||
|
||||
var memoryChart = new Chart($('#memory-stats-chart').get(0).getContext('2d')).Line({
|
||||
labels: memoryLabels,
|
||||
datasets: [memoryDataset]
|
||||
},
|
||||
{
|
||||
scaleLabel: function (valueObj) {
|
||||
return humansizeFilter(parseInt(valueObj.value, 10));
|
||||
},
|
||||
responsive: true
|
||||
//scaleOverride: true,
|
||||
//scaleSteps: 10,
|
||||
//scaleStepWidth: Math.ceil(initialStats.memory_stats.limit / 10),
|
||||
//scaleStartValue: 0
|
||||
});
|
||||
var networkChart = new Chart($('#network-stats-chart').get(0).getContext("2d")).Line({
|
||||
labels: networkLabels,
|
||||
datasets: [networkRxDataset, networkTxDataset]
|
||||
}, {
|
||||
scaleLabel: function (valueObj) {
|
||||
return humansizeFilter(parseInt(valueObj.value, 10));
|
||||
},
|
||||
responsive: true
|
||||
});
|
||||
$scope.networkLegend = $sce.trustAsHtml(networkChart.generateLegend());
|
||||
|
||||
function updateStats() {
|
||||
Container.stats({id: $stateParams.id}, function (d) {
|
||||
var arr = Object.keys(d).map(function (key) {
|
||||
return d[key];
|
||||
});
|
||||
|
||||
var memoryChart = new Chart($('#memory-stats-chart').get(0).getContext('2d')).Line({
|
||||
labels: memoryLabels,
|
||||
datasets: [memoryDataset]
|
||||
},
|
||||
{
|
||||
scaleLabel: function (valueObj) {
|
||||
return humansizeFilter(parseInt(valueObj.value, 10));
|
||||
},
|
||||
responsive: true
|
||||
//scaleOverride: true,
|
||||
//scaleSteps: 10,
|
||||
//scaleStepWidth: Math.ceil(initialStats.memory_stats.limit / 10),
|
||||
//scaleStartValue: 0
|
||||
});
|
||||
var networkChart = new Chart($('#network-stats-chart').get(0).getContext("2d")).Line({
|
||||
labels: networkLabels,
|
||||
datasets: [networkRxDataset, networkTxDataset]
|
||||
}, {
|
||||
scaleLabel: function (valueObj) {
|
||||
return humansizeFilter(parseInt(valueObj.value, 10));
|
||||
},
|
||||
responsive: true
|
||||
});
|
||||
$scope.networkLegend = $sce.trustAsHtml(networkChart.generateLegend());
|
||||
|
||||
function updateStats() {
|
||||
Container.stats({id: $routeParams.id}, function (d) {
|
||||
var arr = Object.keys(d).map(function (key) {
|
||||
return d[key];
|
||||
});
|
||||
if (arr.join('').indexOf('no such id') !== -1) {
|
||||
Messages.error('Unable to retrieve stats', 'Is this container running?');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update graph with latest data
|
||||
$scope.data = d;
|
||||
updateCpuChart(d);
|
||||
updateMemoryChart(d);
|
||||
updateNetworkChart(d);
|
||||
timeout = $timeout(updateStats, 2000);
|
||||
}, function () {
|
||||
Messages.error('Unable to retrieve stats', 'Is this container running?');
|
||||
});
|
||||
if (arr.join('').indexOf('no such id') !== -1) {
|
||||
Messages.error('Unable to retrieve stats', 'Is this container running?');
|
||||
return;
|
||||
}
|
||||
|
||||
var timeout;
|
||||
$scope.$on('$destroy', function () {
|
||||
$timeout.cancel(timeout);
|
||||
});
|
||||
// Update graph with latest data
|
||||
$scope.data = d;
|
||||
updateCpuChart(d);
|
||||
updateMemoryChart(d);
|
||||
updateNetworkChart(d);
|
||||
timeout = $timeout(updateStats, 5000);
|
||||
}, function () {
|
||||
Messages.error('Unable to retrieve stats', 'Is this container running?');
|
||||
timeout = $timeout(updateStats, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
updateStats();
|
||||
var timeout;
|
||||
$scope.$on('$destroy', function () {
|
||||
$timeout.cancel(timeout);
|
||||
});
|
||||
|
||||
function updateCpuChart(data) {
|
||||
console.log('updateCpuChart', data);
|
||||
cpuChart.addData([calculateCPUPercent(data)], new Date(data.read).toLocaleTimeString());
|
||||
cpuChart.removeData();
|
||||
}
|
||||
updateStats();
|
||||
|
||||
function updateMemoryChart(data) {
|
||||
console.log('updateMemoryChart', data);
|
||||
memoryChart.addData([data.memory_stats.usage], new Date(data.read).toLocaleTimeString());
|
||||
memoryChart.removeData();
|
||||
}
|
||||
function updateCpuChart(data) {
|
||||
cpuChart.addData([calculateCPUPercent(data)], new Date(data.read).toLocaleTimeString());
|
||||
cpuChart.removeData();
|
||||
}
|
||||
|
||||
var lastRxBytes = 0, lastTxBytes = 0;
|
||||
function updateMemoryChart(data) {
|
||||
memoryChart.addData([data.memory_stats.usage], new Date(data.read).toLocaleTimeString());
|
||||
memoryChart.removeData();
|
||||
}
|
||||
|
||||
function updateNetworkChart(data) {
|
||||
var rxBytes = 0, txBytes = 0;
|
||||
if (lastRxBytes !== 0 || lastTxBytes !== 0) {
|
||||
// These will be zero on first call, ignore to prevent large graph spike
|
||||
rxBytes = data.network.rx_bytes - lastRxBytes;
|
||||
txBytes = data.network.tx_bytes - lastTxBytes;
|
||||
}
|
||||
lastRxBytes = data.network.rx_bytes;
|
||||
lastTxBytes = data.network.tx_bytes;
|
||||
console.log('updateNetworkChart', data);
|
||||
networkChart.addData([rxBytes, txBytes], new Date(data.read).toLocaleTimeString());
|
||||
networkChart.removeData();
|
||||
}
|
||||
var lastRxBytes = 0, lastTxBytes = 0;
|
||||
|
||||
function calculateCPUPercent(stats) {
|
||||
// Same algorithm the official client uses: https://github.com/docker/docker/blob/master/api/client/stats.go#L195-L208
|
||||
var prevCpu = stats.precpu_stats;
|
||||
var curCpu = stats.cpu_stats;
|
||||
function updateNetworkChart(data) {
|
||||
// 1.9+ contains an object of networks, for now we'll just show stats for the first network
|
||||
// TODO: Show graphs for all networks
|
||||
if (data.networks) {
|
||||
$scope.networkName = Object.keys(data.networks)[0];
|
||||
data.network = data.networks[$scope.networkName];
|
||||
}
|
||||
var rxBytes = 0, txBytes = 0;
|
||||
if (lastRxBytes !== 0 || lastTxBytes !== 0) {
|
||||
// These will be zero on first call, ignore to prevent large graph spike
|
||||
rxBytes = data.network.rx_bytes - lastRxBytes;
|
||||
txBytes = data.network.tx_bytes - lastTxBytes;
|
||||
}
|
||||
lastRxBytes = data.network.rx_bytes;
|
||||
lastTxBytes = data.network.tx_bytes;
|
||||
networkChart.addData([rxBytes, txBytes], new Date(data.read).toLocaleTimeString());
|
||||
networkChart.removeData();
|
||||
}
|
||||
|
||||
var cpuPercent = 0.0;
|
||||
function calculateCPUPercent(stats) {
|
||||
// Same algorithm the official client uses: https://github.com/docker/docker/blob/master/api/client/stats.go#L195-L208
|
||||
var prevCpu = stats.precpu_stats;
|
||||
var curCpu = stats.cpu_stats;
|
||||
|
||||
// calculate the change for the cpu usage of the container in between readings
|
||||
var cpuDelta = curCpu.cpu_usage.total_usage - prevCpu.cpu_usage.total_usage;
|
||||
// calculate the change for the entire system between readings
|
||||
var systemDelta = curCpu.system_cpu_usage - prevCpu.system_cpu_usage;
|
||||
var cpuPercent = 0.0;
|
||||
|
||||
if (systemDelta > 0.0 && cpuDelta > 0.0) {
|
||||
//console.log('size thing:', curCpu.cpu_usage.percpu_usage);
|
||||
cpuPercent = (cpuDelta / systemDelta) * curCpu.cpu_usage.percpu_usage.length * 100.0;
|
||||
}
|
||||
return cpuPercent;
|
||||
}
|
||||
}])
|
||||
;
|
||||
// calculate the change for the cpu usage of the container in between readings
|
||||
var cpuDelta = curCpu.cpu_usage.total_usage - prevCpu.cpu_usage.total_usage;
|
||||
// calculate the change for the entire system between readings
|
||||
var systemDelta = curCpu.system_cpu_usage - prevCpu.system_cpu_usage;
|
||||
|
||||
if (systemDelta > 0.0 && cpuDelta > 0.0) {
|
||||
cpuPercent = (cpuDelta / systemDelta) * curCpu.cpu_usage.percpu_usage.length * 100.0;
|
||||
}
|
||||
return cpuPercent;
|
||||
}
|
||||
});
|
||||
|
||||
Container.get({id: $stateParams.id}, function (d) {
|
||||
$scope.container = d;
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e.data);
|
||||
});
|
||||
$scope.getTop();
|
||||
}]);
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Cluster overview">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="swarm" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Swarm</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-object-group" title="Cluster status"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Nodes</td>
|
||||
<td>{{ swarm.Nodes }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Images</td>
|
||||
<td>{{ info.Images }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Swarm version</td>
|
||||
<td>{{ docker.Version|swarmversion }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Docker API version</td>
|
||||
<td>{{ docker.ApiVersion }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Strategy</td>
|
||||
<td>{{ swarm.Strategy }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total CPU</td>
|
||||
<td>{{ info.NCPU }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total memory</td>
|
||||
<td>{{ info.MemTotal|humansize }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Operating system</td>
|
||||
<td>{{ info.OperatingSystem }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kernel version</td>
|
||||
<td>{{ info.KernelVersion }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Go version</td>
|
||||
<td>{{ docker.GoVersion }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-hdd-o" title="Node status"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('Name')">
|
||||
Name
|
||||
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('cpu')">
|
||||
CPU
|
||||
<span ng-show="sortType == 'cpu' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'cpu' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('memory')">
|
||||
Memory
|
||||
<span ng-show="sortType == 'memory' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'memory' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('IP')">
|
||||
IP
|
||||
<span ng-show="sortType == 'IP' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'IP' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('Engine')">
|
||||
Engine
|
||||
<span ng-show="sortType == 'Engine' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Engine' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('Status')">
|
||||
Status
|
||||
<span ng-show="sortType == 'Status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="node in (state.filteredNodes = (swarm.Status | filter:state.filter | orderBy:sortType:sortReverse))">
|
||||
<td>{{ node.name }}</td>
|
||||
<td>{{ node.cpu }}</td>
|
||||
<td>{{ node.memory }}</td>
|
||||
<td>{{ node.ip }}</td>
|
||||
<td>{{ node.version }}</td>
|
||||
<td><span class="label label-{{ node.status|nodestatusbadge }}">{{ node.status }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,62 @@
|
||||
angular.module('swarm', [])
|
||||
.controller('SwarmController', ['$scope', 'Info', 'Version', 'Settings',
|
||||
function ($scope, Info, Version, Settings) {
|
||||
|
||||
$scope.sortType = 'Name';
|
||||
$scope.sortReverse = true;
|
||||
$scope.info = {};
|
||||
$scope.docker = {};
|
||||
$scope.swarm = {};
|
||||
|
||||
$scope.order = function(sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
Version.get({}, function (d) {
|
||||
$scope.docker = d;
|
||||
});
|
||||
Info.get({}, function (d) {
|
||||
$scope.info = d;
|
||||
extractSwarmInfo(d);
|
||||
});
|
||||
|
||||
function extractSwarmInfo(info) {
|
||||
// Swarm info is available in SystemStatus object
|
||||
var systemStatus = info.SystemStatus;
|
||||
// Swarm strategy
|
||||
$scope.swarm[systemStatus[1][0]] = systemStatus[1][1];
|
||||
// Swarm filters
|
||||
$scope.swarm[systemStatus[2][0]] = systemStatus[2][1];
|
||||
// Swarm node count
|
||||
var node_count = parseInt(systemStatus[3][1], 10);
|
||||
$scope.swarm[systemStatus[3][0]] = node_count;
|
||||
|
||||
$scope.swarm.Status = [];
|
||||
extractNodesInfo(systemStatus, node_count);
|
||||
}
|
||||
|
||||
function extractNodesInfo(info, node_count) {
|
||||
// First information for node1 available at element #4 of SystemStatus
|
||||
// The next 10 elements are information related to the node
|
||||
var node_offset = 4;
|
||||
for (i = 0; i < node_count; i++) {
|
||||
extractNodeInfo(info, node_offset);
|
||||
node_offset += 9;
|
||||
}
|
||||
}
|
||||
|
||||
function extractNodeInfo(info, offset) {
|
||||
var node = {};
|
||||
node.name = info[offset][0];
|
||||
node.ip = info[offset][1];
|
||||
node.id = info[offset + 1][1];
|
||||
node.status = info[offset + 2][1];
|
||||
node.containers = info[offset + 3][1];
|
||||
node.cpu = info[offset + 4][1].split('/')[1];
|
||||
node.memory = info[offset + 5][1].split('/')[1];
|
||||
node.labels = info[offset + 6][1];
|
||||
node.version = info[offset + 8][1];
|
||||
$scope.swarm.Status.push(node);
|
||||
}
|
||||
}]);
|
||||
@@ -0,0 +1,67 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Volume list">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="volumes" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Volumes</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="col-lg-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-cubes" title="Volumes">
|
||||
<div class="pull-right">
|
||||
<i id="loadVolumesSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<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" />
|
||||
</div>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>
|
||||
<a ui-sref="volumes" ng-click="order('Name')">
|
||||
Name
|
||||
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="volumes" 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="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>
|
||||
<tr ng-repeat="volume in (state.filteredVolumes = (volumes | filter:state.filter | orderBy:sortType:sortReverse))">
|
||||
<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>
|
||||
@@ -0,0 +1,62 @@
|
||||
angular.module('volumes', [])
|
||||
.controller('VolumesController', ['$scope', '$state', 'Volume', 'Messages', 'errorMsgFilter',
|
||||
function ($scope, $state, Volume, Messages, errorMsgFilter) {
|
||||
$scope.state = {};
|
||||
$scope.state.selectedItemCount = 0;
|
||||
$scope.sortType = 'Name';
|
||||
$scope.sortReverse = true;
|
||||
|
||||
$scope.config = {
|
||||
Name: ''
|
||||
};
|
||||
|
||||
$scope.order = function(sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
$scope.selectItem = function (item) {
|
||||
if (item.Checked) {
|
||||
$scope.state.selectedItemCount++;
|
||||
} else {
|
||||
$scope.state.selectedItemCount--;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.removeAction = function () {
|
||||
$('#loadVolumesSpinner').show();
|
||||
var counter = 0;
|
||||
var complete = function () {
|
||||
counter = counter - 1;
|
||||
if (counter === 0) {
|
||||
$('#loadVolumesSpinner').hide();
|
||||
}
|
||||
};
|
||||
angular.forEach($scope.volumes, function (volume) {
|
||||
if (volume.Checked) {
|
||||
counter = counter + 1;
|
||||
Volume.remove({name: volume.Name}, function (d) {
|
||||
Messages.send("Volume deleted", volume.Name);
|
||||
var index = $scope.volumes.indexOf(volume);
|
||||
$scope.volumes.splice(index, 1);
|
||||
complete();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e.data);
|
||||
complete();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function fetchVolumes() {
|
||||
$('#loadVolumesSpinner').show();
|
||||
Volume.query({}, function (d) {
|
||||
$scope.volumes = d.Volumes;
|
||||
$('#loadVolumesSpinner').hide();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e.data);
|
||||
$('#loadVolumesSpinner').hide();
|
||||
});
|
||||
}
|
||||
fetchVolumes();
|
||||
}]);
|
||||
@@ -0,0 +1,11 @@
|
||||
angular
|
||||
.module('uifordocker')
|
||||
.directive('rdHeaderContent', function rdHeaderContent() {
|
||||
var directive = {
|
||||
requires: '^rdHeader',
|
||||
transclude: true,
|
||||
template: '<div class="breadcrumb-links" ng-transclude></div>',
|
||||
restrict: 'E'
|
||||
};
|
||||
return directive;
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
angular
|
||||
.module('uifordocker')
|
||||
.directive('rdHeaderTitle', function rdHeaderTitle() {
|
||||
var directive = {
|
||||
requires: '^rdHeader',
|
||||
scope: {
|
||||
title: '@'
|
||||
},
|
||||
transclude: true,
|
||||
template: '<div class="page">{{title}}<span class="header_title_content" ng-transclude><span></div>',
|
||||
restrict: 'E'
|
||||
};
|
||||
return directive;
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
angular
|
||||
.module('uifordocker')
|
||||
.directive('rdHeader', function rdHeader() {
|
||||
var directive = {
|
||||
scope: {
|
||||
"ngModel": "="
|
||||
},
|
||||
transclude: true,
|
||||
template: '<div class="row header"><div class="col-xs-12"><div class="meta" ng-transclude></div></div></div>',
|
||||
restrict: 'EA'
|
||||
};
|
||||
return directive;
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
angular
|
||||
.module('uifordocker')
|
||||
.directive('rdLoading', function rdLoading() {
|
||||
var directive = {
|
||||
restrict: 'AE',
|
||||
template: '<div class="loading"><div class="double-bounce1"></div><div class="double-bounce2"></div></div>'
|
||||
};
|
||||
return directive;
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
angular
|
||||
.module('uifordocker')
|
||||
.directive('rdWidgetBody', function rdWidgetBody() {
|
||||
var directive = {
|
||||
requires: '^rdWidget',
|
||||
scope: {
|
||||
loading: '@?',
|
||||
classes: '@?'
|
||||
},
|
||||
transclude: true,
|
||||
template: '<div class="widget-body" ng-class="classes"><rd-loading ng-show="loading"></rd-loading><div ng-hide="loading" class="widget-content" ng-transclude></div></div>',
|
||||
restrict: 'E'
|
||||
};
|
||||
return directive;
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
angular
|
||||
.module('uifordocker')
|
||||
.directive('rdWidgetFooter', function rdWidgetFooter() {
|
||||
var directive = {
|
||||
requires: '^rdWidget',
|
||||
transclude: true,
|
||||
template: '<div class="widget-footer" ng-transclude></div>',
|
||||
restrict: 'E'
|
||||
};
|
||||
return directive;
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
angular
|
||||
.module('uifordocker')
|
||||
.directive('rdWidgetHeader', function rdWidgetTitle() {
|
||||
var directive = {
|
||||
requires: '^rdWidget',
|
||||
scope: {
|
||||
title: '@',
|
||||
icon: '@'
|
||||
},
|
||||
transclude: true,
|
||||
template: '<div class="widget-header"><div class="row"><span class="pull-left"><i class="fa" ng-class="icon"></i> {{title}} </span><span class="pull-right col-xs-6 col-sm-4" ng-transclude></span></div></div>',
|
||||
restrict: 'E'
|
||||
};
|
||||
return directive;
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
angular
|
||||
.module('uifordocker')
|
||||
.directive('rdWidgetTaskbar', function rdWidgetTaskbar() {
|
||||
var directive = {
|
||||
requires: '^rdWidget',
|
||||
scope: {
|
||||
classes: '@?'
|
||||
},
|
||||
transclude: true,
|
||||
template: '<div class="widget-header"><div class="row"><div ng-class="classes" ng-transclude></div></div></div>',
|
||||
restrict: 'E'
|
||||
};
|
||||
return directive;
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
angular
|
||||
.module('uifordocker')
|
||||
.directive('rdWidget', function rdWidget() {
|
||||
var directive = {
|
||||
scope: {
|
||||
"ngModel": "="
|
||||
},
|
||||
transclude: true,
|
||||
template: '<div class="widget" ng-transclude></div>',
|
||||
restrict: 'EA'
|
||||
};
|
||||
return directive;
|
||||
});
|
||||
@@ -1,117 +1,199 @@
|
||||
angular.module('dockerui.filters', [])
|
||||
.filter('truncate', function () {
|
||||
'use strict';
|
||||
return function (text, length, end) {
|
||||
if (isNaN(length)) {
|
||||
length = 10;
|
||||
}
|
||||
angular.module('uifordocker.filters', [])
|
||||
.filter('truncate', function () {
|
||||
'use strict';
|
||||
return function (text, length, end) {
|
||||
if (isNaN(length)) {
|
||||
length = 10;
|
||||
}
|
||||
|
||||
if (end === undefined) {
|
||||
end = '...';
|
||||
}
|
||||
if (end === undefined) {
|
||||
end = '...';
|
||||
}
|
||||
|
||||
if (text.length <= length || text.length - end.length <= length) {
|
||||
return text;
|
||||
}
|
||||
else {
|
||||
return String(text).substring(0, length - end.length) + end;
|
||||
}
|
||||
};
|
||||
})
|
||||
.filter('statusbadge', function () {
|
||||
'use strict';
|
||||
return function (text) {
|
||||
if (text === 'Ghost') {
|
||||
return 'important';
|
||||
} else if (text.indexOf('Exit') !== -1 && text !== 'Exit 0') {
|
||||
return 'warning';
|
||||
}
|
||||
return 'success';
|
||||
};
|
||||
})
|
||||
.filter('getstatetext', function () {
|
||||
'use strict';
|
||||
return function (state) {
|
||||
if (state === undefined) {
|
||||
return '';
|
||||
}
|
||||
if (state.Ghost && state.Running) {
|
||||
return 'Ghost';
|
||||
}
|
||||
if (state.Running && state.Paused) {
|
||||
return 'Running (Paused)';
|
||||
}
|
||||
if (state.Running) {
|
||||
return 'Running';
|
||||
}
|
||||
return 'Stopped';
|
||||
};
|
||||
})
|
||||
.filter('getstatelabel', function () {
|
||||
'use strict';
|
||||
return function (state) {
|
||||
if (state === undefined) {
|
||||
return 'label-default';
|
||||
}
|
||||
|
||||
if (state.Ghost && state.Running) {
|
||||
return 'label-important';
|
||||
}
|
||||
if (state.Running) {
|
||||
return 'label-success';
|
||||
}
|
||||
return 'label-default';
|
||||
};
|
||||
})
|
||||
.filter('humansize', function () {
|
||||
'use strict';
|
||||
return function (bytes) {
|
||||
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
if (bytes === 0) {
|
||||
return 'n/a';
|
||||
}
|
||||
var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)), 10);
|
||||
var value = bytes / Math.pow(1024, i);
|
||||
var decimalPlaces = (i < 1) ? 0 : (i - 1);
|
||||
return value.toFixed(decimalPlaces) + ' ' + sizes[[i]];
|
||||
};
|
||||
})
|
||||
.filter('containername', function () {
|
||||
'use strict';
|
||||
return function (container) {
|
||||
var name = container.Names[0];
|
||||
return name.substring(1, name.length);
|
||||
};
|
||||
})
|
||||
.filter('repotag', function () {
|
||||
'use strict';
|
||||
return function (image) {
|
||||
if (image.RepoTags && image.RepoTags.length > 0) {
|
||||
var tag = image.RepoTags[0];
|
||||
if (tag === '<none>:<none>') {
|
||||
tag = '';
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
})
|
||||
.filter('getdate', function () {
|
||||
'use strict';
|
||||
return function (data) {
|
||||
//Multiply by 1000 for the unix format
|
||||
var date = new Date(data * 1000);
|
||||
return date.toDateString();
|
||||
};
|
||||
})
|
||||
.filter('errorMsg', function () {
|
||||
return function (object) {
|
||||
var idx = 0;
|
||||
var msg = '';
|
||||
while (object[idx] && typeof(object[idx]) === 'string') {
|
||||
msg += object[idx];
|
||||
idx++;
|
||||
}
|
||||
return msg;
|
||||
};
|
||||
});
|
||||
if (text.length <= length || text.length - end.length <= length) {
|
||||
return text;
|
||||
}
|
||||
else {
|
||||
return String(text).substring(0, length - end.length) + end;
|
||||
}
|
||||
};
|
||||
})
|
||||
.filter('containerstatusbadge', function () {
|
||||
'use strict';
|
||||
return function (text) {
|
||||
var status = _.toLower(text);
|
||||
if (status.indexOf('paused') !== -1) {
|
||||
return 'warning';
|
||||
} else if (status.indexOf('created') !== -1) {
|
||||
return 'info';
|
||||
} else if (status.indexOf('exited') !== -1) {
|
||||
return 'danger';
|
||||
}
|
||||
return 'success';
|
||||
};
|
||||
})
|
||||
.filter('containerstatus', function () {
|
||||
'use strict';
|
||||
return function (text) {
|
||||
var status = _.toLower(text);
|
||||
if (status.indexOf('paused') !== -1) {
|
||||
return 'paused';
|
||||
} else if (status.indexOf('created') !== -1) {
|
||||
return 'created';
|
||||
} else if (status.indexOf('exited') !== -1) {
|
||||
return 'stopped';
|
||||
}
|
||||
return 'running';
|
||||
};
|
||||
})
|
||||
.filter('nodestatusbadge', function () {
|
||||
'use strict';
|
||||
return function (text) {
|
||||
if (text === 'Unhealthy') {
|
||||
return 'danger';
|
||||
}
|
||||
return 'success';
|
||||
};
|
||||
})
|
||||
.filter('trimcontainername', function () {
|
||||
'use strict';
|
||||
return function (name) {
|
||||
if (name) {
|
||||
return (name.indexOf('/') === 0 ? name.replace('/','') : name);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
})
|
||||
.filter('capitalize', function () {
|
||||
'use strict';
|
||||
return function (text) {
|
||||
return _.capitalize(text);
|
||||
};
|
||||
})
|
||||
.filter('getstatetext', function () {
|
||||
'use strict';
|
||||
return function (state) {
|
||||
if (state === undefined) {
|
||||
return '';
|
||||
}
|
||||
if (state.Ghost && state.Running) {
|
||||
return 'Ghost';
|
||||
}
|
||||
if (state.Running && state.Paused) {
|
||||
return 'Running (Paused)';
|
||||
}
|
||||
if (state.Running) {
|
||||
return 'Running';
|
||||
}
|
||||
return 'Stopped';
|
||||
};
|
||||
})
|
||||
.filter('getstatelabel', function () {
|
||||
'use strict';
|
||||
return function (state) {
|
||||
if (state === undefined) {
|
||||
return 'label-default';
|
||||
}
|
||||
if (state.Ghost && state.Running) {
|
||||
return 'label-important';
|
||||
}
|
||||
if (state.Running) {
|
||||
return 'label-success';
|
||||
}
|
||||
return 'label-default';
|
||||
};
|
||||
})
|
||||
.filter('humansize', function () {
|
||||
'use strict';
|
||||
return function (bytes) {
|
||||
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
if (bytes === 0) {
|
||||
return 'n/a';
|
||||
}
|
||||
var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)), 10);
|
||||
var value = bytes / Math.pow(1024, i);
|
||||
var decimalPlaces = (i < 1) ? 0 : (i - 1);
|
||||
return value.toFixed(decimalPlaces) + ' ' + sizes[[i]];
|
||||
};
|
||||
})
|
||||
.filter('containername', function () {
|
||||
'use strict';
|
||||
return function (container) {
|
||||
var name = container.Names[0];
|
||||
return name.substring(1, name.length);
|
||||
};
|
||||
})
|
||||
.filter('swarmcontainername', function () {
|
||||
'use strict';
|
||||
return function (container) {
|
||||
return _.split(container.Names[0], '/')[2];
|
||||
};
|
||||
})
|
||||
.filter('swarmversion', function () {
|
||||
'use strict';
|
||||
return function (text) {
|
||||
return _.split(text, '/')[1];
|
||||
};
|
||||
})
|
||||
.filter('swarmhostname', function () {
|
||||
'use strict';
|
||||
return function (container) {
|
||||
return _.split(container.Names[0], '/')[1];
|
||||
};
|
||||
})
|
||||
.filter('repotags', function () {
|
||||
'use strict';
|
||||
return function (image) {
|
||||
if (image.RepoTags && image.RepoTags.length > 0) {
|
||||
var tag = image.RepoTags[0];
|
||||
if (tag === '<none>:<none>') {
|
||||
return [];
|
||||
}
|
||||
return image.RepoTags;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
})
|
||||
.filter('getisodatefromtimestamp', function () {
|
||||
'use strict';
|
||||
return function (timestamp) {
|
||||
return moment.unix(timestamp).format('YYYY-MM-DD HH:mm:ss');
|
||||
};
|
||||
})
|
||||
.filter('getisodate', function () {
|
||||
'use strict';
|
||||
return function (date) {
|
||||
return moment(date).format('YYYY-MM-DD HH:mm:ss');
|
||||
};
|
||||
})
|
||||
.filter('command', function () {
|
||||
'use strict';
|
||||
return function (command) {
|
||||
if (command) {
|
||||
return command.join(' ');
|
||||
}
|
||||
};
|
||||
})
|
||||
.filter('key', function () {
|
||||
'use strict';
|
||||
return function (pair) {
|
||||
return pair.slice(0, pair.indexOf('='));
|
||||
};
|
||||
})
|
||||
.filter('value', function () {
|
||||
'use strict';
|
||||
return function (pair) {
|
||||
return pair.slice(pair.indexOf('=') + 1);
|
||||
};
|
||||
})
|
||||
.filter('errorMsg', function () {
|
||||
return function (object) {
|
||||
var idx = 0;
|
||||
var msg = '';
|
||||
while (object[idx] && typeof(object[idx]) === 'string') {
|
||||
msg += object[idx];
|
||||
idx++;
|
||||
}
|
||||
return msg;
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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.
|
||||
function jsonObjectsToArrayHandler(data) {
|
||||
var str = "[" + data.replace(/\n/g, " ").replace(/\}\s*\{/g, "}, {") + "]";
|
||||
return angular.fromJson(str);
|
||||
}
|
||||
|
||||
// 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 = angular.fromJson(data);
|
||||
if (!Array.isArray(response)) {
|
||||
var arr = [];
|
||||
arr.push(response);
|
||||
return arr;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
angular.module('dockerui.services', ['ngResource'])
|
||||
angular.module('uifordocker.services', ['ngResource', 'ngSanitize'])
|
||||
.factory('Container', ['$resource', 'Settings', function ContainerFactory($resource, Settings) {
|
||||
'use strict';
|
||||
// Resource for interacting with the docker containers
|
||||
@@ -18,9 +18,17 @@ angular.module('dockerui.services', ['ngResource'])
|
||||
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: 2000}
|
||||
stats: {method: 'GET', params: {id: '@id', stream: false, action: 'stats'}, timeout: 5000},
|
||||
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'}}
|
||||
});
|
||||
}])
|
||||
.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
|
||||
@@ -31,8 +39,10 @@ angular.module('dockerui.services', ['ngResource'])
|
||||
url: Settings.url + '/commit',
|
||||
params: {
|
||||
'container': params.id,
|
||||
'repo': params.repo
|
||||
}
|
||||
'tag': params.tag || null,
|
||||
'repo': params.repo || null
|
||||
},
|
||||
data: params.config
|
||||
}).success(callback).error(function (data, status, headers, config) {
|
||||
console.log(error, data);
|
||||
});
|
||||
@@ -82,20 +92,34 @@ angular.module('dockerui.services', ['ngResource'])
|
||||
get: {method: 'GET', params: {action: 'json'}},
|
||||
search: {method: 'GET', params: {action: 'search'}},
|
||||
history: {method: 'GET', params: {action: 'history'}, isArray: true},
|
||||
create: {
|
||||
method: 'POST', isArray: true, transformResponse: [function f(data) {
|
||||
var str = data.replace(/\n/g, " ").replace(/\}\W*\{/g, "}, {");
|
||||
return angular.fromJson("[" + str + "]");
|
||||
}],
|
||||
params: {action: 'create', fromImage: '@fromImage', repo: '@repo', tag: '@tag', registry: '@registry'}
|
||||
},
|
||||
insert: {method: 'POST', params: {id: '@id', action: 'insert'}},
|
||||
push: {method: 'POST', params: {id: '@id', action: 'push'}},
|
||||
tag: {method: 'POST', params: {id: '@id', action: 'tag', force: 0, repo: '@repo', tag: '@tag'}},
|
||||
remove: {method: 'DELETE', params: {id: '@id'}, isArray: true}
|
||||
inspect: {method: 'GET', params: {id: '@id', action: 'json'}},
|
||||
push: {
|
||||
method: 'POST', params: {action: 'push', id: '@tag'},
|
||||
isArray: true, transformResponse: jsonObjectsToArrayHandler
|
||||
},
|
||||
create: {
|
||||
method: 'POST', params: {action: 'create', fromImage: '@fromImage', tag: '@tag'},
|
||||
isArray: true, transformResponse: jsonObjectsToArrayHandler
|
||||
},
|
||||
remove: {
|
||||
method: 'DELETE', params: {id: '@id'},
|
||||
isArray: true, transformResponse: deleteImageHandler
|
||||
}
|
||||
});
|
||||
}])
|
||||
.factory('Docker', ['$resource', 'Settings', function DockerFactory($resource, Settings) {
|
||||
.factory('Events', ['$resource', 'Settings', function EventFactory($resource, Settings) {
|
||||
'use strict';
|
||||
// http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#/monitor-docker-s-events
|
||||
return $resource(Settings.url + '/events', {}, {
|
||||
query: {
|
||||
method: 'GET', params: {since: '@since', until: '@until'},
|
||||
isArray: true, transformResponse: jsonObjectsToArrayHandler
|
||||
}
|
||||
});
|
||||
}])
|
||||
.factory('Version', ['$resource', 'Settings', function VersionFactory($resource, Settings) {
|
||||
'use strict';
|
||||
// http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#show-the-docker-version-information
|
||||
return $resource(Settings.url + '/version', {}, {
|
||||
@@ -110,50 +134,60 @@ angular.module('dockerui.services', ['ngResource'])
|
||||
update: {method: 'POST'}
|
||||
});
|
||||
}])
|
||||
.factory('System', ['$resource', 'Settings', function SystemFactory($resource, Settings) {
|
||||
.factory('Info', ['$resource', 'Settings', function InfoFactory($resource, Settings) {
|
||||
'use strict';
|
||||
// http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#display-system-wide-information
|
||||
return $resource(Settings.url + '/info', {}, {
|
||||
get: {method: 'GET'}
|
||||
});
|
||||
}])
|
||||
.factory('Settings', ['DOCKER_ENDPOINT', 'DOCKER_PORT', 'DOCKER_API_VERSION', 'UI_VERSION', function SettingsFactory(DOCKER_ENDPOINT, DOCKER_PORT, DOCKER_API_VERSION, UI_VERSION) {
|
||||
.factory('Network', ['$resource', 'Settings', function NetworkFactory($resource, Settings) {
|
||||
'use strict';
|
||||
// http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#2-5-networks
|
||||
return $resource(Settings.url + '/networks/:id/:action', {id: '@id'}, {
|
||||
query: {method: 'GET', isArray: true},
|
||||
get: {method: 'GET'},
|
||||
create: {method: 'POST', params: {action: 'create'}},
|
||||
remove: {method: 'DELETE'},
|
||||
connect: {method: 'POST', params: {action: 'connect'}},
|
||||
disconnect: {method: 'POST', params: {action: 'disconnect'}}
|
||||
});
|
||||
}])
|
||||
.factory('Volume', ['$resource', 'Settings', function VolumeFactory($resource, Settings) {
|
||||
'use strict';
|
||||
// http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#2-5-networks
|
||||
return $resource(Settings.url + '/volumes/:name/:action', {name: '@name'}, {
|
||||
query: {method: 'GET'},
|
||||
get: {method: 'GET'},
|
||||
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('Settings', ['DOCKER_ENDPOINT', 'DOCKER_PORT', 'UI_VERSION', function SettingsFactory(DOCKER_ENDPOINT, DOCKER_PORT, UI_VERSION) {
|
||||
'use strict';
|
||||
var url = DOCKER_ENDPOINT;
|
||||
if (DOCKER_PORT) {
|
||||
url = url + DOCKER_PORT + '\\' + DOCKER_PORT;
|
||||
}
|
||||
var firstLoad = (localStorage.getItem('firstLoad') || 'true') === 'true';
|
||||
return {
|
||||
displayAll: false,
|
||||
endpoint: DOCKER_ENDPOINT,
|
||||
version: DOCKER_API_VERSION,
|
||||
rawUrl: DOCKER_ENDPOINT + DOCKER_PORT + '/' + DOCKER_API_VERSION,
|
||||
uiVersion: UI_VERSION,
|
||||
url: url,
|
||||
firstLoad: true
|
||||
displayAll: true,
|
||||
endpoint: DOCKER_ENDPOINT,
|
||||
uiVersion: UI_VERSION,
|
||||
url: url,
|
||||
firstLoad: firstLoad
|
||||
};
|
||||
}])
|
||||
.factory('ViewSpinner', function ViewSpinnerFactory() {
|
||||
'use strict';
|
||||
var spinner = new Spinner();
|
||||
var target = document.getElementById('view');
|
||||
|
||||
return {
|
||||
spin: function () {
|
||||
spinner.spin(target);
|
||||
},
|
||||
stop: function () {
|
||||
spinner.stop();
|
||||
}
|
||||
};
|
||||
})
|
||||
.factory('Messages', ['$rootScope', function MessagesFactory($rootScope) {
|
||||
.factory('Messages', ['$rootScope', '$sanitize', function MessagesFactory($rootScope, $sanitize) {
|
||||
'use strict';
|
||||
return {
|
||||
send: function (title, text) {
|
||||
$.gritter.add({
|
||||
title: title,
|
||||
text: text,
|
||||
title: $sanitize(title),
|
||||
text: $sanitize(text),
|
||||
time: 2000,
|
||||
before_open: function () {
|
||||
if ($('.gritter-item-wrapper').length === 3) {
|
||||
@@ -164,8 +198,8 @@ angular.module('dockerui.services', ['ngResource'])
|
||||
},
|
||||
error: function (title, text) {
|
||||
$.gritter.add({
|
||||
title: title,
|
||||
text: text,
|
||||
title: $sanitize(title),
|
||||
text: $sanitize(text),
|
||||
time: 10000,
|
||||
before_open: function () {
|
||||
if ($('.gritter-item-wrapper').length === 4) {
|
||||
@@ -176,23 +210,6 @@ angular.module('dockerui.services', ['ngResource'])
|
||||
}
|
||||
};
|
||||
}])
|
||||
.factory('Dockerfile', ['Settings', function DockerfileFactory(Settings) {
|
||||
'use strict';
|
||||
// http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#build-image-from-a-dockerfile
|
||||
var url = Settings.rawUrl + '/build';
|
||||
return {
|
||||
build: function (file, callback) {
|
||||
var data = new FormData();
|
||||
var dockerfile = new Blob([file], {type: 'text/text'});
|
||||
data.append('Dockerfile', dockerfile);
|
||||
|
||||
var request = new XMLHttpRequest();
|
||||
request.onload = callback;
|
||||
request.open('POST', url);
|
||||
request.send(data);
|
||||
}
|
||||
};
|
||||
}])
|
||||
.factory('LineChart', ['Settings', function LineChartFactory(Settings) {
|
||||
'use strict';
|
||||
return {
|
||||
@@ -222,9 +239,10 @@ angular.module('dockerui.services', ['ngResource'])
|
||||
labels.push(k);
|
||||
data.push(map[k]);
|
||||
if (map[k] > max) {
|
||||
max = map[k];
|
||||
max = map[k];
|
||||
}
|
||||
}
|
||||
var steps = Math.min(max, 10);
|
||||
var dataset = {
|
||||
fillColor: "rgba(151,187,205,0.5)",
|
||||
strokeColor: "rgba(151,187,205,1)",
|
||||
@@ -237,10 +255,11 @@ angular.module('dockerui.services', ['ngResource'])
|
||||
datasets: [dataset]
|
||||
},
|
||||
{
|
||||
scaleStepWidth: 1,
|
||||
scaleStepWidth: Math.ceil(max / steps),
|
||||
pointDotRadius: 1,
|
||||
scaleIntegersOnly: true,
|
||||
scaleOverride: true,
|
||||
scaleSteps: max
|
||||
scaleSteps: steps
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,20 +1,134 @@
|
||||
function ImageViewModel(data) {
|
||||
this.Id = data.Id;
|
||||
this.Tag = data.Tag;
|
||||
this.Repository = data.Repository;
|
||||
this.Created = data.Created;
|
||||
this.Checked = false;
|
||||
this.RepoTags = data.RepoTags;
|
||||
this.VirtualSize = data.VirtualSize;
|
||||
this.Id = data.Id;
|
||||
this.Tag = data.Tag;
|
||||
this.Repository = data.Repository;
|
||||
this.Created = data.Created;
|
||||
this.Checked = false;
|
||||
this.RepoTags = data.RepoTags;
|
||||
this.VirtualSize = data.VirtualSize;
|
||||
}
|
||||
|
||||
function ContainerViewModel(data) {
|
||||
this.Id = data.Id;
|
||||
this.Image = data.Image;
|
||||
this.Command = data.Command;
|
||||
this.Created = data.Created;
|
||||
this.SizeRw = data.SizeRw;
|
||||
this.Status = data.Status;
|
||||
this.Checked = false;
|
||||
this.Names = data.Names;
|
||||
this.Id = data.Id;
|
||||
this.Status = data.Status;
|
||||
this.Names = data.Names;
|
||||
// Unavailable in Docker < 1.10
|
||||
if (data.NetworkSettings) {
|
||||
this.IP = data.NetworkSettings.Networks[Object.keys(data.NetworkSettings.Networks)[0]].IPAddress;
|
||||
}
|
||||
this.Image = data.Image;
|
||||
this.Command = data.Command;
|
||||
this.Checked = false;
|
||||
}
|
||||
|
||||
function createEventDetails(event) {
|
||||
var eventAttr = event.Actor.Attributes;
|
||||
var details = '';
|
||||
switch (event.Type) {
|
||||
case 'container':
|
||||
switch (event.Action) {
|
||||
case 'stop':
|
||||
details = 'Container ' + eventAttr.name + ' stopped';
|
||||
break;
|
||||
case 'destroy':
|
||||
details = 'Container ' + eventAttr.name + ' deleted';
|
||||
break;
|
||||
case 'create':
|
||||
details = 'Container ' + eventAttr.name + ' created';
|
||||
break;
|
||||
case 'start':
|
||||
details = 'Container ' + eventAttr.name + ' started';
|
||||
break;
|
||||
case 'kill':
|
||||
details = 'Container ' + eventAttr.name + ' killed';
|
||||
break;
|
||||
case 'die':
|
||||
details = 'Container ' + eventAttr.name + ' exited with status code ' + eventAttr.exitCode;
|
||||
break;
|
||||
case 'commit':
|
||||
details = 'Container ' + eventAttr.name + ' committed';
|
||||
break;
|
||||
case 'restart':
|
||||
details = 'Container ' + eventAttr.name + ' restarted';
|
||||
break;
|
||||
case 'pause':
|
||||
details = 'Container ' + eventAttr.name + ' paused';
|
||||
break;
|
||||
case 'unpause':
|
||||
details = 'Container ' + eventAttr.name + ' unpaused';
|
||||
break;
|
||||
default:
|
||||
if (event.Action.indexOf('exec_create') === 0) {
|
||||
details = 'Exec instance created';
|
||||
} else if (event.Action.indexOf('exec_start') === 0) {
|
||||
details = 'Exec instance started';
|
||||
} else {
|
||||
details = 'Unsupported event';
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'image':
|
||||
switch (event.Action) {
|
||||
case 'delete':
|
||||
details = 'Image deleted';
|
||||
break;
|
||||
case 'tag':
|
||||
details = 'New tag created for ' + eventAttr.name;
|
||||
break;
|
||||
case 'untag':
|
||||
details = 'Image untagged';
|
||||
break;
|
||||
case 'pull':
|
||||
details = 'Image ' + event.Actor.ID + ' pulled';
|
||||
break;
|
||||
default:
|
||||
details = 'Unsupported event';
|
||||
}
|
||||
break;
|
||||
case 'network':
|
||||
switch (event.Action) {
|
||||
case 'create':
|
||||
details = 'Network ' + eventAttr.name + ' created';
|
||||
break;
|
||||
case 'destroy':
|
||||
details = 'Network ' + eventAttr.name + ' deleted';
|
||||
break;
|
||||
case 'connect':
|
||||
details = 'Container connected to ' + eventAttr.name + ' network';
|
||||
break;
|
||||
case 'disconnect':
|
||||
details = 'Container disconnected from ' + eventAttr.name + ' network';
|
||||
break;
|
||||
default:
|
||||
details = 'Unsupported event';
|
||||
}
|
||||
break;
|
||||
case 'volume':
|
||||
switch (event.Action) {
|
||||
case 'create':
|
||||
details = 'Volume ' + event.Actor.ID + ' created';
|
||||
break;
|
||||
case 'destroy':
|
||||
details = 'Volume ' + event.Actor.ID + ' deleted';
|
||||
break;
|
||||
default:
|
||||
details = 'Unsupported event';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
details = 'Unsupported event';
|
||||
}
|
||||
return details;
|
||||
}
|
||||
|
||||
function EventViewModel(data) {
|
||||
// Type, Action, Actor unavailable in Docker < 1.10
|
||||
this.Time = data.time;
|
||||
if (data.Type) {
|
||||
this.Type = data.Type;
|
||||
this.Details = createEventDetails(data);
|
||||
} else {
|
||||
this.Type = data.status;
|
||||
this.Details = data.from;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,3 +114,91 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: inline;
|
||||
width: 100%;
|
||||
max-width: 155px;
|
||||
height: 100%;
|
||||
max-height: 55px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.containerNameInput {
|
||||
width: 85%;
|
||||
border:none;
|
||||
background:none;
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
.containerNameInput:active, .containerNameInput:focus {
|
||||
outline:none;
|
||||
}
|
||||
|
||||
#network-legend {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#network-legend span {
|
||||
display: inline;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.header_title_content {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.form-horizontal .control-label.text-left{
|
||||
text-align: left;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
margin-top: 1px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-top: 1px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.text-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.fa.green-icon {
|
||||
color: #23ae89;
|
||||
}
|
||||
|
||||
.fa.red-icon {
|
||||
color: #ae2323;
|
||||
}
|
||||
|
||||
.fa.white-icon {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.image-tag {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.label.tag {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.widget .widget-body table tbody .image-tag {
|
||||
font-size: 90% !important;
|
||||
}
|
||||
|
||||
.terminal-container {
|
||||
width: 100%;
|
||||
padding: 10px 5px;
|
||||
}
|
||||
|
||||
.interactive {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 180 KiB |
@@ -1,13 +1,15 @@
|
||||
{
|
||||
"name": "dockerui",
|
||||
"version": "0.8.0",
|
||||
"homepage": "https://github.com/crosbymichael/dockerui",
|
||||
"name": "uifordocker",
|
||||
"version": "1.7.0",
|
||||
"homepage": "https://github.com/kevana/ui-for-docker",
|
||||
"authors": [
|
||||
"Michael Crosby <crosbymichael@gmail.com>",
|
||||
"Kevan Ahlquist <ahlqu039@umn.edu>"
|
||||
"Kevan Ahlquist <ahlquistkd@gmail.com>",
|
||||
"Anthony Lapenna <anthony.lapenna@cloudinovasi.id>"
|
||||
],
|
||||
"description": "A web interface for the Docker Remote API.",
|
||||
"keywords": [
|
||||
"uifordocker",
|
||||
"dockerui",
|
||||
"docker",
|
||||
"api"
|
||||
@@ -22,19 +24,24 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"Chart.js": "1.0.2",
|
||||
"angular": "1.3.15",
|
||||
"angular-bootstrap": "0.12.0",
|
||||
"angular-mocks": "1.3.15",
|
||||
"angular-oboe": "*",
|
||||
"angular-resource": "1.3.15",
|
||||
"angular-route": "1.3.15",
|
||||
"angular-visjs": "0.0.7",
|
||||
"bootstrap": "3.3.0",
|
||||
"angular": "~1.5.0",
|
||||
"angular-cookies": "~1.5.0",
|
||||
"angular-bootstrap": "~1.0.3",
|
||||
"angular-ui-router": "^0.2.15",
|
||||
"angular-sanitize": "~1.5.0",
|
||||
"angular-mocks": "~1.5.0",
|
||||
"angular-resource": "~1.5.0",
|
||||
"angular-ui-select": "~0.17.1",
|
||||
"bootstrap": "~3.3.6",
|
||||
"font-awesome": "~4.6.3",
|
||||
"jquery": "1.11.1",
|
||||
"jquery.gritter": "1.7.4",
|
||||
"spin.js": "1.3"
|
||||
"lodash": "4.12.0",
|
||||
"rdash-ui": "1.0.*",
|
||||
"moment": "~2.14.1",
|
||||
"xterm.js": "~1.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"angular": "1.3.15"
|
||||
"angular": "1.5.5"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 55 KiB |
@@ -1,100 +0,0 @@
|
||||
package main // import "github.com/crosbymichael/dockerui"
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
endpoint = flag.String("e", "/var/run/docker.sock", "Dockerd endpoint")
|
||||
addr = flag.String("p", ":9000", "Address and port to serve dockerui")
|
||||
assets = flag.String("a", ".", "Path to the assets")
|
||||
)
|
||||
|
||||
type UnixHandler struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func (h *UnixHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := net.Dial("unix", h.path)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
c := httputil.NewClientConn(conn, nil)
|
||||
defer c.Close()
|
||||
|
||||
res, err := c.Do(r)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
copyHeader(w.Header(), res.Header)
|
||||
if _, err := io.Copy(w, res.Body); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func copyHeader(dst, src http.Header) {
|
||||
for k, vv := range src {
|
||||
for _, v := range vv {
|
||||
dst.Add(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createTcpHandler(e string) http.Handler {
|
||||
u, err := url.Parse(e)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return httputil.NewSingleHostReverseProxy(u)
|
||||
}
|
||||
|
||||
func createUnixHandler(e string) http.Handler {
|
||||
return &UnixHandler{e}
|
||||
}
|
||||
|
||||
func createHandler(dir string, e string) http.Handler {
|
||||
var (
|
||||
mux = http.NewServeMux()
|
||||
fileHandler = http.FileServer(http.Dir(dir))
|
||||
h http.Handler
|
||||
)
|
||||
|
||||
if strings.Contains(e, "http") {
|
||||
h = createTcpHandler(e)
|
||||
} else {
|
||||
if _, err := os.Stat(e); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
log.Fatalf("unix socket %s does not exist", e)
|
||||
}
|
||||
log.Fatal(err)
|
||||
}
|
||||
h = createUnixHandler(e)
|
||||
}
|
||||
|
||||
mux.Handle("/dockerapi/", http.StripPrefix("/dockerapi", h))
|
||||
mux.Handle("/", fileHandler)
|
||||
return mux
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
handler := createHandler(*assets, *endpoint)
|
||||
if err := http.ListenAndServe(*addr, handler); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
FROM nginx:latest
|
||||
|
||||
COPY default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY users.htpasswd /etc/nginx/users.htpasswd
|
||||
@@ -0,0 +1,17 @@
|
||||
upstream dockerui {
|
||||
server dockerui:9000;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
auth_basic "Docker UI";
|
||||
auth_basic_user_file /etc/nginx/users.htpasswd;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_pass http://dockerui;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
dockerui:
|
||||
image: cloudinovasi/ui-for-docker
|
||||
command: -e http://<SWARM_HOST>:<SWARM_PORT>
|
||||
|
||||
nginx:
|
||||
build: .
|
||||
links:
|
||||
- dockerui
|
||||
ports:
|
||||
- 80:80
|
||||
@@ -0,0 +1 @@
|
||||
user:{PLAIN}password
|
||||
@@ -14,11 +14,34 @@ module.exports = function (grunt) {
|
||||
|
||||
// Default task.
|
||||
grunt.registerTask('default', ['jshint', 'build', 'karma:unit']);
|
||||
grunt.registerTask('build', ['clean:app', 'if:binaryNotExist', 'html2js', 'concat', 'clean:tmpl', 'recess:build', 'copy']);
|
||||
grunt.registerTask('release', ['clean:all', 'if:binaryNotExist', 'html2js', 'uglify', 'clean:tmpl', 'jshint', 'karma:unit', 'concat:index', 'recess:min', 'copy']);
|
||||
grunt.registerTask('build', [
|
||||
'clean:app',
|
||||
'if:binaryNotExist',
|
||||
'html2js',
|
||||
'concat',
|
||||
'clean:tmpl',
|
||||
'recess:build',
|
||||
'copy'
|
||||
]);
|
||||
grunt.registerTask('release', [
|
||||
'clean:all',
|
||||
'if:binaryNotExist',
|
||||
'html2js',
|
||||
'uglify',
|
||||
'clean:tmpl',
|
||||
'jshint',
|
||||
//'karma:unit',
|
||||
'concat:index',
|
||||
'recess:min',
|
||||
'copy'
|
||||
]);
|
||||
grunt.registerTask('lint', ['jshint']);
|
||||
grunt.registerTask('test-watch', ['karma:watch']);
|
||||
grunt.registerTask('run', ['if:binaryNotExist', 'build', 'shell:buildImage', 'shell:run']);
|
||||
grunt.registerTask('run-swarm', ['if:binaryNotExist', 'build', 'shell:buildImage', 'shell:runSwarm', 'watch:buildSwarm']);
|
||||
grunt.registerTask('run-dev', ['if:binaryNotExist', 'shell:buildImage', 'shell:run', 'watch:build']);
|
||||
grunt.registerTask('run-ssl', ['if:binaryNotExist', 'shell:buildImage', 'shell:runSsl', 'watch:buildSsl']);
|
||||
grunt.registerTask('clear', ['clean:app']);
|
||||
|
||||
// Print a timestamp (useful for when watching)
|
||||
grunt.registerTask('timestamp', function () {
|
||||
@@ -44,13 +67,13 @@ module.exports = function (grunt) {
|
||||
js: ['app/**/*.js', '!app/**/*.spec.js'],
|
||||
jsTpl: ['<%= distdir %>/templates/**/*.js'],
|
||||
jsVendor: [
|
||||
'bower_components/jquery/dist/jquery.js',
|
||||
'bower_components/jquery/dist/jquery.min.js',
|
||||
'bower_components/bootstrap/dist/js/bootstrap.min.js',
|
||||
'bower_components/Chart.js/Chart.min.js',
|
||||
'bower_components/lodash/dist/lodash.min.js',
|
||||
'bower_components/moment/min/moment.min.js',
|
||||
'bower_components/xterm.js/src/xterm.js',
|
||||
'assets/js/jquery.gritter.js', // Using custom version to fix error in minified build due to "use strict"
|
||||
'bower_components/bootstrap/dist/js/bootstrap.js',
|
||||
'bower_components/spin.js/spin.js',
|
||||
'bower_components/vis/dist/vis.js',
|
||||
'bower_components/Chart.js/Chart.js',
|
||||
'bower_components/oboe/dist/oboe-browser.js',
|
||||
'assets/js/legend.js' // Not a bower package
|
||||
],
|
||||
specs: ['test/**/*.spec.js'],
|
||||
@@ -61,18 +84,23 @@ module.exports = function (grunt) {
|
||||
cssVendor: [
|
||||
'bower_components/bootstrap/dist/css/bootstrap.css',
|
||||
'bower_components/jquery.gritter/css/jquery.gritter.css',
|
||||
'bower_components/vis/dist/vis.css'
|
||||
'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'
|
||||
]
|
||||
},
|
||||
clean: {
|
||||
all: ['<%= distdir %>/*'],
|
||||
app: ['<%= distdir %>/*', '!<%= distdir %>/dockerui'],
|
||||
app: ['<%= distdir %>/*', '!<%= distdir %>/ui-for-docker'],
|
||||
tmpl: ['<%= distdir %>/templates']
|
||||
},
|
||||
copy: {
|
||||
assets: {
|
||||
files: [
|
||||
{dest: '<%= distdir %>/fonts/', src: '**', expand: true, cwd: 'bower_components/bootstrap/fonts/'},
|
||||
{dest: '<%= distdir %>/fonts/', src: '*.{ttf,woff,woff2,eof,svg}', expand: true, cwd: 'bower_components/bootstrap/fonts/'},
|
||||
{dest: '<%= distdir %>/fonts/', src: '*.{ttf,woff,woff2,eof,svg}', expand: true, cwd: 'bower_components/font-awesome/fonts/'},
|
||||
{dest: '<%= distdir %>/fonts/', src: '*.{ttf,woff,woff2,eof,svg}', expand: true, cwd: 'bower_components/rdash-ui/dist/fonts/'},
|
||||
{
|
||||
dest: '<%= distdir %>/images/',
|
||||
src: ['**', '!trees.jpg'],
|
||||
@@ -80,18 +108,10 @@ module.exports = function (grunt) {
|
||||
cwd: 'bower_components/jquery.gritter/images/'
|
||||
},
|
||||
{
|
||||
dest: '<%= distdir %>/img',
|
||||
src: [
|
||||
'network/downArrow.png',
|
||||
'network/leftArrow.png',
|
||||
'network/upArrow.png',
|
||||
'network/rightArrow.png',
|
||||
'network/minus.png',
|
||||
'network/plus.png',
|
||||
'network/zoomExtends.png'
|
||||
],
|
||||
dest: '<%= distdir %>/images/',
|
||||
src: ['**'],
|
||||
expand: true,
|
||||
cwd: 'bower_components/vis/dist/img'
|
||||
cwd: 'assets/images/'
|
||||
},
|
||||
{dest: '<%= distdir %>/ico', src: '**', expand: true, cwd: 'assets/ico'}
|
||||
]
|
||||
@@ -118,11 +138,11 @@ module.exports = function (grunt) {
|
||||
process: true
|
||||
},
|
||||
src: ['<%= src.js %>', '<%= src.jsTpl %>'],
|
||||
dest: '<%= distdir %>/<%= pkg.name %>.js'
|
||||
dest: '<%= distdir %>/js/<%= pkg.name %>.js'
|
||||
},
|
||||
vendor: {
|
||||
src: ['<%= src.jsVendor %>'],
|
||||
dest: '<%= distdir %>/vendor.js'
|
||||
dest: '<%= distdir %>/js/vendor.js'
|
||||
},
|
||||
index: {
|
||||
src: ['index.html'],
|
||||
@@ -132,13 +152,14 @@ module.exports = function (grunt) {
|
||||
}
|
||||
},
|
||||
angular: {
|
||||
src: ['bower_components/angular/angular.js',
|
||||
'bower_components/angular-route/angular-route.js',
|
||||
'bower_components/angular-resource/angular-resource.js',
|
||||
'bower_components/angular-bootstrap/ui-bootstrap-tpls.js',
|
||||
'bower_components/angular-oboe/dist/angular-oboe.js',
|
||||
'bower_components/angular-visjs/angular-vis.js'],
|
||||
dest: '<%= distdir %>/angular.js'
|
||||
src: ['bower_components/angular/angular.min.js',
|
||||
'bower_components/angular-sanitize/angular-sanitize.min.js',
|
||||
'bower_components/angular-cookies/angular-cookies.min.js',
|
||||
'bower_components/angular-ui-router/release/angular-ui-router.min.js',
|
||||
'bower_components/angular-resource/angular-resource.min.js',
|
||||
'bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js',
|
||||
'bower_components/angular-ui-select/dist/select.min.js'],
|
||||
dest: '<%= distdir %>/js/angular.js'
|
||||
}
|
||||
},
|
||||
uglify: {
|
||||
@@ -147,28 +168,28 @@ module.exports = function (grunt) {
|
||||
banner: "<%= banner %>"
|
||||
},
|
||||
src: ['<%= src.js %>', '<%= src.jsTpl %>'],
|
||||
dest: '<%= distdir %>/<%= pkg.name %>.js'
|
||||
dest: '<%= distdir %>/js/<%= pkg.name %>.js'
|
||||
},
|
||||
vendor: {
|
||||
options: {
|
||||
preserveComments: 'some' // Preserve license comments
|
||||
},
|
||||
src: ['<%= src.jsVendor %>'],
|
||||
dest: '<%= distdir %>/vendor.js'
|
||||
dest: '<%= distdir %>/js/vendor.js'
|
||||
},
|
||||
angular: {
|
||||
options: {
|
||||
preserveComments: 'some' // Preserve license comments
|
||||
},
|
||||
src: ['<%= concat.angular.src %>'],
|
||||
dest: '<%= distdir %>/angular.js'
|
||||
dest: '<%= distdir %>/js/angular.js'
|
||||
}
|
||||
},
|
||||
recess: { // TODO: not maintained, unable to preserve license comments, switch out for something better.
|
||||
build: {
|
||||
files: {
|
||||
'<%= distdir %>/<%= pkg.name %>.css': ['<%= src.css %>'],
|
||||
'<%= distdir %>/vendor.css': ['<%= src.cssVendor %>']
|
||||
'<%= distdir %>/css/<%= pkg.name %>.css': ['<%= src.css %>'],
|
||||
'<%= distdir %>/css/vendor.css': ['<%= src.cssVendor %>']
|
||||
},
|
||||
options: {
|
||||
compile: true,
|
||||
@@ -177,8 +198,8 @@ module.exports = function (grunt) {
|
||||
},
|
||||
min: {
|
||||
files: {
|
||||
'<%= distdir %>/<%= pkg.name %>.css': ['<%= src.css %>'],
|
||||
'<%= distdir %>/vendor.css': ['<%= src.cssVendor %>']
|
||||
'<%= distdir %>/css/<%= pkg.name %>.css': ['<%= src.css %>'],
|
||||
'<%= distdir %>/css/vendor.css': ['<%= src.cssVendor %>']
|
||||
},
|
||||
options: {
|
||||
compile: true,
|
||||
@@ -201,6 +222,14 @@ module.exports = function (grunt) {
|
||||
* Tried using a host volume with -v, copying files with `docker cp`, restating container, none worked
|
||||
* Rebuilding image on each change was only method that worked, takes ~4s per change to update
|
||||
*/
|
||||
},
|
||||
buildSwarm: {
|
||||
files: ['<%= src.js %>', '<%= src.specs %>', '<%= src.css %>', '<%= src.tpl %>', '<%= src.html %>'],
|
||||
tasks: ['build', 'shell:buildImage', 'shell:runSwarm', 'shell:cleanImages']
|
||||
},
|
||||
buildSsl: {
|
||||
files: ['<%= src.js %>', '<%= src.specs %>', '<%= src.css %>', '<%= src.tpl %>', '<%= src.html %>'],
|
||||
tasks: ['build', 'shell:buildImage', 'shell:runSsl', 'shell:cleanImages']
|
||||
}
|
||||
},
|
||||
jshint: {
|
||||
@@ -223,21 +252,35 @@ module.exports = function (grunt) {
|
||||
},
|
||||
shell: {
|
||||
buildImage: {
|
||||
command: 'docker build --rm -t dockerui .'
|
||||
command: 'docker build --rm -t ui-for-docker .'
|
||||
},
|
||||
buildBinary: {
|
||||
command: [
|
||||
'docker run --rm -v $(pwd):/src centurylink/golang-builder',
|
||||
'shasum dockerui > dockerui-checksum.txt',
|
||||
'docker run --rm -v $(pwd)/api:/src centurylink/golang-builder',
|
||||
'shasum api/ui-for-docker > ui-for-docker-checksum.txt',
|
||||
'mkdir -p dist',
|
||||
'mv dockerui dist/'
|
||||
].join('&&')
|
||||
'mv api/ui-for-docker dist/'
|
||||
].join(' && ')
|
||||
},
|
||||
run: {
|
||||
command: [
|
||||
'docker stop dockerui',
|
||||
'docker rm dockerui',
|
||||
'docker run --privileged -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock --name dockerui dockerui'
|
||||
'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 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 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: {
|
||||
@@ -247,7 +290,7 @@ module.exports = function (grunt) {
|
||||
'if': {
|
||||
binaryNotExist: {
|
||||
options: {
|
||||
executable: 'dist/dockerui'
|
||||
executable: 'dist/ui-for-docker'
|
||||
},
|
||||
ifFalse: ['shell:buildBinary']
|
||||
}
|
||||
|
||||