Compare commits

..

137 Commits
1.3.0 ... 1.7.0

Author SHA1 Message Date
Anthony Lapenna
cbbcb51162 Merge branch 'release/1.7.0' 2016-08-18 15:47:37 +12:00
Anthony Lapenna
954a6a11b7 chore(version): bump version number 2016-08-18 15:47:18 +12:00
Anthony Lapenna
0c5e98b47d Updated README. 2016-08-17 21:55:30 +12:00
Anthony Lapenna
d941fef8d6 docs(logo): add documentation about the --logo flag (#141) 2016-08-17 21:52:49 +12:00
Anthony Lapenna
496de850c1 fix(container-creation): allow to specify an address in the host port binding (#139) 2016-08-17 19:31:06 +12:00
Anthony Lapenna
29fa33fb2b fix(container-creation): fix unregistered ports bindings when creating a container (#138) 2016-08-17 19:03:36 +12:00
Anthony Lapenna
06fbb5ba34 Merge branch 'develop' of github.com:cloud-inovasi/cloudinovasi-ui into develop 2016-08-17 19:01:28 +12:00
Anthony Lapenna
a32f6f343d refactor(container-creation): remove changes related to internal version (#137) 2016-08-17 18:43:53 +12:00
Anthony Lapenna
61d7b4f64c refactor(container-creation): remove changes related to internal version 2016-08-17 18:42:56 +12:00
Anthony Lapenna
1840ab4bba docs(reverse-proxy): add reverse proxy instructions (#136) 2016-08-17 18:31:23 +12:00
Anthony Lapenna
ccb812cc33 feat(console): add protocol awareness to switch between ws:// and wss:// (#135) 2016-08-17 18:23:30 +12:00
Anthony Lapenna
b098cd5638 feat(image): add the ability to tag an image (#134) 2016-08-17 18:05:17 +12:00
Anthony Lapenna
eefa7ca138 refactor(global): revert merge with internal (#133) 2016-08-17 17:25:42 +12:00
Anthony Lapenna
b5dcdc8807 refactor(api): remove the binary from versioning (#128) 2016-08-17 13:57:51 +12:00
Anthony Lapenna
4b4e5d5ebd Merge branch 'develop' of github.com:cloud-inovasi/cloudinovasi-ui into develop 2016-08-17 13:52:52 +12:00
Anthony Lapenna
54fd9561f0 refactor(handlers): remove duplicated code (#127) 2016-08-17 13:50:55 +12:00
Anthony Lapenna
fb67769928 Merge branch 'develop' of github.com:cloud-inovasi/cloudinovasi-ui into develop 2016-08-17 13:49:17 +12:00
Anthony Lapenna
9c8e632a09 merge branch 'feat107-push-registry' into internal 2016-08-17 12:32:09 +12:00
Anthony Lapenna
0b6c2b032a feat(image): add the ability to push an image tag (#126) 2016-08-17 12:29:13 +12:00
Anthony Lapenna
f0e4cdc13e feat(image): add the ability to push an image tag 2016-08-17 12:27:54 +12:00
Anthony Lapenna
cfe31fbeac merge branch feat106-external-logo into internal 2016-08-10 18:40:05 +12:00
Anthony Lapenna
722dc0b3af feat(global): add the --logo flag to specify an external logo picture (#120) 2016-08-10 18:38:33 +12:00
Anthony Lapenna
de3353feba feat(global): add the --logo flag to specify an external logo picture 2016-08-10 18:37:25 +12:00
Anthony Lapenna
145e45b4a8 feat(ui): network creation support for standalone engine (#119) 2016-08-10 18:20:18 +12:00
Anthony Lapenna
d0f57809d6 merge branch 'feat57-swarm-host-ip' into internal 2016-08-10 18:13:39 +12:00
Anthony Lapenna
6c29377992 feat(ui): display swarm host IP instead of swarm hostname in containers view (#118) 2016-08-10 18:12:33 +12:00
Anthony Lapenna
164902c0cb feat(ui): display swarm host IP instead of swarm hostname in containers view 2016-08-10 18:11:36 +12:00
Anthony Lapenna
75466cb57f merge branch 'feat96-date-format-iso8601' into internal 2016-08-10 16:06:35 +12:00
Anthony Lapenna
0e8fff7a51 feat(ui): format all dates to use ISO8601 (#117) 2016-08-10 16:05:28 +12:00
Anthony Lapenna
7b72da857f feat(ui): format all dates to use ISO8601 2016-08-10 16:04:19 +12:00
Anthony Lapenna
b89546a1e0 Merge branch 'fix-108-dashboard-no-volumes' into internal 2016-08-10 15:41:34 +12:00
Anthony Lapenna
22122a27b5 fix(dashboard): fix an error when no volumes are availables (#116) 2016-08-10 15:40:35 +12:00
Anthony Lapenna
24a9e9f61c fix(dashboard): fix an error when no volumes are availables 2016-08-10 15:39:59 +12:00
Anthony Lapenna
cf5378f604 Merge branch 'chore-revert-libraries' into internal 2016-08-10 15:33:15 +12:00
Anthony Lapenna
1d8f51c141 chore(gruntfile): revert commit to use minified libraries (#115) 2016-08-10 15:32:39 +12:00
Anthony Lapenna
87798cd1c8 chore(gruntfile): revert commit to use minified libraries 2016-08-10 15:31:58 +12:00
Anthony Lapenna
fd1496df93 Merge branch 'fix-113-images-view-delete' into internal 2016-08-10 15:27:32 +12:00
Anthony Lapenna
ea6e11000d fix(images): display an error message when unable to remove an image (#114) 2016-08-10 15:27:03 +12:00
Anthony Lapenna
ef257f65cf fix(images): display an error message when unable to remove an image 2016-08-10 15:26:08 +12:00
Anthony Lapenna
01a707c8e7 merge branch 'refactor-image-view' into internal 2016-08-10 15:15:54 +12:00
Anthony Lapenna
9293b28ef4 refactor(ui): updated image view (#112) 2016-08-10 15:14:10 +12:00
Anthony Lapenna
30c0fda1b6 refactor(ui): updated image view 2016-08-10 15:05:33 +12:00
Anthony Lapenna
548a458b9a Merge branch "feat107-remove-select-all" into internal 2016-08-04 11:31:35 +12:00
Anthony Lapenna
111cd4ac64 feat(ui): remove the ability to select all entities from list views (#104) 2016-08-04 11:29:29 +12:00
Anthony Lapenna
54ab81a7de feat(ui): remove the ability to select all entities from list views 2016-08-04 11:17:34 +12:00
Anthony Lapenna
21344280a9 Merge tag '1.6.0' into develop
Release 1.6.0
2016-08-03 21:56:14 +12:00
Anthony Lapenna
d3fa9736f4 Merge branch 'release/1.6.0' 2016-08-03 21:56:09 +12:00
Anthony Lapenna
f1ec419e3a chore(version): bump version number 2016-08-03 21:50:01 +12:00
Anthony Lapenna
de8c6b4ed8 Merge branch 'style-dashboard-widget-icon' into internal 2016-08-03 21:46:33 +12:00
Anthony Lapenna
e661cef2fe style(dashboard): change the icon in the main widget (#102) 2016-08-03 21:46:08 +12:00
Anthony Lapenna
edf485bbe4 style(dashboard): change the icon in the main widget 2016-08-03 21:45:35 +12:00
Anthony Lapenna
20eecffc40 Merge branch "feat99-container-exec-event" into internal 2016-08-03 21:31:32 +12:00
Anthony Lapenna
232b180eef feat(events): add support for container exec related events (#100) 2016-08-03 21:28:27 +12:00
Anthony Lapenna
4a738ee362 feat(events): add support for container exec related events 2016-08-03 21:27:51 +12:00
Anthony Lapenna
f4d90306b3 Merge branch "refactor-backend-settings" into internal 2016-08-03 21:18:46 +12:00
Anthony Lapenna
7801a91149 refactor(global): replace /config endpoint with /settings to avoid confusion (#98) 2016-08-03 21:13:17 +12:00
Anthony Lapenna
19d4e38d94 refactor(global): replace /config endpoint with /settings to avoid confusion 2016-08-03 21:12:46 +12:00
Anthony Lapenna
bab57e0402 Merge branch 'feat34-container-exec' into internal 2016-08-03 15:28:51 +12:00
Anthony Lapenna
d2b3360bff Merge branch 'refactor-backend' into internal 2016-08-03 15:19:22 +12:00
Anthony Lapenna
1aaa5acbef feat(global): add container exec support (#97) 2016-08-03 15:12:53 +12:00
Anthony Lapenna
5878eed7ec feat(global): add container exec support 2016-08-03 15:11:09 +12:00
Anthony Lapenna
b0ebbdf68c refactor(api): create a new structure for the Go api (#94)
* refactor(api): create a new structure for the Go api

* refactor(api): update the way keyFile parameter is managed
2016-08-01 13:40:12 +12:00
Anthony Lapenna
0ec20d3093 refactor(api): update the way keyFile parameter is managed 2016-07-31 17:46:05 +12:00
Anthony Lapenna
c5ddae12cf refactor(api): create a new structure for the Go api 2016-07-29 15:58:11 +12:00
Anthony Lapenna
b1e1850e9f Merge branch 'feat91-swarm-node-info' into internal 2016-07-27 20:00:33 +12:00
Anthony Lapenna
06c2635e82 feat(ui): add more info about nodes in Swarm view (#92)
* feat(ui): add more info about nodes in Swarm view

* style(ui): update title for section in swarm view
2016-07-27 20:00:00 +12:00
Anthony Lapenna
711ac742e1 style(ui): update title for section in swarm view 2016-07-27 19:59:41 +12:00
Anthony Lapenna
201ab20131 feat(ui): add more info about nodes in Swarm view 2016-07-27 19:54:31 +12:00
Anthony Lapenna
716ba72217 Merge branch 'feat52-active-network-remove-error' into internal 2016-07-27 17:38:10 +12:00
Anthony Lapenna
95b16919a6 feat(ui): display an error message when trying to remove a network with active endpoints (#90) 2016-07-27 17:37:35 +12:00
Anthony Lapenna
d3d000a1d0 feat(ui): display an error message when trying to remove a network with active endpoints 2016-07-27 17:36:22 +12:00
Anthony Lapenna
9499f78121 Merge branch 'docs82-image-preview' into internal 2016-07-27 17:17:22 +12:00
Anthony Lapenna
fa36c9ee5c docs(README): update dashboard.png (#89) 2016-07-27 17:17:00 +12:00
Anthony Lapenna
c460eb4d7a docs(README): update dashboard.png 2016-07-27 17:16:25 +12:00
Anthony Lapenna
ab52270238 merge branch refactor80-module-names into internal 2016-07-27 17:13:08 +12:00
Anthony Lapenna
7c6fdebb3d refactor(ui): rename angular modules from dockerui to uifordocker (#88) 2016-07-27 17:11:24 +12:00
Anthony Lapenna
cf3cd76064 refactor(ui): rename angular modules from dockerui to uifordocker 2016-07-27 17:10:25 +12:00
Anthony Lapenna
5ef6b536ac Merge branch 'feat71-events-view' into internal 2016-07-27 17:06:04 +12:00
Anthony Lapenna
adf5184a5d feat(ui): add events view (#86)
* feat(ui): add events view

* chore(grunt): use minified angular script
2016-07-27 17:05:16 +12:00
Anthony Lapenna
ea596a8701 fix(ui): config endpoint is available at config rather than /config (#83) 2016-07-27 17:04:47 +12:00
Anthony Lapenna
15a3cb7241 chore(grunt): use minified angular script 2016-07-27 11:10:42 +12:00
Anthony Lapenna
f147da3017 feat(ui): add events view 2016-07-27 11:08:18 +12:00
Anthony Lapenna
dfaf2eb6a9 merge branch nginx-support into internal 2016-07-21 11:15:32 +12:00
Anthony Lapenna
97f6a32c78 fix(ui): config endpoint is available at config rather than /config 2016-07-21 11:14:29 +12:00
Anthony Lapenna
bcdd7498a1 chore(version): bump version number 2016-07-14 12:18:29 +12:00
Anthony Lapenna
c45947b573 Merge tag '1.5.0' into develop
Release 1.5.0
2016-07-14 12:12:18 +12:00
Anthony Lapenna
85140c7dcf Merge branch 'release/1.5.0' 2016-07-14 12:12:13 +12:00
Anthony Lapenna
30e9a604cd chore(version): bump version number 2016-07-14 12:12:01 +12:00
Anthony Lapenna
8c769148ad refactor(ui): remove console logging 2016-07-14 12:07:37 +12:00
Anthony Lapenna
4cc08d7211 refactor(ui): remove console logging 2016-07-14 12:07:07 +12:00
Anthony Lapenna
48b6b6340b Merge branch 'feat68-update-repository-field' into internal 2016-07-14 12:03:57 +12:00
Anthony Lapenna
b857970236 feat(ui): replace repository field with tags field in image view (#79) 2016-07-14 12:02:42 +12:00
Anthony Lapenna
1011fde9de feat(ui): replace repository field with tags field in image view 2016-07-14 12:01:57 +12:00
Anthony Lapenna
bee89720d5 refactor(ui): fix lint issue 2016-07-14 11:31:58 +12:00
Anthony Lapenna
23bff41304 Merge branch 'develop' of github.com:cloud-inovasi/cloudinovasi-ui into develop 2016-07-14 11:31:10 +12:00
Anthony Lapenna
8243326692 refactor(ui): fix lint issue 2016-07-14 11:29:41 +12:00
Anthony Lapenna
17ae122595 Merge branch 'style73-widget-header-extra-space' into internal 2016-07-14 11:25:01 +12:00
Anthony Lapenna
b69d72fc8c Merge branch 'style72-table-style' into internal 2016-07-14 11:24:54 +12:00
Anthony Lapenna
c52498993b Merge branch 'style70-typo-engine-view' into internal 2016-07-14 11:24:45 +12:00
Anthony Lapenna
a4a82b4502 merge branch feat54-revamp-internal into internal 2016-07-14 11:24:20 +12:00
Anthony Lapenna
52d953a1c2 style(ui): fix typo in Engine view (#76) 2016-07-14 11:16:23 +12:00
Anthony Lapenna
e145d82947 style(ui): fix extra space in widget-header (#78) 2016-07-14 11:16:16 +12:00
Anthony Lapenna
25df1fe26c style(ui): add table-hover class to all entity tables (#77) 2016-07-14 11:16:10 +12:00
Anthony Lapenna
106718f416 style(ui): fix extra space in widget-header 2016-07-14 11:10:38 +12:00
Anthony Lapenna
8464faa2a1 style(ui): add table-hover class to all entity tables 2016-07-14 11:03:40 +12:00
Anthony Lapenna
9354b911bb style(ui): fix typo in Engine view 2016-07-14 11:00:26 +12:00
Anthony Lapenna
c8a5b82c89 feat(ui): new dashboard view (#75) 2016-07-14 10:58:39 +12:00
Anthony Lapenna
1f884e9584 chore(version): bumped version number 2016-07-13 15:06:10 +12:00
Anthony Lapenna
00b2c92e39 Merge branch 'develop' of github.com:cloud-inovasi/cloudinovasi-ui into develop 2016-07-13 14:54:35 +12:00
Anthony Lapenna
0796778d17 Merge tag '1.4.0' into develop
Release 1.4.0
2016-07-13 14:54:07 +12:00
Anthony Lapenna
1eae1c03f0 Merge branch 'release/1.4.0' 2016-07-13 14:54:03 +12:00
Anthony Lapenna
a9209da167 chore(version): bump version number 2016-07-13 14:53:24 +12:00
Anthony Lapenna
43c2f14289 docs(docker): add info about Docker version support (#64) 2016-07-13 14:47:24 +12:00
Anthony Lapenna
f378d56543 fix(ui): fix bad name for image field in container creation view 2016-07-13 13:47:15 +12:00
Anthony Lapenna
521d146d7b refactor(ui): remove useless createNetwork sources 2016-07-13 13:46:06 +12:00
Anthony Lapenna
dc721f5870 merge feat66-docker-cli-compliant 2016-07-13 13:43:58 +12:00
Anthony Lapenna
3b0d726c2a feat(dockerui): Docker CLI compliant flags (#67) 2016-07-13 12:44:31 +12:00
Anthony Lapenna
f6226d19b8 feat(dockerui): Docker CLI compliant flags 2016-07-13 12:42:20 +12:00
Anthony Lapenna
71c091ae0d feat(ui): docker 1.9 support (#65) 2016-07-13 10:53:03 +12:00
Anthony Lapenna
1fb008212a feat(dockerui): add support for TLS enabled engines (#63) 2016-07-12 20:31:11 +12:00
Anthony Lapenna
cab34e4069 feat(network): add advanced settings in network creation (subnet/gateway) 2016-07-08 17:26:18 +12:00
Anthony Lapenna
e67e20ce18 feat(network): add the ability to specify a subnet/gateway when creating a network (#53) 2016-07-08 17:12:33 +12:00
Anthony Lapenna
d253c0d494 chore(version): bump version number 2016-07-08 16:26:29 +12:00
Anthony Lapenna
c74e8fc732 style(lint): fix jshint issue 2016-07-08 16:20:31 +12:00
Anthony Lapenna
29358e5744 Merge tag '1.3.0' into develop
Release 1.3.0
2016-07-08 16:06:58 +12:00
Anthony Lapenna
f923016052 style(lint): fix jshint issues 2016-07-08 15:50:16 +12:00
Anthony Lapenna
8fd9c2fce2 refactor(ui): remove useless logging statement 2016-07-08 15:39:32 +12:00
Anthony Lapenna
d4ca060945 feat(ui): add the ability to pull an image from a selection of registry 2016-07-08 15:31:09 +12:00
Anthony Lapenna
0350daca8d Merge branch 'hide-containers-dashboard-swarm' into internal 2016-07-07 15:38:42 +12:00
Anthony Lapenna
135b940897 refactor(grunt): remove testing option 2016-07-07 15:35:45 +12:00
Anthony Lapenna
7856276092 fix(ui): hidden containers (using label) are now removed from dashboard and swarm view 2016-07-07 15:34:05 +12:00
Anthony Lapenna
bf14dcc3e8 merge display-all-containers 2016-07-07 14:39:26 +12:00
Anthony Lapenna
337bfa74bb feat(ui): default to display all containers 2016-07-07 14:30:11 +12:00
Anthony Lapenna
418b1ff544 fix(ui): fix display issue with multiple nodes in Swarm view 2016-07-07 13:25:22 +12:00
Anthony Lapenna
fd6645d068 merge fix-viewspinner 2016-07-07 13:15:56 +12:00
Anthony Lapenna
3a6e326e5e feat(ui): replace ViewSpinner with JQuery animations 2016-07-07 12:44:58 +12:00
Anthony Lapenna
b997b787c4 feat(ui): simplify views for internal usage 2016-07-06 19:04:45 +12:00
49 changed files with 1761 additions and 594 deletions

View File

@@ -7,9 +7,16 @@ A fork of the amazing UI for Docker by Michael Crosby and Kevan Ahlquist (https:
UI For Docker is a web interface for the Docker Remote API. The goal is to provide a pure client side implementation so it is effortless to connect and manage docker.
## Goals
* Minimal dependencies - I really want to keep this project a pure html/js app.
* Consistency - The web UI should be consistent with the commands found on the docker CLI.
## Supported Docker versions
The 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
@@ -26,11 +33,16 @@ The `--privileged` flag is required for hosts using SELinux.
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 cloudinovasi/cloudinovasi-ui -e http://127.0.0.1:2375
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H tcp://127.0.0.1:2375
```
```
# Connect to another unix socket:
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H unix:///path/to/docker.sock
```
### Swarm support
@@ -41,7 +53,7 @@ You can access a specific view for you Swarm cluster by defining the `--swarm` f
```
# Connect to a tcp socket and enable Swarm:
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -e http://<SWARM_HOST>:<SWARM_PORT> --swarm
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H tcp://<SWARM_HOST>:<SWARM_PORT> --swarm
```
*NOTE*: Due to Swarm not exposing information in a machine readable way, the app is bound to a specific version of Swarm at the moment.
@@ -54,9 +66,39 @@ UI For Docker listens on port 9000 by default. If you run UI For Docker inside a
$ 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
Ensure that you have access to the CA, the cert and the public key used to access your Docker engine.
These files will need to be named `ca.pem`, `cert.pem` and `key.pem` respectively. Store them somewhere on your disk and mount a volume containing these files inside the UI container:
```
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -v /path/to/certs:/certs -H https://my-docker-host.domain:2376 --tlsverify
```
You can also use the `--tlscacert`, `--tlscert` and `--tlskey` flags if you want to change the default path to the CA, certificate and key file respectively:
```
$ docker run -d -p 9000:9000 cloudinovasi/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.
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`:
@@ -70,13 +112,48 @@ 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:
* `--endpoint`, `-e`: Docker deamon endpoint (default: *"/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*)
* `--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

57
api/api.go Normal file
View File

@@ -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,
}
}

48
api/csrf.go Normal file
View File

@@ -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)
})
}

24
api/exec.go Normal file
View File

@@ -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
}
}

46
api/flags.go Normal file
View File

@@ -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
}

75
api/handler.go Normal file
View File

@@ -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
}

123
api/hijack.go Normal file
View File

@@ -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
}

45
api/main.go Normal file
View File

@@ -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)
}

18
api/settings.go Normal file
View File

@@ -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)
}

27
api/ssl.go Normal file
View File

@@ -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
}

47
api/unix_handler.go Normal file
View File

@@ -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)
}
}
}

View File

@@ -5,16 +5,18 @@ angular.module('uifordocker', [
'ui.select',
'ngCookies',
'ngSanitize',
'dockerui.services',
'dockerui.filters',
'uifordocker.services',
'uifordocker.filters',
'dashboard',
'container',
'containerConsole',
'containerLogs',
'containers',
'createContainer',
'docker',
'events',
'images',
'image',
'containerLogs',
'stats',
'swarm',
'network',
@@ -56,6 +58,11 @@ angular.module('uifordocker', [
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",
@@ -86,6 +93,11 @@ angular.module('uifordocker', [
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',
@@ -143,5 +155,5 @@ angular.module('uifordocker', [
// 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', '/config')
.constant('UI_VERSION', 'v1.3.0');
.constant('CONFIG_ENDPOINT', 'settings')
.constant('UI_VERSION', 'v1.7.0');

View File

@@ -64,6 +64,7 @@
<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">
@@ -83,7 +84,7 @@
<tbody>
<tr>
<td>Created</td>
<td>{{ container.Created | date: 'medium' }}</td>
<td>{{ container.Created|getisodate }}</td>
</tr>
<tr>
<td>Path</td>
@@ -185,7 +186,8 @@
<tbody>
<tr ng-repeat="(key, val) in container.State">
<td>{{key}}</td>
<td>{{ val }}</td>
<td ng-if="key === 'StartedAt' || key === 'FinishedAt'">{{val|getisodate}}</td>
<td ng-if="key !== 'StartedAt' && key !== 'FinishedAt'">{{val}}</td>
</tr>
</tbody>
</table>

View File

@@ -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>

View File

@@ -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();
};
};
}
}]);

View File

@@ -39,7 +39,7 @@
<tr>
<th></th>
<th>
<a ui-sref="containers" ng-click="order('State')">
<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>
@@ -52,7 +52,7 @@
<span ng-show="sortType == 'Names' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<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>
@@ -85,11 +85,11 @@
<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.State|containerstatusbadge }}">{{ container.State }}</span></td>
<td><span class="label label-{{ container.Status|containerstatusbadge }}">{{ container.Status|containerstatus }}</span></td>
<td ng-if="swarm"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername}}</a></td>
<td ng-if="!swarm"><a ui-sref="container({id: container.Id})">{{ container|containername}}</a></td>
<td>{{ container.IP ? container.IP : '-' }}</td>
<td ng-if="swarm">{{ container|swarmhostname}}</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>

View File

@@ -1,9 +1,10 @@
angular.module('containers', [])
.controller('ContainersController', ['$scope', 'Container', 'Settings', 'Messages', 'Config', 'errorMsgFilter',
function ($scope, Container, Settings, Messages, Config, errorMsgFilter) {
.controller('ContainersController', ['$scope', 'Container', 'Info', 'Settings', 'Messages', 'Config', 'errorMsgFilter',
function ($scope, Container, Info, Settings, Messages, Config, errorMsgFilter) {
$scope.state = {};
$scope.state.displayAll = Settings.displayAll;
$scope.state.displayIP = false;
$scope.sortType = 'State';
$scope.sortReverse = true;
$scope.state.selectedItemCount = 0;
@@ -22,7 +23,14 @@ function ($scope, Container, Settings, Messages, Config, errorMsgFilter) {
containers = hideContainers(d);
}
$scope.containers = containers.map(function (container) {
return new ContainerViewModel(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();
});
@@ -149,10 +157,32 @@ function ($scope, Container, Settings, Messages, Config, errorMsgFilter) {
});
};
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;
update({all: Settings.displayAll ? 1 : 0});
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});
}
});
}]);

View File

@@ -9,6 +9,7 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, e
$scope.formValues = {
Console: 'none',
Volumes: [],
AvailableRegistries: [],
Registry: ''
};
@@ -16,6 +17,7 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, e
$scope.config = {
Env: [],
ExposedPorts: {},
HostConfig: {
RestartPolicy: {
Name: 'no'
@@ -27,12 +29,8 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, e
}
};
$scope.resetVolumePath = function(index) {
$scope.formValues.Volumes[index].name = '';
};
$scope.addVolume = function() {
$scope.formValues.Volumes.push({ name: '', containerPath: '', readOnly: false, isPath: false });
$scope.formValues.Volumes.push({ name: '', containerPath: '' });
};
$scope.removeVolume = function(index) {
@@ -58,6 +56,8 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, e
Config.$promise.then(function (c) {
var swarm = c.swarm;
$scope.formValues.AvailableRegistries = c.registries;
Volume.query({}, function (d) {
$scope.availableVolumes = d.Volumes;
}, function (e) {
@@ -72,6 +72,7 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, e
return network;
}
});
$scope.globalNetworkCount = networks.length;
networks.push({Name: "bridge"});
networks.push({Name: "host"});
networks.push({Name: "none"});
@@ -149,7 +150,16 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, e
config.HostConfig.PortBindings.forEach(function (portBinding) {
if (portBinding.hostPort && portBinding.containerPort) {
var key = portBinding.containerPort + "/" + portBinding.protocol;
bindings[key] = [{ HostPort: portBinding.hostPort }];
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;

View File

@@ -20,7 +20,7 @@
<!-- !name-input -->
<!-- image-and-registry-inputs -->
<div class="form-group">
<label for="container_image" class="col-sm-1 control-label text-left">Name</label>
<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>
@@ -107,7 +107,6 @@
<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 -->
@@ -255,6 +254,11 @@
<!-- 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>

View File

@@ -2,13 +2,18 @@ angular.module('createNetwork', [])
.controller('CreateNetworkController', ['$scope', '$state', 'Messages', 'Network', 'errorMsgFilter',
function ($scope, $state, Messages, Network, errorMsgFilter) {
$scope.formValues = {
DriverOptions: []
DriverOptions: [],
Subnet: '',
Gateway: ''
};
$scope.config = {
Driver: 'bridge',
CheckDuplicate: true,
Internal: false
Internal: false,
IPAM: {
Config: []
}
};
$scope.addDriverOption = function() {
@@ -36,6 +41,17 @@ function ($scope, $state, Messages, Network, errorMsgFilter) {
});
}
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) {
@@ -46,6 +62,7 @@ function ($scope, $state, Messages, Network, errorMsgFilter) {
function prepareConfiguration() {
var config = angular.copy($scope.config);
prepareIPAMConfiguration(config);
prepareDriverOptions(config);
return config;
}

View File

@@ -18,6 +18,18 @@
</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>

View File

@@ -1,76 +1,131 @@
<rd-header>
<rd-header-title title="Home"></rd-header-title>
<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>
<div class="row">
<div class="col-lg-3 col-md-6 col-xs-12">
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="!swarm">
<rd-widget>
<rd-widget-body>
<div class="widget-icon blue pull-left">
<i class="fa fa-tasks"></i>
</div>
<div class="title">{{ containerData.total }}</div>
<div class="comment">Containers</div>
<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-3 col-md-6 col-xs-12">
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="swarm">
<rd-widget>
<rd-widget-body>
<div class="widget-icon green pull-left">
<i class="fa fa-tasks"></i>
</div>
<div class="title">{{ containerData.running }}</div>
<div class="comment">Running</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 red pull-left">
<i class="fa fa-tasks"></i>
</div>
<div class="title">{{ containerData.stopped }}</div>
<div class="comment">Stopped</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 gray pull-left">
<i class="fa fa-tasks"></i>
</div>
<div class="title">{{ containerData.ghost }}</div>
<div class="comment">Ghost</div>
<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">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Containers created"></rd-widget-header>
<rd-widget-body>
<canvas id="containers-started-chart" width="770" height="230">
<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>
</rd-widget-body>
</rd-widget>
<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">
<rd-widget>
<rd-widget-header icon="fa-clone" title="Images created"></rd-widget-header>
<rd-widget-body>
<canvas id="images-created-chart" width="770" height="230">
<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>
</rd-widget-body>
</rd-widget>
<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>

View File

@@ -1,49 +1,85 @@
angular.module('dashboard', [])
.controller('DashboardController', ['$scope', 'Config', 'Container', 'Image', 'Settings', 'LineChart',
function ($scope, Config, Container, Image, Settings, LineChart) {
.controller('DashboardController', ['$scope', '$q', 'Config', 'Container', 'Image', 'Network', 'Volume', 'Info',
function ($scope, $q, Config, Container, Image, Network, Volume, Info) {
$scope.containerData = {};
var buildCharts = function (data) {
$scope.containerData.total = 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
};
function prepareContainerData(d) {
var running = 0;
var stopped = 0;
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() {
Container.query({all: 1}, function (d) {
var running = 0;
var ghost = 0;
var stopped = 0;
var containers = d;
if (hiddenLabels) {
containers = hideContainers(d);
}
for (var i = 0; i < containers.length; i++) {
var item = containers[i];
if (item.Status === "Ghost") {
ghost += 1;
} else if (item.Status.indexOf('Exit') !== -1) {
stopped += 1;
} else {
running += 1;
}
}
$scope.containerData.running = running;
$scope.containerData.stopped = stopped;
$scope.containerData.ghost = ghost;
buildCharts(containers);
$('#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();
});
}
@@ -63,6 +99,7 @@ function ($scope, Config, Container, Image, Settings, LineChart) {
};
Config.$promise.then(function (c) {
$scope.swarm = c.swarm;
hiddenLabels = c.hiddenLabels;
fetchDashboardData();
});

View File

@@ -45,7 +45,7 @@
<div class="row">
<div class="col-lg-12">
<rd-widget>
<rd-widget-header icon="fa-object-group" title="Cluster status"></rd-widget-header>
<rd-widget-header icon="fa-object-group" title="Engine status"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>

View File

@@ -0,0 +1,63 @@
<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>
<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 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>
</rd-widget-body>
<rd-widget>
</div>
</div>

View File

@@ -0,0 +1,27 @@
angular.module('events', [])
.controller('EventsController', ['$scope', 'Settings', 'Messages', 'Events',
function ($scope, Settings, Messages, Events) {
$scope.state = {};
$scope.sortType = 'Time';
$scope.sortReverse = true;
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
var from = moment().subtract(24, 'hour').unix();
var to = moment().unix();
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();
});
}]);

View File

@@ -1,97 +1,159 @@
<rd-header>
<rd-header-title title="Image details"></rd-header-title>
<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: id})">{{ id }}</a>
Images > <a ui-sref="image({id: image.Id})">{{ image.Id }}</a>
</rd-header-content>
</rd-header>
<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="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>
<div class="widget-icon grey pull-left">
<i class="fa fa-clone"></i>
</div>
<div class="title">{{ id }}</div>
<div class="comment">Image ID</div>
<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>
</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-danger" ng-click="removeImage(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-clone" title="Image details"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Created</td>
<td>{{ image.Created | date: 'medium'}}</td>
</tr>
<tr>
<td>Tags</td>
<td>ID</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>
{{ image.Id }}
<button class="btn btn-xs btn-danger" ng-click="removeImage(image.Id)">Delete this image</button>
</td>
</tr>
<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 (Virtual Size)</td>
<td>{{ image.Size|humansize }} ({{ image.VirtualSize|humansize }})</td>
</tr>
<tr>
<td>Hostname</td>
<td>{{ image.ContainerConfig.Hostname }}</td>
<td>Size</td>
<td>{{ image.VirtualSize|humansize }}</td>
</tr>
<tr>
<td>User</td>
<td>{{ image.ContainerConfig.User }}</td>
<td>Created</td>
<td>{{ image.Created|getisodate }}</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>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>

View File

@@ -1,33 +1,15 @@
angular.module('image', [])
.controller('ImageController', ['$scope', '$q', '$stateParams', '$state', 'Image', 'Container', 'Messages', 'LineChart',
function ($scope, $q, $stateParams, $state, Image, Container, Messages, LineChart) {
$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) {
$state.go('images', {}, {reload: true});
} else {
$state.go('image', {id: $scope.id}, {reload: true});
}
}, function (e) {
$scope.error = e.data;
$('#error-message').show();
});
$scope.config = {
Image: '',
Registry: ''
};
/**
* Get RepoTags from the /images/query endpoint instead of /image/json,
* for backwards compatibility with Docker API versions older than 1.21
* @param {string} imageId
*/
// 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) {
@@ -38,21 +20,88 @@ function ($scope, $q, $stateParams, $state, Image, Container, Messages, LineChar
});
}
function createImageConfig(imageName, registry) {
var imageNameAndTag = imageName.split(':');
var image = imageNameAndTag[0];
if (registry) {
image = registry + '/' + imageNameAndTag[0];
}
var imageConfig = {
repo: image,
tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest'
};
return imageConfig;
}
$scope.tagImage = function() {
$('#loadingViewSpinner').show();
var image = _.toLower($scope.config.Image);
var registry = _.toLower($scope.config.Registry);
var imageConfig = 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);
});
};
$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);
});
};
$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);
});
};
$('#loadingViewSpinner').show();
Image.get({id: $stateParams.id}, function (d) {
$scope.image = d;
$scope.id = d.Id;
if (d.RepoTags) {
$scope.RepoTags = d.RepoTags;
} else {
getRepoTags($scope.id);
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) {
$('.detail').hide();
$scope.error = "Image not found.<br />" + $stateParams.id;
Messages.error("Unable to find image", $stateParams.id);
} else {
$scope.error = e.data;
Messages.error("Unable to retrieve image info", e.data);
}
$('#error-message').show();
});
}]);

View File

@@ -63,10 +63,10 @@
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
<table class="table">
<table class="table table-hover">
<thead>
<tr>
<th><label><input type="checkbox" ng-model="state.toggle" ng-change="toggleSelectAll()" /> Select</label></th>
<th></th>
<th>
<a ui-sref="images" ng-click="order('Id')">
Id
@@ -76,14 +76,14 @@
</th>
<th>
<a ui-sref="images" ng-click="order('RepoTags')">
Repository
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')">
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>
@@ -101,14 +101,16 @@
<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>{{ image|repotag }}</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|getdate }}</td>
<td>{{ image.Created|getisodatefromtimestamp }}</td>
</tr>
</tbody>
</table>
</div>
</rd-widget-body>
<rd-widget>
</div>
</div>
<rd-widget>
</div>
</div>

View File

@@ -1,15 +1,14 @@
angular.module('images', [])
.controller('ImagesController', ['$scope', '$state', 'Image', 'Messages',
function ($scope, $state, Image, Messages) {
.controller('ImagesController', ['$scope', '$state', 'Config', 'Image', 'Messages',
function ($scope, $state, Config, Image, Messages) {
$scope.state = {};
$scope.sortType = 'Created';
$scope.sortType = 'RepoTags';
$scope.sortReverse = true;
$scope.state.toggle = false;
$scope.state.selectedItemCount = 0;
$scope.config = {
Image: '',
Registry: '',
Registry: ''
};
$scope.order = function(sortType) {
@@ -17,17 +16,6 @@ function ($scope, $state, Image, Messages) {
$scope.sortType = sortType;
};
$scope.toggleSelectAll = function () {
angular.forEach($scope.state.filteredImages, function (i) {
i.Checked = $scope.state.toggle;
});
if ($scope.state.toggle) {
$scope.state.selectedItemCount = $scope.state.filteredImages.length;
} else {
$scope.state.selectedItemCount = 0;
}
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
@@ -83,15 +71,17 @@ function ($scope, $state, Image, Messages) {
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);
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("Failure", e.data);
$('#loadImagesSpinner').hide();
Messages.error("Unable to remove image", e.data);
complete();
});
}
@@ -110,5 +100,9 @@ function ($scope, $state, Image, Messages) {
});
}
fetchImages();
Config.$promise.then(function (c) {
$scope.availableRegistries = c.registries;
fetchImages();
});
}]);

View File

@@ -25,10 +25,10 @@
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
<table class="table">
<table class="table table-hover">
<thead>
<tr>
<th><label><input type="checkbox" ng-model="state.toggle" ng-change="toggleSelectAll()"/> Select</label></th>
<th></th>
<th>
<a ui-sref="networks" ng-click="order('Name')">
Name
@@ -83,13 +83,13 @@
<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:20}}</a></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 }}</td>
<td>{{ network.IPAM.Config[0].Gateway }}</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>

View File

@@ -1,29 +1,29 @@
angular.module('networks', [])
.controller('NetworksController', ['$scope', 'Network', 'Messages', 'errorMsgFilter',
function ($scope, Network, Messages, errorMsgFilter) {
.controller('NetworksController', ['$scope', '$state', 'Network', 'Config', 'Messages', 'errorMsgFilter',
function ($scope, $state, Network, Config, Messages, errorMsgFilter) {
$scope.state = {};
$scope.state.toggle = false;
$scope.state.selectedItemCount = 0;
$scope.state.advancedSettings = false;
$scope.sortType = 'Name';
$scope.sortReverse = true;
$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.toggleSelectAll = function () {
angular.forEach($scope.state.filteredNetworks, function (i) {
i.Checked = $scope.state.toggle;
});
if ($scope.state.toggle) {
$scope.state.selectedItemCount = $scope.state.filteredNetworks.length;
} else {
$scope.state.selectedItemCount = 0;
}
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;
@@ -45,9 +45,14 @@ function ($scope, Network, Messages, errorMsgFilter) {
if (network.Checked) {
counter = counter + 1;
Network.remove({id: network.Id}, function (d) {
Messages.send("Network deleted", network.Id);
var index = $scope.networks.indexOf(network);
$scope.networks.splice(index, 1);
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);
@@ -67,5 +72,6 @@ function ($scope, Network, Messages, errorMsgFilter) {
$('#loadNetworksSpinner').hide();
});
}
fetchNetworks();
}]);

View File

@@ -8,43 +8,7 @@
</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">Swarm 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-6">
<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">
@@ -58,34 +22,48 @@
<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>CPUs</td>
<td>Total CPU</td>
<td>{{ info.NCPU }}</td>
</tr>
<tr>
<td>Total Memory</td>
<td>Total memory</td>
<td>{{ info.MemTotal|humansize }}</td>
</tr>
<tr>
<td>Operating System</td>
<td>Operating system</td>
<td>{{ info.OperatingSystem }}</td>
</tr>
<tr>
<td>Kernel Version</td>
<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 class="col-lg-6">
</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="Nodes status"></rd-widget-header>
<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>
@@ -97,6 +75,20 @@
<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
@@ -123,6 +115,8 @@
<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>

View File

@@ -53,8 +53,8 @@ angular.module('swarm', [])
node.id = info[offset + 1][1];
node.status = info[offset + 2][1];
node.containers = info[offset + 3][1];
node.cpu = info[offset + 4][1];
node.memory = info[offset + 5][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);

View File

@@ -25,10 +25,10 @@
</rd-widget-taskbar>
<rd-widget-body classes="no-padding">
<div class="table-responsive">
<table class="table">
<table class="table table-hover">
<thead>
<tr>
<th><label><input type="checkbox" ng-model="state.toggle" ng-change="toggleSelectAll()"/> Select</label></th>
<th></th>
<th>
<a ui-sref="volumes" ng-click="order('Name')">
Name
@@ -55,7 +55,7 @@
<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:20 }}</td>
<td>{{ volume.Name|truncate:50 }}</td>
<td>{{ volume.Driver }}</td>
<td>{{ volume.Mountpoint }}</td>
</tr>
@@ -63,5 +63,5 @@
</table>
</div>
</rd-widget-body>
<rd-widget>
</div>
<rd-widget>
</div>

View File

@@ -1,28 +1,20 @@
angular.module('volumes', [])
.controller('VolumesController', ['$scope', 'Volume', 'Messages', 'errorMsgFilter',
function ($scope, Volume, Messages, errorMsgFilter) {
.controller('VolumesController', ['$scope', '$state', 'Volume', 'Messages', 'errorMsgFilter',
function ($scope, $state, Volume, Messages, errorMsgFilter) {
$scope.state = {};
$scope.state.toggle = false;
$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.toggleSelectAll = function () {
angular.forEach($scope.state.filteredVolumes, function (i) {
i.Checked = $scope.state.toggle;
});
if ($scope.state.toggle) {
$scope.state.selectedItemCount = $scope.state.filteredVolumes.length;
} else {
$scope.state.selectedItemCount = 0;
}
};
$scope.selectItem = function (item) {
if (item.Checked) {
$scope.state.selectedItemCount++;

View File

@@ -8,7 +8,7 @@ angular
icon: '@'
},
transclude: true,
template: '<div class="widget-header"><div class="row"><div class="pull-left"><i class="fa" ng-class="icon"></i> {{title}} </div><div class="pull-right col-xs-6 col-sm-4" ng-transclude></div></div></div>',
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;

View File

@@ -1,4 +1,4 @@
angular.module('dockerui.filters', [])
angular.module('uifordocker.filters', [])
.filter('truncate', function () {
'use strict';
return function (text, length, end) {
@@ -21,16 +21,31 @@ angular.module('dockerui.filters', [])
.filter('containerstatusbadge', function () {
'use strict';
return function (text) {
if (text === 'paused') {
var status = _.toLower(text);
if (status.indexOf('paused') !== -1) {
return 'warning';
} else if (text === 'created') {
} else if (status.indexOf('created') !== -1) {
return 'info';
} else if (text === 'exited') {
} 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) {
@@ -79,7 +94,6 @@ angular.module('dockerui.filters', [])
if (state === undefined) {
return 'label-default';
}
if (state.Ghost && state.Running) {
return 'label-important';
}
@@ -115,31 +129,61 @@ angular.module('dockerui.filters', [])
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('repotag', function () {
.filter('repotags', function () {
'use strict';
return function (image) {
if (image.RepoTags && image.RepoTags.length > 0) {
var tag = image.RepoTags[0];
if (tag === '<none>:<none>') {
tag = '';
return [];
}
return tag;
return image.RepoTags;
}
return '';
return [];
};
})
.filter('getdate', function () {
.filter('getisodatefromtimestamp', function () {
'use strict';
return function (data) {
//Multiply by 1000 for the unix format
var date = new Date(data * 1000);
return date.toDateString();
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 () {

View File

@@ -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;
}

View File

@@ -1,4 +1,4 @@
angular.module('dockerui.services', ['ngResource', 'ngSanitize'])
angular.module('uifordocker.services', ['ngResource', 'ngSanitize'])
.factory('Container', ['$resource', 'Settings', function ContainerFactory($resource, Settings) {
'use strict';
// Resource for interacting with the docker containers
@@ -18,9 +18,17 @@ angular.module('dockerui.services', ['ngResource', 'ngSanitize'])
create: {method: 'POST', params: {action: 'create'}},
remove: {method: 'DELETE', params: {id: '@id', v: 0}},
rename: {method: 'POST', params: {id: '@id', action: 'rename'}, isArray: false},
stats: {method: 'GET', params: {id: '@id', stream: false, action: 'stats'}, timeout: 5000}
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
@@ -84,18 +92,31 @@ angular.module('dockerui.services', ['ngResource', 'ngSanitize'])
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(/\}\s*\{/g, "}, {") + "]";
return angular.fromJson(str);
}],
params: {action: 'create', fromImage: '@fromImage', tag: '@tag'}
},
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'}}
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('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) {
@@ -142,7 +163,7 @@ angular.module('dockerui.services', ['ngResource', 'ngSanitize'])
remove: {method: 'DELETE'}
});
}])
.factory('Config', ['$resource', 'CONFIG_ENDPOINT', function($resource, CONFIG_ENDPOINT) {
.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) {

View File

@@ -1,19 +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.State = data.State;
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;
}
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;
}
}

View File

@@ -117,7 +117,7 @@
.logo {
display: inline;
width: 100%;
max-width: 160px;
max-width: 155px;
height: 100%;
max-height: 55px;
margin-bottom: 5px;
@@ -165,3 +165,40 @@ input[type="radio"] {
.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;
}

View File

@@ -1,6 +1,6 @@
{
"name": "uifordocker",
"version": "1.3.0",
"version": "1.7.0",
"homepage": "https://github.com/kevana/ui-for-docker",
"authors": [
"Michael Crosby <crosbymichael@gmail.com>",
@@ -30,7 +30,6 @@
"angular-ui-router": "^0.2.15",
"angular-sanitize": "~1.5.0",
"angular-mocks": "~1.5.0",
"angular-oboe": "*",
"angular-resource": "~1.5.0",
"angular-ui-select": "~0.17.1",
"bootstrap": "~3.3.6",
@@ -39,6 +38,8 @@
"jquery.gritter": "1.7.4",
"lodash": "4.12.0",
"rdash-ui": "1.0.*",
"moment": "~2.14.1",
"xterm.js": "~1.0.0"
},
"resolutions": {
"angular": "1.5.5"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -1,188 +0,0 @@
package main // import "github.com/cloudinovasi/ui-for-docker"
import (
"io"
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"encoding/json"
"os"
"strings"
"github.com/gorilla/csrf"
"io/ioutil"
"fmt"
"github.com/gorilla/securecookie"
"gopkg.in/alecthomas/kingpin.v2"
)
var (
endpoint = kingpin.Flag("endpoint", "Dockerd endpoint").Default("/var/run/docker.sock").Short('e').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()
swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool()
labels = LabelParser(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l'))
authKey []byte
authKeyFile = "authKey.dat"
)
type UnixHandler struct {
path string
}
type Config struct {
Swarm bool `json:"swarm"`
HiddenLabels Labels `json:"hiddenLabels"`
}
type Label struct {
Name string `json:"name"`
Value string `json:"value"`
}
type Labels []Label
func (l *Labels) Set(value string) error {
parts := strings.SplitN(value, "=", 2)
if len(parts) != 2 {
return fmt.Errorf("expected HEADER=VALUE got '%s'", value)
}
label := new(Label)
label.Name = parts[0]
label.Value = parts[1]
*l = append(*l, *label)
return nil
}
func (l *Labels) String() string {
return ""
}
func (l *Labels) IsCumulative() bool {
return true
}
func LabelParser(s kingpin.Settings) (target *[]Label) {
target = new([]Label)
s.SetValue((*Labels)(target))
return
}
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 configurationHandler(w http.ResponseWriter, r *http.Request, c Config) {
json.NewEncoder(w).Encode(c)
}
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, d string, e string, c Config) 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)
}
// Use existing csrf authKey if present or generate a new one.
var authKeyPath = d + "/" + authKeyFile
dat, err := ioutil.ReadFile(authKeyPath)
if err != nil {
fmt.Println(err)
authKey = securecookie.GenerateRandomKey(32)
err := ioutil.WriteFile(authKeyPath, authKey, 0644)
if err != nil {
fmt.Println("unable to persist auth key", err)
}
} else {
authKey = dat
}
CSRF := csrf.Protect(
authKey,
csrf.HttpOnly(false),
csrf.Secure(false),
)
mux.Handle("/dockerapi/", http.StripPrefix("/dockerapi", h))
mux.Handle("/", fileHandler)
mux.HandleFunc("/config", func(w http.ResponseWriter, r *http.Request) {
configurationHandler(w, r, c)
})
return CSRF(csrfWrapper(mux))
}
func csrfWrapper(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)
})
}
func main() {
kingpin.Version("1.3.0")
kingpin.Parse()
configuration := Config{
Swarm: *swarm,
HiddenLabels: *labels,
}
handler := createHandler(*assets, *data, *endpoint, configuration)
if err := http.ListenAndServe(*addr, handler); err != nil {
log.Fatal(err)
}
}

View File

@@ -40,6 +40,7 @@ module.exports = function (grunt) {
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)
@@ -67,11 +68,12 @@ module.exports = function (grunt) {
jsTpl: ['<%= distdir %>/templates/**/*.js'],
jsVendor: [
'bower_components/jquery/dist/jquery.min.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.min.js',
'bower_components/Chart.js/Chart.min.js',
'bower_components/lodash/dist/lodash.min.js',
'bower_components/oboe/dist/oboe-browser.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"
'assets/js/legend.js' // Not a bower package
],
specs: ['test/**/*.spec.js'],
@@ -84,7 +86,8 @@ module.exports = function (grunt) {
'bower_components/jquery.gritter/css/jquery.gritter.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/angular-ui-select/dist/select.min.css',
'bower_components/xterm.js/src/xterm.css'
]
},
clean: {
@@ -155,7 +158,6 @@ module.exports = function (grunt) {
'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-oboe/dist/angular-oboe.min.js',
'bower_components/angular-ui-select/dist/select.min.js'],
dest: '<%= distdir %>/js/angular.js'
}
@@ -224,6 +226,10 @@ module.exports = function (grunt) {
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: {
@@ -250,10 +256,10 @@ module.exports = function (grunt) {
},
buildBinary: {
command: [
'docker run --rm -v $(pwd):/src centurylink/golang-builder',
'shasum ui-for-docker > ui-for-docker-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 ui-for-docker dist/'
'mv api/ui-for-docker dist/'
].join(' && ')
},
run: {
@@ -267,7 +273,14 @@ module.exports = function (grunt) {
command: [
'docker stop ui-for-docker',
'docker rm ui-for-docker',
'docker run --privileged -d -p 9000:9000 -v /tmp/docker-ui:/data --name ui-for-docker ui-for-docker -e http://10.0.7.10:4000 --swarm -d /data'
'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: {

View File

@@ -32,7 +32,8 @@
<ul class="sidebar">
<li class="sidebar-main">
<a ng-click="toggleSidebar()">
<img src="images/logo.png" class="img-responsive logo" alt="logo">
<img ng-if="config.logo" ng-src="{{ config.logo }}" class="img-responsive logo" alt="CloudInovasi UI">
<img ng-if="!config.logo" src="images/logo.png" class="img-responsive logo" alt="CloudInovasi UI">
<span class="menu-icon glyphicon glyphicon-transfer"></span>
</a>
</li>
@@ -52,6 +53,9 @@
<li class="sidebar-list">
<a ui-sref="volumes">Volumes <span class="menu-icon fa fa-cubes"></span></a>
</li>
<li class="sidebar-list" ng-if="!config.swarm">
<a ui-sref="events">Events <span class="menu-icon fa fa-history"></span></a>
</li>
<li class="sidebar-list" ng-if="config.swarm">
<a ui-sref="swarm">Swarm <span class="menu-icon fa fa-object-group"></span></a>
</li>

View File

@@ -2,7 +2,7 @@
"author": "Michael Crosby & Kevan Ahlquist",
"name": "uifordocker",
"homepage": "https://github.com/kevana/ui-for-docker",
"version": "1.3.0",
"version": "1.7.0",
"repository": {
"type": "git",
"url": "git@github.com:kevana/ui-for-docker.git"

View File

@@ -134,12 +134,6 @@ describe('filters', function () {
}));
});
describe('getdate', function () {
it('should convert the Docker date to a human readable form', inject(function (getdateFilter) {
expect(getdateFilter(1420424998)).toBe('Sun Jan 04 2015');
}));
});
describe('errorMsgFilter', function () {
it('should convert the $resource object to a string message',
inject(function (errorMsgFilter) {