Compare commits
252 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb9e044e89 | ||
|
|
520532cb9a | ||
|
|
44e09ecadf | ||
|
|
35ced4901a | ||
|
|
134416c9a3 | ||
|
|
8f7f4acc0d | ||
|
|
fde0d3ea9f | ||
|
|
477799af7e | ||
|
|
72570153a5 | ||
|
|
9f335b692f | ||
|
|
e88b22bd45 | ||
|
|
833053a2e1 | ||
|
|
64c52348f3 | ||
|
|
c3b79e6cc2 | ||
|
|
422a982d60 | ||
|
|
6e9fe26fde | ||
|
|
6bfa3096dc | ||
|
|
7cd2da4c6e | ||
|
|
739a5ec299 | ||
|
|
59e65222eb | ||
|
|
01d5d11c01 | ||
|
|
29a59cab44 | ||
|
|
be184c11a6 | ||
|
|
d6ab97ad25 | ||
|
|
6a0f76890e | ||
|
|
1946868248 | ||
|
|
84b02c711a | ||
|
|
679a681749 | ||
|
|
c35d1b14ec | ||
|
|
87df297a56 | ||
|
|
b8e420e0e8 | ||
|
|
f8c8668863 | ||
|
|
ced0746a81 | ||
|
|
39909d774f | ||
|
|
12e6e0557d | ||
|
|
e27282de3c | ||
|
|
fe63f9939a | ||
|
|
b623a5d452 | ||
|
|
d8113df979 | ||
|
|
b3ba36c02a | ||
|
|
37863e3f74 | ||
|
|
da6f39b137 | ||
|
|
4fe63d7102 | ||
|
|
7c8881f37d | ||
|
|
c20069fce0 | ||
|
|
2eb1c9e857 | ||
|
|
48e1fe769e | ||
|
|
2b8bc82d4e | ||
|
|
8f33151647 | ||
|
|
8e743a8d32 | ||
|
|
9f22e01d3b | ||
|
|
502c8718c5 | ||
|
|
220faa52e7 | ||
|
|
857c93bff9 | ||
|
|
ca5cf33c8f | ||
|
|
1cd620a45e | ||
|
|
4eb9a9a0af | ||
|
|
c82abae8e5 | ||
|
|
f56256f897 | ||
|
|
e31749e64d | ||
|
|
89d666f365 | ||
|
|
b502852966 | ||
|
|
e101397a2c | ||
|
|
ddcecc06d4 | ||
|
|
4237f452df | ||
|
|
3f9276ee4c | ||
|
|
5a1f437cf9 | ||
|
|
bb9cebd759 | ||
|
|
62e313d13f | ||
|
|
537ee24078 | ||
|
|
364756d9fa | ||
|
|
6eb1cff8c5 | ||
|
|
44e02c0342 | ||
|
|
b36767cdb7 | ||
|
|
67194109c6 | ||
|
|
08032be2c4 | ||
|
|
74b97a0036 | ||
|
|
eac3239817 | ||
|
|
9698aa7ad5 | ||
|
|
cbce2a70f5 | ||
|
|
a2d91ec2f9 | ||
|
|
d93a69df95 | ||
|
|
fb982ca8f1 | ||
|
|
4b979628b3 | ||
|
|
789750cc86 | ||
|
|
4125361fb5 | ||
|
|
6b8b562e7c | ||
|
|
2e9a117255 | ||
|
|
6d6a7e6923 | ||
|
|
4edb4e014f | ||
|
|
f020e5a633 | ||
|
|
5432424a40 | ||
|
|
e81bfb6f37 | ||
|
|
3c75c5fe25 | ||
|
|
7c5c693f17 | ||
|
|
2d98e33e98 | ||
|
|
4827d33ca1 | ||
|
|
71eb3feac9 | ||
|
|
5f290937d2 | ||
|
|
1c8aa35479 | ||
|
|
faccf2a651 | ||
|
|
4d99c12215 | ||
|
|
7c2047cfbf | ||
|
|
12d5cfe8e4 | ||
|
|
cbbcb51162 | ||
|
|
954a6a11b7 | ||
|
|
0c5e98b47d | ||
|
|
d941fef8d6 | ||
|
|
496de850c1 | ||
|
|
29fa33fb2b | ||
|
|
06fbb5ba34 | ||
|
|
a32f6f343d | ||
|
|
61d7b4f64c | ||
|
|
1840ab4bba | ||
|
|
ccb812cc33 | ||
|
|
b098cd5638 | ||
|
|
eefa7ca138 | ||
|
|
b5dcdc8807 | ||
|
|
4b4e5d5ebd | ||
|
|
54fd9561f0 | ||
|
|
fb67769928 | ||
|
|
9c8e632a09 | ||
|
|
0b6c2b032a | ||
|
|
f0e4cdc13e | ||
|
|
cfe31fbeac | ||
|
|
722dc0b3af | ||
|
|
de3353feba | ||
|
|
145e45b4a8 | ||
|
|
d0f57809d6 | ||
|
|
6c29377992 | ||
|
|
164902c0cb | ||
|
|
75466cb57f | ||
|
|
0e8fff7a51 | ||
|
|
7b72da857f | ||
|
|
b89546a1e0 | ||
|
|
22122a27b5 | ||
|
|
24a9e9f61c | ||
|
|
cf5378f604 | ||
|
|
1d8f51c141 | ||
|
|
87798cd1c8 | ||
|
|
fd1496df93 | ||
|
|
ea6e11000d | ||
|
|
ef257f65cf | ||
|
|
01a707c8e7 | ||
|
|
9293b28ef4 | ||
|
|
30c0fda1b6 | ||
|
|
548a458b9a | ||
|
|
111cd4ac64 | ||
|
|
54ab81a7de | ||
|
|
21344280a9 | ||
|
|
d3fa9736f4 | ||
|
|
f1ec419e3a | ||
|
|
de8c6b4ed8 | ||
|
|
e661cef2fe | ||
|
|
edf485bbe4 | ||
|
|
20eecffc40 | ||
|
|
232b180eef | ||
|
|
4a738ee362 | ||
|
|
f4d90306b3 | ||
|
|
7801a91149 | ||
|
|
19d4e38d94 | ||
|
|
bab57e0402 | ||
|
|
d2b3360bff | ||
|
|
1aaa5acbef | ||
|
|
5878eed7ec | ||
|
|
b0ebbdf68c | ||
|
|
0ec20d3093 | ||
|
|
c5ddae12cf | ||
|
|
b1e1850e9f | ||
|
|
06c2635e82 | ||
|
|
711ac742e1 | ||
|
|
201ab20131 | ||
|
|
716ba72217 | ||
|
|
95b16919a6 | ||
|
|
d3d000a1d0 | ||
|
|
9499f78121 | ||
|
|
fa36c9ee5c | ||
|
|
c460eb4d7a | ||
|
|
ab52270238 | ||
|
|
7c6fdebb3d | ||
|
|
cf3cd76064 | ||
|
|
5ef6b536ac | ||
|
|
adf5184a5d | ||
|
|
ea596a8701 | ||
|
|
15a3cb7241 | ||
|
|
f147da3017 | ||
|
|
dfaf2eb6a9 | ||
|
|
97f6a32c78 | ||
|
|
bcdd7498a1 | ||
|
|
c45947b573 | ||
|
|
85140c7dcf | ||
|
|
30e9a604cd | ||
|
|
8c769148ad | ||
|
|
4cc08d7211 | ||
|
|
48b6b6340b | ||
|
|
b857970236 | ||
|
|
1011fde9de | ||
|
|
bee89720d5 | ||
|
|
23bff41304 | ||
|
|
8243326692 | ||
|
|
17ae122595 | ||
|
|
b69d72fc8c | ||
|
|
c52498993b | ||
|
|
a4a82b4502 | ||
|
|
52d953a1c2 | ||
|
|
e145d82947 | ||
|
|
25df1fe26c | ||
|
|
106718f416 | ||
|
|
8464faa2a1 | ||
|
|
9354b911bb | ||
|
|
c8a5b82c89 | ||
|
|
1f884e9584 | ||
|
|
00b2c92e39 | ||
|
|
0796778d17 | ||
|
|
1eae1c03f0 | ||
|
|
a9209da167 | ||
|
|
43c2f14289 | ||
|
|
f378d56543 | ||
|
|
521d146d7b | ||
|
|
dc721f5870 | ||
|
|
3b0d726c2a | ||
|
|
f6226d19b8 | ||
|
|
71c091ae0d | ||
|
|
1fb008212a | ||
|
|
cab34e4069 | ||
|
|
e67e20ce18 | ||
|
|
d253c0d494 | ||
|
|
c74e8fc732 | ||
|
|
29358e5744 | ||
|
|
b59c102098 | ||
|
|
afaa1433ff | ||
|
|
f923016052 | ||
|
|
ca27e7f27a | ||
|
|
8fd9c2fce2 | ||
|
|
d4ca060945 | ||
|
|
d124c21d1b | ||
|
|
d2fb2cb863 | ||
|
|
0350daca8d | ||
|
|
06f54e300c | ||
|
|
135b940897 | ||
|
|
7856276092 | ||
|
|
bf14dcc3e8 | ||
|
|
21c1778822 | ||
|
|
337bfa74bb | ||
|
|
418b1ff544 | ||
|
|
092d866c73 | ||
|
|
50391c87e2 | ||
|
|
fd6645d068 | ||
|
|
3a6e326e5e | ||
|
|
b997b787c4 | ||
|
|
d227bdfc75 | ||
|
|
4ba6286c97 |
43
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
<!--
|
||||
|
||||
Thanks for opening an issue on Portainer !
|
||||
|
||||
Do you need help or have a question? Come chat with us on gitter: https://gitter.im/portainer/Lobby.
|
||||
|
||||
If you are reporting a new issue, make sure that we do not have any duplicates
|
||||
already open. You can ensure this by searching the issue list for this
|
||||
repository. If there is a duplicate, please close your issue and add a comment
|
||||
to the existing issue instead.
|
||||
|
||||
Also, be sure to check our FAQ and documentation first: https://portainer.readthedocs.io
|
||||
|
||||
If you suspect your issue is a bug, please edit your issue description to
|
||||
include the BUG REPORT INFORMATION shown below.
|
||||
|
||||
---------------------------------------------------
|
||||
BUG REPORT INFORMATION
|
||||
---------------------------------------------------
|
||||
You do NOT have to include this information if this is a FEATURE REQUEST
|
||||
-->
|
||||
|
||||
**Description**
|
||||
|
||||
<!--
|
||||
Briefly describe the problem you are having in a few paragraphs.
|
||||
-->
|
||||
|
||||
**Steps to reproduce the issue:**
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
Any other info e.g. Why do you consider this to be a bug? What did you expect to happen instead?
|
||||
|
||||
**Technical details:**
|
||||
|
||||
* Portainer version:
|
||||
* Target Docker version (the host/cluster you manage):
|
||||
* Target Swarm version (if applicable):
|
||||
* Platform (windows/linux):
|
||||
* Browser:
|
||||
8
.gitignore
vendored
@@ -1,10 +1,4 @@
|
||||
logs/*
|
||||
!.gitkeep
|
||||
*.esproj/*
|
||||
node_modules
|
||||
bower_components
|
||||
.idea
|
||||
*.iml
|
||||
dist
|
||||
dist/*
|
||||
ui-for-docker-checksum.txt
|
||||
portainer-checksum.txt
|
||||
|
||||
57
CONTRIBUTING.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Contributing Guidelines
|
||||
|
||||
Some basic conventions for contributing to this project.
|
||||
|
||||
### General
|
||||
|
||||
Please make sure that there aren't existing pull requests attempting to address the issue mentioned. Likewise, please check for issues related to update, as someone else may be working on the issue in a branch or fork.
|
||||
|
||||
* Non-trivial changes should be discussed in an issue first
|
||||
* Develop in a topic branch, not master
|
||||
|
||||
### Linting
|
||||
|
||||
Please check your code using `grunt lint` before submitting your pull requests.
|
||||
|
||||
### Commit Message Format
|
||||
|
||||
Each commit message should include a **type**, a **scope** and a **subject**:
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
```
|
||||
|
||||
Lines should not exceed 100 characters. This allows the message to be easier to read on github as well as in various git tools and produces a nice, neat commit log ie:
|
||||
|
||||
```
|
||||
#271 feat(containers): add exposed ports in the containers view
|
||||
#270 fix(templates): fix a display issue in the templates view
|
||||
#269 style(dashboard): update dashboard with new layout
|
||||
```
|
||||
|
||||
#### Type
|
||||
|
||||
Must be one of the following:
|
||||
|
||||
* **feat**: A new feature
|
||||
* **fix**: A bug fix
|
||||
* **docs**: Documentation only changes
|
||||
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
|
||||
semi-colons, etc)
|
||||
* **refactor**: A code change that neither fixes a bug or adds a feature
|
||||
* **test**: Adding missing tests
|
||||
* **chore**: Changes to the build process or auxiliary tools and libraries such as documentation
|
||||
generation
|
||||
|
||||
#### Scope
|
||||
|
||||
The scope could be anything specifying place of the commit change. For example `networks`,
|
||||
`containers`, `images` etc...
|
||||
|
||||
#### Subject
|
||||
|
||||
The subject contains succinct description of the change:
|
||||
|
||||
* use the imperative, present tense: "change" not "changed" nor "changes"
|
||||
* don't capitalize first letter
|
||||
* no dot (.) at the end
|
||||
@@ -1,8 +1,9 @@
|
||||
FROM scratch
|
||||
FROM centurylink/ca-certs
|
||||
|
||||
COPY dist /
|
||||
|
||||
VOLUME /data
|
||||
|
||||
EXPOSE 9000
|
||||
ENTRYPOINT ["/ui-for-docker"]
|
||||
|
||||
ENTRYPOINT ["/portainer"]
|
||||
|
||||
6
LICENSE
@@ -1,4 +1,4 @@
|
||||
Cloudinovasi-ui: Copyright (c) 2016 Cloudinovasi
|
||||
Portainer: Copyright (c) 2016 Portainer.io
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -16,9 +16,9 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
SOFTWARE.
|
||||
|
||||
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (anthonylapenna at cloudinovasi dot id)
|
||||
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (portainer.io)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
115
README.md
@@ -1,82 +1,83 @@
|
||||
# Cloudinovasi UI for Docker
|
||||
# Portainer
|
||||
|
||||
A fork of the amazing UI for Docker by Michael Crosby and Kevan Ahlquist (https://github.com/kevana/ui-for-docker) using the rdash-angular theme (https://github.com/rdash/rdash-angular).
|
||||
The easiest way to manage Docker.
|
||||
|
||||

|
||||
[](https://microbadger.com/images/portainer/portainer "Latest version on Docker Hub")
|
||||
[](http://microbadger.com/images/portainer/portainer "Image size")
|
||||
[](http://portainer.readthedocs.io/en/stable/?badge=stable)
|
||||
[](https://gitter.im/portainer/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
|
||||
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.
|
||||
Portainer is a lightweight management UI which allows you to **easily** manage your Docker host or Swarm cluster.
|
||||
|
||||
## 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.
|
||||
# Usage
|
||||
|
||||
## Run
|
||||
It's really simple to deploy it using Docker:
|
||||
|
||||
### Quickstart
|
||||
|
||||
1. Run: `docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui`
|
||||
|
||||
2. Open your browser to `http://<dockerd host ip>:9000`
|
||||
|
||||
Bind mounting the Unix socket into the UI For Docker container is much more secure than exposing your docker daemon over TCP.
|
||||
|
||||
The `--privileged` flag is required for hosts using SELinux.
|
||||
|
||||
### Specify socket to connect to Docker daemon
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
# Connect to a tcp socket:
|
||||
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -e http://127.0.0.1:2375
|
||||
```shell
|
||||
$ docker run -d -p 9000:9000 portainer/portainer -H tcp://<DOCKER_HOST>:<DOCKER_PORT>
|
||||
```
|
||||
|
||||
### Swarm support
|
||||
Just point it at your targeted Docker host and then access Portainer by hitting [http://localhost:9000](http://localhost:9000) with a web browser.
|
||||
|
||||
**Supported Swarm version: 1.2.3**
|
||||
If your target is a Docker Swarm cluster or a Docker cluster using *swarm mode*, just add the flag `--swarm`:
|
||||
|
||||
You can access a specific view for you Swarm cluster by defining the `--swarm` flag:
|
||||
|
||||
```
|
||||
# Connect to a tcp socket and enable Swarm:
|
||||
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -e http://<SWARM_HOST>:<SWARM_PORT> --swarm
|
||||
```shell
|
||||
$ docker run -d -p 9000:9000 portainer/portainer -H tcp://<SWARM_HOST>:<SWARM_PORT> --swarm
|
||||
```
|
||||
|
||||
*NOTE*: Due to Swarm not exposing information in a machine readable way, the app is bound to a specific version of Swarm at the moment.
|
||||
If you don't specify any target, its default behaviour is to use a bind mount on the Docker socket so you can easily deploy it to manage your local Docker host:
|
||||
|
||||
### Change address/port UI For Docker is served on
|
||||
UI For Docker listens on port 9000 by default. If you run UI For Docker inside a container then you can bind the container's internal port to any external address and port:
|
||||
|
||||
```
|
||||
# Expose UI For Docker on 10.20.30.1:80
|
||||
$ docker run -d -p 10.20.30.1:80:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui
|
||||
```shell
|
||||
$ docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock portainer/portainer
|
||||
```
|
||||
|
||||
### Hide containers with specific labels
|
||||
Have a look at our [documentation](http://portainer.readthedocs.io/en/stable/deployment.html) for more deployment options.
|
||||
|
||||
You can hide specific containers in the containers view by using the `-hide-label` or `-l` options and specifying a label.
|
||||
# Configuration
|
||||
|
||||
Portainer is easy to tune using CLI flags.
|
||||
|
||||
## Hiding specific containers
|
||||
|
||||
Portainer allows you to hide container with a specific label by using the `-l` flag.
|
||||
|
||||
For example, take a container started with the label `owner=acme`:
|
||||
|
||||
```
|
||||
```shell
|
||||
$ docker run -d --label owner=acme nginx
|
||||
```
|
||||
|
||||
You can hide it in the view by starting the ui with:
|
||||
|
||||
```
|
||||
$ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui -l owner=acme
|
||||
Simply add the `-l owner=acme` option on the CLI when starting Portainer:
|
||||
```shell
|
||||
$ docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock portainer/portainer -l owner=acme
|
||||
```
|
||||
|
||||
### Available options
|
||||
## Use your own templates
|
||||
|
||||
The following options are available for the `ui-for-docker` binary:
|
||||
Portainer allows you to rapidly deploy containers using `App Templates`.
|
||||
|
||||
* `--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*)
|
||||
* `--hide-label`, `-l`: Hide containers with a specific label in the UI
|
||||
By default [Portainer templates](https://raw.githubusercontent.com/portainer/templates/master/templates.json) will be used but you can also define your own templates.
|
||||
|
||||
Add the `--templates` flag and specify the external location of your templates when starting Portainer:
|
||||
|
||||
```shell
|
||||
$ docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock portainer/portainer --templates http://my-host.my-domain/templates.json
|
||||
```
|
||||
|
||||
For more information about hosting your own template definitions and the format, see the [templates documentation](http://portainer.readthedocs.io/en/stable/templates.html).
|
||||
|
||||
Check our [documentation](http://portainer.readthedocs.io/en/stable/configuration.html) for more configuration options.
|
||||
|
||||
# FAQ
|
||||
|
||||
Be sure to check our [FAQ](http://portainer.readthedocs.io/en/stable/faq.html) if you are missing some information.
|
||||
|
||||
# Limitations
|
||||
|
||||
Portainer has full support for the following Docker versions:
|
||||
|
||||
* Docker 1.10 to Docker 1.12 (including `swarm-mode`)
|
||||
* Docker Swarm >= 1.2.3
|
||||
|
||||
Partial support for the following Docker versions (some features may not be available):
|
||||
|
||||
* Docker 1.9
|
||||
|
||||
60
api/api.go
Normal file
@@ -0,0 +1,60 @@
|
||||
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
|
||||
templatesURL string
|
||||
}
|
||||
|
||||
apiConfig struct {
|
||||
Endpoint string
|
||||
BindAddress string
|
||||
AssetPath string
|
||||
DataPath string
|
||||
SwarmSupport bool
|
||||
TLSEnabled bool
|
||||
TLSCACertPath string
|
||||
TLSCertPath string
|
||||
TLSKeyPath string
|
||||
TemplatesURL 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,
|
||||
templatesURL: apiConfig.TemplatesURL,
|
||||
}
|
||||
}
|
||||
48
api/csrf.go
Normal 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
@@ -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
@@ -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
|
||||
}
|
||||
78
api/handler.go
Normal file
@@ -0,0 +1,78 @@
|
||||
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)
|
||||
})
|
||||
mux.HandleFunc("/templates", func(w http.ResponseWriter, r *http.Request) {
|
||||
templatesHandler(w, r, a.templatesURL)
|
||||
})
|
||||
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
@@ -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
|
||||
}
|
||||
47
api/main.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package main // import "github.com/portainer/portainer"
|
||||
|
||||
import (
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
// main is the entry point of the program
|
||||
func main() {
|
||||
kingpin.Version("1.9.2")
|
||||
var (
|
||||
endpoint = kingpin.Flag("host", "Dockerd endpoint").Default("unix:///var/run/docker.sock").Short('H').String()
|
||||
addr = kingpin.Flag("bind", "Address and port to serve Portainer").Default(":9000").Short('p').String()
|
||||
assets = kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String()
|
||||
data = kingpin.Flag("data", "Path to the data").Default(".").Short('d').String()
|
||||
tlsverify = kingpin.Flag("tlsverify", "TLS support").Default("false").Bool()
|
||||
tlscacert = kingpin.Flag("tlscacert", "Path to the CA").Default("/certs/ca.pem").String()
|
||||
tlscert = kingpin.Flag("tlscert", "Path to the TLS certificate file").Default("/certs/cert.pem").String()
|
||||
tlskey = kingpin.Flag("tlskey", "Path to the TLS key").Default("/certs/key.pem").String()
|
||||
swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool()
|
||||
labels = pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l'))
|
||||
logo = kingpin.Flag("logo", "URL for the logo displayed in the UI").String()
|
||||
templates = kingpin.Flag("templates", "URL to the templates (apps) definitions").Default("https://raw.githubusercontent.com/portainer/templates/master/templates.json").Short('t').String()
|
||||
)
|
||||
kingpin.Parse()
|
||||
|
||||
apiConfig := apiConfig{
|
||||
Endpoint: *endpoint,
|
||||
BindAddress: *addr,
|
||||
AssetPath: *assets,
|
||||
DataPath: *data,
|
||||
SwarmSupport: *swarm,
|
||||
TLSEnabled: *tlsverify,
|
||||
TLSCACertPath: *tlscacert,
|
||||
TLSCertPath: *tlscert,
|
||||
TLSKeyPath: *tlskey,
|
||||
TemplatesURL: *templates,
|
||||
}
|
||||
|
||||
settings := &Settings{
|
||||
Swarm: *swarm,
|
||||
HiddenLabels: *labels,
|
||||
Logo: *logo,
|
||||
}
|
||||
|
||||
api := newAPI(apiConfig)
|
||||
api.run(settings)
|
||||
}
|
||||
18
api/settings.go
Normal 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"`
|
||||
}
|
||||
|
||||
// settingsHandler 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
@@ -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
|
||||
}
|
||||
27
api/templates.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// templatesHandler defines a handler function used to retrieve the templates from a URL and put them in the response
|
||||
func templatesHandler(w http.ResponseWriter, r *http.Request, templatesURL string) {
|
||||
resp, err := http.Get(templatesURL)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Error making request to %s: %s", templatesURL, err.Error()), http.StatusInternalServerError)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "Error reading body from templates URL", http.StatusInternalServerError)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(body)
|
||||
}
|
||||
47
api/unix_handler.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
68
app/app.js
@@ -1,25 +1,33 @@
|
||||
angular.module('uifordocker', [
|
||||
'uifordocker.templates',
|
||||
angular.module('portainer', [
|
||||
'portainer.templates',
|
||||
'ui.bootstrap',
|
||||
'ui.router',
|
||||
'ui.select',
|
||||
'ngCookies',
|
||||
'ngSanitize',
|
||||
'dockerui.services',
|
||||
'dockerui.filters',
|
||||
'portainer.services',
|
||||
'portainer.helpers',
|
||||
'portainer.filters',
|
||||
'dashboard',
|
||||
'container',
|
||||
'containerConsole',
|
||||
'containerLogs',
|
||||
'containers',
|
||||
'createContainer',
|
||||
'docker',
|
||||
'events',
|
||||
'images',
|
||||
'image',
|
||||
'containerLogs',
|
||||
'service',
|
||||
'services',
|
||||
'createService',
|
||||
'stats',
|
||||
'swarm',
|
||||
'network',
|
||||
'networks',
|
||||
'createNetwork',
|
||||
'task',
|
||||
'templates',
|
||||
'volumes',
|
||||
'createVolume'])
|
||||
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', function ($stateProvider, $urlRouterProvider, $httpProvider) {
|
||||
@@ -56,6 +64,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",
|
||||
@@ -71,21 +84,31 @@ angular.module('uifordocker', [
|
||||
templateUrl: 'app/components/createContainer/createcontainer.html',
|
||||
controller: 'CreateContainerController'
|
||||
})
|
||||
.state('actions.create.volume', {
|
||||
url: "/volume",
|
||||
templateUrl: 'app/components/createVolume/createvolume.html',
|
||||
controller: 'CreateVolumeController'
|
||||
})
|
||||
.state('actions.create.network', {
|
||||
url: "/network",
|
||||
templateUrl: 'app/components/createNetwork/createnetwork.html',
|
||||
controller: 'CreateNetworkController'
|
||||
})
|
||||
.state('actions.create.service', {
|
||||
url: "/service",
|
||||
templateUrl: 'app/components/createService/createservice.html',
|
||||
controller: 'CreateServiceController'
|
||||
})
|
||||
.state('actions.create.volume', {
|
||||
url: "/volume",
|
||||
templateUrl: 'app/components/createVolume/createvolume.html',
|
||||
controller: 'CreateVolumeController'
|
||||
})
|
||||
.state('docker', {
|
||||
url: '/docker/',
|
||||
templateUrl: 'app/components/docker/docker.html',
|
||||
controller: 'DockerController'
|
||||
})
|
||||
.state('events', {
|
||||
url: '/events/',
|
||||
templateUrl: 'app/components/events/events.html',
|
||||
controller: 'EventsController'
|
||||
})
|
||||
.state('images', {
|
||||
url: '/images/',
|
||||
templateUrl: 'app/components/images/images.html',
|
||||
@@ -106,6 +129,26 @@ angular.module('uifordocker', [
|
||||
templateUrl: 'app/components/network/network.html',
|
||||
controller: 'NetworkController'
|
||||
})
|
||||
.state('services', {
|
||||
url: '/services/',
|
||||
templateUrl: 'app/components/services/services.html',
|
||||
controller: 'ServicesController'
|
||||
})
|
||||
.state('service', {
|
||||
url: '^/service/:id/',
|
||||
templateUrl: 'app/components/service/service.html',
|
||||
controller: 'ServiceController'
|
||||
})
|
||||
.state('task', {
|
||||
url: '^/task/:id',
|
||||
templateUrl: 'app/components/task/task.html',
|
||||
controller: 'TaskController'
|
||||
})
|
||||
.state('templates', {
|
||||
url: '/templates/',
|
||||
templateUrl: 'app/components/templates/templates.html',
|
||||
controller: 'TemplatesController'
|
||||
})
|
||||
.state('volumes', {
|
||||
url: '/volumes/',
|
||||
templateUrl: 'app/components/volumes/volumes.html',
|
||||
@@ -143,5 +186,6 @@ 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.2.0');
|
||||
.constant('CONFIG_ENDPOINT', 'settings')
|
||||
.constant('TEMPLATES_ENDPOINT', 'templates')
|
||||
.constant('UI_VERSION', 'v1.9.2');
|
||||
|
||||
@@ -1,71 +1,25 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Container details"></rd-header-title>
|
||||
<rd-header-title title="Container details">
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-6 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon grey pull-left">
|
||||
<i class="fa fa-tasks"></i>
|
||||
</div>
|
||||
<div ng-if="!container.edit">
|
||||
<div class="title">{{ container.Name|trimcontainername }}</div>
|
||||
<div class="comment">
|
||||
Name <a href="" ng-click="container.edit = true;"><i class="fa fa-edit"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="container.edit">
|
||||
<div class="title"><input type="text" class="containerNameInput" ng-model="container.newContainerName"></div>
|
||||
<div class="comment">
|
||||
Name
|
||||
<a href="" ng-click="container.edit = false;"><i class="fa fa-times"></i></a>
|
||||
<a href="" ng-click="renameContainer()"><i class="fa fa-check-square-o"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div ng-class="{true: 'widget-icon green pull-left', false: 'widget-icon red pull-left'}[container.State.Running]">
|
||||
<i class="fa fa-heartbeat"></i>
|
||||
</div>
|
||||
<div class="title">{{ container.State|getstatetext }}</div>
|
||||
<div class="comment">State</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon grey pull-left">
|
||||
<i class="fa fa-cogs"></i>
|
||||
</div>
|
||||
<div class="title">
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<button class="btn btn-primary" ng-click="commit()">Commit</button>
|
||||
<button class="btn btn-primary" ng-click="start()" ng-disabled="container.State.Running">Start</button>
|
||||
<button class="btn btn-primary" ng-click="stop()" ng-disabled="!container.State.Running">Stop</button>
|
||||
<button class="btn btn-primary" ng-click="kill()" ng-disabled="!container.State.Running">Kill</button>
|
||||
<button class="btn btn-primary" ng-click="restart()">Restart</button>
|
||||
<button class="btn btn-primary" ng-click="pause()" ng-disabled="!container.State.Running && !container.State.Paused">Pause</button>
|
||||
<button class="btn btn-primary" ng-click="unpause()" ng-disabled="!container.State.Paused">Unpause</button>
|
||||
<button class="btn btn-danger" ng-click="remove()" ng-disabled="container.State.Running">Remove</button>
|
||||
</div>
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<a class="btn btn-default" type="button" ui-sref="stats({id: container.Id})">Stats</a>
|
||||
<a class="btn btn-default" type="button" ui-sref="logs({id: container.Id})">Logs</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment">
|
||||
Actions
|
||||
<rd-widget-header icon="fa-cogs" title="Actions"></rd-widget-header>
|
||||
<rd-widget-body classes="padding">
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<button class="btn btn-primary" ng-click="start()" ng-if="!container.State.Running"><i class="fa fa-play btn-ico" aria-hidden="true"></i>Start</button>
|
||||
<button class="btn btn-danger" ng-click="stop()" ng-if="container.State.Running"><i class="fa fa-stop btn-ico" aria-hidden="true"></i>Stop</button>
|
||||
<button class="btn btn-danger" ng-click="kill()" ng-if="container.State.Running"><i class="fa fa-bomb btn-ico" aria-hidden="true"></i>Kill</button>
|
||||
<button class="btn btn-primary" ng-click="restart()" ng-if="container.State.Running"><i class="fa fa-refresh btn-ico" aria-hidden="true"></i>Restart</button>
|
||||
<button class="btn btn-primary" ng-click="pause()" ng-if="container.State.Running && !container.State.Paused"><i class="fa fa-pause btn-ico" aria-hidden="true"></i>Pause</button>
|
||||
<button class="btn btn-primary" ng-click="unpause()" ng-if="container.State.Paused"><i class="fa fa-play btn-ico" aria-hidden="true"></i>Resume</button>
|
||||
<button class="btn btn-danger" ng-click="remove()" ng-disabled="container.State.Running"><i class="fa fa-trash btn-ico" aria-hidden="true"></i>Remove</button>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
@@ -73,44 +27,144 @@
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-9">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Container status"></rd-widget-header>
|
||||
<rd-widget-header icon="fa-server" title="Container status"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Created</td>
|
||||
<td>{{ container.Created | date: 'medium' }}</td>
|
||||
<td>Name</td>
|
||||
<td ng-if="!container.edit">
|
||||
{{ container.Name|trimcontainername }}
|
||||
<a href="" data-toggle="tooltip" title="Edit container name" ng-click="container.edit = true;"><i class="fa fa-edit"></i></a>
|
||||
</td>
|
||||
<td ng-if="container.edit">
|
||||
<input type="text" class="containerNameInput" ng-model="container.newContainerName">
|
||||
<a href="" ng-click="container.edit = false;"><i class="fa fa-times"></i></a>
|
||||
<a href="" ng-click="renameContainer()"><i class="fa fa-check-square-o"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="container.NetworkSettings.IPAddress">
|
||||
<td>IP address</td>
|
||||
<td>{{ container.NetworkSettings.IPAddress }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Path</td>
|
||||
<td>{{ container.Path }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Args</td>
|
||||
<td>{{ container.Args.join(' ') || 'None' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Exposed Ports</td>
|
||||
<td>Status</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li ng-repeat="(k, v) in container.Config.ExposedPorts">{{ k }}</li>
|
||||
</ul>
|
||||
<i ng-class="{true: 'fa fa-heartbeat text-icon green-icon', false: 'fa fa-heartbeat text-icon red-icon'}[container.State.Running]"></i>
|
||||
{{ container.State|getstatetext }} since {{ activityTime }}<span ng-if="!container.State.Running"> with exit code {{ container.State.ExitCode }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="container.State.Running">
|
||||
<td>Start time</td>
|
||||
<td>{{ container.State.StartedAt|getisodate }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!container.State.Running">
|
||||
<td>Finished</td>
|
||||
<td>{{ container.State.FinishedAt|getisodate }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<a class="btn btn-outline-secondary" type="button" ui-sref="stats({id: container.Id})"><i class="fa fa-area-chart btn-ico" aria-hidden="true"></i>Stats</a>
|
||||
<a class="btn btn-outline-secondary" type="button" ui-sref="logs({id: container.Id})"><i class="fa fa-exclamation-circle btn-ico" aria-hidden="true"></i>Logs</a>
|
||||
<a class="btn btn-outline-secondary" type="button" ui-sref="console({id: container.Id})"><i class="fa fa-terminal btn-ico" aria-hidden="true"></i>Console</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-clone" title="Create image"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<!-- tag-description -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">
|
||||
You can create an image from this container, this allows you to backup important data or save
|
||||
helpful configurations. You'll be able to spin up another container based on this image afterward.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !tag-description -->
|
||||
<!-- name-and-registry-inputs -->
|
||||
<div class="form-group">
|
||||
<label for="image_name" class="col-sm-1 control-label text-left">Name</label>
|
||||
<div class="col-sm-7">
|
||||
<input type="text" class="form-control" ng-model="config.Image" id="image_name" placeholder="e.g. myImage:myTag">
|
||||
</div>
|
||||
<label for="image_registry" class="col-sm-1 control-label text-left">Registry</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" ng-model="config.Registry" id="image_registry" placeholder="optional">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-and-registry-inputs -->
|
||||
<!-- tag-note -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">Note: if you don't specify the tag in the image name, <span class="label label-default">latest</span> will be used.</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !tag-note -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-default btn-sm" ng-disabled="!config.Image" ng-click="commit()">Create</button>
|
||||
<i id="createImageSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-server" title="Container details"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Image</td>
|
||||
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
|
||||
</tr>
|
||||
<tr ng-if="portBindings.length > 0">
|
||||
<td>Port configuration</td>
|
||||
<td>
|
||||
<div ng-repeat="portMapping in portBindings">
|
||||
{{ portMapping.container }} <i class="fa fa-long-arrow-right"></i> {{ portMapping.host }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Environment</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li ng-repeat="k in container.Config.Env">{{ k }}</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>CMD</td>
|
||||
<td><code>{{ container.Config.Cmd|command }}</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ENV</td>
|
||||
<td>
|
||||
<table class="table table-bordered table-condensed">
|
||||
<tr ng-repeat="var in container.Config.Env">
|
||||
<td>{{ var|key: '=' }}</td>
|
||||
<td>{{ var|value: '=' }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!(container.Config.Labels | emptyobject)">
|
||||
<td>Labels</td>
|
||||
<td>
|
||||
<table role="table" class="table">
|
||||
<table class="table table-bordered table-condensed">
|
||||
<tr ng-repeat="(k, v) in container.Config.Labels">
|
||||
<td>{{ k }}</td>
|
||||
<td>{{ v }}</td>
|
||||
@@ -118,76 +172,33 @@
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Publish all ports</td>
|
||||
<td>{{ container.HostConfig.PublishAllPorts }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Ports</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li ng-repeat="(containerport, hostports) in container.NetworkSettings.Ports">
|
||||
{{ containerport }} =>
|
||||
<span class="label label-default" style="margin-right: 5px;" ng-repeat="(k,v) in hostports">{{ v.HostIp }}:{{ v.HostPort }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Hostname</td>
|
||||
<td>{{ container.Config.Hostname }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IPAddress</td>
|
||||
<td>{{ container.NetworkSettings.IPAddress }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cmd</td>
|
||||
<td>{{ container.Config.Cmd }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Entrypoint</td>
|
||||
<td>{{ container.Config.Entrypoint.join(' ') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bindings</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li ng-repeat="b in container.HostConfig.Binds">{{ b }}</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Volumes</td>
|
||||
<td>{{ container.Volumes }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>SysInitpath</td>
|
||||
<td>{{ container.SysInitPath }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Image</td>
|
||||
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="container.HostConfig.Binds.length > 0">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Container state details"></rd-widget-header>
|
||||
<rd-widget-header icon="fa-cubes" title="Volumes"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Host</th>
|
||||
<th>Container</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="(key, val) in container.State">
|
||||
<td>{{key}}</td>
|
||||
<td>{{ val }}</td>
|
||||
<tr ng-repeat="vol in container.HostConfig.Binds">
|
||||
<td>{{ vol|key: ':' }}</td>
|
||||
<td>{{ vol|value: ':' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
|
||||
@@ -1,208 +1,157 @@
|
||||
angular.module('container', [])
|
||||
.controller('ContainerController', ['$scope', '$stateParams', '$state', '$filter', 'Container', 'ContainerCommit', 'Image', 'Messages', 'ViewSpinner', '$timeout',
|
||||
function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Image, Messages, ViewSpinner, $timeout) {
|
||||
$scope.changes = [];
|
||||
$scope.editEnv = false;
|
||||
$scope.editPorts = false;
|
||||
$scope.editBinds = false;
|
||||
$scope.newCfg = {
|
||||
Env: [],
|
||||
Ports: {}
|
||||
.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ImageHelper', 'Messages',
|
||||
function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ImageHelper, Messages) {
|
||||
$scope.activityTime = 0;
|
||||
$scope.portBindings = [];
|
||||
$scope.config = {
|
||||
Image: '',
|
||||
Registry: ''
|
||||
};
|
||||
|
||||
var update = function () {
|
||||
ViewSpinner.spin();
|
||||
$('#loadingViewSpinner').show();
|
||||
Container.get({id: $stateParams.id}, function (d) {
|
||||
$scope.container = d;
|
||||
$scope.container.edit = false;
|
||||
$scope.container.newContainerName = $filter('trimcontainername')(d.Name);
|
||||
|
||||
// fill up env
|
||||
if (d.Config.Env) {
|
||||
$scope.newCfg.Env = d.Config.Env.map(function (entry) {
|
||||
return {name: entry.split('=')[0], value: entry.split('=')[1]};
|
||||
if (d.State.Running) {
|
||||
$scope.activityTime = moment.duration(moment(d.State.StartedAt).utc().diff(moment().utc())).humanize();
|
||||
} else {
|
||||
$scope.activityTime = moment.duration(moment().utc().diff(moment(d.State.FinishedAt).utc())).humanize();
|
||||
}
|
||||
|
||||
$scope.portBindings = [];
|
||||
if (d.NetworkSettings.Ports) {
|
||||
angular.forEach(Object.keys(d.NetworkSettings.Ports), function(portMapping) {
|
||||
if (d.NetworkSettings.Ports[portMapping]) {
|
||||
var mapping = {};
|
||||
mapping.container = portMapping;
|
||||
mapping.host = d.NetworkSettings.Ports[portMapping][0].HostIp + ':' + d.NetworkSettings.Ports[portMapping][0].HostPort;
|
||||
$scope.portBindings.push(mapping);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// fill up ports
|
||||
$scope.newCfg.Ports = {};
|
||||
angular.forEach(d.Config.ExposedPorts, function(i, port) {
|
||||
if (d.HostConfig.PortBindings && port in d.HostConfig.PortBindings) {
|
||||
$scope.newCfg.Ports[port] = d.HostConfig.PortBindings[port];
|
||||
}
|
||||
else {
|
||||
$scope.newCfg.Ports[port] = [];
|
||||
}
|
||||
});
|
||||
|
||||
// fill up bindings
|
||||
$scope.newCfg.Binds = [];
|
||||
var defaultBinds = {};
|
||||
angular.forEach(d.Config.Volumes, function(value, vol) {
|
||||
defaultBinds[vol] = { ContPath: vol, HostPath: '', ReadOnly: false, DefaultBind: true };
|
||||
});
|
||||
angular.forEach(d.HostConfig.Binds, function(binding, i) {
|
||||
var mountpoint = binding.split(':')[0];
|
||||
var vol = binding.split(':')[1] || '';
|
||||
var ro = binding.split(':').length > 2 && binding.split(':')[2] === 'ro';
|
||||
var defaultBind = false;
|
||||
if (vol === '') {
|
||||
vol = mountpoint;
|
||||
mountpoint = '';
|
||||
}
|
||||
|
||||
if (vol in defaultBinds) {
|
||||
delete defaultBinds[vol];
|
||||
defaultBind = true;
|
||||
}
|
||||
$scope.newCfg.Binds.push({ ContPath: vol, HostPath: mountpoint, ReadOnly: ro, DefaultBind: defaultBind });
|
||||
});
|
||||
angular.forEach(defaultBinds, function(bind) {
|
||||
$scope.newCfg.Binds.push(bind);
|
||||
});
|
||||
|
||||
ViewSpinner.stop();
|
||||
$('#loadingViewSpinner').hide();
|
||||
}, function (e) {
|
||||
if (e.status === 404) {
|
||||
$('.detail').hide();
|
||||
Messages.error("Not found", "Container not found.");
|
||||
} else {
|
||||
Messages.error("Failure", e.data);
|
||||
}
|
||||
ViewSpinner.stop();
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to retrieve container info");
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
$scope.start = function () {
|
||||
ViewSpinner.spin();
|
||||
Container.start({
|
||||
id: $scope.container.Id,
|
||||
HostConfig: $scope.container.HostConfig
|
||||
}, function (d) {
|
||||
$('#loadingViewSpinner').show();
|
||||
Container.start({id: $scope.container.Id}, {}, function (d) {
|
||||
update();
|
||||
Messages.send("Container started", $stateParams.id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", "Container failed to start." + e.data);
|
||||
Messages.error("Failure", e, "Unable to start container");
|
||||
});
|
||||
};
|
||||
|
||||
$scope.stop = function () {
|
||||
ViewSpinner.spin();
|
||||
$('#loadingViewSpinner').show();
|
||||
Container.stop({id: $stateParams.id}, function (d) {
|
||||
update();
|
||||
Messages.send("Container stopped", $stateParams.id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", "Container failed to stop." + e.data);
|
||||
Messages.error("Failure", e, "Unable to stop container");
|
||||
});
|
||||
};
|
||||
|
||||
$scope.kill = function () {
|
||||
ViewSpinner.spin();
|
||||
$('#loadingViewSpinner').show();
|
||||
Container.kill({id: $stateParams.id}, function (d) {
|
||||
update();
|
||||
Messages.send("Container killed", $stateParams.id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", "Container failed to die." + e.data);
|
||||
Messages.error("Failure", e, "Unable to kill container");
|
||||
});
|
||||
};
|
||||
|
||||
$scope.commit = function () {
|
||||
ViewSpinner.spin();
|
||||
ContainerCommit.commit({id: $stateParams.id, repo: $scope.container.Config.Image}, function (d) {
|
||||
$('#createImageSpinner').show();
|
||||
var image = _.toLower($scope.config.Image);
|
||||
var registry = _.toLower($scope.config.Registry);
|
||||
var imageConfig = ImageHelper.createImageConfig(image, registry);
|
||||
ContainerCommit.commit({id: $stateParams.id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) {
|
||||
$('#createImageSpinner').hide();
|
||||
update();
|
||||
Messages.send("Container commited", $stateParams.id);
|
||||
}, function (e) {
|
||||
$('#createImageSpinner').hide();
|
||||
update();
|
||||
Messages.error("Failure", "Container failed to commit." + e.data);
|
||||
Messages.error("Failure", e, "Unable to commit container");
|
||||
});
|
||||
};
|
||||
|
||||
$scope.pause = function () {
|
||||
ViewSpinner.spin();
|
||||
$('#loadingViewSpinner').show();
|
||||
Container.pause({id: $stateParams.id}, function (d) {
|
||||
update();
|
||||
Messages.send("Container paused", $stateParams.id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", "Container failed to pause." + e.data);
|
||||
Messages.error("Failure", e, "Unable to pause container");
|
||||
});
|
||||
};
|
||||
|
||||
$scope.unpause = function () {
|
||||
ViewSpinner.spin();
|
||||
$('#loadingViewSpinner').show();
|
||||
Container.unpause({id: $stateParams.id}, function (d) {
|
||||
update();
|
||||
Messages.send("Container unpaused", $stateParams.id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", "Container failed to unpause." + e.data);
|
||||
Messages.error("Failure", e, "Unable to unpause container");
|
||||
});
|
||||
};
|
||||
|
||||
$scope.remove = function () {
|
||||
ViewSpinner.spin();
|
||||
$('#loadingViewSpinner').show();
|
||||
Container.remove({id: $stateParams.id}, function (d) {
|
||||
update();
|
||||
$state.go('containers', {}, {reload: true});
|
||||
Messages.send("Container removed", $stateParams.id);
|
||||
if (d.message) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.send("Error", d.message);
|
||||
}
|
||||
else {
|
||||
$state.go('containers', {}, {reload: true});
|
||||
Messages.send("Container removed", $stateParams.id);
|
||||
}
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", "Container failed to remove." + e.data);
|
||||
Messages.error("Failure", e, "Unable to remove container");
|
||||
});
|
||||
};
|
||||
|
||||
$scope.restart = function () {
|
||||
ViewSpinner.spin();
|
||||
$('#loadingViewSpinner').show();
|
||||
Container.restart({id: $stateParams.id}, function (d) {
|
||||
update();
|
||||
Messages.send("Container restarted", $stateParams.id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", "Container failed to restart." + e.data);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.hasContent = function (data) {
|
||||
return data !== null && data !== undefined;
|
||||
};
|
||||
|
||||
$scope.getChanges = function () {
|
||||
ViewSpinner.spin();
|
||||
Container.changes({id: $stateParams.id}, function (d) {
|
||||
$scope.changes = d;
|
||||
ViewSpinner.stop();
|
||||
Messages.error("Failure", e, "Unable to restart container");
|
||||
});
|
||||
};
|
||||
|
||||
$scope.renameContainer = function () {
|
||||
// #FIXME fix me later to handle http status to show the correct error message
|
||||
Container.rename({id: $stateParams.id, 'name': $scope.container.newContainerName}, function (data) {
|
||||
if (data.name) {
|
||||
$scope.container.Name = data.name;
|
||||
Messages.send("Container renamed", $stateParams.id);
|
||||
} else {
|
||||
Container.rename({id: $stateParams.id, 'name': $scope.container.newContainerName}, function (d) {
|
||||
if (d.message) {
|
||||
$scope.container.newContainerName = $scope.container.Name;
|
||||
Messages.error("Failure", "Container failed to rename.");
|
||||
Messages.error("Unable to rename container", {}, d.message);
|
||||
} else {
|
||||
$scope.container.Name = $scope.container.newContainerName;
|
||||
Messages.send("Container successfully renamed", d.name);
|
||||
}
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e, 'Unable to rename container');
|
||||
});
|
||||
$scope.container.edit = false;
|
||||
};
|
||||
|
||||
$scope.addEntry = function (array, entry) {
|
||||
array.push(entry);
|
||||
};
|
||||
$scope.rmEntry = function (array, entry) {
|
||||
var idx = array.indexOf(entry);
|
||||
array.splice(idx, 1);
|
||||
};
|
||||
|
||||
$scope.toggleEdit = function() {
|
||||
$scope.edit = !$scope.edit;
|
||||
};
|
||||
|
||||
update();
|
||||
$scope.getChanges();
|
||||
}]);
|
||||
|
||||
43
app/components/containerConsole/containerConsole.html
Normal 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>
|
||||
106
app/components/containerConsole/containerConsoleController.js
Normal file
@@ -0,0 +1,106 @@
|
||||
angular.module('containerConsole', [])
|
||||
.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Settings', 'Container', 'Exec', '$timeout', 'Messages',
|
||||
function ($scope, $stateParams, Settings, Container, Exec, $timeout, Messages) {
|
||||
$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 && 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.message) {
|
||||
$('#loadConsoleSpinner').hide();
|
||||
Messages.error("Error", {}, d.message);
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}, function (e) {
|
||||
$('#loadConsoleSpinner').hide();
|
||||
Messages.error("Failure", e, 'Unable to start an exec instance');
|
||||
});
|
||||
};
|
||||
|
||||
$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) {
|
||||
if (d.message) {
|
||||
Messages.error('Error', {}, 'Unable to resize TTY');
|
||||
}
|
||||
}, function (e) {
|
||||
Messages.error("Failure", {}, '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();
|
||||
|
||||
term.on('data', function (data) {
|
||||
socket.send(data);
|
||||
});
|
||||
term.open(document.getElementById('terminal-container'));
|
||||
term.resize(width, height);
|
||||
term.setOption('cursorBlink', true);
|
||||
|
||||
socket.onmessage = function (e) {
|
||||
term.write(e.data);
|
||||
};
|
||||
socket.onerror = function (error) {
|
||||
$scope.connected = false;
|
||||
|
||||
};
|
||||
socket.onclose = function(evt) {
|
||||
$scope.connected = false;
|
||||
};
|
||||
};
|
||||
}
|
||||
}]);
|
||||
@@ -1,6 +1,6 @@
|
||||
angular.module('containerLogs', [])
|
||||
.controller('ContainerLogsController', ['$scope', '$stateParams', '$anchorScroll', 'ContainerLogs', 'Container', 'ViewSpinner',
|
||||
function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container, ViewSpinner) {
|
||||
.controller('ContainerLogsController', ['$scope', '$stateParams', '$anchorScroll', 'ContainerLogs', 'Container',
|
||||
function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container) {
|
||||
$scope.state = {};
|
||||
$scope.state.displayTimestampsOut = false;
|
||||
$scope.state.displayTimestampsErr = false;
|
||||
@@ -8,24 +8,20 @@ function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container, ViewSpi
|
||||
$scope.stderr = '';
|
||||
$scope.tailLines = 2000;
|
||||
|
||||
ViewSpinner.spin();
|
||||
$('#loadingViewSpinner').show();
|
||||
Container.get({id: $stateParams.id}, function (d) {
|
||||
$scope.container = d;
|
||||
ViewSpinner.stop();
|
||||
$('#loadingViewSpinner').hide();
|
||||
}, function (e) {
|
||||
if (e.status === 404) {
|
||||
Messages.error("Not found", "Container not found.");
|
||||
} else {
|
||||
Messages.error("Failure", e.data);
|
||||
}
|
||||
ViewSpinner.stop();
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to retrieve container info");
|
||||
});
|
||||
|
||||
function getLogs() {
|
||||
ViewSpinner.spin();
|
||||
$('#loadingViewSpinner').show();
|
||||
getLogsStdout();
|
||||
getLogsStderr();
|
||||
ViewSpinner.stop();
|
||||
$('#loadingViewSpinner').hide();
|
||||
}
|
||||
|
||||
function getLogsStderr() {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Container logs"></rd-header-title>
|
||||
<rd-header-title title="Container logs">
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Logs
|
||||
</rd-header-content>
|
||||
@@ -10,7 +12,7 @@
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon grey pull-left">
|
||||
<i class="fa fa-tasks"></i>
|
||||
<i class="fa fa-server"></i>
|
||||
</div>
|
||||
<div class="title">{{ container.Name|trimcontainername }}</div>
|
||||
<div class="comment">Name</div>
|
||||
|
||||
@@ -9,23 +9,26 @@
|
||||
|
||||
<div class="col-lg-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Containers">
|
||||
<rd-widget-header icon="fa-server" title="Containers">
|
||||
<div class="pull-right">
|
||||
<i id="loadContainersSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-lg-12">
|
||||
<div class="pull-left">
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<button type="button" class="btn btn-primary" ng-click="startAction()" ng-disabled="!state.selectedItemCount">Start</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="stopAction()" ng-disabled="!state.selectedItemCount">Stop</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="killAction()" ng-disabled="!state.selectedItemCount">Kill</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="restartAction()" ng-disabled="!state.selectedItemCount">Restart</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="pauseAction()" ng-disabled="!state.selectedItemCount">Pause</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="unpauseAction()" ng-disabled="!state.selectedItemCount">Unpause</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount">Remove</button>
|
||||
<button type="button" class="btn btn-primary btn-responsive" ng-click="startAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-play btn-ico" aria-hidden="true"></i>Start</button>
|
||||
<button type="button" class="btn btn-primary btn-responsive" ng-click="stopAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-stop btn-ico" aria-hidden="true"></i>Stop</button>
|
||||
<button type="button" class="btn btn-primary btn-responsive" ng-click="killAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-bomb btn-ico" aria-hidden="true"></i>Kill</button>
|
||||
<button type="button" class="btn btn-primary btn-responsive" ng-click="restartAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-refresh btn-ico" aria-hidden="true"></i>Restart</button>
|
||||
<button type="button" class="btn btn-primary btn-responsive" ng-click="pauseAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-pause btn-ico" aria-hidden="true"></i>Pause</button>
|
||||
<button type="button" class="btn btn-primary btn-responsive" ng-click="unpauseAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-play btn-ico" aria-hidden="true"></i>Resume</button>
|
||||
<button type="button" class="btn btn-danger btn-responsive" ng-click="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash btn-ico" aria-hidden="true"></i>Remove</button>
|
||||
</div>
|
||||
<a class="btn btn-default" type="button" ui-sref="actions.create.container">Add container</a>
|
||||
<a class="btn btn-default btn-responsive" type="button" ui-sref="actions.create.container">Add container</a>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<input type="checkbox" ng-model="state.displayAll" id="displayAll" ng-change="toggleGetAll()"/><label for="displayAll">Display All</label>
|
||||
<input type="checkbox" ng-model="state.displayAll" id="displayAll" ng-change="toggleGetAll()" style="margin-top: -2px; margin-right: 5px;"/><label for="displayAll">Show all containers</label>
|
||||
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
|
||||
</div>
|
||||
</rd-widget-taskbar>
|
||||
@@ -36,10 +39,10 @@
|
||||
<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>
|
||||
<span ng-show="sortType == 'Status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
@@ -49,20 +52,6 @@
|
||||
<span ng-show="sortType == 'Names' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="containers" ng-click="order('IP')">
|
||||
IP Address
|
||||
<span ng-show="sortType == 'IP' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'IP' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th ng-if="swarm">
|
||||
<a ui-sref="containers" ng-click="order('Host')">
|
||||
Host
|
||||
<span ng-show="sortType == 'Host' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Host' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="containers" ng-click="order('Image')">
|
||||
Image
|
||||
@@ -70,11 +59,25 @@
|
||||
<span ng-show="sortType == 'Image' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th ng-if="state.displayIP">
|
||||
<a ui-sref="containers" ng-click="order('IP')">
|
||||
IP Address
|
||||
<span ng-show="sortType == 'IP' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'IP' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th ng-if="swarm && !swarm_mode">
|
||||
<a ui-sref="containers" ng-click="order('Host')">
|
||||
Host IP
|
||||
<span ng-show="sortType == 'Host' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Host' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="containers" ng-click="order('Command')">
|
||||
Command
|
||||
<span ng-show="sortType == 'Command' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Command' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
<a ui-sref="containers" ng-click="order('Ports')">
|
||||
Exposed Ports
|
||||
<span ng-show="sortType == 'Ports' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Ports' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
@@ -82,13 +85,21 @@
|
||||
<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 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><span class="label label-{{ container.Status|containerstatusbadge }}">{{ container.Status|containerstatus }}</span></td>
|
||||
<td ng-if="swarm && !swarm_mode"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername}}</a></td>
|
||||
<td ng-if="!swarm || swarm_mode"><a ui-sref="container({id: container.Id})">{{ container|containername}}</a></td>
|
||||
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
|
||||
<td>{{ container.Command|truncate:60 }}</td>
|
||||
<td ng-if="state.displayIP">{{ container.IP ? container.IP : '-' }}</td>
|
||||
<td ng-if="swarm && !swarm_mode">{{ container.hostIP }}</td>
|
||||
<td>
|
||||
<a ng-if="container.Ports.length > 0" ng-repeat="p in container.Ports" class="image-tag" ng-href="http://{{p.host}}:{{p.public}}" target="_blank">
|
||||
<i class="fa fa-external-link" aria-hidden="true"></i> {{p.public}}:{{ p.private }}
|
||||
</a>
|
||||
<span ng-if="container.Ports.length == 0" >-</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="containers.length == 0">
|
||||
<td colspan="8" class="text-center text-muted">No containers available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
angular.module('containers', [])
|
||||
.controller('ContainersController', ['$scope', 'Container', 'Settings', 'Messages', 'ViewSpinner', 'Config', 'errorMsgFilter',
|
||||
function ($scope, Container, Settings, Messages, ViewSpinner, Config, errorMsgFilter) {
|
||||
|
||||
.controller('ContainersController', ['$scope', 'Container', 'ContainerHelper', 'Info', 'Settings', 'Messages', 'Config',
|
||||
function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config) {
|
||||
$scope.state = {};
|
||||
$scope.state.displayAll = Settings.displayAll;
|
||||
$scope.state.displayIP = false;
|
||||
$scope.sortType = 'State';
|
||||
$scope.sortReverse = true;
|
||||
$scope.sortReverse = false;
|
||||
$scope.state.selectedItemCount = 0;
|
||||
$scope.swarm_mode = false;
|
||||
$scope.containers = [];
|
||||
|
||||
$scope.order = function (sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
@@ -14,27 +16,34 @@ function ($scope, Container, Settings, Messages, ViewSpinner, Config, errorMsgFi
|
||||
};
|
||||
|
||||
var update = function (data) {
|
||||
ViewSpinner.spin();
|
||||
$('#loadContainersSpinner').show();
|
||||
$scope.state.selectedItemCount = 0;
|
||||
Container.query(data, function (d) {
|
||||
var containers = d;
|
||||
if (hiddenLabels) {
|
||||
containers = hideContainers(d);
|
||||
if ($scope.containersToHideLabels) {
|
||||
containers = ContainerHelper.hideContainers(d, $scope.containersToHideLabels);
|
||||
}
|
||||
$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 && !$scope.swarm_mode) {
|
||||
model.hostIP = $scope.swarm_hosts[_.split(container.Names[0], '/')[1]];
|
||||
}
|
||||
return model;
|
||||
});
|
||||
ViewSpinner.stop();
|
||||
$('#loadContainersSpinner').hide();
|
||||
});
|
||||
};
|
||||
|
||||
var batch = function (items, action, msg) {
|
||||
ViewSpinner.spin();
|
||||
$('#loadContainersSpinner').show();
|
||||
var counter = 0;
|
||||
var complete = function () {
|
||||
counter = counter - 1;
|
||||
if (counter === 0) {
|
||||
ViewSpinner.stop();
|
||||
$('#loadContainersSpinner').hide();
|
||||
update({all: Settings.displayAll ? 1 : 0});
|
||||
}
|
||||
};
|
||||
@@ -42,37 +51,25 @@ function ($scope, Container, Settings, Messages, ViewSpinner, Config, errorMsgFi
|
||||
if (c.Checked) {
|
||||
counter = counter + 1;
|
||||
if (action === Container.start) {
|
||||
Container.get({id: c.Id}, function (d) {
|
||||
c = d;
|
||||
action({id: c.Id, HostConfig: c.HostConfig || {}}, function (d) {
|
||||
Messages.send("Container " + msg, c.Id);
|
||||
complete();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e.data);
|
||||
complete();
|
||||
});
|
||||
action({id: c.Id}, {}, function (d) {
|
||||
Messages.send("Container " + msg, c.Id);
|
||||
complete();
|
||||
}, function (e) {
|
||||
if (e.status === 404) {
|
||||
$('.detail').hide();
|
||||
Messages.error("Not found", "Container not found.");
|
||||
} else {
|
||||
Messages.error("Failure", e.data);
|
||||
}
|
||||
Messages.error("Failure", e, "Unable to start container");
|
||||
complete();
|
||||
});
|
||||
}
|
||||
else if (action === Container.remove) {
|
||||
action({id: c.Id}, function (d) {
|
||||
var error = errorMsgFilter(d);
|
||||
if (error) {
|
||||
Messages.send("Error", "Unable to remove running container");
|
||||
if (d.message) {
|
||||
Messages.send("Error", d.message);
|
||||
}
|
||||
else {
|
||||
Messages.send("Container " + msg, c.Id);
|
||||
}
|
||||
complete();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e.data);
|
||||
Messages.error("Failure", e, 'Unable to remove container');
|
||||
complete();
|
||||
});
|
||||
}
|
||||
@@ -81,7 +78,7 @@ function ($scope, Container, Settings, Messages, ViewSpinner, Config, errorMsgFi
|
||||
Messages.send("Container " + msg, c.Id);
|
||||
complete();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e.data);
|
||||
Messages.error("Failure", e, 'An error occured');
|
||||
complete();
|
||||
});
|
||||
|
||||
@@ -89,7 +86,7 @@ function ($scope, Container, Settings, Messages, ViewSpinner, Config, errorMsgFi
|
||||
}
|
||||
});
|
||||
if (counter === 0) {
|
||||
ViewSpinner.stop();
|
||||
$('#loadContainersSpinner').hide();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -134,25 +131,36 @@ function ($scope, Container, Settings, Messages, ViewSpinner, Config, errorMsgFi
|
||||
batch($scope.containers, Container.remove, "Removed");
|
||||
};
|
||||
|
||||
var hideContainers = function (containers) {
|
||||
return containers.filter(function (container) {
|
||||
var filterContainer = false;
|
||||
hiddenLabels.forEach(function(label, index) {
|
||||
if (_.has(container.Labels, label.name) &&
|
||||
container.Labels[label.name] === label.value) {
|
||||
filterContainer = true;
|
||||
}
|
||||
});
|
||||
if (!filterContainer) {
|
||||
return container;
|
||||
}
|
||||
});
|
||||
};
|
||||
function retrieveSwarmHostsInfo(data) {
|
||||
var swarm_hosts = {};
|
||||
var systemStatus = data.SystemStatus;
|
||||
var node_count = parseInt(systemStatus[3][1], 10);
|
||||
var node_offset = 4;
|
||||
for (i = 0; i < node_count; i++) {
|
||||
var host = {};
|
||||
host.name = _.trim(systemStatus[node_offset][0]);
|
||||
host.ip = _.split(systemStatus[node_offset][1], ':')[0];
|
||||
swarm_hosts[host.name] = host.ip;
|
||||
node_offset += 9;
|
||||
}
|
||||
return swarm_hosts;
|
||||
}
|
||||
|
||||
$scope.swarm = false;
|
||||
Config.$promise.then(function (c) {
|
||||
hiddenLabels = c.hiddenLabels;
|
||||
$scope.containersToHideLabels = c.hiddenLabels;
|
||||
$scope.swarm = c.swarm;
|
||||
update({all: Settings.displayAll ? 1 : 0});
|
||||
if (c.swarm) {
|
||||
Info.get({}, function (d) {
|
||||
if (!_.startsWith(d.ServerVersion, 'swarm')) {
|
||||
$scope.swarm_mode = true;
|
||||
} else {
|
||||
$scope.swarm_hosts = retrieveSwarmHostsInfo(d);
|
||||
}
|
||||
update({all: Settings.displayAll ? 1 : 0});
|
||||
});
|
||||
} else {
|
||||
update({all: Settings.displayAll ? 1 : 0});
|
||||
}
|
||||
});
|
||||
}]);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
angular.module('createContainer', [])
|
||||
.controller('CreateContainerController', ['$scope', '$state', 'Config', 'Container', 'Image', 'Volume', 'Network', 'Messages', 'ViewSpinner', 'errorMsgFilter',
|
||||
function ($scope, $state, Config, Container, Image, Volume, Network, Messages, ViewSpinner, errorMsgFilter) {
|
||||
.controller('CreateContainerController', ['$scope', '$state', 'Config', 'Info', 'Container', 'Image', 'Volume', 'Network', 'Messages',
|
||||
function ($scope, $state, Config, Info, Container, Image, Volume, Network, Messages) {
|
||||
|
||||
$scope.state = {
|
||||
alwaysPull: true
|
||||
@@ -8,11 +8,16 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, V
|
||||
|
||||
$scope.formValues = {
|
||||
Console: 'none',
|
||||
Volumes: []
|
||||
Volumes: [],
|
||||
AvailableRegistries: [],
|
||||
Registry: ''
|
||||
};
|
||||
|
||||
$scope.imageConfig = {};
|
||||
|
||||
$scope.config = {
|
||||
Env: [],
|
||||
ExposedPorts: {},
|
||||
HostConfig: {
|
||||
RestartPolicy: {
|
||||
Name: 'no'
|
||||
@@ -24,12 +29,8 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, V
|
||||
}
|
||||
};
|
||||
|
||||
$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) {
|
||||
@@ -54,11 +55,18 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, V
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
var swarm = c.swarm;
|
||||
Info.get({}, function(info) {
|
||||
if (swarm && !_.startsWith(info.ServerVersion, 'swarm')) {
|
||||
$scope.swarm_mode = true;
|
||||
}
|
||||
});
|
||||
|
||||
$scope.formValues.AvailableRegistries = c.registries;
|
||||
|
||||
Volume.query({}, function (d) {
|
||||
$scope.availableVolumes = d.Volumes;
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e.data);
|
||||
Messages.error("Failure", e, "Unable to retrieve volumes");
|
||||
});
|
||||
|
||||
Network.query({}, function (d) {
|
||||
@@ -69,76 +77,97 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, V
|
||||
return network;
|
||||
}
|
||||
});
|
||||
$scope.globalNetworkCount = networks.length;
|
||||
networks.push({Name: "bridge"});
|
||||
networks.push({Name: "host"});
|
||||
networks.push({Name: "none"});
|
||||
}
|
||||
$scope.availableNetworks = networks;
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e.data);
|
||||
Messages.error("Failure", e, "Unable to retrieve networks");
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: centralize, already present in templatesController
|
||||
function createContainer(config) {
|
||||
ViewSpinner.spin();
|
||||
Container.create(config, function (d) {
|
||||
if (d.Id) {
|
||||
var reqBody = config.HostConfig || {};
|
||||
reqBody.id = d.Id;
|
||||
Container.start(reqBody, function (cd) {
|
||||
ViewSpinner.stop();
|
||||
Messages.send('Container Started', d.Id);
|
||||
$state.go('containers', {}, {reload: true});
|
||||
}, function (e) {
|
||||
ViewSpinner.stop();
|
||||
Messages.error('Error', errorMsgFilter(e));
|
||||
});
|
||||
if (d.message) {
|
||||
$('#createContainerSpinner').hide();
|
||||
Messages.error('Error', {}, d.message);
|
||||
} else {
|
||||
ViewSpinner.stop();
|
||||
Messages.error('Error', errorMsgFilter(d));
|
||||
Container.start({id: d.Id}, {}, function (cd) {
|
||||
if (cd.message) {
|
||||
$('#createContainerSpinner').hide();
|
||||
Messages.error('Error', {}, cd.message);
|
||||
} else {
|
||||
$('#createContainerSpinner').hide();
|
||||
Messages.send('Container Started', d.Id);
|
||||
$state.go('containers', {}, {reload: true});
|
||||
}
|
||||
}, function (e) {
|
||||
$('#createContainerSpinner').hide();
|
||||
Messages.error("Failure", e, 'Unable to start container');
|
||||
});
|
||||
}
|
||||
}, function (e) {
|
||||
ViewSpinner.stop();
|
||||
Messages.error('Error', errorMsgFilter(e));
|
||||
$('#createContainerSpinner').hide();
|
||||
Messages.error("Failure", e, 'Unable to create container');
|
||||
});
|
||||
}
|
||||
|
||||
function createImageConfig(imageName) {
|
||||
// TODO: centralize, already present in templatesController
|
||||
function pullImageAndCreateContainer(config) {
|
||||
Image.create($scope.imageConfig, function (data) {
|
||||
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
|
||||
if (err) {
|
||||
var detail = data[data.length - 1];
|
||||
$('#createContainerSpinner').hide();
|
||||
Messages.error('Error', {}, detail.error);
|
||||
} else {
|
||||
createContainer(config);
|
||||
}
|
||||
}, function (e) {
|
||||
$('#createContainerSpinner').hide();
|
||||
Messages.error('Failure', e, 'Unable to pull image');
|
||||
});
|
||||
}
|
||||
|
||||
function createImageConfig(imageName, registry) {
|
||||
var imageNameAndTag = imageName.split(':');
|
||||
var image = imageNameAndTag[0];
|
||||
if (registry) {
|
||||
image = registry + '/' + imageNameAndTag[0];
|
||||
}
|
||||
var imageConfig = {
|
||||
fromImage: imageNameAndTag[0],
|
||||
fromImage: image,
|
||||
tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest'
|
||||
};
|
||||
return imageConfig;
|
||||
}
|
||||
|
||||
function pullImageAndCreateContainer(config) {
|
||||
ViewSpinner.spin();
|
||||
|
||||
function prepareImageConfig(config) {
|
||||
var image = _.toLower(config.Image);
|
||||
var imageConfig = createImageConfig(image);
|
||||
|
||||
Image.create(imageConfig, function (data) {
|
||||
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
|
||||
if (err) {
|
||||
var detail = data[data.length - 1];
|
||||
ViewSpinner.stop();
|
||||
Messages.error('Error', detail.error);
|
||||
} else {
|
||||
createContainer(config);
|
||||
}
|
||||
}, function (e) {
|
||||
ViewSpinner.stop();
|
||||
Messages.error('Error', 'Unable to pull image ' + image);
|
||||
});
|
||||
var registry = $scope.formValues.Registry;
|
||||
var imageConfig = createImageConfig(image, registry);
|
||||
config.Image = imageConfig.fromImage + ':' + imageConfig.tag;
|
||||
$scope.imageConfig = imageConfig;
|
||||
}
|
||||
|
||||
function preparePortBindings(config) {
|
||||
var bindings = {};
|
||||
config.HostConfig.PortBindings.forEach(function (portBinding) {
|
||||
if (portBinding.hostPort && portBinding.containerPort) {
|
||||
if (portBinding.containerPort) {
|
||||
var key = portBinding.containerPort + "/" + portBinding.protocol;
|
||||
bindings[key] = [{ HostPort: portBinding.hostPort }];
|
||||
var binding = {};
|
||||
if (portBinding.hostPort && portBinding.hostPort.indexOf(':') > -1) {
|
||||
var hostAndPort = portBinding.hostPort.split(':');
|
||||
binding.HostIp = hostAndPort[0];
|
||||
binding.HostPort = hostAndPort[1];
|
||||
} else {
|
||||
binding.HostPort = portBinding.hostPort;
|
||||
}
|
||||
bindings[key] = [binding];
|
||||
config.ExposedPorts[key] = {};
|
||||
}
|
||||
});
|
||||
config.HostConfig.PortBindings = bindings;
|
||||
@@ -192,6 +221,7 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, V
|
||||
|
||||
function prepareConfiguration() {
|
||||
var config = angular.copy($scope.config);
|
||||
prepareImageConfig(config);
|
||||
preparePortBindings(config);
|
||||
prepareConsole(config);
|
||||
prepareEnvironmentVariables(config);
|
||||
@@ -201,7 +231,7 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, V
|
||||
|
||||
$scope.create = function () {
|
||||
var config = prepareConfiguration();
|
||||
|
||||
$('#createContainerSpinner').show();
|
||||
if ($scope.state.alwaysPull) {
|
||||
pullImageAndCreateContainer(config);
|
||||
} else {
|
||||
|
||||
@@ -18,11 +18,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- image input -->
|
||||
<!-- image-and-registry-inputs -->
|
||||
<div class="form-group">
|
||||
<label for="container_image" class="col-sm-1 control-label text-left">Image</label>
|
||||
<div class="col-sm-11">
|
||||
<input type="text" class="form-control" ng-model="config.Image" id="container_image" placeholder="ubuntu:trusty">
|
||||
<div class="col-sm-7">
|
||||
<input type="text" class="form-control" ng-model="config.Image" id="container_image" placeholder="e.g. ubuntu:trusty">
|
||||
</div>
|
||||
<label for="image_registry" class="col-sm-1 control-label text-left">Registry</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" ng-model="formValues.Registry" id="image_registry" placeholder="leave empty to use DockerHub">
|
||||
</div>
|
||||
<div class="col-sm-offset-1 col-sm-11">
|
||||
<div class="checkbox">
|
||||
@@ -32,7 +36,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !image-input -->
|
||||
<!-- !image-and-registry-inputs -->
|
||||
<!-- restart-policy -->
|
||||
<div class="form-group">
|
||||
<label class="col-sm-1 control-label text-left">Restart policy</label>
|
||||
@@ -65,7 +69,7 @@
|
||||
<div ng-repeat="portBinding in config.HostConfig.PortBindings" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">host</span>
|
||||
<input type="text" class="form-control" ng-model="portBinding.hostPort" placeholder="e.g. 80 or 1.2.3.4:80">
|
||||
<input type="text" class="form-control" ng-model="portBinding.hostPort" placeholder="e.g. 80 or 1.2.3.4:80 (optional)">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">container</span>
|
||||
@@ -103,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 -->
|
||||
@@ -251,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 && !swarm_mode">
|
||||
<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>
|
||||
@@ -306,6 +314,9 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" style="text-align: center;">
|
||||
<div>
|
||||
<i id="createContainerSpinner" class="fa fa-cog fa-3x fa-spin" style="margin-bottom: 5px; display: none;"></i>
|
||||
</div>
|
||||
<button type="button" class="btn btn-default btn-lg" ng-click="create()">Create</button>
|
||||
<a type="button" class="btn btn-default btn-lg" ui-sref="containers">Cancel</a>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
angular.module('createNetwork', [])
|
||||
.controller('CreateNetworkController', ['$scope', '$state', 'Messages', 'Network', 'ViewSpinner', 'errorMsgFilter',
|
||||
function ($scope, $state, Messages, Network, ViewSpinner, errorMsgFilter) {
|
||||
.controller('CreateNetworkController', ['$scope', '$state', 'Messages', 'Network',
|
||||
function ($scope, $state, Messages, Network) {
|
||||
$scope.formValues = {
|
||||
DriverOptions: []
|
||||
DriverOptions: [],
|
||||
Subnet: '',
|
||||
Gateway: ''
|
||||
};
|
||||
|
||||
$scope.config = {
|
||||
Driver: 'bridge',
|
||||
CheckDuplicate: true,
|
||||
Internal: false
|
||||
Internal: false,
|
||||
// Force IPAM Driver to 'default', should not be required.
|
||||
// See: https://github.com/docker/docker/issues/25735
|
||||
IPAM: {
|
||||
Driver: 'default',
|
||||
Config: []
|
||||
}
|
||||
};
|
||||
|
||||
$scope.addDriverOption = function() {
|
||||
@@ -20,22 +28,33 @@ function ($scope, $state, Messages, Network, ViewSpinner, errorMsgFilter) {
|
||||
};
|
||||
|
||||
function createNetwork(config) {
|
||||
ViewSpinner.spin();
|
||||
$('#createNetworkSpinner').show();
|
||||
Network.create(config, function (d) {
|
||||
if (d.Id) {
|
||||
Messages.send("Network created", d.Id);
|
||||
ViewSpinner.stop();
|
||||
$state.go('networks', {}, {reload: true});
|
||||
if (d.message) {
|
||||
$('#createNetworkSpinner').hide();
|
||||
Messages.error('Unable to create network', {}, d.message);
|
||||
} else {
|
||||
ViewSpinner.stop();
|
||||
Messages.error('Unable to create network', errorMsgFilter(d));
|
||||
Messages.send("Network created", d.Id);
|
||||
$('#createNetworkSpinner').hide();
|
||||
$state.go('networks', {}, {reload: true});
|
||||
}
|
||||
}, function (e) {
|
||||
ViewSpinner.stop();
|
||||
Messages.error('Unable to create network', e.data);
|
||||
$('#createNetworkSpinner').hide();
|
||||
Messages.error("Failure", e, 'Unable to create network');
|
||||
});
|
||||
}
|
||||
|
||||
function prepareIPAMConfiguration(config) {
|
||||
if ($scope.formValues.Subnet) {
|
||||
var ipamConfig = {};
|
||||
ipamConfig.Subnet = $scope.formValues.Subnet;
|
||||
if ($scope.formValues.Gateway) {
|
||||
ipamConfig.Gateway = $scope.formValues.Gateway ;
|
||||
}
|
||||
config.IPAM.Config.push(ipamConfig);
|
||||
}
|
||||
}
|
||||
|
||||
function prepareDriverOptions(config) {
|
||||
var options = {};
|
||||
$scope.formValues.DriverOptions.forEach(function (option) {
|
||||
@@ -46,6 +65,7 @@ function ($scope, $state, Messages, Network, ViewSpinner, errorMsgFilter) {
|
||||
|
||||
function prepareConfiguration() {
|
||||
var config = angular.copy($scope.config);
|
||||
prepareIPAMConfiguration(config);
|
||||
prepareDriverOptions(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -74,7 +86,10 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" style="text-align: center;">
|
||||
<button type="button" class="btn btn-default btn-lg" ng-click="create()">Create</button>
|
||||
<div>
|
||||
<i id="createNetworkSpinner" class="fa fa-cog fa-3x fa-spin" style="margin-bottom: 5px; display: none;"></i>
|
||||
</div>
|
||||
<button type="button" class="btn btn-default btn-lg" ng-disabled="!config.Name" ng-click="create()">Create</button>
|
||||
<a type="button" class="btn btn-default btn-lg" ui-sref="networks">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
178
app/components/createService/createServiceController.js
Normal file
@@ -0,0 +1,178 @@
|
||||
angular.module('createService', [])
|
||||
.controller('CreateServiceController', ['$scope', '$state', 'Service', 'Volume', 'Network', 'ImageHelper', 'Messages',
|
||||
function ($scope, $state, Service, Volume, Network, ImageHelper, Messages) {
|
||||
|
||||
$scope.formValues = {
|
||||
Name: '',
|
||||
Image: '',
|
||||
Registry: '',
|
||||
Mode: 'replicated',
|
||||
Replicas: 1,
|
||||
Command: '',
|
||||
WorkingDir: '',
|
||||
User: '',
|
||||
Env: [],
|
||||
Volumes: [],
|
||||
Network: '',
|
||||
ExtraNetworks: [],
|
||||
Ports: []
|
||||
};
|
||||
|
||||
$scope.addPortBinding = function() {
|
||||
$scope.formValues.Ports.push({ PublishedPort: '', TargetPort: '', Protocol: 'tcp' });
|
||||
};
|
||||
|
||||
$scope.removePortBinding = function(index) {
|
||||
$scope.formValues.Ports.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addExtraNetwork = function() {
|
||||
$scope.formValues.ExtraNetworks.push({ Name: '' });
|
||||
};
|
||||
|
||||
$scope.removeExtraNetwork = function(index) {
|
||||
$scope.formValues.ExtraNetworks.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addVolume = function() {
|
||||
$scope.formValues.Volumes.push({ name: '', containerPath: '' });
|
||||
};
|
||||
|
||||
$scope.removeVolume = function(index) {
|
||||
$scope.formValues.Volumes.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addEnvironmentVariable = function() {
|
||||
$scope.formValues.Env.push({ name: '', value: ''});
|
||||
};
|
||||
|
||||
$scope.removeEnvironmentVariable = function(index) {
|
||||
$scope.formValues.Env.splice(index, 1);
|
||||
};
|
||||
|
||||
function prepareImageConfig(config, input) {
|
||||
var imageConfig = ImageHelper.createImageConfig(input.Image, input.Registry);
|
||||
config.TaskTemplate.ContainerSpec.Image = imageConfig.repo + ':' + imageConfig.tag;
|
||||
}
|
||||
|
||||
function preparePortsConfig(config, input) {
|
||||
var ports = [];
|
||||
input.Ports.forEach(function (binding) {
|
||||
if (binding.PublishedPort && binding.TargetPort) {
|
||||
ports.push({ PublishedPort: +binding.PublishedPort, TargetPort: +binding.TargetPort, Protocol: binding.Protocol });
|
||||
}
|
||||
});
|
||||
config.EndpointSpec.Ports = ports;
|
||||
}
|
||||
|
||||
function prepareSchedulingConfig(config, input) {
|
||||
if (input.Mode === 'replicated') {
|
||||
config.Mode.Replicated = {
|
||||
Replicas: input.Replicas
|
||||
};
|
||||
} else {
|
||||
config.Mode.Global = {};
|
||||
}
|
||||
}
|
||||
|
||||
function prepareCommandConfig(config, input) {
|
||||
if (input.Command) {
|
||||
config.TaskTemplate.ContainerSpec.Command = _.split(input.Command, ' ');
|
||||
}
|
||||
if (input.User) {
|
||||
config.TaskTemplate.ContainerSpec.User = input.User;
|
||||
}
|
||||
if (input.WorkingDir) {
|
||||
config.TaskTemplate.ContainerSpec.Dir = input.WorkingDir;
|
||||
}
|
||||
}
|
||||
|
||||
function prepareEnvConfig(config, input) {
|
||||
var env = [];
|
||||
input.Env.forEach(function (v) {
|
||||
if (v.name && v.value) {
|
||||
env.push(v.name + "=" + v.value);
|
||||
}
|
||||
});
|
||||
config.TaskTemplate.ContainerSpec.Env = env;
|
||||
}
|
||||
|
||||
function prepareVolumes(config, input) {
|
||||
input.Volumes.forEach(function (volume) {
|
||||
if (volume.Source && volume.Target) {
|
||||
var mount = {};
|
||||
mount.Type = volume.Bind ? 'bind' : 'volume';
|
||||
mount.ReadOnly = volume.ReadOnly ? true : false;
|
||||
mount.Source = volume.Source;
|
||||
mount.Target = volume.Target;
|
||||
config.TaskTemplate.ContainerSpec.Mounts.push(mount);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function prepareNetworks(config, input) {
|
||||
var networks = [];
|
||||
if (input.Network) {
|
||||
networks.push({ Target: input.Network });
|
||||
}
|
||||
input.ExtraNetworks.forEach(function (network) {
|
||||
networks.push({ Target: network.Name });
|
||||
});
|
||||
config.Networks = _.uniqWith(networks, _.isEqual);
|
||||
}
|
||||
|
||||
function prepareConfiguration() {
|
||||
var input = $scope.formValues;
|
||||
var config = {
|
||||
Name: input.Name,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
Mounts: []
|
||||
}
|
||||
},
|
||||
Mode: {},
|
||||
EndpointSpec: {}
|
||||
};
|
||||
prepareSchedulingConfig(config, input);
|
||||
prepareImageConfig(config, input);
|
||||
preparePortsConfig(config, input);
|
||||
prepareCommandConfig(config, input);
|
||||
prepareEnvConfig(config, input);
|
||||
prepareVolumes(config, input);
|
||||
prepareNetworks(config, input);
|
||||
return config;
|
||||
}
|
||||
|
||||
function createNewService(config) {
|
||||
Service.create(config, function (d) {
|
||||
$('#createServiceSpinner').hide();
|
||||
Messages.send('Service created', d.ID);
|
||||
$state.go('services', {}, {reload: true});
|
||||
}, function (e) {
|
||||
$('#createServiceSpinner').hide();
|
||||
Messages.error("Failure", e, 'Unable to create service');
|
||||
});
|
||||
}
|
||||
|
||||
$scope.create = function createService() {
|
||||
$('#createServiceSpinner').show();
|
||||
var config = prepareConfiguration();
|
||||
createNewService(config);
|
||||
};
|
||||
|
||||
Volume.query({}, function (d) {
|
||||
$scope.availableVolumes = d.Volumes;
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e, "Unable to retrieve volumes");
|
||||
});
|
||||
|
||||
Network.query({}, function (d) {
|
||||
$scope.availableNetworks = d.filter(function (network) {
|
||||
if (network.Scope === 'swarm') {
|
||||
return network;
|
||||
}
|
||||
});
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e, "Unable to retrieve networks");
|
||||
});
|
||||
}]);
|
||||
272
app/components/createService/createservice.html
Normal file
@@ -0,0 +1,272 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Create service"></rd-header-title>
|
||||
<rd-header-content>
|
||||
Services > Add service
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="service_name" class="col-sm-1 control-label text-left">Name</label>
|
||||
<div class="col-sm-11">
|
||||
<input type="text" class="form-control" ng-model="formValues.Name" id="service_name" placeholder="e.g. myService">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- image-and-registry-inputs -->
|
||||
<div class="form-group">
|
||||
<label for="service_image" class="col-sm-1 control-label text-left">Image</label>
|
||||
<div class="col-sm-7">
|
||||
<input type="text" class="form-control" ng-model="formValues.Image" id="service_image" placeholder="e.g. nginx:latest">
|
||||
</div>
|
||||
<label for="image_registry" class="col-sm-1 control-label text-left">Registry</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" ng-model="formValues.Registry" id="image_registry" placeholder="leave empty to use DockerHub">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !image-and-registry-inputs -->
|
||||
<!-- scheduling-mode -->
|
||||
<div class="form-group">
|
||||
<label class="col-sm-1 control-label text-left">Scheduling mode</label>
|
||||
<div class="col-sm-11">
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="service_scheduling" ng-model="formValues.Mode" value="global">
|
||||
Global
|
||||
</label>
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="service_scheduling" ng-model="formValues.Mode" value="replicated">
|
||||
Replicated
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="formValues.Mode === 'replicated'">
|
||||
<label for="replicas" class="col-sm-1 control-label text-left">Replicas</label>
|
||||
<div class="col-sm-1">
|
||||
<input type="number" class="form-control" ng-model="formValues.Replicas" id="replicas" placeholder="e.g. 3">
|
||||
</div>
|
||||
<div class="col-sm-10"></div>
|
||||
</div>
|
||||
<!-- !scheduling-mode -->
|
||||
<!-- port-mapping -->
|
||||
<div class="form-group">
|
||||
<label for="container_ports" class="col-sm-1 control-label text-left">Port mapping</label>
|
||||
<div class="col-sm-11">
|
||||
<span class="label label-default clickable" ng-click="addPortBinding()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> map port
|
||||
</span>
|
||||
</div>
|
||||
<!-- port-mapping-input-list -->
|
||||
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="portBinding in formValues.Ports" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">host</span>
|
||||
<input type="text" class="form-control" ng-model="portBinding.PublishedPort" placeholder="e.g. 8080">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">container</span>
|
||||
<input type="text" class="form-control" ng-model="portBinding.TargetPort" placeholder="e.g. 80">
|
||||
</div>
|
||||
<div class="input-group col-sm-1 input-group-sm">
|
||||
<select class="selectpicker form-control" ng-model="portBinding.Protocol">
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-click="removePortBinding($index)">
|
||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !port-mapping-input-list -->
|
||||
</div>
|
||||
<!-- !port-mapping -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active clickable"><a data-target="#command" data-toggle="tab">Command</a></li>
|
||||
<li class="clickable"><a data-target="#volumes" data-toggle="tab">Volumes</a></li>
|
||||
<li class="clickable"><a data-target="#network" data-toggle="tab">Network</a></li>
|
||||
</ul>
|
||||
<!-- tab-content -->
|
||||
<div class="tab-content">
|
||||
<!-- tab-command -->
|
||||
<div class="tab-pane active" id="command">
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<!-- command-input -->
|
||||
<div class="form-group">
|
||||
<label for="service_command" class="col-sm-1 control-label text-left">Command</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" ng-model="formValues.Command" id="service_command" placeholder="e.g. /usr/bin/nginx -t -c /mynginx.conf">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !command-input -->
|
||||
<!-- workdir-user-input -->
|
||||
<div class="form-group">
|
||||
<label for="service_workingdir" class="col-sm-1 control-label text-left">Working Dir</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" class="form-control" ng-model="formValues.WorkingDir" id="service_workingdir" placeholder="e.g. /myapp">
|
||||
</div>
|
||||
<label for="service_user" class="col-sm-1 control-label text-left">User</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" class="form-control" ng-model="formValues.User" id="service_user" placeholder="e.g. nginx">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !workdir-user-input -->
|
||||
<!-- environment-variables -->
|
||||
<div class="form-group">
|
||||
<label for="service_env" class="col-sm-1 control-label text-left">Environment variables</label>
|
||||
<div class="col-sm-11">
|
||||
<span class="label label-default clickable" ng-click="addEnvironmentVariable()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> environment variable
|
||||
</span>
|
||||
</div>
|
||||
<!-- environment-variable-input-list -->
|
||||
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="variable in formValues.Env" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input type="text" class="form-control" ng-model="variable.name" placeholder="e.g. FOO">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="variable.value" placeholder="e.g. bar">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-click="removeEnvironmentVariable($index)">
|
||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !environment-variable-input-list -->
|
||||
</div>
|
||||
<!-- !environment-variables -->
|
||||
</form>
|
||||
</div>
|
||||
<!-- !tab-command -->
|
||||
<!-- tab-volume -->
|
||||
<div class="tab-pane" id="volumes">
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<!-- volumes -->
|
||||
<div class="form-group">
|
||||
<label for="service_volumes" class="col-sm-1 control-label text-left">Volumes</label>
|
||||
<div class="col-sm-11">
|
||||
<span class="label label-default clickable" ng-click="addVolume()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> volume
|
||||
</span>
|
||||
</div>
|
||||
<!-- volumes-input-list -->
|
||||
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="volume in formValues.Volumes" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-1 input-group-sm">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="volume.ReadOnly"> Read-only
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon"><input type="checkbox" ng-model="volume.Bind">bind</span>
|
||||
<select class="selectpicker form-control" ng-model="volume.Source" ng-if="!volume.Bind">
|
||||
<option selected disabled hidden value="">Select a volume</option>
|
||||
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option>
|
||||
</select>
|
||||
<input ng-if="volume.Bind" type="text" class="form-control" ng-model="volume.Source" placeholder="e.g. /path/on/host">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">container</span>
|
||||
<input type="text" class="form-control" ng-model="volume.Target" placeholder="e.g. /path/in/container">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-click="removeVolume($index)">
|
||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !volumes-input-list -->
|
||||
</div>
|
||||
<!-- !volumes -->
|
||||
</form>
|
||||
</div>
|
||||
<!-- !tab-volume -->
|
||||
<!-- tab-network -->
|
||||
<div class="tab-pane" id="network">
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<!-- network-input -->
|
||||
<div class="form-group">
|
||||
<label for="container_network" class="col-sm-1 control-label text-left">Network</label>
|
||||
<div class="col-sm-9">
|
||||
<select class="selectpicker form-control" ng-model="formValues.Network">
|
||||
<option selected disabled hidden value="">Select a network</option>
|
||||
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-2"></div>
|
||||
</div>
|
||||
<!-- !network-input -->
|
||||
<!-- extra-networks -->
|
||||
<div class="form-group">
|
||||
<label for="service_extra_networks" class="col-sm-1 control-label text-left">Extra networks</label>
|
||||
<div class="col-sm-11">
|
||||
<span class="label label-default clickable" ng-click="addExtraNetwork()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> network
|
||||
</span>
|
||||
</div>
|
||||
<!-- network-input-list -->
|
||||
<div style="margin-top: 10px;">
|
||||
<div class="col-sm-12" ng-repeat="network in formValues.ExtraNetworks" style="margin-top: 5px;">
|
||||
<div class="input-group col-sm-9 input-group-sm col-sm-offset-1">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-click="removeExtraNetwork($index)">
|
||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
<select class="selectpicker form-control" ng-model="network.Name">
|
||||
<option selected disabled hidden value="">Select a network</option>
|
||||
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !network-input-list -->
|
||||
</div>
|
||||
<!-- !extra-networks -->
|
||||
</form>
|
||||
</div>
|
||||
<!-- !tab-network -->
|
||||
<!-- tab-security -->
|
||||
<div class="tab-pane" id="security">
|
||||
</div>
|
||||
<!-- !tab-security -->
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" style="text-align: center;">
|
||||
<div>
|
||||
<i id="createServiceSpinner" class="fa fa-cog fa-3x fa-spin" style="margin-bottom: 5px; display: none;"></i>
|
||||
</div>
|
||||
<button type="button" class="btn btn-default btn-lg" ng-click="create()">Create</button>
|
||||
<a type="button" class="btn btn-default btn-lg" ui-sref="services">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,6 +1,6 @@
|
||||
angular.module('createVolume', [])
|
||||
.controller('CreateVolumeController', ['$scope', '$state', 'Volume', 'Messages', 'ViewSpinner', 'errorMsgFilter',
|
||||
function ($scope, $state, Volume, Messages, ViewSpinner, errorMsgFilter) {
|
||||
.controller('CreateVolumeController', ['$scope', '$state', 'Volume', 'Messages',
|
||||
function ($scope, $state, Volume, Messages) {
|
||||
|
||||
$scope.formValues = {
|
||||
DriverOptions: []
|
||||
@@ -19,19 +19,19 @@ function ($scope, $state, Volume, Messages, ViewSpinner, errorMsgFilter) {
|
||||
};
|
||||
|
||||
function createVolume(config) {
|
||||
ViewSpinner.spin();
|
||||
$('#createVolumeSpinner').show();
|
||||
Volume.create(config, function (d) {
|
||||
if (d.Name) {
|
||||
Messages.send("Volume created", d.Name);
|
||||
ViewSpinner.stop();
|
||||
$state.go('volumes', {}, {reload: true});
|
||||
if (d.message) {
|
||||
$('#createVolumeSpinner').hide();
|
||||
Messages.error('Unable to create volume', {}, d.message);
|
||||
} else {
|
||||
ViewSpinner.stop();
|
||||
Messages.error('Unable to create volume', errorMsgFilter(d));
|
||||
Messages.send("Volume created", d.Name);
|
||||
$('#createVolumeSpinner').hide();
|
||||
$state.go('volumes', {}, {reload: true});
|
||||
}
|
||||
}, function (e) {
|
||||
ViewSpinner.stop();
|
||||
Messages.error('Unable to create volume', e.data);
|
||||
$('#createVolumeSpinner').hide();
|
||||
Messages.error("Failure", e, 'Unable to create volume');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,9 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" style="text-align: center;">
|
||||
<div>
|
||||
<i id="createVolumeSpinner" class="fa fa-cog fa-3x fa-spin" style="margin-bottom: 5px; display: none;"></i>
|
||||
</div>
|
||||
<button type="button" class="btn btn-default btn-lg" ng-click="create()">Create</button>
|
||||
<a type="button" class="btn btn-default btn-lg" ui-sref="volumes">Cancel</a>
|
||||
</div>
|
||||
|
||||
@@ -1,76 +1,153 @@
|
||||
<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_mode || !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 && !swarm_mode">
|
||||
<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-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[0][1] == 'primary' ? infoData.SystemStatus[3][1] : infoData.SystemStatus[4][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: 2 }}</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 && swarm_mode">
|
||||
<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="Swarm info"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="2"><span class="small text-muted">This node is part of a Swarm cluster</span></td>
|
||||
</tr>
|
||||
<tr >
|
||||
<td>Node role</td>
|
||||
<td>{{ infoData.Swarm.ControlAvailable ? 'Manager' : 'Worker' }}</td>
|
||||
</tr>
|
||||
<tr ng-if="infoData.Swarm.ControlAvailable">
|
||||
<td>Nodes in the cluster</td>
|
||||
<td>{{ infoData.Swarm.Nodes }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div 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-server"></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>
|
||||
|
||||
@@ -1,46 +1,94 @@
|
||||
angular.module('dashboard', [])
|
||||
.controller('DashboardController', ['$scope', 'Container', 'Image', 'Settings', 'LineChart', function ($scope, Container, Image, Settings, LineChart) {
|
||||
.controller('DashboardController', ['$scope', '$q', 'Config', 'Container', 'ContainerHelper', 'Image', 'Network', 'Volume', 'Info',
|
||||
function ($scope, $q, Config, Container, ContainerHelper, 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
|
||||
};
|
||||
$scope.swarm_mode = false;
|
||||
|
||||
Container.query({all: 1}, function (d) {
|
||||
function prepareContainerData(d, containersToHideLabels) {
|
||||
var running = 0;
|
||||
var ghost = 0;
|
||||
var stopped = 0;
|
||||
|
||||
// TODO: centralize that
|
||||
var containers = d.filter(function (container) {
|
||||
return container.Image !== 'swarm';
|
||||
});
|
||||
var containers = d;
|
||||
if (containersToHideLabels) {
|
||||
containers = ContainerHelper.hideContainers(d, containersToHideLabels);
|
||||
}
|
||||
|
||||
for (var i = 0; i < containers.length; i++) {
|
||||
var item = containers[i];
|
||||
if (item.Status === "Ghost") {
|
||||
ghost += 1;
|
||||
if (item.Status.indexOf('Up') !== -1) {
|
||||
running += 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;
|
||||
$scope.containerData.total = containers.length;
|
||||
}
|
||||
|
||||
buildCharts(containers);
|
||||
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;
|
||||
if ($scope.swarm && !_.startsWith(info.ServerVersion, 'swarm')) {
|
||||
$scope.swarm_mode = true;
|
||||
}
|
||||
}
|
||||
|
||||
function fetchDashboardData(containersToHideLabels) {
|
||||
$('#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], containersToHideLabels);
|
||||
prepareImageData(d[1]);
|
||||
prepareVolumeData(d[2]);
|
||||
prepareNetworkData(d[3]);
|
||||
prepareInfoData(d[4]);
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
}
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.swarm = c.swarm;
|
||||
fetchDashboardData(c.hiddenLabels);
|
||||
});
|
||||
}]);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
angular.module('dashboard')
|
||||
.controller('MasterCtrl', ['$scope', '$cookieStore', 'Settings', 'Config', function ($scope, $cookieStore, Settings, Config) {
|
||||
.controller('MasterCtrl', ['$scope', '$cookieStore', 'Settings', 'Config', 'Info',
|
||||
function ($scope, $cookieStore, Settings, Config, Info) {
|
||||
/**
|
||||
* Sidebar Toggle & Cookie Control
|
||||
*/
|
||||
@@ -9,7 +10,20 @@ angular.module('dashboard')
|
||||
return window.innerWidth;
|
||||
};
|
||||
|
||||
$scope.config = Config;
|
||||
$scope.swarm_mode = false;
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.swarm = c.swarm;
|
||||
Info.get({}, function(d) {
|
||||
if ($scope.swarm && !_.startsWith(d.ServerVersion, 'swarm')) {
|
||||
$scope.swarm_mode = true;
|
||||
$scope.swarm_manager = false;
|
||||
if (d.Swarm.ControlAvailable) {
|
||||
$scope.swarm_manager = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$scope.$watch($scope.getWidth, function(newValue, oldValue) {
|
||||
if (newValue >= mobileView) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
63
app/components/events/events.html
Normal 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>
|
||||
27
app/components/events/eventsController.js
Normal 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) {
|
||||
$('#loadEventsSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to load events");
|
||||
});
|
||||
}]);
|
||||
@@ -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)"><i class="fa fa-trash btn-ico" aria-hidden="true"></i>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>
|
||||
|
||||
@@ -1,33 +1,14 @@
|
||||
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 = [];
|
||||
|
||||
$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();
|
||||
});
|
||||
.controller('ImageController', ['$scope', '$stateParams', '$state', 'Image', 'ImageHelper', 'Messages',
|
||||
function ($scope, $stateParams, $state, Image, ImageHelper, Messages) {
|
||||
$scope.RepoTags = [];
|
||||
$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 +19,71 @@ function ($scope, $q, $stateParams, $state, Image, Container, Messages, LineChar
|
||||
});
|
||||
}
|
||||
|
||||
$scope.tagImage = function() {
|
||||
$('#loadingViewSpinner').show();
|
||||
var image = _.toLower($scope.config.Image);
|
||||
var registry = _.toLower($scope.config.Registry);
|
||||
var imageConfig = ImageHelper.createImageConfig(image, registry);
|
||||
Image.tag({id: $stateParams.id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) {
|
||||
Messages.send('Image successfully tagged');
|
||||
$('#loadingViewSpinner').hide();
|
||||
$state.go('image', {id: $stateParams.id}, {reload: true});
|
||||
}, function(e) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to tag image");
|
||||
});
|
||||
};
|
||||
|
||||
$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("Failure", e, "Unable to push image");
|
||||
});
|
||||
};
|
||||
|
||||
$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("Failure", e, 'Unable to remove image');
|
||||
});
|
||||
};
|
||||
|
||||
$('#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;
|
||||
} else {
|
||||
$scope.error = e.data;
|
||||
}
|
||||
$('#error-message').show();
|
||||
Messages.error("Failure", e, "Unable to retrieve image info");
|
||||
});
|
||||
}]);
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
|
||||
<rd-header-content>Images</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
@@ -15,14 +14,18 @@
|
||||
</rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<!-- name-input -->
|
||||
<!-- 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-11">
|
||||
<div class="col-sm-7">
|
||||
<input type="text" class="form-control" ng-model="config.Image" id="image_name" placeholder="e.g. ubuntu:trusty">
|
||||
</div>
|
||||
<label for="image_registry" class="col-sm-1 control-label text-left">Registry</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" ng-model="config.Registry" id="image_registry" placeholder="leave empty to use DockerHub">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- !name-and-registry-inputs -->
|
||||
<!-- tag-note -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
@@ -33,6 +36,7 @@
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-default btn-sm" ng-disabled="!config.Image" ng-click="pullImage()">Pull</button>
|
||||
<i id="pullImageSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -45,10 +49,13 @@
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-clone" title="Images">
|
||||
<div class="pull-right">
|
||||
<i id="loadImagesSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-lg-12">
|
||||
<div class="pull-left">
|
||||
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount">Remove</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash btn-ico" aria-hidden="true"></i>Remove</button>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
|
||||
@@ -56,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
|
||||
@@ -69,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>
|
||||
@@ -94,14 +101,19 @@
|
||||
<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>
|
||||
<tr ng-if="images.length == 0">
|
||||
<td colspan="5" class="text-center text-muted">No images available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
<rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
<rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
angular.module('images', [])
|
||||
.controller('ImagesController', ['$scope', '$state', 'Image', 'ViewSpinner', 'Messages',
|
||||
function ($scope, $state, Image, ViewSpinner, 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.images = [];
|
||||
|
||||
$scope.config = {
|
||||
Image: ''
|
||||
Image: '',
|
||||
Registry: ''
|
||||
};
|
||||
|
||||
$scope.order = function(sortType) {
|
||||
@@ -16,17 +17,6 @@ function ($scope, $state, Image, ViewSpinner, 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++;
|
||||
@@ -35,56 +25,64 @@ function ($scope, $state, Image, ViewSpinner, Messages) {
|
||||
}
|
||||
};
|
||||
|
||||
function createImageConfig(imageName) {
|
||||
function createImageConfig(imageName, registry) {
|
||||
var imageNameAndTag = imageName.split(':');
|
||||
var image = imageNameAndTag[0];
|
||||
if (registry) {
|
||||
image = registry + '/' + imageNameAndTag[0];
|
||||
}
|
||||
var imageConfig = {
|
||||
fromImage: imageNameAndTag[0],
|
||||
fromImage: image,
|
||||
tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest'
|
||||
};
|
||||
return imageConfig;
|
||||
}
|
||||
|
||||
$scope.pullImage = function() {
|
||||
ViewSpinner.spin();
|
||||
$('#pullImageSpinner').show();
|
||||
var image = _.toLower($scope.config.Image);
|
||||
var imageConfig = createImageConfig(image);
|
||||
var registry = _.toLower($scope.config.Registry);
|
||||
var imageConfig = createImageConfig(image, registry);
|
||||
Image.create(imageConfig, function (data) {
|
||||
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
|
||||
if (err) {
|
||||
var detail = data[data.length - 1];
|
||||
ViewSpinner.stop();
|
||||
Messages.error('Error', detail.error);
|
||||
$('#pullImageSpinner').hide();
|
||||
Messages.error('Error', {}, detail.error);
|
||||
} else {
|
||||
ViewSpinner.stop();
|
||||
$('#pullImageSpinner').hide();
|
||||
$state.go('images', {}, {reload: true});
|
||||
}
|
||||
}, function (e) {
|
||||
ViewSpinner.stop();
|
||||
Messages.error('Error', 'Unable to pull image ' + image);
|
||||
$('#pullImageSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to pull image");
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeAction = function () {
|
||||
ViewSpinner.spin();
|
||||
$('#loadImagesSpinner').show();
|
||||
var counter = 0;
|
||||
var complete = function () {
|
||||
counter = counter - 1;
|
||||
if (counter === 0) {
|
||||
ViewSpinner.stop();
|
||||
$('#loadImagesSpinner').hide();
|
||||
}
|
||||
};
|
||||
angular.forEach($scope.images, function (i) {
|
||||
if (i.Checked) {
|
||||
counter = counter + 1;
|
||||
Image.remove({id: i.Id}, function (d) {
|
||||
angular.forEach(d, function (resource) {
|
||||
Messages.send("Image deleted", resource.Deleted);
|
||||
});
|
||||
var index = $scope.images.indexOf(i);
|
||||
$scope.images.splice(index, 1);
|
||||
if (d[0].message) {
|
||||
$('#loadImagesSpinner').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);
|
||||
Messages.error("Failure", e, 'Unable to remove image');
|
||||
complete();
|
||||
});
|
||||
}
|
||||
@@ -92,17 +90,19 @@ function ($scope, $state, Image, ViewSpinner, Messages) {
|
||||
};
|
||||
|
||||
function fetchImages() {
|
||||
ViewSpinner.spin();
|
||||
Image.query({}, function (d) {
|
||||
$scope.images = d.map(function (item) {
|
||||
return new ImageViewModel(item);
|
||||
});
|
||||
ViewSpinner.stop();
|
||||
$('#loadImagesSpinner').hide();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e.data);
|
||||
ViewSpinner.stop();
|
||||
$('#loadImagesSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to retrieve images");
|
||||
});
|
||||
}
|
||||
|
||||
fetchImages();
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.availableRegistries = c.registries;
|
||||
fetchImages();
|
||||
});
|
||||
}]);
|
||||
|
||||
@@ -1,123 +1,63 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Network details"></rd-header-title>
|
||||
<rd-header-title title="Network details">
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
Networks > <a ui-sref="network({id: network.Id})">{{ network.Name }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-9 col-md-9 col-xs-9">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon grey pull-left">
|
||||
<i class="fa fa-sitemap"></i>
|
||||
</div>
|
||||
<div class="title">{{ network.Name }}</div>
|
||||
<div class="comment">Name</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-3 col-xs-3">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon grey pull-left">
|
||||
<i class="fa fa-cogs"></i>
|
||||
</div>
|
||||
<div class="title">
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<button class="btn btn-default" disabled>Connect container...</button>
|
||||
<button class="btn btn-danger" ng-click="remove(id)">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment">
|
||||
Actions
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-sitemap" title="Network details"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Id</td>
|
||||
<td>{{ network.Id }}</td>
|
||||
<td>Name</td>
|
||||
<td>{{ network.Name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Scope</td>
|
||||
<td>{{ network.Scope }}</td>
|
||||
<td>ID</td>
|
||||
<td>
|
||||
{{ network.Id }}
|
||||
<button class="btn btn-xs btn-danger" ng-click="removeNetwork(network.Id)"><i class="fa fa-trash btn-ico" aria-hidden="true"></i>Delete this network</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Driver</td>
|
||||
<td>{{ network.Driver }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IPAM</td>
|
||||
<td>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<td>Driver</td>
|
||||
<td>{{ network.IPAM.Driver }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Subnet</td>
|
||||
<td>{{ network.IPAM.Config[0].Subnet }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Gateway</td>
|
||||
<td>{{ network.IPAM.Config[0].Gateway }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td>Scope</td>
|
||||
<td>{{ network.Scope }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Containers</td>
|
||||
<td>
|
||||
<table class="table table-striped" ng-repeat="(Id, container) in network.Containers">
|
||||
<tr>
|
||||
<td>Id</td>
|
||||
<td><a ui-sref="container({id: Id})">{{ Id }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>EndpointID</td>
|
||||
<td>{{ container.EndpointID}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MacAddress</td>
|
||||
<td>{{ container.MacAddress}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IPv4Address</td>
|
||||
<td>{{ container.IPv4Address}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IPv6Address</td>
|
||||
<td>{{ container.IPv6Address}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<button ng-click="disconnect(network.Id, Id)" class="btn btn-danger">
|
||||
Disconnect from network
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<tr ng-if="network.IPAM.Config[0].Subnet">
|
||||
<td>Subnet</td>
|
||||
<td>{{ network.IPAM.Config[0].Subnet }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Options</td>
|
||||
<td>
|
||||
<table role="table" class="table table-striped">
|
||||
<tr ng-repeat="(k, v) in network.Options">
|
||||
<td>{{ k }}</td>
|
||||
<td>{{ v }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<tr ng-if="network.IPAM.Config[0].Gateway">
|
||||
<td>Gateway</td>
|
||||
<td>{{ network.IPAM.Config[0].Gateway }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="!(network.Options | emptyobject)">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-cogs" title="Network options"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr ng-repeat="(key, value) in network.Options">
|
||||
<td>{{ key }}</td>
|
||||
<td>{{ value }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,37 +1,30 @@
|
||||
angular.module('network', [])
|
||||
.controller('NetworkController', ['$scope', 'Network', 'ViewSpinner', 'Messages', '$state', '$stateParams', 'errorMsgFilter',
|
||||
function ($scope, Network, ViewSpinner, Messages, $state, $stateParams, errorMsgFilter) {
|
||||
.controller('NetworkController', ['$scope', '$state', '$stateParams', 'Network', 'Messages',
|
||||
function ($scope, $state, $stateParams, Network, Messages) {
|
||||
|
||||
$scope.disconnect = function disconnect(networkId, containerId) {
|
||||
ViewSpinner.spin();
|
||||
Network.disconnect({id: $stateParams.id}, {Container: containerId}, function (d) {
|
||||
ViewSpinner.stop();
|
||||
Messages.send("Container disconnected", containerId);
|
||||
$state.go('network', {id: $stateParams.id}, {reload: true});
|
||||
}, function (e) {
|
||||
ViewSpinner.stop();
|
||||
Messages.error("Failure", e.data);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.remove = function remove(networkId) {
|
||||
ViewSpinner.spin();
|
||||
$scope.removeNetwork = function removeNetwork(networkId) {
|
||||
$('#loadingViewSpinner').show();
|
||||
Network.remove({id: $stateParams.id}, function (d) {
|
||||
ViewSpinner.stop();
|
||||
Messages.send("Network removed", "");
|
||||
$state.go('networks', {});
|
||||
if (d.message) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.send("Error", {}, d.message);
|
||||
} else {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.send("Network removed", $stateParams.id);
|
||||
$state.go('networks', {});
|
||||
}
|
||||
}, function (e) {
|
||||
ViewSpinner.stop();
|
||||
Messages.error("Failure", e.data);
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to remove network");
|
||||
});
|
||||
};
|
||||
|
||||
ViewSpinner.spin();
|
||||
$('#loadingViewSpinner').show();
|
||||
Network.get({id: $stateParams.id}, function (d) {
|
||||
$scope.network = d;
|
||||
ViewSpinner.stop();
|
||||
$('#loadingViewSpinner').hide();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e.data);
|
||||
ViewSpinner.stop();
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to retrieve network info");
|
||||
});
|
||||
}]);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
<div ng-include="template" ng-controller="CreateNetworkController"></div>
|
||||
|
||||
<rd-header>
|
||||
<rd-header-title title="Network list">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="networks" ui-sref-opts="{reload: true}">
|
||||
@@ -9,91 +7,137 @@
|
||||
<rd-header-content>Networks</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
|
||||
<div class="col-lg-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-sitemap" title="Networks">
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-lg-12">
|
||||
<div class="pull-left">
|
||||
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount">Remove</button>
|
||||
<a class="btn btn-default" type="button" ui-sref="actions.create.network">Add network</a>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
|
||||
</div>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><label><input type="checkbox" ng-model="state.toggle" ng-change="toggleSelectAll()"/> Select</label></th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('Name')">
|
||||
Name
|
||||
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('Id')">
|
||||
Id
|
||||
<span ng-show="sortType == 'Id' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Id' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('Scope')">
|
||||
Scope
|
||||
<span ng-show="sortType == 'Scope' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Scope' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('Driver')">
|
||||
Driver
|
||||
<span ng-show="sortType == 'Driver' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('IPAM.Driver')">
|
||||
IPAM Driver
|
||||
<span ng-show="sortType == 'IPAM.Driver' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'IPAM.Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('IPAM.Config[0].Subnet')">
|
||||
IPAM Subnet
|
||||
<span ng-show="sortType == 'IPAM.Config[0].Subnet' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'IPAM.Config[0].Subnet' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('IPAM.Config[0].Gateway')">
|
||||
IPAM Gateway
|
||||
<span ng-show="sortType == 'IPAM.Config[0].Gateway' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'IPAM.Config[0].Gateway' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="network in ( state.filteredNetworks = (networks | filter:state.filter | orderBy:sortType:sortReverse))">
|
||||
<td><input type="checkbox" ng-model="network.Checked" ng-change="selectItem(network)"/></td>
|
||||
<td><a ui-sref="network({id: network.Id})">{{ network.Name|truncate:20}}</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>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
<rd-widget>
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-plus" title="Add a network">
|
||||
</rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="network_name" class="col-sm-1 control-label text-left">Name</label>
|
||||
<div class="col-sm-11">
|
||||
<input type="text" class="form-control" ng-model="config.Name" id="network_name" placeholder="e.g. myNetwork">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- tag-note -->
|
||||
<div class="form-group" ng-if="swarm">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">Note: The network will be created using the overlay driver and will allow containers to communicate across the hosts of your cluster.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="!swarm">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">Note: The network will be created using the bridge driver.</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !tag-note -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-default btn-sm" ng-disabled="!config.Name" ng-click="createNetwork()">Create</button>
|
||||
<button type="button" class="btn btn-default btn-sm" ui-sref="actions.create.network">Advanced settings...</button>
|
||||
<i id="createNetworkSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-sitemap" title="Networks">
|
||||
<div class="pull-right">
|
||||
<i id="loadNetworksSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-lg-12">
|
||||
<div class="pull-left">
|
||||
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash btn-ico" aria-hidden="true"></i>Remove</button>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
|
||||
</div>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('Name')">
|
||||
Name
|
||||
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('Id')">
|
||||
Id
|
||||
<span ng-show="sortType == 'Id' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Id' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('Scope')">
|
||||
Scope
|
||||
<span ng-show="sortType == 'Scope' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Scope' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('Driver')">
|
||||
Driver
|
||||
<span ng-show="sortType == 'Driver' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('IPAM.Driver')">
|
||||
IPAM Driver
|
||||
<span ng-show="sortType == 'IPAM.Driver' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'IPAM.Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('IPAM.Config[0].Subnet')">
|
||||
IPAM Subnet
|
||||
<span ng-show="sortType == 'IPAM.Config[0].Subnet' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'IPAM.Config[0].Subnet' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('IPAM.Config[0].Gateway')">
|
||||
IPAM Gateway
|
||||
<span ng-show="sortType == 'IPAM.Config[0].Gateway' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'IPAM.Config[0].Gateway' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="network in ( state.filteredNetworks = (networks | filter:state.filter | orderBy:sortType:sortReverse))">
|
||||
<td><input type="checkbox" ng-model="network.Checked" ng-change="selectItem(network)"/></td>
|
||||
<td><a ui-sref="network({id: network.Id})">{{ network.Name|truncate:40}}</a></td>
|
||||
<td>{{ network.Id }}</td>
|
||||
<td>{{ network.Scope }}</td>
|
||||
<td>{{ network.Driver }}</td>
|
||||
<td>{{ network.IPAM.Driver }}</td>
|
||||
<td>{{ network.IPAM.Config[0].Subnet ? network.IPAM.Config[0].Subnet : '-' }}</td>
|
||||
<td>{{ network.IPAM.Config[0].Gateway ? network.IPAM.Config[0].Gateway : '-' }}</td>
|
||||
</tr>
|
||||
<tr ng-if="networks.length == 0">
|
||||
<td colspan="8" class="text-center text-muted">No networks available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
<rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,29 +1,53 @@
|
||||
angular.module('networks', [])
|
||||
.controller('NetworksController', ['$scope', 'Network', 'ViewSpinner', 'Messages', 'errorMsgFilter',
|
||||
function ($scope, Network, ViewSpinner, Messages, errorMsgFilter) {
|
||||
|
||||
.controller('NetworksController', ['$scope', '$state', 'Network', 'Config', 'Messages',
|
||||
function ($scope, $state, Network, Config, Messages) {
|
||||
$scope.state = {};
|
||||
$scope.state.toggle = false;
|
||||
$scope.state.selectedItemCount = 0;
|
||||
$scope.state.advancedSettings = false;
|
||||
$scope.sortType = 'Name';
|
||||
$scope.sortReverse = true;
|
||||
$scope.sortReverse = false;
|
||||
$scope.networks = [];
|
||||
|
||||
$scope.config = {
|
||||
Name: ''
|
||||
};
|
||||
|
||||
function prepareNetworkConfiguration() {
|
||||
var config = angular.copy($scope.config);
|
||||
if ($scope.swarm) {
|
||||
config.Driver = 'overlay';
|
||||
// Force IPAM Driver to 'default', should not be required.
|
||||
// See: https://github.com/docker/docker/issues/25735
|
||||
config.IPAM = {
|
||||
Driver: 'default'
|
||||
};
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
$scope.createNetwork = function() {
|
||||
$('#createNetworkSpinner').show();
|
||||
var config = prepareNetworkConfiguration();
|
||||
Network.create(config, function (d) {
|
||||
if (d.message) {
|
||||
$('#createNetworkSpinner').hide();
|
||||
Messages.error('Unable to create network', {}, d.message);
|
||||
} else {
|
||||
Messages.send("Network created", d.Id);
|
||||
$('#createNetworkSpinner').hide();
|
||||
$state.go('networks', {}, {reload: true});
|
||||
}
|
||||
}, function (e) {
|
||||
$('#createNetworkSpinner').hide();
|
||||
Messages.error("Failure", e, 'Unable to create network');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.order = function(sortType) {
|
||||
$scope.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++;
|
||||
@@ -33,24 +57,28 @@ function ($scope, Network, ViewSpinner, Messages, errorMsgFilter) {
|
||||
};
|
||||
|
||||
$scope.removeAction = function () {
|
||||
ViewSpinner.spin();
|
||||
$('#loadNetworksSpinner').show();
|
||||
var counter = 0;
|
||||
var complete = function () {
|
||||
counter = counter - 1;
|
||||
if (counter === 0) {
|
||||
ViewSpinner.stop();
|
||||
$('#loadNetworksSpinner').hide();
|
||||
}
|
||||
};
|
||||
angular.forEach($scope.networks, function (network) {
|
||||
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);
|
||||
if (d.message) {
|
||||
Messages.send("Error", d.message);
|
||||
} else {
|
||||
Messages.send("Network removed", network.Id);
|
||||
var index = $scope.networks.indexOf(network);
|
||||
$scope.networks.splice(index, 1);
|
||||
}
|
||||
complete();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e.data);
|
||||
Messages.error("Failure", e, 'Unable to remove network');
|
||||
complete();
|
||||
});
|
||||
}
|
||||
@@ -58,14 +86,18 @@ function ($scope, Network, ViewSpinner, Messages, errorMsgFilter) {
|
||||
};
|
||||
|
||||
function fetchNetworks() {
|
||||
ViewSpinner.spin();
|
||||
$('#loadNetworksSpinner').show();
|
||||
Network.query({}, function (d) {
|
||||
$scope.networks = d;
|
||||
ViewSpinner.stop();
|
||||
$('#loadNetworksSpinner').hide();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e.data);
|
||||
ViewSpinner.stop();
|
||||
$('#loadNetworksSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to retrieve networks");
|
||||
});
|
||||
}
|
||||
fetchNetworks();
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.swarm = c.swarm;
|
||||
fetchNetworks();
|
||||
});
|
||||
}]);
|
||||
|
||||
150
app/components/service/service.html
Normal file
@@ -0,0 +1,150 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Service details">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="service({id: service.Id})" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
Services > <a ui-sref="service({id: service.Id})">{{ service.Name }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-list-alt" title="Service details"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td ng-if="!service.EditName">
|
||||
{{ service.Name }}
|
||||
<a href="" data-toggle="tooltip" title="Edit service name" ng-click="service.EditName = true;"><i class="fa fa-edit"></i></a>
|
||||
</td>
|
||||
<td ng-if="service.EditName">
|
||||
<input type="text" class="containerNameInput" ng-model="service.newServiceName">
|
||||
<a class="interactive" ng-click="service.EditName = false;"><i class="fa fa-times"></i></a>
|
||||
<a class="interactive" ng-click="renameService(service)"><i class="fa fa-check-square-o"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ID</td>
|
||||
<td>
|
||||
{{ service.Id }}
|
||||
<button class="btn btn-xs btn-danger" ng-click="removeService()"><i class="fa fa-trash btn-ico" aria-hidden="true"></i>Delete this service</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Scheduling mode</td>
|
||||
<td>{{ service.Mode }}</td>
|
||||
</tr>
|
||||
<tr ng-if="service.Mode === 'replicated'">
|
||||
<td>Replicas</td>
|
||||
<td>
|
||||
<span ng-if="service.Mode === 'replicated' && !service.Scale">
|
||||
{{ service.Replicas }}
|
||||
<a class="interactive" ng-click="service.Scale = true; service.ReplicaCount = service.Replicas;"><i class="fa fa-arrows-v" aria-hidden="true"></i> Scale</a>
|
||||
</span>
|
||||
<span ng-if="service.Mode === 'replicated' && service.Scale">
|
||||
<input class="input-sm" type="number" ng-model="service.Replicas" />
|
||||
<a class="interactive" ng-click="service.Scale = false;"><i class="fa fa-times"></i></a>
|
||||
<a class="interactive" ng-click="scaleService(service)"><i class="fa fa-check-square-o"></i></a>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Image</td>
|
||||
<td>{{ service.Image }}</td>
|
||||
</tr>
|
||||
<tr ng-if="service.Ports">
|
||||
<td>Published ports</td>
|
||||
<td>
|
||||
<div ng-repeat="mapping in service.Ports">
|
||||
{{ mapping.TargetPort }} <i class="fa fa-long-arrow-right"></i> {{ mapping.PublishedPort }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="service.Env">
|
||||
<td>Env</td>
|
||||
<td>
|
||||
<table class="table table-bordered table-condensed">
|
||||
<tr ng-repeat="var in service.Env">
|
||||
<td>{{ var|key: '=' }}</td>
|
||||
<td>{{ var|value: '=' }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="service.Labels">
|
||||
<td>Labels</td>
|
||||
<td>
|
||||
<table class="table table-bordered table-condensed">
|
||||
<tr ng-repeat="(k, v) in service.Labels">
|
||||
<td>{{ k }}</td>
|
||||
<td>{{ v }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="tasks.length > 0">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Associated tasks"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>
|
||||
<a ui-sref="service" ng-click="order('Status')">
|
||||
Status
|
||||
<span ng-show="sortType == 'Status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="service" ng-click="order('Slot')">
|
||||
Slot
|
||||
<span ng-show="sortType == 'Slot' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Slot' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th ng-if="displayNode">
|
||||
<a ui-sref="service" ng-click="order('Node')">
|
||||
Node
|
||||
<span ng-show="sortType == 'Node' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Node' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="service" ng-click="order('UpdatedAt')">
|
||||
Last update
|
||||
<span ng-show="sortType == 'UpdatedAt' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'UpdatedAt' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="task in (filteredTasks = ( tasks | orderBy:sortType:sortReverse))">
|
||||
<td><a ui-sref="task({ id: task.Id })">{{ task.Id }}</a></td>
|
||||
<td><span class="label label-{{ task.Status|taskstatusbadge }}">{{ task.Status }}</span></td>
|
||||
<td>{{ task.Slot }}</td>
|
||||
<td ng-if="displayNode">{{ task.Node }}</td>
|
||||
<td>{{ task.Updated|getisodate }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
97
app/components/service/serviceController.js
Normal file
@@ -0,0 +1,97 @@
|
||||
angular.module('service', [])
|
||||
.controller('ServiceController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Task', 'Node', 'Messages',
|
||||
function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Messages) {
|
||||
|
||||
$scope.service = {};
|
||||
$scope.tasks = [];
|
||||
$scope.displayNode = false;
|
||||
$scope.sortType = 'Status';
|
||||
$scope.sortReverse = false;
|
||||
|
||||
$scope.order = function (sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
$scope.renameService = function renameService(service) {
|
||||
$('#loadServicesSpinner').show();
|
||||
var serviceName = service.Name;
|
||||
var config = ServiceHelper.serviceToConfig(service.Model);
|
||||
config.Name = service.newServiceName;
|
||||
Service.update({ id: service.Id, version: service.Version }, config, function (data) {
|
||||
$('#loadServicesSpinner').hide();
|
||||
Messages.send("Service successfully renamed", "New name: " + service.newServiceName);
|
||||
$state.go('service', {id: service.Id}, {reload: true});
|
||||
}, function (e) {
|
||||
$('#loadServicesSpinner').hide();
|
||||
service.EditName = false;
|
||||
service.Name = serviceName;
|
||||
Messages.error("Failure", e, "Unable to rename service");
|
||||
});
|
||||
};
|
||||
|
||||
$scope.scaleService = function scaleService(service) {
|
||||
$('#loadServicesSpinner').show();
|
||||
var config = ServiceHelper.serviceToConfig(service.Model);
|
||||
config.Mode.Replicated.Replicas = service.Replicas;
|
||||
Service.update({ id: service.Id, version: service.Version }, config, function (data) {
|
||||
$('#loadServicesSpinner').hide();
|
||||
Messages.send("Service successfully scaled", "New replica count: " + service.Replicas);
|
||||
$state.go('service', {id: service.Id}, {reload: true});
|
||||
}, function (e) {
|
||||
$('#loadServicesSpinner').hide();
|
||||
service.Scale = false;
|
||||
service.Replicas = service.ReplicaCount;
|
||||
Messages.error("Failure", e, "Unable to scale service");
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeService = function removeService() {
|
||||
$('#loadingViewSpinner').show();
|
||||
Service.remove({id: $stateParams.id}, function (d) {
|
||||
if (d.message) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.send("Error", {}, d.message);
|
||||
} else {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.send("Service removed", $stateParams.id);
|
||||
$state.go('services', {});
|
||||
}
|
||||
}, function (e) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to remove service");
|
||||
});
|
||||
};
|
||||
|
||||
function fetchServiceDetails() {
|
||||
$('#loadingViewSpinner').show();
|
||||
Service.get({id: $stateParams.id}, function (d) {
|
||||
var service = new ServiceViewModel(d);
|
||||
service.newServiceName = service.Name;
|
||||
$scope.service = service;
|
||||
Task.query({filters: {service: [service.Name]}}, function (tasks) {
|
||||
Node.query({}, function (nodes) {
|
||||
$scope.displayNode = true;
|
||||
$scope.tasks = tasks.map(function (task) {
|
||||
return new TaskViewModel(task, nodes);
|
||||
});
|
||||
$('#loadingViewSpinner').hide();
|
||||
}, function (e) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
$scope.tasks = tasks.map(function (task) {
|
||||
return new TaskViewModel(task, null);
|
||||
});
|
||||
Messages.error("Failure", e, "Unable to retrieve node information");
|
||||
});
|
||||
}, function (e) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to retrieve tasks associated to the service");
|
||||
});
|
||||
}, function (e) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to retrieve service details");
|
||||
});
|
||||
}
|
||||
|
||||
fetchServiceDetails();
|
||||
}]);
|
||||
78
app/components/services/services.html
Normal file
@@ -0,0 +1,78 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Service list">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="services" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Services</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-list-alt" title="Services">
|
||||
<div class="pull-right">
|
||||
<i id="loadServicesSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-lg-12 col-md-12 col-xs-12">
|
||||
<div class="pull-left">
|
||||
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash btn-ico" aria-hidden="true"></i>Remove</button>
|
||||
<a class="btn btn-default btn-responsive" type="button" ui-sref="actions.create.service">Add service</a>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
|
||||
</div>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<th></th>
|
||||
<th>
|
||||
<a ui-sref="services" ng-click="order('Name')">
|
||||
Name
|
||||
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="services" ng-click="order('Image')">
|
||||
Image
|
||||
<span ng-show="sortType == 'Image' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Image' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="services" ng-click="order('Mode')">
|
||||
Scheduling mode
|
||||
<span ng-show="sortType == 'Mode' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Mode' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="service in (state.filteredServices = ( services | filter:state.filter | orderBy:sortType:sortReverse))">
|
||||
<td><input type="checkbox" ng-model="service.Checked" ng-change="selectItem(service)"/></td>
|
||||
<td><a ui-sref="service({id: service.Id})">{{ service.Name }}</a></td>
|
||||
<td>{{ service.Image }}</td>
|
||||
<td>
|
||||
{{ service.Mode }}
|
||||
<span ng-if="service.Mode === 'replicated' && !service.Scale">
|
||||
<code data-toggle="tooltip" title="Replicas">{{ service.Replicas }}</code>
|
||||
<a class="interactive" ng-click="service.Scale = true; service.ReplicaCount = service.Replicas;"><i class="fa fa-arrows-v" aria-hidden="true"></i> Scale</a>
|
||||
</span>
|
||||
<span ng-if="service.Mode === 'replicated' && service.Scale">
|
||||
<input class="input-sm" type="number" ng-model="service.Replicas" />
|
||||
<a class="interactive" ng-click="service.Scale = false;"><i class="fa fa-times"></i></a>
|
||||
<a class="interactive" ng-click="scaleService(service)"><i class="fa fa-check-square-o"></i></a>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
<rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
84
app/components/services/servicesController.js
Normal file
@@ -0,0 +1,84 @@
|
||||
angular.module('services', [])
|
||||
.controller('ServicesController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Messages',
|
||||
function ($scope, $stateParams, $state, Service, ServiceHelper, Messages) {
|
||||
|
||||
$scope.services = [];
|
||||
$scope.state = {};
|
||||
$scope.state.selectedItemCount = 0;
|
||||
$scope.sortType = 'Name';
|
||||
$scope.sortReverse = false;
|
||||
|
||||
$scope.scaleService = function scaleService(service) {
|
||||
$('#loadServicesSpinner').show();
|
||||
var config = ServiceHelper.serviceToConfig(service.Model);
|
||||
config.Mode.Replicated.Replicas = service.Replicas;
|
||||
Service.update({ id: service.Id, version: service.Version }, config, function (data) {
|
||||
$('#loadServicesSpinner').hide();
|
||||
Messages.send("Service successfully scaled", "New replica count: " + service.Replicas);
|
||||
$state.go('services', {}, {reload: true});
|
||||
}, function (e) {
|
||||
$('#loadServicesSpinner').hide();
|
||||
service.Scale = false;
|
||||
service.Replicas = service.ReplicaCount;
|
||||
Messages.error("Failure", e, "Unable to scale service");
|
||||
});
|
||||
};
|
||||
|
||||
$scope.order = function (sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
$scope.selectItem = function (item) {
|
||||
if (item.Checked) {
|
||||
$scope.state.selectedItemCount++;
|
||||
} else {
|
||||
$scope.state.selectedItemCount--;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.removeAction = function () {
|
||||
$('#loadServicesSpinner').show();
|
||||
var counter = 0;
|
||||
var complete = function () {
|
||||
counter = counter - 1;
|
||||
if (counter === 0) {
|
||||
$('#loadServicesSpinner').hide();
|
||||
}
|
||||
};
|
||||
angular.forEach($scope.services, function (service) {
|
||||
if (service.Checked) {
|
||||
counter = counter + 1;
|
||||
Service.remove({id: service.Id}, function (d) {
|
||||
if (d.message) {
|
||||
$('#loadServicesSpinner').hide();
|
||||
Messages.error("Unable to remove service", {}, d[0].message);
|
||||
} else {
|
||||
Messages.send("Service deleted", service.Id);
|
||||
var index = $scope.services.indexOf(service);
|
||||
$scope.services.splice(index, 1);
|
||||
}
|
||||
complete();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e, 'Unable to remove service');
|
||||
complete();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function fetchServices() {
|
||||
$('#loadServicesSpinner').show();
|
||||
Service.query({}, function (d) {
|
||||
$scope.services = d.map(function (service) {
|
||||
return new ServiceViewModel(service);
|
||||
});
|
||||
$('#loadServicesSpinner').hide();
|
||||
}, function(e) {
|
||||
$('#loadServicesSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to retrieve services");
|
||||
});
|
||||
}
|
||||
|
||||
fetchServices();
|
||||
}]);
|
||||
@@ -10,7 +10,7 @@
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon grey pull-left">
|
||||
<i class="fa fa-tasks"></i>
|
||||
<i class="fa fa-server"></i>
|
||||
</div>
|
||||
<div class="title">{{ container.Name|trimcontainername }}</div>
|
||||
<div class="comment">
|
||||
|
||||
@@ -87,7 +87,7 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
|
||||
},
|
||||
{
|
||||
scaleLabel: function (valueObj) {
|
||||
return humansizeFilter(parseInt(valueObj.value, 10));
|
||||
return humansizeFilter(parseInt(valueObj.value, 10), 2);
|
||||
},
|
||||
responsive: true
|
||||
//scaleOverride: true,
|
||||
@@ -100,7 +100,7 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
|
||||
datasets: [networkRxDataset, networkTxDataset]
|
||||
}, {
|
||||
scaleLabel: function (valueObj) {
|
||||
return humansizeFilter(parseInt(valueObj.value, 10));
|
||||
return humansizeFilter(parseInt(valueObj.value, 10), 2);
|
||||
},
|
||||
responsive: true
|
||||
});
|
||||
@@ -112,7 +112,7 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
|
||||
return d[key];
|
||||
});
|
||||
if (arr.join('').indexOf('no such id') !== -1) {
|
||||
Messages.error('Unable to retrieve stats', 'Is this container running?');
|
||||
Messages.error('Unable to retrieve stats', {}, 'Is this container running?');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
|
||||
updateNetworkChart(d);
|
||||
timeout = $timeout(updateStats, 5000);
|
||||
}, function () {
|
||||
Messages.error('Unable to retrieve stats', 'Is this container running?');
|
||||
Messages.error('Unable to retrieve stats', {}, 'Is this container running?');
|
||||
timeout = $timeout(updateStats, 5000);
|
||||
});
|
||||
}
|
||||
@@ -188,7 +188,7 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
|
||||
Container.get({id: $stateParams.id}, function (d) {
|
||||
$scope.container = d;
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e.data);
|
||||
Messages.error("Failure", e, "Unable to retrieve container info");
|
||||
});
|
||||
$scope.getTop();
|
||||
}]);
|
||||
|
||||
@@ -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">
|
||||
@@ -52,44 +16,58 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Nodes</td>
|
||||
<td>{{ swarm.Nodes }}</td>
|
||||
<td ng-if="!swarm_mode">{{ swarm.Nodes }}</td>
|
||||
<td ng-if="swarm_mode">{{ info.Swarm.Nodes }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Containers</td>
|
||||
<td>{{ info.Containers }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr ng-if="!swarm_mode">
|
||||
<td>Images</td>
|
||||
<td>{{ info.Images }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!swarm_mode">
|
||||
<td>Swarm version</td>
|
||||
<td>{{ docker.Version|swarmversion }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Docker API version</td>
|
||||
<td>{{ docker.ApiVersion }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!swarm_mode">
|
||||
<td>Strategy</td>
|
||||
<td>{{ swarm.Strategy }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CPUs</td>
|
||||
<td>{{ info.NCPU }}</td>
|
||||
<td>Total CPU</td>
|
||||
<td ng-if="!swarm_mode">{{ info.NCPU }}</td>
|
||||
<td ng-if="swarm_mode">{{ totalCPU }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total Memory</td>
|
||||
<td>{{ info.MemTotal|humansize }}</td>
|
||||
<td>Total memory</td>
|
||||
<td ng-if="!swarm_mode">{{ info.MemTotal|humansize: 2 }}</td>
|
||||
<td ng-if="swarm_mode">{{ totalMemory|humansize: 2 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Operating System</td>
|
||||
<tr ng-if="!swarm_mode">
|
||||
<td>Operating system</td>
|
||||
<td>{{ info.OperatingSystem }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kernel Version</td>
|
||||
<tr ng-if="!swarm_mode">
|
||||
<td>Kernel version</td>
|
||||
<td>{{ info.KernelVersion }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!swarm_mode">
|
||||
<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" ng-if="!swarm_mode">
|
||||
<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>
|
||||
@@ -101,6 +79,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
|
||||
@@ -108,13 +100,6 @@
|
||||
<span ng-show="sortType == 'IP' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('Containers')">
|
||||
Containers
|
||||
<span ng-show="sortType == 'Containers' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Containers' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('Engine')">
|
||||
Engine
|
||||
@@ -134,8 +119,9 @@
|
||||
<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.containers }}</td>
|
||||
<td>{{ node.version }}</td>
|
||||
<td><span class="label label-{{ node.status|nodestatusbadge }}">{{ node.status }}</span></td>
|
||||
</tr>
|
||||
@@ -144,4 +130,69 @@
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" ng-if="swarm_mode">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-hdd-o" title="Node status"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('Name')">
|
||||
Name
|
||||
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('type')">
|
||||
Role
|
||||
<span ng-show="sortType == 'type' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'type' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('cpu')">
|
||||
CPU
|
||||
<span ng-show="sortType == 'cpu' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'cpu' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('memory')">
|
||||
Memory
|
||||
<span ng-show="sortType == 'memory' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'memory' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('Engine')">
|
||||
Engine
|
||||
<span ng-show="sortType == 'Engine' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Engine' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('Status')">
|
||||
Status
|
||||
<span ng-show="sortType == 'Status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="node in (state.filteredNodes = (nodes | filter:state.filter | orderBy:sortType:sortReverse))">
|
||||
<td>{{ node.Description.Hostname }}</td>
|
||||
<td>{{ node.Spec.Role }}</td>
|
||||
<td>{{ node.Description.Resources.NanoCPUs / 1000000000 }}</td>
|
||||
<td>{{ node.Description.Resources.MemoryBytes|humansize }}</td>
|
||||
<td>{{ node.Description.Engine.EngineVersion }}</td>
|
||||
<td><span class="label label-{{ node.Status.State|nodestatusbadge }}">{{ node.Status.State }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,62 +1,82 @@
|
||||
angular.module('swarm', [])
|
||||
.controller('SwarmController', ['$scope', 'Info', 'Version', 'Settings',
|
||||
function ($scope, Info, Version, Settings) {
|
||||
.controller('SwarmController', ['$scope', 'Info', 'Version', 'Node',
|
||||
function ($scope, Info, Version, Node) {
|
||||
|
||||
$scope.sortType = 'Name';
|
||||
$scope.sortReverse = true;
|
||||
$scope.info = {};
|
||||
$scope.docker = {};
|
||||
$scope.swarm = {};
|
||||
$scope.sortType = 'Name';
|
||||
$scope.sortReverse = true;
|
||||
$scope.info = {};
|
||||
$scope.docker = {};
|
||||
$scope.swarm = {};
|
||||
$scope.swarm_mode = false;
|
||||
$scope.totalCPU = 0;
|
||||
$scope.totalMemory = 0;
|
||||
|
||||
$scope.order = function(sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
$scope.order = function(sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
Version.get({}, function (d) {
|
||||
$scope.docker = d;
|
||||
});
|
||||
Info.get({}, function (d) {
|
||||
$scope.info = d;
|
||||
extractSwarmInfo(d);
|
||||
Version.get({}, function (d) {
|
||||
$scope.docker = d;
|
||||
});
|
||||
|
||||
Info.get({}, function (d) {
|
||||
$scope.info = d;
|
||||
if (!_.startsWith(d.ServerVersion, 'swarm')) {
|
||||
$scope.swarm_mode = true;
|
||||
Node.query({}, function(d) {
|
||||
$scope.nodes = d;
|
||||
var CPU = 0, memory = 0;
|
||||
angular.forEach(d, function(node) {
|
||||
CPU += node.Description.Resources.NanoCPUs;
|
||||
memory += node.Description.Resources.MemoryBytes;
|
||||
});
|
||||
$scope.totalCPU = CPU / 1000000000;
|
||||
$scope.totalMemory = memory;
|
||||
});
|
||||
} else {
|
||||
extractSwarmInfo(d);
|
||||
}
|
||||
});
|
||||
|
||||
function extractSwarmInfo(info) {
|
||||
// Swarm info is available in SystemStatus object
|
||||
var systemStatus = info.SystemStatus;
|
||||
// Swarm strategy
|
||||
$scope.swarm[systemStatus[1][0]] = systemStatus[1][1];
|
||||
// Swarm filters
|
||||
$scope.swarm[systemStatus[2][0]] = systemStatus[2][1];
|
||||
// Swarm node count
|
||||
var node_count = parseInt(systemStatus[3][1], 10);
|
||||
$scope.swarm[systemStatus[3][0]] = node_count;
|
||||
function extractSwarmInfo(info) {
|
||||
// Swarm info is available in SystemStatus object
|
||||
var systemStatus = info.SystemStatus;
|
||||
// Swarm strategy
|
||||
$scope.swarm[systemStatus[1][0]] = systemStatus[1][1];
|
||||
// Swarm filters
|
||||
$scope.swarm[systemStatus[2][0]] = systemStatus[2][1];
|
||||
// Swarm node count
|
||||
var nodes = systemStatus[0][1] === 'primary' ? systemStatus[3][1] : systemStatus[4][1];
|
||||
var node_count = parseInt(nodes, 10);
|
||||
$scope.swarm[systemStatus[3][0]] = node_count;
|
||||
|
||||
$scope.swarm.Status = [];
|
||||
extractNodesInfo(systemStatus, node_count);
|
||||
}
|
||||
$scope.swarm.Status = [];
|
||||
extractNodesInfo(systemStatus, node_count);
|
||||
}
|
||||
|
||||
function extractNodesInfo(info, node_count) {
|
||||
// First information for node1 available at element #4 of SystemStatus
|
||||
// The next 10 elements are information related to the node
|
||||
var node_offset = 4;
|
||||
for (i = 0; i < node_count; i++) {
|
||||
extractNodeInfo(info, node_offset);
|
||||
node_offset += 10;
|
||||
}
|
||||
}
|
||||
function extractNodesInfo(info, node_count) {
|
||||
// First information for node1 available at element #4 of SystemStatus if connected to a primary
|
||||
// If connected to a replica, information for node1 is available at element #5
|
||||
// The next 10 elements are information related to the node
|
||||
var node_offset = info[0][1] === 'primary' ? 4 : 5;
|
||||
for (i = 0; i < node_count; i++) {
|
||||
extractNodeInfo(info, node_offset);
|
||||
node_offset += 9;
|
||||
}
|
||||
}
|
||||
|
||||
function extractNodeInfo(info, offset) {
|
||||
var node = {};
|
||||
node.name = info[offset][0];
|
||||
node.ip = info[offset][1];
|
||||
node.id = info[offset + 1][1];
|
||||
node.status = info[offset + 2][1];
|
||||
node.containers = info[offset + 3][1];
|
||||
node.cpu = info[offset + 4][1];
|
||||
node.memory = info[offset + 5][1];
|
||||
node.labels = info[offset + 6][1];
|
||||
node.version = info[offset + 8][1];
|
||||
$scope.swarm.Status.push(node);
|
||||
}
|
||||
}]);
|
||||
function extractNodeInfo(info, offset) {
|
||||
var node = {};
|
||||
node.name = info[offset][0];
|
||||
node.ip = info[offset][1];
|
||||
node.id = info[offset + 1][1];
|
||||
node.status = info[offset + 2][1];
|
||||
node.containers = info[offset + 3][1];
|
||||
node.cpu = info[offset + 4][1].split('/')[1];
|
||||
node.memory = info[offset + 5][1].split('/')[1];
|
||||
node.labels = info[offset + 6][1];
|
||||
node.version = info[offset + 8][1];
|
||||
$scope.swarm.Status.push(node);
|
||||
}
|
||||
}]);
|
||||
|
||||
50
app/components/task/task.html
Normal file
@@ -0,0 +1,50 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Task details">
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
Services > <a ui-sref="service({id: task.ServiceID})">{{ serviceName }}</a> > {{ task.ID }}
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Task status"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>ID</td>
|
||||
<td>{{ task.ID }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>State</td>
|
||||
<td><span class="label label-{{ task.Status.State|taskstatusbadge }}">{{ task.Status.State }}</span></td>
|
||||
</tr>
|
||||
<tr ng-if="task.Status.Err">
|
||||
<td>Error message</td>
|
||||
<td><code>{{ task.Status.Err }}</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Image</td>
|
||||
<td>{{ task.Spec.ContainerSpec.Image }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Slot</td>
|
||||
<td>{{ task.Slot }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Created</td>
|
||||
<td>{{ task.CreatedAt|getisodate }}</td>
|
||||
</tr>
|
||||
<tr ng-if="task.Status.ContainerStatus.ContainerID">
|
||||
<td>Container ID</td>
|
||||
<td>{{ task.Status.ContainerStatus.ContainerID }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
29
app/components/task/taskController.js
Normal file
@@ -0,0 +1,29 @@
|
||||
angular.module('task', [])
|
||||
.controller('TaskController', ['$scope', '$stateParams', '$state', 'Task', 'Service', 'Messages',
|
||||
function ($scope, $stateParams, $state, Task, Service, Messages) {
|
||||
|
||||
$scope.task = {};
|
||||
$scope.serviceName = 'service';
|
||||
$scope.isTaskRunning = false;
|
||||
|
||||
function fetchTaskDetails() {
|
||||
$('#loadingViewSpinner').show();
|
||||
Task.get({id: $stateParams.id}, function (d) {
|
||||
$scope.task = d;
|
||||
fetchAssociatedServiceDetails(d.ServiceID);
|
||||
$('#loadingViewSpinner').hide();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e, "Unable to retrieve task details");
|
||||
});
|
||||
}
|
||||
|
||||
function fetchAssociatedServiceDetails(serviceId) {
|
||||
Service.get({id: serviceId}, function (d) {
|
||||
$scope.serviceName = d.Spec.Name;
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e, "Unable to retrieve associated service details");
|
||||
});
|
||||
}
|
||||
|
||||
fetchTaskDetails();
|
||||
}]);
|
||||
92
app/components/templates/templates.html
Normal file
@@ -0,0 +1,92 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Application templates list">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="templates" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Templates</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row" ng-if="selectedTemplate">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-custom-header icon="selectedTemplate.logo" title="selectedTemplate.image">
|
||||
</rd-widget-custom-header>
|
||||
<rd-widget-body classes="padding">
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group" ng-if="globalNetworkCount === 0 && !swarm_mode">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">When using Swarm, we recommend deploying containers in a shared network. Looks like you don't have any shared network, head over the <a ui-sref="networks">networks view</a> to create one.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="swarm_mode">
|
||||
<div class="col-sm-12">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
<span class="small text-muted">App templates cannot be used with swarm-mode at the moment. You can still use them to quickly deploy containers to the Docker host.</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- name-and-network-inputs -->
|
||||
<div class="form-group">
|
||||
<label for="image_registry" class="col-sm-2 control-label text-left">Name</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" class="form-control" ng-model="formValues.name" placeholder="e.g. web (optional)">
|
||||
</div>
|
||||
<label for="container_network" class="col-sm-2 control-label text-right">Network</label>
|
||||
<div class="col-sm-4">
|
||||
<select class="selectpicker form-control" ng-options="net.Name for net in availableNetworks" ng-model="formValues.network">
|
||||
<option disabled hidden value="">Select a network</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-and-network-inputs -->
|
||||
<div ng-repeat="var in selectedTemplate.env" ng-if="!var.set" class="form-group">
|
||||
<label for="field_{{ $index }}" class="col-sm-2 control-label text-left">{{ var.label }}</label>
|
||||
<div class="col-sm-10">
|
||||
<select ng-if="(!swarm || swarm && swarm_mode) && var.type === 'container'" ng-options="container|containername for container in runningContainers" class="selectpicker form-control" ng-model="var.value">
|
||||
<option selected disabled hidden value="">Select a container</option>
|
||||
</select>
|
||||
<select ng-if="swarm && !swarm_mode && var.type === 'container'" ng-options="container|swarmcontainername for container in runningContainers" class="selectpicker form-control" ng-model="var.value">
|
||||
<option selected disabled hidden value="">Select a container</option>
|
||||
</select>
|
||||
<input ng-if="!var.type || !var.type === 'container'" type="text" class="form-control" ng-model="var.value" id="field_{{ $index }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-default btn-sm" ng-disabled="!formValues.network" ng-click="createTemplate()">Create</button>
|
||||
<i id="createContainerSpinner" 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" ng-if="selectedTemplate">
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-rocket" title="Available templates">
|
||||
<div class="pull-right">
|
||||
<i id="loadTemplatesSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="padding">
|
||||
<div class="template-list">
|
||||
<div ng-repeat="tpl in templates" class="container-template hvr-grow" id="template_{{ $index }}" ng-click="selectTemplate($index)">
|
||||
<img class="logo" ng-src="{{ tpl.logo }}" />
|
||||
<div class="title">{{ tpl.title }}</div>
|
||||
<div class="description">{{ tpl.description }}</div>
|
||||
</div>
|
||||
<div ng-if="templates.length == 0" class="text-center text-muted">
|
||||
No templates available.
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
205
app/components/templates/templatesController.js
Normal file
@@ -0,0 +1,205 @@
|
||||
angular.module('templates', [])
|
||||
.controller('TemplatesController', ['$scope', '$q', '$state', '$filter', 'Config', 'Info', 'Container', 'ContainerHelper', 'Image', 'Volume', 'Network', 'Templates', 'Messages',
|
||||
function ($scope, $q, $state, $filter, Config, Info, Container, ContainerHelper, Image, Volume, Network, Templates, Messages) {
|
||||
$scope.templates = [];
|
||||
$scope.selectedTemplate = null;
|
||||
$scope.formValues = {
|
||||
network: "",
|
||||
name: ""
|
||||
};
|
||||
$scope.templates = [];
|
||||
|
||||
var selectedItem = -1;
|
||||
|
||||
// TODO: centralize, already present in createContainerController
|
||||
function createContainer(config) {
|
||||
Container.create(config, function (d) {
|
||||
if (d.message) {
|
||||
$('#createContainerSpinner').hide();
|
||||
Messages.error('Error', {}, d.message);
|
||||
} else {
|
||||
Container.start({id: d.Id}, {}, function (cd) {
|
||||
if (cd.message) {
|
||||
$('#createContainerSpinner').hide();
|
||||
Messages.error('Error', {}, cd.message);
|
||||
} else {
|
||||
$('#createContainerSpinner').hide();
|
||||
Messages.send('Container Started', d.Id);
|
||||
$state.go('containers', {}, {reload: true});
|
||||
}
|
||||
}, function (e) {
|
||||
$('#createContainerSpinner').hide();
|
||||
Messages.error("Failure", e, 'Unable to start container');
|
||||
});
|
||||
}
|
||||
}, function (e) {
|
||||
$('#createContainerSpinner').hide();
|
||||
Messages.error("Failure", e, 'Unable to create container');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// TODO: centralize, already present in createContainerController
|
||||
function pullImageAndCreateContainer(imageConfig, containerConfig) {
|
||||
Image.create(imageConfig, function (data) {
|
||||
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
|
||||
if (err) {
|
||||
var detail = data[data.length - 1];
|
||||
$('#createContainerSpinner').hide();
|
||||
Messages.error("Error", {}, detail.error);
|
||||
} else {
|
||||
createContainer(containerConfig);
|
||||
}
|
||||
}, function (e) {
|
||||
$('#createContainerSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to pull image");
|
||||
});
|
||||
}
|
||||
|
||||
function getInitialConfiguration() {
|
||||
return {
|
||||
Env: [],
|
||||
OpenStdin: false,
|
||||
Tty: false,
|
||||
ExposedPorts: {},
|
||||
HostConfig: {
|
||||
RestartPolicy: {
|
||||
Name: 'no'
|
||||
},
|
||||
PortBindings: {},
|
||||
Binds: [],
|
||||
NetworkMode: $scope.formValues.network.Name,
|
||||
Privileged: false
|
||||
},
|
||||
Volumes: {},
|
||||
name: $scope.formValues.name
|
||||
};
|
||||
}
|
||||
|
||||
function createConfigFromTemplate(template) {
|
||||
var containerConfig = getInitialConfiguration();
|
||||
containerConfig.Image = template.image;
|
||||
if (template.env) {
|
||||
template.env.forEach(function (v) {
|
||||
if (v.value || v.set) {
|
||||
var val;
|
||||
if (v.type && v.type === 'container') {
|
||||
if ($scope.swarm && $scope.formValues.network.Scope === 'global') {
|
||||
val = $filter('swarmcontainername')(v.value);
|
||||
} else {
|
||||
var container = v.value;
|
||||
val = container.NetworkSettings.Networks[Object.keys(container.NetworkSettings.Networks)[0]].IPAddress;
|
||||
}
|
||||
} else {
|
||||
val = v.set ? v.set : v.value;
|
||||
}
|
||||
containerConfig.Env.push(v.name + "=" + val);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (template.ports) {
|
||||
template.ports.forEach(function (p) {
|
||||
containerConfig.ExposedPorts[p] = {};
|
||||
containerConfig.HostConfig.PortBindings[p] = [{ HostPort: ""}];
|
||||
});
|
||||
}
|
||||
return containerConfig;
|
||||
}
|
||||
|
||||
function prepareVolumeQueries(template, containerConfig) {
|
||||
var volumeQueries = [];
|
||||
if (template.volumes) {
|
||||
template.volumes.forEach(function (vol) {
|
||||
volumeQueries.push(
|
||||
Volume.create({}, function (d) {
|
||||
if (d.message) {
|
||||
Messages.error("Unable to create volume", {}, d.message);
|
||||
} else {
|
||||
Messages.send("Volume created", d.Name);
|
||||
containerConfig.Volumes[vol] = {};
|
||||
containerConfig.HostConfig.Binds.push(d.Name + ':' + vol);
|
||||
}
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e, "Unable to create volume");
|
||||
}).$promise
|
||||
);
|
||||
});
|
||||
}
|
||||
return volumeQueries;
|
||||
}
|
||||
|
||||
$scope.createTemplate = function() {
|
||||
$('#createContainerSpinner').show();
|
||||
var template = $scope.selectedTemplate;
|
||||
var containerConfig = createConfigFromTemplate(template);
|
||||
var imageConfig = {
|
||||
fromImage: template.image.split(':')[0],
|
||||
tag: template.image.split(':')[1] ? template.image.split(':')[1] : 'latest'
|
||||
};
|
||||
var createVolumeQueries = prepareVolumeQueries(template, containerConfig);
|
||||
$q.all(createVolumeQueries).then(function (d) {
|
||||
pullImageAndCreateContainer(imageConfig, containerConfig);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.selectTemplate = function(id) {
|
||||
$('#template_' + id).toggleClass("container-template--selected");
|
||||
if (selectedItem === id) {
|
||||
selectedItem = -1;
|
||||
$scope.selectedTemplate = null;
|
||||
} else {
|
||||
$('#template_' + selectedItem).toggleClass("container-template--selected");
|
||||
selectedItem = id;
|
||||
$scope.selectedTemplate = $scope.templates[id];
|
||||
}
|
||||
};
|
||||
|
||||
function initTemplates() {
|
||||
Templates.get(function (data) {
|
||||
$scope.templates = data;
|
||||
$('#loadTemplatesSpinner').hide();
|
||||
}, function (e) {
|
||||
$('#loadTemplatesSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to retrieve apps list");
|
||||
});
|
||||
}
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.swarm = c.swarm;
|
||||
Info.get({}, function(info) {
|
||||
if ($scope.swarm && !_.startsWith(info.ServerVersion, 'swarm')) {
|
||||
$scope.swarm_mode = true;
|
||||
}
|
||||
});
|
||||
var containersToHideLabels = c.hiddenLabels;
|
||||
Network.query({}, function (d) {
|
||||
var networks = d;
|
||||
if ($scope.swarm) {
|
||||
networks = d.filter(function (network) {
|
||||
if (network.Scope === 'global') {
|
||||
return network;
|
||||
}
|
||||
});
|
||||
$scope.globalNetworkCount = networks.length;
|
||||
networks.push({Scope: "local", Name: "bridge"});
|
||||
networks.push({Scope: "local", Name: "host"});
|
||||
networks.push({Scope: "local", Name: "none"});
|
||||
} else {
|
||||
$scope.formValues.network = _.find(networks, function(o) { return o.Name === "bridge"; });
|
||||
}
|
||||
$scope.availableNetworks = networks;
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e, "Unable to retrieve networks");
|
||||
});
|
||||
Container.query({all: 0}, function (d) {
|
||||
var containers = d;
|
||||
if (containersToHideLabels) {
|
||||
containers = ContainerHelper.hideContainers(d, containersToHideLabels);
|
||||
}
|
||||
$scope.runningContainers = containers;
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e, "Unable to retrieve running containers");
|
||||
});
|
||||
initTemplates();
|
||||
});
|
||||
}]);
|
||||
@@ -1,5 +1,3 @@
|
||||
<div ng-include="template" ng-controller="CreateVolumeController"></div>
|
||||
|
||||
<rd-header>
|
||||
<rd-header-title title="Volume list">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="volumes" ui-sref-opts="{reload: true}">
|
||||
@@ -12,10 +10,13 @@
|
||||
<div class="col-lg-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-cubes" title="Volumes">
|
||||
<div class="pull-right">
|
||||
<i id="loadVolumesSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-lg-12">
|
||||
<div class="pull-left">
|
||||
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount">Remove</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash btn-ico" aria-hidden="true"></i>Remove</button>
|
||||
<a class="btn btn-default" type="button" ui-sref="actions.create.volume">Add volume</a>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
@@ -24,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
|
||||
@@ -54,13 +55,16 @@
|
||||
<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>
|
||||
<tr ng-if="volumes.length == 0">
|
||||
<td colspan="4" class="text-center text-muted">No volumes available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
<rd-widget>
|
||||
</div>
|
||||
<rd-widget>
|
||||
</div>
|
||||
|
||||
@@ -1,28 +1,21 @@
|
||||
angular.module('volumes', [])
|
||||
.controller('VolumesController', ['$scope', 'Volume', 'ViewSpinner', 'Messages', 'errorMsgFilter',
|
||||
function ($scope, Volume, ViewSpinner, Messages, errorMsgFilter) {
|
||||
.controller('VolumesController', ['$scope', '$state', 'Volume', 'Messages',
|
||||
function ($scope, $state, Volume, Messages) {
|
||||
$scope.state = {};
|
||||
$scope.state.toggle = false;
|
||||
$scope.state.selectedItemCount = 0;
|
||||
$scope.sortType = 'Name';
|
||||
$scope.sortReverse = true;
|
||||
$scope.volumes = [];
|
||||
|
||||
$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++;
|
||||
@@ -32,24 +25,28 @@ function ($scope, Volume, ViewSpinner, Messages, errorMsgFilter) {
|
||||
};
|
||||
|
||||
$scope.removeAction = function () {
|
||||
ViewSpinner.spin();
|
||||
$('#loadVolumesSpinner').show();
|
||||
var counter = 0;
|
||||
var complete = function () {
|
||||
counter = counter - 1;
|
||||
if (counter === 0) {
|
||||
ViewSpinner.stop();
|
||||
$('#loadVolumesSpinner').hide();
|
||||
}
|
||||
};
|
||||
angular.forEach($scope.volumes, function (volume) {
|
||||
if (volume.Checked) {
|
||||
counter = counter + 1;
|
||||
Volume.remove({name: volume.Name}, function (d) {
|
||||
Messages.send("Volume deleted", volume.Name);
|
||||
var index = $scope.volumes.indexOf(volume);
|
||||
$scope.volumes.splice(index, 1);
|
||||
if (d.message) {
|
||||
Messages.error("Unable to remove volume", {}, d.message);
|
||||
} else {
|
||||
Messages.send("Volume deleted", volume.Name);
|
||||
var index = $scope.volumes.indexOf(volume);
|
||||
$scope.volumes.splice(index, 1);
|
||||
}
|
||||
complete();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e.data);
|
||||
Messages.error("Failure", e, "Unable to remove volume");
|
||||
complete();
|
||||
});
|
||||
}
|
||||
@@ -57,13 +54,13 @@ function ($scope, Volume, ViewSpinner, Messages, errorMsgFilter) {
|
||||
};
|
||||
|
||||
function fetchVolumes() {
|
||||
ViewSpinner.spin();
|
||||
$('#loadVolumesSpinner').show();
|
||||
Volume.query({}, function (d) {
|
||||
$scope.volumes = d.Volumes;
|
||||
ViewSpinner.stop();
|
||||
$('#loadVolumesSpinner').hide();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e.data);
|
||||
ViewSpinner.stop();
|
||||
$('#loadVolumesSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to retrieve volumes");
|
||||
});
|
||||
}
|
||||
fetchVolumes();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
angular
|
||||
.module('uifordocker')
|
||||
.module('portainer')
|
||||
.directive('rdHeaderContent', function rdHeaderContent() {
|
||||
var directive = {
|
||||
requires: '^rdHeader',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
angular
|
||||
.module('uifordocker')
|
||||
.module('portainer')
|
||||
.directive('rdHeaderTitle', function rdHeaderTitle() {
|
||||
var directive = {
|
||||
requires: '^rdHeader',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
angular
|
||||
.module('uifordocker')
|
||||
.module('portainer')
|
||||
.directive('rdHeader', function rdHeader() {
|
||||
var directive = {
|
||||
scope: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
angular
|
||||
.module('uifordocker')
|
||||
.module('portainer')
|
||||
.directive('rdLoading', function rdLoading() {
|
||||
var directive = {
|
||||
restrict: 'AE',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
angular
|
||||
.module('uifordocker')
|
||||
.module('portainer')
|
||||
.directive('rdWidgetBody', function rdWidgetBody() {
|
||||
var directive = {
|
||||
requires: '^rdWidget',
|
||||
|
||||
15
app/directives/widget-custom-header.js
Normal file
@@ -0,0 +1,15 @@
|
||||
angular
|
||||
.module('portainer')
|
||||
.directive('rdWidgetCustomHeader', function rdWidgetCustomHeader() {
|
||||
var directive = {
|
||||
requires: '^rdWidget',
|
||||
scope: {
|
||||
title: '=',
|
||||
icon: '='
|
||||
},
|
||||
transclude: true,
|
||||
template: '<div class="widget-header"><div class="row"><span class="pull-left"><img class="custom-header-ico" ng-src="{{icon}}"></img> <span class="small text-muted"> {{title}} </span> </span><span class="pull-right col-xs-6 col-sm-4" ng-transclude></span></div></div>',
|
||||
restrict: 'E'
|
||||
};
|
||||
return directive;
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
angular
|
||||
.module('uifordocker')
|
||||
.module('portainer')
|
||||
.directive('rdWidgetFooter', function rdWidgetFooter() {
|
||||
var directive = {
|
||||
requires: '^rdWidget',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
angular
|
||||
.module('uifordocker')
|
||||
.module('portainer')
|
||||
.directive('rdWidgetHeader', function rdWidgetTitle() {
|
||||
var directive = {
|
||||
requires: '^rdWidget',
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
angular
|
||||
.module('uifordocker')
|
||||
.module('portainer')
|
||||
.directive('rdWidgetTaskbar', function rdWidgetTaskbar() {
|
||||
var directive = {
|
||||
requires: '^rdWidget',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
angular
|
||||
.module('uifordocker')
|
||||
.module('portainer')
|
||||
.directive('rdWidget', function rdWidget() {
|
||||
var directive = {
|
||||
scope: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
angular.module('dockerui.filters', [])
|
||||
angular.module('portainer.filters', [])
|
||||
.filter('truncate', function () {
|
||||
'use strict';
|
||||
return function (text, length, end) {
|
||||
@@ -18,19 +18,52 @@ angular.module('dockerui.filters', [])
|
||||
}
|
||||
};
|
||||
})
|
||||
.filter('taskstatusbadge', function () {
|
||||
'use strict';
|
||||
return function (text) {
|
||||
var status = _.toLower(text);
|
||||
if (status.indexOf('new') !== -1 || status.indexOf('allocated') !== -1 ||
|
||||
status.indexOf('assigned') !== -1 || status.indexOf('accepted') !== -1) {
|
||||
return 'info';
|
||||
} else if (status.indexOf('pending') !== -1) {
|
||||
return 'warning';
|
||||
} else if (status.indexOf('shutdown') !== -1 || status.indexOf('failed') !== -1 ||
|
||||
status.indexOf('rejected') !== -1) {
|
||||
return 'danger';
|
||||
} else if (status.indexOf('complete') !== -1) {
|
||||
return 'primary';
|
||||
}
|
||||
return 'success';
|
||||
};
|
||||
})
|
||||
.filter('containerstatusbadge', function () {
|
||||
'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 +112,6 @@ angular.module('dockerui.filters', [])
|
||||
if (state === undefined) {
|
||||
return 'label-default';
|
||||
}
|
||||
|
||||
if (state.Ghost && state.Running) {
|
||||
return 'label-important';
|
||||
}
|
||||
@@ -91,15 +123,13 @@ angular.module('dockerui.filters', [])
|
||||
})
|
||||
.filter('humansize', function () {
|
||||
'use strict';
|
||||
return function (bytes) {
|
||||
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
if (bytes === 0) {
|
||||
return 'n/a';
|
||||
return function (bytes, round) {
|
||||
if (!round) {
|
||||
round = 1;
|
||||
}
|
||||
if (bytes || bytes === 0) {
|
||||
return filesize(bytes, {base: 10, round: round});
|
||||
}
|
||||
var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)), 10);
|
||||
var value = bytes / Math.pow(1024, i);
|
||||
var decimalPlaces = (i < 1) ? 0 : (i - 1);
|
||||
return value.toFixed(decimalPlaces) + ' ' + sizes[[i]];
|
||||
};
|
||||
})
|
||||
.filter('containername', function () {
|
||||
@@ -115,41 +145,72 @@ 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('errorMsg', function () {
|
||||
return function (object) {
|
||||
var idx = 0;
|
||||
var msg = '';
|
||||
while (object[idx] && typeof(object[idx]) === 'string') {
|
||||
msg += object[idx];
|
||||
idx++;
|
||||
.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(' ');
|
||||
}
|
||||
return msg;
|
||||
};
|
||||
})
|
||||
.filter('key', function () {
|
||||
'use strict';
|
||||
return function (pair, separator) {
|
||||
return pair.slice(0, pair.indexOf(separator));
|
||||
};
|
||||
})
|
||||
.filter('value', function () {
|
||||
'use strict';
|
||||
return function (pair, separator) {
|
||||
return pair.slice(pair.indexOf(separator) + 1);
|
||||
};
|
||||
})
|
||||
.filter('emptyobject', function () {
|
||||
'use strict';
|
||||
return function (obj) {
|
||||
return _.isEmpty(obj);
|
||||
};
|
||||
})
|
||||
.filter('ipaddress', function () {
|
||||
'use strict';
|
||||
return function (ip) {
|
||||
return ip.slice(0, ip.indexOf('/'));
|
||||
};
|
||||
});
|
||||
|
||||
51
app/shared/helpers.js
Normal file
@@ -0,0 +1,51 @@
|
||||
angular.module('portainer.helpers', [])
|
||||
.factory('ImageHelper', [function ImageHelperFactory() {
|
||||
'use strict';
|
||||
return {
|
||||
createImageConfig: function(imageName, registry) {
|
||||
var imageNameAndTag = imageName.split(':');
|
||||
var image = imageNameAndTag[0];
|
||||
if (registry) {
|
||||
image = registry + '/' + imageNameAndTag[0];
|
||||
}
|
||||
var imageConfig = {
|
||||
repo: image,
|
||||
tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest'
|
||||
};
|
||||
return imageConfig;
|
||||
}
|
||||
};
|
||||
}])
|
||||
.factory('ContainerHelper', [function ContainerHelperFactory() {
|
||||
'use strict';
|
||||
return {
|
||||
hideContainers: function(containers, containersToHideLabels) {
|
||||
return containers.filter(function (container) {
|
||||
var filterContainer = false;
|
||||
containersToHideLabels.forEach(function(label, index) {
|
||||
if (_.has(container.Labels, label.name) &&
|
||||
container.Labels[label.name] === label.value) {
|
||||
filterContainer = true;
|
||||
}
|
||||
});
|
||||
if (!filterContainer) {
|
||||
return container;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}])
|
||||
.factory('ServiceHelper', [function ServiceHelperFactory() {
|
||||
'use strict';
|
||||
return {
|
||||
serviceToConfig: function(service) {
|
||||
return {
|
||||
Name: service.Spec.Name,
|
||||
TaskTemplate: service.Spec.TaskTemplate,
|
||||
Mode: service.Spec.Mode,
|
||||
Networks: service.Spec.Networks,
|
||||
EndpointSpec: service.Spec.EndpointSpec
|
||||
};
|
||||
}
|
||||
};
|
||||
}]);
|
||||
68
app/shared/responseHandlers.js
Normal file
@@ -0,0 +1,68 @@
|
||||
function isJSONArray(jsonString) {
|
||||
return Object.prototype.toString.call(jsonString) === '[object Array]';
|
||||
}
|
||||
|
||||
function isJSON(jsonString) {
|
||||
try {
|
||||
var o = JSON.parse(jsonString);
|
||||
if (o && typeof o === "object") {
|
||||
return o;
|
||||
}
|
||||
}
|
||||
catch (e) { }
|
||||
return false;
|
||||
}
|
||||
|
||||
// The Docker API often returns a list of JSON object.
|
||||
// This handler wrap the JSON objects in an array.
|
||||
// Used by the API in: Image push, Image create, Events query.
|
||||
function jsonObjectsToArrayHandler(data) {
|
||||
var str = "[" + data.replace(/\n/g, " ").replace(/\}\s*\{/g, "}, {") + "]";
|
||||
return angular.fromJson(str);
|
||||
}
|
||||
|
||||
// The Docker API often returns an empty string or a valid JSON object on success (Docker 1.9 -> Docker 1.12).
|
||||
// On error, it returns either an error message as a string (Docker < 1.12) or a JSON object with the field message
|
||||
// container the error (Docker = 1.12)
|
||||
// This handler ensure a valid JSON object is returned in any case.
|
||||
// Used by the API in: container deletion, network deletion, network creation, volume creation,
|
||||
// container exec, exec resize.
|
||||
function genericHandler(data) {
|
||||
var response = {};
|
||||
// No data is returned when deletion is successful (Docker 1.9 -> 1.12)
|
||||
if (!data) {
|
||||
return response;
|
||||
}
|
||||
// A string is returned on failure (Docker < 1.12)
|
||||
else if (!isJSON(data)) {
|
||||
response.message = data;
|
||||
}
|
||||
// Docker 1.12 returns a valid JSON object when an error occurs
|
||||
else {
|
||||
response = angular.fromJson(data);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// Image delete API returns an array on success (Docker 1.9 -> Docker 1.12).
|
||||
// On error, it returns either an error message as a string (Docker < 1.12) or a JSON object with the field message
|
||||
// container the error (Docker = 1.12).
|
||||
// This handler returns the original array on success or a newly created array containing
|
||||
// only one JSON object with the field message filled with the error message on failure.
|
||||
function deleteImageHandler(data) {
|
||||
// A string is returned on failure (Docker < 1.12)
|
||||
var response = [];
|
||||
if (!isJSON(data)) {
|
||||
response.push({message: data});
|
||||
}
|
||||
// A JSON object is returned on failure (Docker = 1.12)
|
||||
else if (!isJSONArray) {
|
||||
var json = angular.fromJson(data);
|
||||
response.push(json);
|
||||
}
|
||||
// An array is returned on success (Docker 1.9 -> 1.12)
|
||||
else {
|
||||
response = angular.fromJson(data);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
angular.module('dockerui.services', ['ngResource', 'ngSanitize'])
|
||||
angular.module('portainer.services', ['ngResource', 'ngSanitize'])
|
||||
.factory('Container', ['$resource', 'Settings', function ContainerFactory($resource, Settings) {
|
||||
'use strict';
|
||||
// Resource for interacting with the docker containers
|
||||
@@ -8,38 +8,70 @@ angular.module('dockerui.services', ['ngResource', 'ngSanitize'])
|
||||
}, {
|
||||
query: {method: 'GET', params: {all: 0, action: 'json'}, isArray: true},
|
||||
get: {method: 'GET', params: {action: 'json'}},
|
||||
start: {method: 'POST', params: {id: '@id', action: 'start'}},
|
||||
stop: {method: 'POST', params: {id: '@id', t: 5, action: 'stop'}},
|
||||
restart: {method: 'POST', params: {id: '@id', t: 5, action: 'restart'}},
|
||||
kill: {method: 'POST', params: {id: '@id', action: 'kill'}},
|
||||
pause: {method: 'POST', params: {id: '@id', action: 'pause'}},
|
||||
unpause: {method: 'POST', params: {id: '@id', action: 'unpause'}},
|
||||
changes: {method: 'GET', params: {action: 'changes'}, isArray: true},
|
||||
create: {method: 'POST', params: {action: 'create'}},
|
||||
remove: {method: 'DELETE', params: {id: '@id', v: 0}},
|
||||
rename: {method: 'POST', params: {id: '@id', action: 'rename'}, isArray: false},
|
||||
stats: {method: 'GET', params: {id: '@id', stream: false, action: 'stats'}, timeout: 5000}
|
||||
stats: {method: 'GET', params: {id: '@id', stream: false, action: 'stats'}, timeout: 5000},
|
||||
start: {
|
||||
method: 'POST', params: {id: '@id', action: 'start'},
|
||||
transformResponse: genericHandler
|
||||
},
|
||||
create: {
|
||||
method: 'POST', params: {action: 'create'},
|
||||
transformResponse: genericHandler
|
||||
},
|
||||
remove: {
|
||||
method: 'DELETE', params: {id: '@id', v: 0},
|
||||
transformResponse: genericHandler
|
||||
},
|
||||
rename: {
|
||||
method: 'POST', params: {id: '@id', action: 'rename', name: '@name'},
|
||||
transformResponse: genericHandler
|
||||
},
|
||||
exec: {
|
||||
method: 'POST', params: {id: '@id', action: 'exec'},
|
||||
transformResponse: genericHandler
|
||||
}
|
||||
});
|
||||
}])
|
||||
.factory('Service', ['$resource', 'Settings', function ServiceFactory($resource, Settings) {
|
||||
'use strict';
|
||||
// https://docs.docker.com/engine/reference/api/docker_remote_api_<%= remoteApiVersion %>/#/3-9-services
|
||||
return $resource(Settings.url + '/services/:id/:action', {}, {
|
||||
get: { method: 'GET', params: {id: '@id'} },
|
||||
query: { method: 'GET', isArray: true },
|
||||
create: { method: 'POST', params: {action: 'create'} },
|
||||
update: { method: 'POST', params: {id: '@id', action: 'update', version: '@version'} },
|
||||
remove: { method: 'DELETE', params: {id: '@id'} }
|
||||
});
|
||||
}])
|
||||
.factory('Task', ['$resource', 'Settings', function TaskFactory($resource, Settings) {
|
||||
'use strict';
|
||||
// https://docs.docker.com/engine/reference/api/docker_remote_api_<%= remoteApiVersion %>/#/3-9-services
|
||||
return $resource(Settings.url + '/tasks/:id', {}, {
|
||||
get: { method: 'GET', params: {id: '@id'} },
|
||||
query: { method: 'GET', isArray: true, params: {filters: '@filters'} }
|
||||
});
|
||||
}])
|
||||
.factory('Exec', ['$resource', 'Settings', function ExecFactory($resource, Settings) {
|
||||
'use strict';
|
||||
// https://docs.docker.com/engine/reference/api/docker_remote_api_<%= remoteApiVersion %>/#/exec-resize
|
||||
return $resource(Settings.url + '/exec/:id/:action', {}, {
|
||||
resize: {
|
||||
method: 'POST', params: {id: '@id', action: 'resize', h: '@height', w: '@width'},
|
||||
transformResponse: genericHandler
|
||||
}
|
||||
});
|
||||
}])
|
||||
.factory('ContainerCommit', ['$resource', '$http', 'Settings', function ContainerCommitFactory($resource, $http, Settings) {
|
||||
'use strict';
|
||||
// http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#create-a-new-image-from-a-container-s-changes
|
||||
return {
|
||||
commit: function (params, callback) {
|
||||
$http({
|
||||
method: 'POST',
|
||||
url: Settings.url + '/commit',
|
||||
params: {
|
||||
'container': params.id,
|
||||
'tag': params.tag || null,
|
||||
'repo': params.repo || null
|
||||
},
|
||||
data: params.config
|
||||
}).success(callback).error(function (data, status, headers, config) {
|
||||
console.log(error, data);
|
||||
});
|
||||
}
|
||||
};
|
||||
return $resource(Settings.url + '/commit', {}, {
|
||||
commit: {method: 'POST', params: {container: '@id', repo: '@repo', tag: '@tag'}}
|
||||
});
|
||||
}])
|
||||
.factory('ContainerLogs', ['$resource', '$http', 'Settings', function ContainerLogsFactory($resource, $http, Settings) {
|
||||
'use strict';
|
||||
@@ -84,18 +116,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) {
|
||||
@@ -105,6 +150,22 @@ angular.module('dockerui.services', ['ngResource', 'ngSanitize'])
|
||||
get: {method: 'GET'}
|
||||
});
|
||||
}])
|
||||
.factory('Node', ['$resource', 'Settings', function NodeFactory($resource, Settings) {
|
||||
'use strict';
|
||||
// https://docs.docker.com/engine/reference/api/docker_remote_api_<%= remoteApiVersion %>/#/3-7-nodes
|
||||
return $resource(Settings.url + '/nodes', {}, {
|
||||
query: {
|
||||
method: 'GET', isArray: true
|
||||
}
|
||||
});
|
||||
}])
|
||||
.factory('Swarm', ['$resource', 'Settings', function SwarmFactory($resource, Settings) {
|
||||
'use strict';
|
||||
// https://docs.docker.com/engine/reference/api/docker_remote_api_<%= remoteApiVersion %>/#/3-8-swarm
|
||||
return $resource(Settings.url + '/swarm', {}, {
|
||||
get: {method: 'GET'}
|
||||
});
|
||||
}])
|
||||
.factory('Auth', ['$resource', 'Settings', function AuthFactory($resource, Settings) {
|
||||
'use strict';
|
||||
// http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#check-auth-configuration
|
||||
@@ -126,8 +187,8 @@ angular.module('dockerui.services', ['ngResource', 'ngSanitize'])
|
||||
return $resource(Settings.url + '/networks/:id/:action', {id: '@id'}, {
|
||||
query: {method: 'GET', isArray: true},
|
||||
get: {method: 'GET'},
|
||||
create: {method: 'POST', params: {action: 'create'}},
|
||||
remove: {method: 'DELETE'},
|
||||
create: {method: 'POST', params: {action: 'create'}, transformResponse: genericHandler},
|
||||
remove: { method: 'DELETE', transformResponse: genericHandler },
|
||||
connect: {method: 'POST', params: {action: 'connect'}},
|
||||
disconnect: {method: 'POST', params: {action: 'disconnect'}}
|
||||
});
|
||||
@@ -138,13 +199,20 @@ angular.module('dockerui.services', ['ngResource', 'ngSanitize'])
|
||||
return $resource(Settings.url + '/volumes/:name/:action', {name: '@name'}, {
|
||||
query: {method: 'GET'},
|
||||
get: {method: 'GET'},
|
||||
create: {method: 'POST', params: {action: 'create'}},
|
||||
remove: {method: 'DELETE'}
|
||||
create: {method: 'POST', params: {action: 'create'}, transformResponse: genericHandler},
|
||||
remove: {
|
||||
method: 'DELETE', transformResponse: genericHandler
|
||||
}
|
||||
});
|
||||
}])
|
||||
.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('Templates', ['$resource', 'TEMPLATES_ENDPOINT', function TemplatesFactory($resource, TEMPLATES_ENDPOINT) {
|
||||
return $resource(TEMPLATES_ENDPOINT, {}, {
|
||||
get: {method: 'GET', isArray: true}
|
||||
});
|
||||
}])
|
||||
.factory('Settings', ['DOCKER_ENDPOINT', 'DOCKER_PORT', 'UI_VERSION', function SettingsFactory(DOCKER_ENDPOINT, DOCKER_PORT, UI_VERSION) {
|
||||
'use strict';
|
||||
var url = DOCKER_ENDPOINT;
|
||||
@@ -153,27 +221,13 @@ angular.module('dockerui.services', ['ngResource', 'ngSanitize'])
|
||||
}
|
||||
var firstLoad = (localStorage.getItem('firstLoad') || 'true') === 'true';
|
||||
return {
|
||||
displayAll: false,
|
||||
displayAll: true,
|
||||
endpoint: DOCKER_ENDPOINT,
|
||||
uiVersion: UI_VERSION,
|
||||
url: url,
|
||||
firstLoad: firstLoad
|
||||
};
|
||||
}])
|
||||
.factory('ViewSpinner', function ViewSpinnerFactory() {
|
||||
'use strict';
|
||||
var spinner = new Spinner();
|
||||
var target = document.getElementById('view');
|
||||
|
||||
return {
|
||||
spin: function () {
|
||||
spinner.spin(target);
|
||||
},
|
||||
stop: function () {
|
||||
spinner.stop();
|
||||
}
|
||||
};
|
||||
})
|
||||
.factory('Messages', ['$rootScope', '$sanitize', function MessagesFactory($rootScope, $sanitize) {
|
||||
'use strict';
|
||||
return {
|
||||
@@ -189,10 +243,18 @@ angular.module('dockerui.services', ['ngResource', 'ngSanitize'])
|
||||
}
|
||||
});
|
||||
},
|
||||
error: function (title, text) {
|
||||
error: function (title, e, fallbackText) {
|
||||
var msg = fallbackText;
|
||||
if (e.data && e.data.message) {
|
||||
msg = e.data.message;
|
||||
} else if (e.message) {
|
||||
msg = e.message;
|
||||
} else if (e.data && e.data.length > 0 && e.data[0].message) {
|
||||
msg = e.data[0].message;
|
||||
}
|
||||
$.gritter.add({
|
||||
title: $sanitize(title),
|
||||
text: $sanitize(text),
|
||||
text: $sanitize(msg),
|
||||
time: 10000,
|
||||
before_open: function () {
|
||||
if ($('.gritter-item-wrapper').length === 4) {
|
||||
|
||||
@@ -1,19 +1,191 @@
|
||||
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 TaskViewModel(data, node_data) {
|
||||
this.Id = data.ID;
|
||||
this.Created = data.CreatedAt;
|
||||
this.Updated = data.UpdatedAt;
|
||||
this.Slot = data.Slot;
|
||||
this.Status = data.Status.State;
|
||||
if (node_data) {
|
||||
for (var i = 0; i < node_data.length; ++i) {
|
||||
if (data.NodeID === node_data[i].ID) {
|
||||
this.Node = node_data[i].Description.Hostname;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ServiceViewModel(data) {
|
||||
this.Model = data;
|
||||
this.Id = data.ID;
|
||||
this.Name = data.Spec.Name;
|
||||
this.Image = data.Spec.TaskTemplate.ContainerSpec.Image;
|
||||
this.Version = data.Version.Index;
|
||||
if (data.Spec.Mode.Replicated) {
|
||||
this.Mode = 'replicated' ;
|
||||
this.Replicas = data.Spec.Mode.Replicated.Replicas;
|
||||
} else {
|
||||
this.Mode = 'global';
|
||||
}
|
||||
if (data.Spec.Labels) {
|
||||
this.Labels = data.Spec.Labels;
|
||||
}
|
||||
if (data.Spec.TaskTemplate.ContainerSpec.Env) {
|
||||
this.Env = data.Spec.TaskTemplate.ContainerSpec.Env;
|
||||
}
|
||||
if (data.Endpoint.Ports) {
|
||||
this.Ports = data.Endpoint.Ports;
|
||||
}
|
||||
this.Checked = false;
|
||||
this.Scale = false;
|
||||
this.EditName = false;
|
||||
}
|
||||
|
||||
function ContainerViewModel(data) {
|
||||
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;
|
||||
this.Ports = [];
|
||||
for (var i = 0; i < data.Ports.length; ++i) {
|
||||
var p = data.Ports[i];
|
||||
if (p.PublicPort) {
|
||||
this.Ports.push({ host: p.IP, private: p.PrivatePort, public: p.PublicPort });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createEventDetails(event) {
|
||||
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;
|
||||
case 'attach':
|
||||
details = 'Container ' + eventAttr.name + ' attached';
|
||||
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;
|
||||
case 'mount':
|
||||
details = 'Volume ' + event.Actor.ID + ' mounted';
|
||||
break;
|
||||
case 'unmount':
|
||||
details = 'Volume ' + event.Actor.ID + ' unmounted';
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,114 @@ 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;
|
||||
}
|
||||
|
||||
.action-group {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.btn-ico {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.template-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.custom-header-ico {
|
||||
max-width: 16px;
|
||||
max-height: 16px;
|
||||
}
|
||||
|
||||
.container-template {
|
||||
font-size: 1em;
|
||||
width: 256px;
|
||||
height: 128px;
|
||||
margin: 10px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
border: 2px solid #f6f6f6;
|
||||
color: #30426a;
|
||||
}
|
||||
|
||||
.container-template--selected {
|
||||
background-color: #ececec;
|
||||
color: #2d3e63;
|
||||
}
|
||||
|
||||
.container-template:hover {
|
||||
background-color: #ececec;
|
||||
color: #2d3e63;
|
||||
}
|
||||
|
||||
.container-template .logo {
|
||||
max-width: 48px;
|
||||
max-height: 48px;
|
||||
}
|
||||
|
||||
.container-template .title {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container-template .description {
|
||||
text-align: center;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.btn-responsive {
|
||||
padding: 5px 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1107px) {
|
||||
.btn-responsive {
|
||||
padding: 6px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.42857143;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 880 B |
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 880 B |
|
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 3.0 KiB |
26
bower.json
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "uifordocker",
|
||||
"version": "1.2.0",
|
||||
"homepage": "https://github.com/kevana/ui-for-docker",
|
||||
"name": "portainer",
|
||||
"version": "1.9.2",
|
||||
"homepage": "https://github.com/portainer/portainer",
|
||||
"authors": [
|
||||
"Michael Crosby <crosbymichael@gmail.com>",
|
||||
"Kevan Ahlquist <ahlquistkd@gmail.com>",
|
||||
"Anthony Lapenna <anthony.lapenna@cloudinovasi.id>"
|
||||
"Anthony Lapenna <anthony.lapenna at gmail dot com>"
|
||||
],
|
||||
"description": "A web interface for the Docker Remote API.",
|
||||
"keywords": [
|
||||
"uifordocker",
|
||||
"dockerui",
|
||||
"docker",
|
||||
"api"
|
||||
"ui",
|
||||
"api",
|
||||
"portainer",
|
||||
"uifordocker",
|
||||
"dockerui"
|
||||
],
|
||||
"license": "MIT",
|
||||
"ignore": [
|
||||
@@ -30,16 +30,18 @@
|
||||
"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",
|
||||
"font-awesome": "~4.5.0",
|
||||
"font-awesome": "~4.6.3",
|
||||
"filesize": "~3.3.0",
|
||||
"Hover": "2.0.2",
|
||||
"jquery": "1.11.1",
|
||||
"jquery.gritter": "1.7.4",
|
||||
"lodash": "4.12.0",
|
||||
"rdash-ui": "1.0.*",
|
||||
"spin.js": "1.3"
|
||||
"moment": "~2.14.1",
|
||||
"xterm.js": "~2.0.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"angular": "1.5.5"
|
||||
|
||||
16
build.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
VERSION=$1
|
||||
|
||||
if [[ $# -ne 1 ]] ; then
|
||||
echo "Usage: $(basename $0) <VERSION>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
grunt release
|
||||
rm -rf /tmp/portainer-build && mkdir -pv /tmp/portainer-build/portainer
|
||||
mv dist/* /tmp/portainer-build/portainer
|
||||
cd /tmp/portainer-build
|
||||
tar cvpfz portainer-${VERSION}.tar.gz portainer
|
||||
|
||||
exit 0
|
||||
BIN
dashboard.png
|
Before Width: | Height: | Size: 67 KiB |
188
dockerui.go
@@ -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.2.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)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
FROM nginx:latest
|
||||
|
||||
COPY default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY users.htpasswd /etc/nginx/users.htpasswd
|
||||
@@ -1,17 +0,0 @@
|
||||
upstream dockerui {
|
||||
server dockerui:9000;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
auth_basic "Docker UI";
|
||||
auth_basic_user_file /etc/nginx/users.htpasswd;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_pass http://dockerui;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
dockerui:
|
||||
image: cloudinovasi/ui-for-docker
|
||||
command: -e http://<SWARM_HOST>:<SWARM_PORT>
|
||||
|
||||
nginx:
|
||||
build: .
|
||||
links:
|
||||
- dockerui
|
||||
ports:
|
||||
- 80:80
|
||||
@@ -1 +0,0 @@
|
||||
user:{PLAIN}password
|
||||
48
gruntFile.js
@@ -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,12 +68,13 @@ 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/spin.js/spin.js',
|
||||
'bower_components/Chart.js/Chart.min.js',
|
||||
'bower_components/lodash/dist/lodash.min.js',
|
||||
'bower_components/oboe/dist/oboe-browser.js',
|
||||
'bower_components/filesize/lib/filesize.min.js',
|
||||
'bower_components/moment/min/moment.min.js',
|
||||
'bower_components/xterm.js/dist/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'],
|
||||
@@ -85,12 +87,14 @@ 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/dist/xterm.css',
|
||||
'bower_components/Hover/css/hover-min.css'
|
||||
]
|
||||
},
|
||||
clean: {
|
||||
all: ['<%= distdir %>/*'],
|
||||
app: ['<%= distdir %>/*', '!<%= distdir %>/ui-for-docker'],
|
||||
app: ['<%= distdir %>/*', '!<%= distdir %>/portainer'],
|
||||
tmpl: ['<%= distdir %>/templates']
|
||||
},
|
||||
copy: {
|
||||
@@ -156,7 +160,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'
|
||||
}
|
||||
@@ -225,6 +228,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: {
|
||||
@@ -247,28 +254,35 @@ module.exports = function (grunt) {
|
||||
},
|
||||
shell: {
|
||||
buildImage: {
|
||||
command: 'docker build --rm -t ui-for-docker .'
|
||||
command: 'docker build --rm -t portainer .'
|
||||
},
|
||||
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/portainer > portainer-checksum.txt',
|
||||
'mkdir -p dist',
|
||||
'mv ui-for-docker dist/'
|
||||
'mv api/portainer dist/'
|
||||
].join(' && ')
|
||||
},
|
||||
run: {
|
||||
command: [
|
||||
'docker stop ui-for-docker',
|
||||
'docker rm ui-for-docker',
|
||||
'docker run --privileged -d -p 9000:9000 -v /tmp/docker-ui:/data -v /var/run/docker.sock:/var/run/docker.sock --name ui-for-docker ui-for-docker -d /data'
|
||||
'docker stop portainer',
|
||||
'docker rm portainer',
|
||||
'docker run --privileged -d -p 9000:9000 -v /tmp/portainer:/data -v /var/run/docker.sock:/var/run/docker.sock --name portainer portainer -d /data'
|
||||
].join(';')
|
||||
},
|
||||
runSwarm: {
|
||||
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 stop portainer',
|
||||
'docker rm portainer',
|
||||
'docker run -d -p 9000:9000 -v /tmp/portainer:/data --name portainer portainer -H tcp://10.0.7.10:2375 --swarm -d /data'
|
||||
].join(';')
|
||||
},
|
||||
runSsl: {
|
||||
command: [
|
||||
'docker stop portainer',
|
||||
'docker rm portainer',
|
||||
'docker run -d -p 9000:9000 -v /tmp/portainer:/data -v /tmp/docker-ssl:/certs --name portainer portainer -H tcp://10.0.7.10:2376 -d /data --tlsverify'
|
||||
].join(';')
|
||||
},
|
||||
cleanImages: {
|
||||
@@ -278,7 +292,7 @@ module.exports = function (grunt) {
|
||||
'if': {
|
||||
binaryNotExist: {
|
||||
options: {
|
||||
executable: 'dist/ui-for-docker'
|
||||
executable: 'dist/portainer'
|
||||
},
|
||||
ifFalse: ['shell:buildBinary']
|
||||
}
|
||||
|
||||
23
index.html
@@ -2,7 +2,7 @@
|
||||
<html lang="en" ng-app="<%= pkg.name %>">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>CloudInovasi UI for Docker</title>
|
||||
<title>Portainer</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="<%= pkg.author %>">
|
||||
@@ -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">
|
||||
<img ng-if="!config.logo" src="images/logo.png" class="img-responsive logo" alt="Portainer">
|
||||
<span class="menu-icon glyphicon glyphicon-transfer"></span>
|
||||
</a>
|
||||
</li>
|
||||
@@ -40,6 +41,12 @@
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="index">Dashboard <span class="menu-icon fa fa-tachometer"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="templates">App Templates <span class="menu-icon fa fa-rocket"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="swarm_mode">
|
||||
<a ui-sref="services">Services <span class="menu-icon fa fa-list-alt"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="containers">Containers <span class="menu-icon fa fa-server"></span></a>
|
||||
</li>
|
||||
@@ -52,16 +59,22 @@
|
||||
<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">
|
||||
<li class="sidebar-list" ng-if="!swarm">
|
||||
<a ui-sref="events">Events <span class="menu-icon fa fa-history"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="(swarm && !swarm_mode) || (swarm_mode && swarm_manager)">
|
||||
<a ui-sref="swarm">Swarm <span class="menu-icon fa fa-object-group"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="!config.swarm">
|
||||
<li class="sidebar-list" ng-if="!swarm">
|
||||
<a ui-sref="docker">Docker <span class="menu-icon fa fa-cogs"></span></a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="sidebar-footer">
|
||||
<div class="col-xs-12">
|
||||
<a href="https://github.com/cloud-inovasi/cloudinovasi-ui" target="_blank">CloudInovasi UI {{ uiVersion }}</a>
|
||||
<a href="https://github.com/portainer/portainer" target="_blank">
|
||||
<i class="fa fa-github" aria-hidden="true"></i>
|
||||
Portainer {{ uiVersion }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
14
package.json
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"author": "Michael Crosby & Kevan Ahlquist",
|
||||
"name": "uifordocker",
|
||||
"homepage": "https://github.com/kevana/ui-for-docker",
|
||||
"version": "1.2.0",
|
||||
"author": "Portainer.io",
|
||||
"name": "portainer",
|
||||
"homepage": "http://portainer.io",
|
||||
"version": "1.9.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:kevana/ui-for-docker.git"
|
||||
"url": "git@github.com:portainer/portainer.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/kevana/ui-for-docker/issues"
|
||||
"url": "https://github.com/portainer/portainer/issues"
|
||||
},
|
||||
"licenses": [
|
||||
{
|
||||
"type": "MIT",
|
||||
"url": "https://raw.githubusercontent.com/kevana/ui-for-docker/master/LICENSE"
|
||||
"url": "https://raw.githubusercontent.com/portainer/portainer/develop/LICENSE"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
describe('ContainerController', function () {
|
||||
var $scope, $httpBackend, mockContainer, $routeParams;
|
||||
|
||||
beforeEach(module('dockerui'));
|
||||
beforeEach(module('portainer'));
|
||||
|
||||
|
||||
beforeEach(inject(function ($rootScope, $controller, _$routeParams_) {
|
||||
@@ -22,8 +22,8 @@ describe('ContainerController', function () {
|
||||
$httpBackend.expectGET('dockerapi/containers/json').respond({
|
||||
'Created': 1421817232,
|
||||
'id': 'b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f',
|
||||
'Image': 'dockerui:latest',
|
||||
'Name': '/dockerui'
|
||||
'Image': 'portainer:latest',
|
||||
'Name': '/portainer'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ describe('ContainerController', function () {
|
||||
$scope.container = {
|
||||
'Created': 1421817232,
|
||||
'id': 'b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f',
|
||||
'Image': 'dockerui:latest',
|
||||
'Name': '/dockerui'
|
||||
'Image': 'portainer:latest',
|
||||
'Name': '/portainer'
|
||||
};
|
||||
$scope.container.newContainerName = "newName";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
describe("ContainerTopController", function () {
|
||||
var $scope, $httpBackend, $routeParams;
|
||||
|
||||
beforeEach(angular.mock.module('dockerui'));
|
||||
beforeEach(angular.mock.module('portainer'));
|
||||
|
||||
beforeEach(inject(function (_$rootScope_, _$httpBackend_, $controller, _$routeParams_) {
|
||||
$scope = _$rootScope_.$new();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
describe('NetworkController', function () {
|
||||
var $scope, $httpBackend, $routeParams;
|
||||
|
||||
beforeEach(module('dockerui'));
|
||||
beforeEach(module('portainer'));
|
||||
beforeEach(inject(function (_$httpBackend_, $controller, _$routeParams_) {
|
||||
$scope = {};
|
||||
$httpBackend = _$httpBackend_;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
describe('NetworksController', function () {
|
||||
var $scope, $httpBackend, $routeParams;
|
||||
|
||||
beforeEach(module('dockerui'));
|
||||
beforeEach(module('portainer'));
|
||||
beforeEach(inject(function (_$httpBackend_, $controller, _$routeParams_) {
|
||||
$scope = {};
|
||||
$httpBackend = _$httpBackend_;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
describe('startContainerController', function () {
|
||||
var scope, $location, createController, mockContainer, $httpBackend;
|
||||
|
||||
beforeEach(angular.mock.module('dockerui'));
|
||||
beforeEach(angular.mock.module('portainer'));
|
||||
|
||||
beforeEach(inject(function ($rootScope, $controller, _$location_) {
|
||||
$location = _$location_;
|
||||
@@ -20,11 +20,11 @@ describe('startContainerController', function () {
|
||||
}));
|
||||
function expectGetContainers() {
|
||||
$httpBackend.expectGET('dockerapi/containers/json?all=1').respond([{
|
||||
'Command': './dockerui -e /docker.sock',
|
||||
'Command': './portainer -e /docker.sock',
|
||||
'Created': 1421817232,
|
||||
'Id': 'b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f',
|
||||
'Image': 'dockerui:latest',
|
||||
'Names': ['/dockerui'],
|
||||
'Image': 'portainer:latest',
|
||||
'Names': ['/portainer'],
|
||||
'Ports': [{
|
||||
'IP': '0.0.0.0',
|
||||
'PrivatePort': 9000,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
describe("StatsController", function () {
|
||||
var $scope, $httpBackend, $routeParams;
|
||||
|
||||
beforeEach(angular.mock.module('dockerui'));
|
||||
beforeEach(angular.mock.module('portainer'));
|
||||
|
||||
beforeEach(inject(function (_$rootScope_, _$httpBackend_, $controller, _$routeParams_) {
|
||||
$scope = _$rootScope_.$new();
|
||||
|
||||