Compare commits

..

22 Commits

Author SHA1 Message Date
Anthony Lapenna 0aa84422da chore(version): bump version number 2016-08-18 15:49:18 +12:00
Anthony Lapenna f744b4c234 fix(jshint): fix lint issue 2016-08-18 13:37:10 +12:00
Anthony Lapenna c2ffb73358 feat(image): use a dropdown box to list available registries when tagging an image 2016-08-18 08:51:35 +12:00
Anthony Lapenna ada649b05a Merge branch 'docs140-logo-flag' into internal 2016-08-17 21:53:36 +12:00
Anthony Lapenna 7e5a9313a0 docs(logo): add documentation about the --logo flag 2016-08-17 21:51:39 +12:00
Anthony Lapenna 62eda46bac Revert "refactor(container-creation): remove changes related to internal version"
This reverts commit 61d7b4f64c.
2016-08-17 19:35:41 +12:00
Anthony Lapenna 76c179948e Merge branch 'fix122-invalid-port-specification' into internal 2016-08-17 19:35:38 +12:00
Anthony Lapenna ff04044001 fix(container-creation): allow to specify an address in the host port binding 2016-08-17 19:30:21 +12:00
Anthony Lapenna a752e8bc54 Merge branch 'fix123-container-creation-missing-port-bindings' into internal 2016-08-17 19:06:17 +12:00
Anthony Lapenna f14cbc5967 fix(container-creation): fix unregistered ports bindings when creating a container 2016-08-17 18:57:38 +12:00
Anthony Lapenna 7ebf842601 Merge branch 'docs-reverse-proxy' into internal 2016-08-17 18:32:26 +12:00
Anthony Lapenna 8dcac532d6 docs(reverse-proxy): add reverse proxy instructions 2016-08-17 18:30:21 +12:00
Anthony Lapenna 39d3753acb Merge branch 'feat111-exec-protocol-awareness' into internal 2016-08-17 18:24:10 +12:00
Anthony Lapenna 1e13aa660e feat(console): add protocol awareness to switch between ws:// and wss:// 2016-08-17 18:22:31 +12:00
Anthony Lapenna 0bd599f98a fix(api): remove git conflict trace 2016-08-17 18:16:01 +12:00
Anthony Lapenna f6e05538d8 Revert "refactor(global): revert merge with internal (#133)"
This reverts commit eefa7ca138.
2016-08-17 18:09:55 +12:00
Anthony Lapenna 0acbc10095 Merge branch 'feat125-tag-image' into internal 2016-08-17 18:06:09 +12:00
Anthony Lapenna aa1017c260 feat(image): add the ability to tag an image 2016-08-17 18:04:29 +12:00
Anthony Lapenna 9cb7483e0a Merge branch 'refactor-remove-binary' into internal 2016-08-17 13:58:15 +12:00
Anthony Lapenna 7d4ae1a6fd refactor(api): remove the binary from versioning 2016-08-17 13:56:09 +12:00
Anthony Lapenna fa5c1d16b6 Merge branch 'refactor124-response-handlers' into internal 2016-08-17 13:53:57 +12:00
Anthony Lapenna c928af71a4 refactor(handlers): remove duplicated code 2016-08-17 13:42:33 +12:00
100 changed files with 1816 additions and 4064 deletions
-43
View File
@@ -1,43 +0,0 @@
<!--
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:
+7 -1
View File
@@ -1,4 +1,10 @@
logs/*
!.gitkeep
*.esproj/*
node_modules
bower_components
.idea
*.iml
dist
portainer-checksum.txt
dist/*
ui-for-docker-checksum.txt
+1 -1
View File
@@ -1 +1 @@
portainer
dockerui
-57
View File
@@ -1,57 +0,0 @@
# 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
+8
View File
@@ -0,0 +1,8 @@
FROM scratch
COPY dist /
VOLUME /data
EXPOSE 9000
ENTRYPOINT ["/ui-for-docker"]
+17 -17
View File
@@ -1,24 +1,24 @@
Portainer: Copyright (c) 2016 Portainer.io
Cloudinovasi-ui: Copyright (c) 2016 Cloudinovasi
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Portainer contains code which was originally under this license:
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (portainer.io)
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (anthonylapenna at cloudinovasi dot id)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+1
View File
@@ -0,0 +1 @@
web: dockerui -p ":$PORT" -e "$DOCKER_ENDPOINT"
+151 -32
View File
@@ -1,52 +1,171 @@
# Cloudinovasi UI for Docker
<p align="center">
<img title="portainer" src='http://portainer.io/images/logo_alt.png' />
</p>
This UI is dedicated to CloudInovasi internal usage.
[![Microbadger version](https://images.microbadger.com/badges/version/portainer/portainer.svg)](https://microbadger.com/images/portainer/portainer "Latest version on Docker Hub")
[![Microbadger](https://images.microbadger.com/badges/image/portainer/portainer.svg)](http://microbadger.com/images/portainer/portainer "Image size")
[![Documentation Status](https://readthedocs.org/projects/portainer/badge/?version=latest)](http://portainer.readthedocs.io/en/latest/?badge=latest)
[![Gitter](https://badges.gitter.im/portainer/Lobby.svg)](https://gitter.im/portainer/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6)
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).
**_Portainer_** is a lightweight management UI which allows you to **easily** manage your Docker host or Swarm cluster.
![Dashboard](/dashboard.png)
**_Portainer_** is meant to be as **simple** to deploy as it is to use. It consists of a single container that can run on any Docker engine (Docker for Linux and Docker for Windows are supported).
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_** allows you to manage your Docker containers, images, volumes, networks and more ! It is compatible with the *standalone Docker* engine and with *Docker Swarm*.
## 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.
## Demo
## Supported Docker versions
<img src="http://portainer.io/images/screenshots/portainer.gif" width="77%"/>
The current Docker version support policy is the following: `N` to `N-2` included where `N` is the latest version.
You can try out the public demo instance: http://demo.portainer.io/ (login with the username **demo** and the password **tryportainer**).
At the moment, the following versions are supported: 1.9, 1.10 & 1.11.
Please note that the public demo cluster is **reset every 15min**.
## Run
## Getting started
### Quickstart
* [Deploy Portainer](https://portainer.readthedocs.io/en/latest/deployment.html)
* [Documentation](https://portainer.readthedocs.io)
1. Run: `docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui`
## Getting help
2. Open your browser to `http://<dockerd host ip>:9000`
* Issues: https://github.com/portainer/portainer/issues
* FAQ: https://portainer.readthedocs.io/en/latest/faq.html
* Gitter (chat): https://gitter.im/portainer/Lobby
* Slack: http://portainer.io/slack/
Bind mounting the Unix socket into the UI For Docker container is much more secure than exposing your docker daemon over TCP.
## Reporting bugs and contributing
The `--privileged` flag is required for hosts using SELinux.
* Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new).
* Want to help us build **_portainer_**? Follow our [contribution guidelines](https://portainer.readthedocs.io/en/latest/contribute.html) to build it locally and make a pull request. We need all the help we can get!
### Specify socket to connect to Docker daemon
## Limitations
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`.
**_Portainer_** has full support for the following Docker versions:
You can use the `--host`, `-H` flags to change this socket:
* Docker 1.10 to Docker 1.12 (including `swarm-mode`)
* Docker Swarm >= 1.2.3
```
# Connect to a tcp socket:
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H tcp://127.0.0.1:2375
```
Partial support for the following Docker versions (some features may not be available):
```
# Connect to another unix socket:
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H unix:///path/to/docker.sock
```
* Docker 1.9
### Swarm support
**Supported Swarm version: 1.2.3**
You can access a specific view for you Swarm cluster by defining the `--swarm` flag:
```
# Connect to a tcp socket and enable Swarm:
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H tcp://<SWARM_HOST>:<SWARM_PORT> --swarm
```
*NOTE*: Due to Swarm not exposing information in a machine readable way, the app is bound to a specific version of Swarm at the moment.
### Change address/port UI For Docker is served on
UI For Docker listens on port 9000 by default. If you run UI For Docker inside a container then you can bind the container's internal port to any external address and port:
```
# Expose UI For Docker on 10.20.30.1:80
$ docker run -d -p 10.20.30.1:80:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui
```
### Access a Docker engine protected via TLS
Ensure that you have access to the CA, the cert and the public key used to access your Docker engine.
These files will need to be named `ca.pem`, `cert.pem` and `key.pem` respectively. Store them somewhere on your disk and mount a volume containing these files inside the UI container:
```
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -v /path/to/certs:/certs -H https://my-docker-host.domain:2376 --tlsverify
```
You can also use the `--tlscacert`, `--tlscert` and `--tlskey` flags if you want to change the default path to the CA, certificate and key file respectively:
```
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -v /path/to/certs:/certs -H https://my-docker-host.domain:2376 --tlsverify --tlscacert /certs/myCa.pem --tlscert /certs/myCert.pem --tlskey /certs/myKey.pem
```
*Note*: Replace `/path/to/certs` to the path to the certificate files on your disk.
### Use your own logo
You can use the `--logo` flag to specify an URL to your own logo.
For example, using the Docker logo:
```
$ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui --logo "https://www.docker.com/sites/all/themes/docker/assets/images/brand-full.svg"
```
The custom logo will replace the CloudInovasi logo in the UI.
### Hide containers with specific labels
You can hide specific containers in the containers view by using the `--hide-label` or `-l` options and specifying a label.
For example, take a container started with the label `owner=acme`:
```
$ docker run -d --label owner=acme nginx
```
You can hide it in the view by starting the ui with:
```
$ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui -l owner=acme
```
### Custom Docker registries support
You can specify the support of others registries than DockerHub by using the `--registries` or `-r` options and specifying a registry using the format *REGISTRY_NAME=REGISTRY_ADDRESS*.
For example, if I want the registry 'myCustomRegistry' pointing to *myregistry.domain.com:5000* available in the UI:
```
$ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui -r myCustomRegistry=myregistry.domain.com:5000
```
### Reverse proxy configuration
Has been tested with Nginx 1.11.
Use the following configuration to host the UI at `myhost.mydomain.com/dockerui`:
```nginx
upstream cloudinovasi-ui {
server ADDRESS:PORT;
}
server {
listen 80;
location /dockerui/ {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://cloudinovasi-ui/;
}
location /dockerui/ws/ {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_pass http://cloudinovasi-ui/ws/;
}
}
```
Replace `ADDRESS:PORT` with the CloudInovasi UI container details.
### Available options
The following options are available for the `ui-for-docker` binary:
* `--host`, `-H`: Docker daemon endpoint (default: `"unix:///var/run/docker.sock"`)
* `--bind`, `-p`: Address and port to serve UI For Docker (default: `":9000"`)
* `--data`, `-d`: Path to the data folder (default: `"."`)
* `--assets`, `-a`: Path to the assets (default: `"."`)
* `--swarm`, `-s`: Swarm cluster support (default: `false`)
* `--registries`, `-r`: Available registries in the UI (format *REGISTRY_NAME=REGISTRY_ADDRESS*)
* `--tlsverify`: TLS support (default: `false`)
* `--tlscacert`: Path to the CA (default `/certs/ca.pem`)
* `--tlscert`: Path to the TLS certificate file (default `/certs/cert.pem`)
* `--tlskey`: Path to the TLS key (default `/certs/key.pem`)
* `--hide-label`, `-l`: Hide containers with a specific label in the UI
* `--logo`: URL to a picture to be displayed as a logo in the UI
+10 -14
View File
@@ -9,12 +9,11 @@ import (
type (
api struct {
endpoint *url.URL
bindAddress string
assetPath string
dataPath string
tlsConfig *tls.Config
templatesURL string
endpoint *url.URL
bindAddress string
assetPath string
dataPath string
tlsConfig *tls.Config
}
apiConfig struct {
@@ -27,13 +26,11 @@ type (
TLSCACertPath string
TLSCertPath string
TLSKeyPath string
TemplatesURL string
}
)
func (a *api) run(settings *Settings) {
handler := a.newHandler(settings)
log.Printf("Starting portainer on %s", a.bindAddress)
if err := http.ListenAndServe(a.bindAddress, handler); err != nil {
log.Fatal(err)
}
@@ -51,11 +48,10 @@ func newAPI(apiConfig apiConfig) *api {
}
return &api{
endpoint: endpointURL,
bindAddress: apiConfig.BindAddress,
assetPath: apiConfig.AssetPath,
dataPath: apiConfig.DataPath,
tlsConfig: tlsConfig,
templatesURL: apiConfig.TemplatesURL,
endpoint: endpointURL,
bindAddress: apiConfig.BindAddress,
assetPath: apiConfig.AssetPath,
dataPath: apiConfig.DataPath,
tlsConfig: tlsConfig,
}
}
+2 -9
View File
@@ -17,6 +17,7 @@ func (a *api) newHandler(settings *Settings) http.Handler {
)
handler := a.newAPIHandler()
CSRFHandler := newCSRFHandler(a.dataPath)
mux.Handle("/", fileHandler)
mux.Handle("/dockerapi/", http.StripPrefix("/dockerapi", handler))
@@ -24,15 +25,7 @@ func (a *api) newHandler(settings *Settings) http.Handler {
mux.HandleFunc("/settings", func(w http.ResponseWriter, r *http.Request) {
settingsHandler(w, r, settings)
})
mux.HandleFunc("/templates", func(w http.ResponseWriter, r *http.Request) {
templatesHandler(w, r, a.templatesURL)
})
// CSRF protection is disabled for the moment
// CSRFHandler := newCSRFHandler(a.dataPath)
// return CSRFHandler(newCSRFWrapper(mux))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mux.ServeHTTP(w, r)
})
return CSRFHandler(newCSRFWrapper(mux))
}
// newAPIHandler initializes a new http.Handler based on the URL scheme
+14 -14
View File
@@ -1,4 +1,4 @@
package main // import "github.com/portainer/portainer"
package main // import "github.com/cloudinovasi/ui-for-docker"
import (
"gopkg.in/alecthomas/kingpin.v2"
@@ -6,20 +6,20 @@ import (
// main is the entry point of the program
func main() {
kingpin.Version("1.10.2")
kingpin.Version("1.7.0")
var (
endpoint = kingpin.Flag("host", "Dockerd endpoint").Default("unix:///var/run/docker.sock").Short('H').String()
addr = kingpin.Flag("bind", "Address and port to serve Portainer").Default(":9000").Short('p').String()
assets = kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String()
data = kingpin.Flag("data", "Path to the data").Default(".").Short('d').String()
tlsverify = kingpin.Flag("tlsverify", "TLS support").Default("false").Bool()
tlscacert = kingpin.Flag("tlscacert", "Path to the CA").Default("/certs/ca.pem").String()
tlscert = kingpin.Flag("tlscert", "Path to the TLS certificate file").Default("/certs/cert.pem").String()
tlskey = kingpin.Flag("tlskey", "Path to the TLS key").Default("/certs/key.pem").String()
swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool()
labels = pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l'))
endpoint = kingpin.Flag("host", "Dockerd endpoint").Default("unix:///var/run/docker.sock").Short('H').String()
addr = kingpin.Flag("bind", "Address and port to serve UI For Docker").Default(":9000").Short('p').String()
assets = kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String()
data = kingpin.Flag("data", "Path to the data").Default(".").Short('d').String()
tlsverify = kingpin.Flag("tlsverify", "TLS support").Default("false").Bool()
tlscacert = kingpin.Flag("tlscacert", "Path to the CA").Default("/certs/ca.pem").String()
tlscert = kingpin.Flag("tlscert", "Path to the TLS certificate file").Default("/certs/cert.pem").String()
tlskey = kingpin.Flag("tlskey", "Path to the TLS key").Default("/certs/key.pem").String()
swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool()
labels = pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l'))
registries = pairs(kingpin.Flag("registries", "Supported Docker registries").Short('r'))
logo = kingpin.Flag("logo", "URL for the logo displayed in the UI").String()
templates = kingpin.Flag("templates", "URL to the templates (apps) definitions").Default("https://raw.githubusercontent.com/portainer/templates/master/templates.json").Short('t').String()
)
kingpin.Parse()
@@ -33,12 +33,12 @@ func main() {
TLSCACertPath: *tlscacert,
TLSCertPath: *tlscert,
TLSKeyPath: *tlskey,
TemplatesURL: *templates,
}
settings := &Settings{
Swarm: *swarm,
HiddenLabels: *labels,
Registries: *registries,
Logo: *logo,
}
+2 -1
View File
@@ -9,10 +9,11 @@ import (
type Settings struct {
Swarm bool `json:"swarm"`
HiddenLabels pairList `json:"hiddenLabels"`
Registries pairList `json:"registries"`
Logo string `json:"logo"`
}
// settingsHandler defines a handler function used to encode the configuration in JSON
// configurationHandler defines a handler function used to encode the configuration in JSON
func settingsHandler(w http.ResponseWriter, r *http.Request, s *Settings) {
json.NewEncoder(w).Encode(*s)
}
-27
View File
@@ -1,27 +0,0 @@
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
)
// templatesHandler defines a handler function used to retrieve the templates from a URL and put them in the response
func templatesHandler(w http.ResponseWriter, r *http.Request, templatesURL string) {
resp, err := http.Get(templatesURL)
if err != nil {
http.Error(w, fmt.Sprintf("Error making request to %s: %s", templatesURL, err.Error()), http.StatusInternalServerError)
log.Print(err)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
http.Error(w, "Error reading body from templates URL", http.StatusInternalServerError)
log.Print(err)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(body)
}
+18 -54
View File
@@ -1,14 +1,12 @@
angular.module('portainer', [
'portainer.templates',
angular.module('uifordocker', [
'uifordocker.templates',
'ui.bootstrap',
'ui.router',
'ui.select',
'ngCookies',
'ngSanitize',
'angularUtils.directives.dirPagination',
'portainer.services',
'portainer.helpers',
'portainer.filters',
'uifordocker.services',
'uifordocker.filters',
'dashboard',
'container',
'containerConsole',
@@ -19,21 +17,17 @@ angular.module('portainer', [
'events',
'images',
'image',
'service',
'services',
'createService',
'stats',
'swarm',
'network',
'networks',
'createNetwork',
'task',
'templates',
'volumes',
'createVolume'])
'volumes'])
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', function ($stateProvider, $urlRouterProvider, $httpProvider) {
'use strict';
$httpProvider.defaults.xsrfCookieName = 'csrfToken';
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token';
$urlRouterProvider.otherwise('/');
$stateProvider
@@ -82,21 +76,16 @@ angular.module('portainer', [
templateUrl: 'app/components/createContainer/createcontainer.html',
controller: 'CreateContainerController'
})
.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('actions.create.network', {
url: "/network",
templateUrl: 'app/components/createNetwork/createnetwork.html',
controller: 'CreateNetworkController'
})
.state('docker', {
url: '/docker/',
templateUrl: 'app/components/docker/docker.html',
@@ -127,26 +116,6 @@ angular.module('portainer', [
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',
@@ -159,8 +128,6 @@ angular.module('portainer', [
});
// The Docker API likes to return plaintext errors, this catches them and disp
// $httpProvider.defaults.xsrfCookieName = 'csrfToken';
// $httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token';
$httpProvider.interceptors.push(function() {
return {
'response': function(response) {
@@ -172,11 +139,10 @@ angular.module('portainer', [
time: 10000
});
}
// CSRF protection is disabled for the moment
// var csrfToken = response.headers('X-Csrf-Token');
// if (csrfToken) {
// document.cookie = 'csrfToken=' + csrfToken;
// }
var csrfToken = response.headers('X-Csrf-Token');
if (csrfToken) {
document.cookie = 'csrfToken=' + csrfToken;
}
return response;
}
};
@@ -188,6 +154,4 @@ angular.module('portainer', [
.constant('DOCKER_ENDPOINT', 'dockerapi')
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is requred. If you have a port, prefix it with a ':' i.e. :4243
.constant('CONFIG_ENDPOINT', 'settings')
.constant('TEMPLATES_ENDPOINT', 'templates')
.constant('PAGINATION_MAX_ITEMS', 10)
.constant('UI_VERSION', 'v1.10.2');
.constant('UI_VERSION', 'v1.7.0');
+145 -185
View File
@@ -3,23 +3,72 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a>
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-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 space-right" aria-hidden="true"></i>Start</button>
<button class="btn btn-danger" ng-click="stop()" ng-if="container.State.Running"><i class="fa fa-stop space-right" aria-hidden="true"></i>Stop</button>
<button class="btn btn-danger" ng-click="kill()" ng-if="container.State.Running"><i class="fa fa-bomb space-right" aria-hidden="true"></i>Kill</button>
<button class="btn btn-primary" ng-click="restart()" ng-if="container.State.Running"><i class="fa fa-refresh space-right" 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 space-right" aria-hidden="true"></i>Pause</button>
<button class="btn btn-primary" ng-click="unpause()" ng-if="container.State.Paused"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button>
<button class="btn btn-danger" ng-click="remove()" ng-disabled="container.State.Running"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
<rd-widget-body>
<div class="widget-icon grey pull-left">
<i class="fa fa-cogs"></i>
</div>
<div class="title">
<div class="btn-group" role="group" aria-label="...">
<button class="btn btn-primary" ng-click="commit()">Commit</button>
<button class="btn btn-primary" ng-click="start()" ng-disabled="container.State.Running">Start</button>
<button class="btn btn-primary" ng-click="stop()" ng-disabled="!container.State.Running">Stop</button>
<button class="btn btn-primary" ng-click="kill()" ng-disabled="!container.State.Running">Kill</button>
<button class="btn btn-primary" ng-click="restart()">Restart</button>
<button class="btn btn-primary" ng-click="pause()" ng-disabled="!container.State.Running && !container.State.Paused">Pause</button>
<button class="btn btn-primary" ng-click="unpause()" ng-disabled="!container.State.Paused">Unpause</button>
<button class="btn btn-danger" ng-click="remove()" ng-disabled="container.State.Running">Remove</button>
</div>
<div class="btn-group" role="group" aria-label="...">
<a class="btn btn-default" type="button" ui-sref="stats({id: container.Id})">Stats</a>
<a class="btn btn-default" type="button" ui-sref="logs({id: container.Id})">Logs</a>
<a class="btn btn-default" type="button" ui-sref="console({id: container.Id})">Console</a>
</div>
</div>
<div class="comment">
Actions
</div>
</rd-widget-body>
</rd-widget>
@@ -27,144 +76,44 @@
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<div class="col-lg-9">
<rd-widget>
<rd-widget-header icon="fa-server" title="Container status"></rd-widget-header>
<rd-widget-header icon="fa-tasks" title="Container status"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Name</td>
<td ng-if="!container.edit">
{{ container.Name|trimcontainername }}
<a href="" data-toggle="tooltip" title="Edit container name" ng-click="container.edit = true;"><i class="fa fa-edit"></i></a>
</td>
<td ng-if="container.edit">
<input type="text" class="containerNameInput" ng-model="container.newContainerName">
<a href="" ng-click="container.edit = false;"><i class="fa fa-times"></i></a>
<a href="" ng-click="renameContainer()"><i class="fa fa-check-square-o"></i></a>
</td>
</tr>
<tr ng-if="container.NetworkSettings.IPAddress">
<td>IP address</td>
<td>{{ container.NetworkSettings.IPAddress }}</td>
<td>Created</td>
<td>{{ container.Created|getisodate }}</td>
</tr>
<tr>
<td>Status</td>
<td>Path</td>
<td>{{ container.Path }}</td>
</tr>
<tr>
<td>Args</td>
<td>{{ container.Args.join(' ') || 'None' }}</td>
</tr>
<tr>
<td>Exposed Ports</td>
<td>
<i ng-class="{true: 'fa fa-heartbeat space-right green-icon', false: 'fa fa-heartbeat space-right red-icon'}[container.State.Running]"></i>
{{ container.State|getstatetext }} since {{ activityTime }}<span ng-if="!container.State.Running"> with exit code {{ container.State.ExitCode }}</span>
<ul>
<li ng-repeat="(k, v) in container.Config.ExposedPorts">{{ k }}</li>
</ul>
</td>
</tr>
<tr ng-if="container.State.Running">
<td>Start time</td>
<td>{{ container.State.StartedAt|getisodate }}</td>
</tr>
<tr ng-if="!container.State.Running">
<td>Finished</td>
<td>{{ container.State.FinishedAt|getisodate }}</td>
</tr>
<tr>
<td colspan="2">
<div class="btn-group" role="group" aria-label="...">
<a class="btn btn-outline-secondary" type="button" ui-sref="stats({id: container.Id})"><i class="fa fa-area-chart space-right" 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 space-right" 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 space-right" 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>Environment</td>
<td>
<div ng-repeat="portMapping in portBindings">
{{ portMapping.container }} <i class="fa fa-long-arrow-right"></i> {{ portMapping.host }}
</div>
<ul>
<li ng-repeat="k in container.Config.Env">{{ k }}</li>
</ul>
</td>
</tr>
<tr>
<td>CMD</td>
<td><code>{{ container.Config.Cmd|command }}</code></td>
</tr>
<tr>
<td>ENV</td>
<td>
<table class="table table-bordered table-condensed">
<tr ng-repeat="var in container.Config.Env">
<td>{{ var|key: '=' }}</td>
<td>{{ var|value: '=' }}</td>
</tr>
</table>
</td>
</tr>
<tr ng-if="!(container.Config.Labels | emptyobject)">
<td>Labels</td>
<td>
<table class="table table-bordered table-condensed">
<table role="table" class="table">
<tr ng-repeat="(k, v) in container.Config.Labels">
<td>{{ k }}</td>
<td>{{ v }}</td>
@@ -172,66 +121,77 @@
</table>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="container.HostConfig.Binds.length > 0">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<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>
<td>Publish all ports</td>
<td>{{ container.HostConfig.PublishAllPorts }}</td>
</tr>
</thead>
<tbody>
<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 class="row" ng-if="!(container.NetworkSettings.Networks | emptyobject)">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-sitemap" title="Connected networks"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<thead>
<th>Network Name</th>
<th>IP Address</th>
<th>Gateway</th>
<th>MacAddress</th>
<th>Actions</th>
</thead>
<tbody>
<tr dir-paginate="(key, value) in container.NetworkSettings.Networks | itemsPerPage: pagination_count">
<td><a ui-sref="network({id: value.NetworkID})">{{ key }}</a></td>
<td>{{ value.IPAddress || '-' }}</td>
<td>{{ value.Gateway || '-' }}</td>
<td>{{ value.MacAddress || '-' }}</td>
<tr>
<td>Ports</td>
<td>
<button type="button" class="btn btn-xs btn-danger" ng-click="containerLeaveNetwork(container, value.NetworkID)"><i class="fa fa-trash space-right" aria-hidden="true"></i>Leave Network</button>
<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>
<div class="pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="col-lg-3">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Container state details"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr ng-repeat="(key, val) in container.State">
<td>{{key}}</td>
<td ng-if="key === 'StartedAt' || key === 'FinishedAt'">{{val|getisodate}}</td>
<td ng-if="key !== 'StartedAt' && key !== 'FinishedAt'">{{val}}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
<div>
+105 -72
View File
@@ -1,13 +1,14 @@
angular.module('container', [])
.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ImageHelper', 'Network', 'Messages', 'Settings',
function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ImageHelper, Network, Messages, Settings) {
$scope.activityTime = 0;
$scope.portBindings = [];
$scope.config = {
Image: '',
Registry: ''
.controller('ContainerController', ['$scope', '$stateParams', '$state', '$filter', 'Container', 'ContainerCommit', 'Image', 'Messages', '$timeout',
function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Image, Messages, $timeout) {
$scope.changes = [];
$scope.editEnv = false;
$scope.editPorts = false;
$scope.editBinds = false;
$scope.newCfg = {
Env: [],
Ports: {}
};
$scope.pagination_count = Settings.pagination_count;
var update = function () {
$('#loadingViewSpinner').show();
@@ -16,38 +17,74 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
$scope.container.edit = false;
$scope.container.newContainerName = $filter('trimcontainername')(d.Name);
if (d.State.Running) {
$scope.activityTime = moment.duration(moment(d.State.StartedAt).utc().diff(moment().utc())).humanize();
} else {
$scope.activityTime = moment.duration(moment().utc().diff(moment(d.State.FinishedAt).utc())).humanize();
}
$scope.portBindings = [];
if (d.NetworkSettings.Ports) {
angular.forEach(Object.keys(d.NetworkSettings.Ports), function(portMapping) {
if (d.NetworkSettings.Ports[portMapping]) {
var mapping = {};
mapping.container = portMapping;
mapping.host = d.NetworkSettings.Ports[portMapping][0].HostIp + ':' + d.NetworkSettings.Ports[portMapping][0].HostPort;
$scope.portBindings.push(mapping);
}
// fill up env
if (d.Config.Env) {
$scope.newCfg.Env = d.Config.Env.map(function (entry) {
return {name: entry.split('=')[0], value: entry.split('=')[1]};
});
}
// fill up ports
$scope.newCfg.Ports = {};
angular.forEach(d.Config.ExposedPorts, function(i, port) {
if (d.HostConfig.PortBindings && port in d.HostConfig.PortBindings) {
$scope.newCfg.Ports[port] = d.HostConfig.PortBindings[port];
}
else {
$scope.newCfg.Ports[port] = [];
}
});
// fill up bindings
$scope.newCfg.Binds = [];
var defaultBinds = {};
angular.forEach(d.Config.Volumes, function(value, vol) {
defaultBinds[vol] = { ContPath: vol, HostPath: '', ReadOnly: false, DefaultBind: true };
});
angular.forEach(d.HostConfig.Binds, function(binding, i) {
var mountpoint = binding.split(':')[0];
var vol = binding.split(':')[1] || '';
var ro = binding.split(':').length > 2 && binding.split(':')[2] === 'ro';
var defaultBind = false;
if (vol === '') {
vol = mountpoint;
mountpoint = '';
}
if (vol in defaultBinds) {
delete defaultBinds[vol];
defaultBind = true;
}
$scope.newCfg.Binds.push({ ContPath: vol, HostPath: mountpoint, ReadOnly: ro, DefaultBind: defaultBind });
});
angular.forEach(defaultBinds, function(bind) {
$scope.newCfg.Binds.push(bind);
});
$('#loadingViewSpinner').hide();
}, function (e) {
if (e.status === 404) {
$('.detail').hide();
Messages.error("Not found", "Container not found.");
} else {
Messages.error("Failure", e.data);
}
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve container info");
});
};
$scope.start = function () {
$('#loadingViewSpinner').show();
Container.start({id: $scope.container.Id}, {}, function (d) {
Container.start({
id: $scope.container.Id,
HostConfig: $scope.container.HostConfig
}, function (d) {
update();
Messages.send("Container started", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to start container");
Messages.error("Failure", "Container failed to start." + e.data);
});
};
@@ -58,7 +95,7 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
Messages.send("Container stopped", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to stop container");
Messages.error("Failure", "Container failed to stop." + e.data);
});
};
@@ -69,26 +106,20 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
Messages.send("Container killed", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to kill container");
Messages.error("Failure", "Container failed to die." + e.data);
});
};
$scope.commit = function () {
$('#createImageSpinner').show();
var image = _.toLower($scope.config.Image);
var registry = _.toLower($scope.config.Registry);
var imageConfig = ImageHelper.createImageConfig(image, registry);
ContainerCommit.commit({id: $stateParams.id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) {
$('#createImageSpinner').hide();
$('#loadingViewSpinner').show();
ContainerCommit.commit({id: $stateParams.id, repo: $scope.container.Config.Image}, function (d) {
update();
Messages.send("Container commited", $stateParams.id);
}, function (e) {
$('#createImageSpinner').hide();
update();
Messages.error("Failure", e, "Unable to commit container");
Messages.error("Failure", "Container failed to commit." + e.data);
});
};
$scope.pause = function () {
$('#loadingViewSpinner').show();
Container.pause({id: $stateParams.id}, function (d) {
@@ -96,7 +127,7 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
Messages.send("Container paused", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to pause container");
Messages.error("Failure", "Container failed to pause." + e.data);
});
};
@@ -107,24 +138,19 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
Messages.send("Container unpaused", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to unpause container");
Messages.error("Failure", "Container failed to unpause." + e.data);
});
};
$scope.remove = function () {
$('#loadingViewSpinner').show();
Container.remove({id: $stateParams.id}, function (d) {
if (d.message) {
$('#loadingViewSpinner').hide();
Messages.send("Error", d.message);
}
else {
$state.go('containers', {}, {reload: true});
Messages.send("Container removed", $stateParams.id);
}
update();
$state.go('containers', {}, {reload: true});
Messages.send("Container removed", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to remove container");
Messages.error("Failure", "Container failed to remove." + e.data);
});
};
@@ -135,41 +161,48 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
Messages.send("Container restarted", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to restart container");
Messages.error("Failure", "Container failed to restart." + e.data);
});
};
$scope.hasContent = function (data) {
return data !== null && data !== undefined;
};
$scope.getChanges = function () {
$('#loadingViewSpinner').show();
Container.changes({id: $stateParams.id}, function (d) {
$scope.changes = d;
$('#loadingViewSpinner').hide();
});
};
$scope.renameContainer = function () {
Container.rename({id: $stateParams.id, 'name': $scope.container.newContainerName}, function (d) {
if (d.message) {
$scope.container.newContainerName = $scope.container.Name;
Messages.error("Unable to rename container", {}, d.message);
// #FIXME fix me later to handle http status to show the correct error message
Container.rename({id: $stateParams.id, 'name': $scope.container.newContainerName}, function (data) {
if (data.name) {
$scope.container.Name = data.name;
Messages.send("Container renamed", $stateParams.id);
} else {
$scope.container.Name = $scope.container.newContainerName;
Messages.send("Container successfully renamed", d.name);
$scope.container.newContainerName = $scope.container.Name;
Messages.error("Failure", "Container failed to rename.");
}
}, function (e) {
Messages.error("Failure", e, 'Unable to rename container');
});
$scope.container.edit = false;
};
$scope.containerLeaveNetwork = function containerLeaveNetwork(container, networkId) {
$('#loadingViewSpinner').show();
Network.disconnect({id: networkId}, { Container: $stateParams.id, Force: false }, function (d) {
if (d.message) {
$('#loadingViewSpinner').hide();
Messages.send("Error", {}, d.message);
} else {
$('#loadingViewSpinner').hide();
Messages.send("Container left network", $stateParams.id);
$state.go('container', {id: $stateParams.id}, {reload: true});
}
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to disconnect container from network");
});
$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();
}]);
@@ -2,12 +2,12 @@
<rd-header-title title="Container console">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content ng-if="state.loaded">
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Console
<rd-header-content>
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Console
</rd-header-content>
</rd-header>
<div class="row" ng-if="state.loaded">
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-terminal" title="Console">
@@ -16,27 +16,18 @@
</div>
</rd-widget-header>
<rd-widget-body>
<form>
<div class="row">
<!-- command-list -->
<div class="col-sm-4">
<div class="input-group">
<span class="input-group-addon">
<i class="fa fa-linux" aria-hidden="true" ng-if="imageOS == 'linux'"></i>
<i class="fa fa-windows" aria-hidden="true" ng-if="imageOS == 'windows'"></i>
</span>
<select class="form-control" ng-model="state.command" id="command">
<option value="bash" ng-if="imageOS == 'linux'">/bin/bash</option>
<option value="sh" ng-if="imageOS == 'linux'">/bin/sh</option>
<option value="powershell" ng-if="imageOS == 'windows'">powershell</option>
<option value="cmd.exe" ng-if="imageOS == 'windows'">cmd.exe</option>
</select>
</div>
<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>
<!-- !command-list -->
<div class="col-sm-8">
<button type="button" class="btn btn-primary" ng-click="connect()" ng-disabled="state.connected">Connect</button>
<button type="button" class="btn btn-default" ng-click="disconnect()" ng-disabled="!state.connected">Disconnect</button>
<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>
@@ -1,37 +1,21 @@
angular.module('containerConsole', [])
.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Settings', 'Container', 'Image', 'Exec', '$timeout', 'Messages',
function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, Messages) {
.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Settings', 'Container', 'Exec', '$timeout', 'Messages', 'errorMsgFilter',
function ($scope, $stateParams, Settings, Container, Exec, $timeout, Messages, errorMsgFilter) {
$scope.state = {};
$scope.state.loaded = false;
$scope.state.connected = false;
$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) {
if (socket !== null) {
socket.close();
}
});
Container.get({id: $stateParams.id}, function(d) {
$scope.container = d;
if (d.message) {
Messages.error("Error", d, 'Unable to retrieve container details');
$('#loadingViewSpinner').hide();
} else {
Image.get({id: d.Image}, function(imgData) {
$scope.imageOS = imgData.Os;
$scope.state.command = imgData.Os === 'windows' ? 'powershell' : 'bash';
$scope.state.loaded = true;
$('#loadingViewSpinner').hide();
}, function (e) {
Messages.error("Failure", e, 'Unable to retrieve image details');
$('#loadingViewSpinner').hide();
});
}
}, function (e) {
Messages.error("Failure", e, 'Unable to retrieve container details');
$('#loadingViewSpinner').hide();
});
@@ -49,10 +33,7 @@ function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, Mess
};
Container.exec(execConfig, function(d) {
if (d.message) {
$('#loadConsoleSpinner').hide();
Messages.error("Error", {}, d.message);
} else {
if (d.Id) {
var execId = d.Id;
resizeTTY(execId, termHeight, termWidth);
var url = window.location.href.split('#')[0] + 'ws/exec?id=' + execId;
@@ -62,15 +43,18 @@ function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, Mess
url = url.replace('http://', 'ws://');
}
initTerm(url, termHeight, termWidth);
} else {
$('#loadConsoleSpinner').hide();
Messages.error('Error', errorMsgFilter(d));
}
}, function (e) {
$('#loadConsoleSpinner').hide();
Messages.error("Failure", e, 'Unable to start an exec instance');
Messages.error("Failure", e.data);
});
};
$scope.disconnect = function() {
$scope.state.connected = false;
$scope.connected = false;
if (socket !== null) {
socket.close();
}
@@ -82,11 +66,10 @@ function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, Mess
function resizeTTY(execId, height, width) {
$timeout(function() {
Exec.resize({id: execId, height: height, width: width}, function (d) {
if (d.message) {
Messages.error('Error', {}, 'Unable to resize TTY');
var error = errorMsgFilter(d);
if (error) {
Messages.error('Error', 'Unable to resize TTY');
}
}, function (e) {
Messages.error("Failure", {}, 'Unable to resize TTY');
});
}, 2000);
@@ -95,26 +78,31 @@ function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, Mess
function initTerm(url, height, width) {
socket = new WebSocket(url);
$scope.state.connected = true;
$scope.connected = true;
socket.onopen = function(evt) {
$('#loadConsoleSpinner').hide();
term = new Terminal();
term = new Terminal({
cols: width,
rows: height,
cursorBlink: true
});
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.state.connected = false;
$scope.connected = false;
};
socket.onclose = function(evt) {
$scope.state.connected = false;
$scope.connected = false;
// term.write("Session terminated");
// term.destroy();
};
};
}
@@ -13,8 +13,12 @@ function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container) {
$scope.container = d;
$('#loadingViewSpinner').hide();
}, function (e) {
if (e.status === 404) {
Messages.error("Not found", "Container not found.");
} else {
Messages.error("Failure", e.data);
}
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve container info");
});
function getLogs() {
@@ -3,7 +3,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Logs
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Logs
</rd-header-content>
</rd-header>
@@ -12,7 +12,7 @@
<rd-widget>
<rd-widget-body>
<div class="widget-icon grey pull-left">
<i class="fa fa-server"></i>
<i class="fa fa-tasks"></i>
</div>
<div class="title">{{ container.Name|trimcontainername }}</div>
<div class="comment">Name</div>
+30 -44
View File
@@ -9,7 +9,7 @@
<div class="col-lg-12">
<rd-widget>
<rd-widget-header icon="fa-server" title="Containers">
<rd-widget-header icon="fa-tasks" title="Containers">
<div class="pull-right">
<i id="loadContainersSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
</div>
@@ -17,15 +17,15 @@
<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 btn-responsive" ng-click="startAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-play space-right" 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 space-right" 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 space-right" 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 space-right" 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 space-right" 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 space-right" 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 space-right" aria-hidden="true"></i>Remove</button>
<button type="button" class="btn btn-primary" ng-click="startAction()" ng-disabled="!state.selectedItemCount">Start</button>
<button type="button" class="btn btn-primary" ng-click="stopAction()" ng-disabled="!state.selectedItemCount">Stop</button>
<button type="button" class="btn btn-primary" ng-click="killAction()" ng-disabled="!state.selectedItemCount">Kill</button>
<button type="button" class="btn btn-primary" ng-click="restartAction()" ng-disabled="!state.selectedItemCount">Restart</button>
<button type="button" class="btn btn-primary" ng-click="pauseAction()" ng-disabled="!state.selectedItemCount">Pause</button>
<button type="button" class="btn btn-primary" ng-click="unpauseAction()" ng-disabled="!state.selectedItemCount">Unpause</button>
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount">Remove</button>
</div>
<a class="btn btn-default btn-responsive" type="button" ui-sref="actions.create.container">Add container</a>
<a class="btn btn-default" type="button" ui-sref="actions.create.container">Add container</a>
</div>
<div class="pull-right">
<input type="checkbox" ng-model="state.displayAll" id="displayAll" ng-change="toggleGetAll()" style="margin-top: -2px; margin-right: 5px;"/><label for="displayAll">Show all containers</label>
@@ -41,8 +41,8 @@
<th>
<a ui-sref="containers" ng-click="order('Status')">
State
<span ng-show="sortType == 'Status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
<span ng-show="sortType == 'State' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'State' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
@@ -52,13 +52,6 @@
<span ng-show="sortType == 'Names' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="containers" ng-click="order('Image')">
Image
<span ng-show="sortType == 'Image' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Image' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-if="state.displayIP">
<a ui-sref="containers" ng-click="order('IP')">
IP Address
@@ -66,49 +59,42 @@
<span ng-show="sortType == 'IP' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-if="swarm && !swarm_mode">
<th ng-if="swarm">
<a ui-sref="containers" ng-click="order('Host')">
Host IP
Host
<span ng-show="sortType == 'Host' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Host' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="containers" ng-click="order('Ports')">
Exposed Ports
<span ng-show="sortType == 'Ports' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Ports' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
<a ui-sref="containers" ng-click="order('Image')">
Image
<span ng-show="sortType == 'Image' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Image' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="containers" ng-click="order('Command')">
Command
<span ng-show="sortType == 'Command' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Command' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="container in (state.filteredContainers = ( containers | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
<tr ng-repeat="container in (state.filteredContainers = ( containers | filter:state.filter | orderBy:sortType:sortReverse))">
<td><input type="checkbox" ng-model="container.Checked" ng-change="selectItem(container)"/></td>
<td><span class="label label-{{ container.Status|containerstatusbadge }}">{{ container.Status|containerstatus }}</span></td>
<td ng-if="swarm && !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 ng-if="swarm"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername}}</a></td>
<td ng-if="!swarm"><a ui-sref="container({id: container.Id})">{{ container|containername}}</a></td>
<td ng-if="state.displayIP">{{ container.IP ? container.IP : '-' }}</td>
<td ng-if="swarm && !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">
<td colspan="8" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="containers.length == 0">
<td colspan="8" class="text-center text-muted">No containers available.</td>
<td ng-if="swarm">{{ container.hostIP }}</td>
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
<td>{{ container.Command|truncate:60 }}</td>
</tr>
</tbody>
</table>
<div ng-if="containers" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
<rd-widget>
@@ -1,14 +1,13 @@
angular.module('containers', [])
.controller('ContainersController', ['$scope', 'Container', 'ContainerHelper', 'Info', 'Settings', 'Messages', 'Config',
function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config) {
.controller('ContainersController', ['$scope', 'Container', 'Info', 'Settings', 'Messages', 'Config', 'errorMsgFilter',
function ($scope, Container, Info, Settings, Messages, Config, errorMsgFilter) {
$scope.state = {};
$scope.state.displayAll = Settings.displayAll;
$scope.state.displayIP = false;
$scope.sortType = 'State';
$scope.sortReverse = false;
$scope.sortReverse = true;
$scope.state.selectedItemCount = 0;
$scope.swarm_mode = false;
$scope.pagination_count = Settings.pagination_count;
$scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
@@ -20,24 +19,20 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config)
$scope.state.selectedItemCount = 0;
Container.query(data, function (d) {
var containers = d;
if ($scope.containersToHideLabels) {
containers = ContainerHelper.hideContainers(d, $scope.containersToHideLabels);
if (hiddenLabels) {
containers = hideContainers(d);
}
$scope.containers = containers.map(function (container) {
var model = new ContainerViewModel(container);
if (model.IP) {
$scope.state.displayIP = true;
}
if ($scope.swarm && !$scope.swarm_mode) {
if ($scope.swarm) {
model.hostIP = $scope.swarm_hosts[_.split(container.Names[0], '/')[1]];
}
return model;
});
$('#loadContainersSpinner').hide();
}, function (e) {
$('#loadContainersSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve containers");
$scope.containers = [];
});
};
@@ -55,25 +50,37 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config)
if (c.Checked) {
counter = counter + 1;
if (action === Container.start) {
action({id: c.Id}, {}, function (d) {
Messages.send("Container " + msg, c.Id);
complete();
Container.get({id: c.Id}, function (d) {
c = d;
action({id: c.Id, HostConfig: c.HostConfig || {}}, function (d) {
Messages.send("Container " + msg, c.Id);
complete();
}, function (e) {
Messages.error("Failure", e.data);
complete();
});
}, function (e) {
Messages.error("Failure", e, "Unable to start container");
if (e.status === 404) {
$('.detail').hide();
Messages.error("Not found", "Container not found.");
} else {
Messages.error("Failure", e.data);
}
complete();
});
}
else if (action === Container.remove) {
action({id: c.Id}, function (d) {
if (d.message) {
Messages.send("Error", d.message);
var error = errorMsgFilter(d);
if (error) {
Messages.send("Error", "Unable to remove running container");
}
else {
Messages.send("Container " + msg, c.Id);
}
complete();
}, function (e) {
Messages.error("Failure", e, 'Unable to remove container');
Messages.error("Failure", e.data);
complete();
});
}
@@ -82,7 +89,7 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config)
Messages.send("Container " + msg, c.Id);
complete();
}, function (e) {
Messages.error("Failure", e, 'An error occured');
Messages.error("Failure", e.data);
complete();
});
@@ -135,6 +142,21 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config)
batch($scope.containers, Container.remove, "Removed");
};
var hideContainers = function (containers) {
return containers.filter(function (container) {
var filterContainer = false;
hiddenLabels.forEach(function(label, index) {
if (_.has(container.Labels, label.name) &&
container.Labels[label.name] === label.value) {
filterContainer = true;
}
});
if (!filterContainer) {
return container;
}
});
};
function retrieveSwarmHostsInfo(data) {
var swarm_hosts = {};
var systemStatus = data.SystemStatus;
@@ -152,15 +174,11 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config)
$scope.swarm = false;
Config.$promise.then(function (c) {
$scope.containersToHideLabels = c.hiddenLabels;
hiddenLabels = c.hiddenLabels;
$scope.swarm = c.swarm;
if (c.swarm) {
Info.get({}, function (d) {
if (!_.startsWith(d.ServerVersion, 'swarm')) {
$scope.swarm_mode = true;
} else {
$scope.swarm_hosts = retrieveSwarmHostsInfo(d);
}
$scope.swarm_hosts = retrieveSwarmHostsInfo(d);
update({all: Settings.displayAll ? 1 : 0});
});
} else {
@@ -1,19 +1,21 @@
angular.module('createContainer', [])
.controller('CreateContainerController', ['$scope', '$state', '$stateParams', '$filter', 'Config', 'Info', 'Container', 'ContainerHelper', 'Image', 'Volume', 'Network', 'Messages',
function ($scope, $state, $stateParams, $filter, Config, Info, Container, ContainerHelper, Image, Volume, Network, Messages) {
.controller('CreateContainerController', ['$scope', '$state', 'Config', 'Container', 'Image', 'Volume', 'Network', 'Messages', 'errorMsgFilter',
function ($scope, $state, Config, Container, Image, Volume, Network, Messages, errorMsgFilter) {
$scope.state = {
alwaysPull: true
};
$scope.formValues = {
alwaysPull: true,
Console: 'none',
Volumes: [],
Registry: '',
NetworkContainer: ''
AvailableRegistries: [],
Registry: ''
};
$scope.imageConfig = {};
$scope.config = {
Image: '',
Env: [],
ExposedPorts: {},
HostConfig: {
@@ -52,23 +54,24 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
};
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;
var swarm = c.swarm;
$scope.formValues.AvailableRegistries = c.registries;
Volume.query({}, function (d) {
$scope.availableVolumes = d.Volumes;
var persistedVolumes = d.Volumes.filter(function (volume) {
if (volume.Driver === 'local-persist') {
return volume;
}
});
$scope.availableVolumes = _.uniqBy(persistedVolumes, 'Name');
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve volumes");
Messages.error("Failure", e.data);
});
Network.query({}, function (d) {
var networks = d;
if ($scope.swarm) {
if (swarm) {
networks = d.filter(function (network) {
if (network.Scope === 'global') {
return network;
@@ -79,67 +82,50 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
networks.push({Name: "host"});
networks.push({Name: "none"});
}
networks.push({Name: "container"});
$scope.availableNetworks = networks;
if (!_.find(networks, {'Name': 'bridge'})) {
$scope.config.HostConfig.NetworkMode = 'nat';
}
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve networks");
});
Container.query({}, 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");
Messages.error("Failure", e.data);
});
});
// TODO: centralize, already present in templatesController
function createContainer(config) {
$('#createContainerSpinner').show();
Container.create(config, function (d) {
if (d.message) {
$('#createContainerSpinner').hide();
Messages.error('Error', {}, d.message);
} else {
Container.start({id: d.Id}, {}, function (cd) {
if (cd.message) {
$('#createContainerSpinner').hide();
Messages.error('Error', {}, cd.message);
} else {
$('#createContainerSpinner').hide();
Messages.send('Container Started', d.Id);
$state.go('containers', {}, {reload: true});
}
if (d.Id) {
var reqBody = config.HostConfig || {};
reqBody.id = d.Id;
Container.start(reqBody, function (cd) {
$('#createContainerSpinner').hide();
Messages.send('Container Started', d.Id);
$state.go('containers', {}, {reload: true});
}, function (e) {
$('#createContainerSpinner').hide();
Messages.error("Failure", e, 'Unable to start container');
Messages.error('Error', errorMsgFilter(e));
});
} else {
$('#createContainerSpinner').hide();
Messages.error('Error', errorMsgFilter(d));
}
}, function (e) {
$('#createContainerSpinner').hide();
Messages.error("Failure", e, 'Unable to create container');
Messages.error('Error', errorMsgFilter(e));
});
}
// TODO: centralize, already present in templatesController
function pullImageAndCreateContainer(config) {
$('#createContainerSpinner').show();
Image.create($scope.imageConfig, function (data) {
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
if (err) {
var detail = data[data.length - 1];
$('#createContainerSpinner').hide();
Messages.error('Error', {}, detail.error);
} else {
createContainer(config);
}
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
if (err) {
var detail = data[data.length - 1];
$('#createContainerSpinner').hide();
Messages.error('Error', detail.error);
} else {
createContainer(config);
}
}, function (e) {
$('#createContainerSpinner').hide();
Messages.error('Failure', e, 'Unable to pull image');
Messages.error('Error', 'Unable to pull image ' + image);
});
}
@@ -167,10 +153,10 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
function preparePortBindings(config) {
var bindings = {};
config.HostConfig.PortBindings.forEach(function (portBinding) {
if (portBinding.containerPort) {
if (portBinding.hostPort && portBinding.containerPort) {
var key = portBinding.containerPort + "/" + portBinding.protocol;
var binding = {};
if (portBinding.hostPort && portBinding.hostPort.indexOf(':') > -1) {
if (portBinding.hostPort.indexOf(':') > -1) {
var hostAndPort = portBinding.hostPort.split(':');
binding.HostIp = hostAndPort[0];
binding.HostPort = hostAndPort[1];
@@ -230,26 +216,8 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
config.Volumes = volumes;
}
function prepareNetworkConfig(config) {
var mode = config.HostConfig.NetworkMode;
var container = $scope.formValues.NetworkContainer;
var containerName = container;
if (container && typeof container === 'object') {
containerName = $filter('trimcontainername')(container.Names[0]);
if ($scope.swarm && !$scope.swarm_mode) {
containerName = $filter('swarmcontainername')(container);
}
}
var networkMode = mode;
if (containerName) {
networkMode += ':' + containerName;
}
config.HostConfig.NetworkMode = networkMode;
}
function prepareConfiguration() {
var config = angular.copy($scope.config);
prepareNetworkConfig(config);
prepareImageConfig(config);
preparePortBindings(config);
prepareConsole(config);
@@ -260,11 +228,12 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
$scope.create = function () {
var config = prepareConfiguration();
$('#createContainerSpinner').show();
if ($scope.formValues.alwaysPull) {
if ($scope.state.alwaysPull) {
pullImageAndCreateContainer(config);
} else {
createContainer(config);
}
};
}]);
@@ -1,7 +1,7 @@
<rd-header>
<rd-header-title title="Create container"></rd-header-title>
<rd-header-content>
<a ui-sref="containers">Containers</a> > Add container
Containers > Add container
</rd-header-content>
</rd-header>
@@ -26,12 +26,15 @@
</div>
<label for="image_registry" class="col-sm-1 control-label text-left">Registry</label>
<div class="col-sm-3">
<input type="text" class="form-control" ng-model="formValues.Registry" id="image_registry" placeholder="leave empty to use DockerHub">
<select class="selectpicker form-control" ng-model="formValues.Registry">
<option value="">Docker Hub</option>
<option ng-repeat="registry in formValues.AvailableRegistries" ng-value="registry.value">{{ registry.name }}</option>
</select>
</div>
<div class="col-sm-offset-1 col-sm-11">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="formValues.alwaysPull"> Always pull image before creating
<input type="checkbox" ng-model="state.alwaysPull"> Always pull image before creating
</label>
</div>
</div>
@@ -53,10 +56,6 @@
<input type="radio" name="container_restart_policy" ng-model="config.HostConfig.RestartPolicy.Name" value="on-failure">
<span class="radio-value">On failure</span>
</label>
<label class="radio-inline">
<input type="radio" name="container_restart_policy" ng-model="config.HostConfig.RestartPolicy.Name" value="unless-stopped">
<span class="radio-value">Unless stopped</span>
</label>
</div>
</div>
<!-- !restart-policy -->
@@ -64,7 +63,7 @@
<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 interactive" ng-click="addPortBinding()">
<span class="label label-default clickable" ng-click="addPortBinding()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> map port
</span>
</div>
@@ -73,7 +72,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 (optional)">
<input type="text" class="form-control" ng-model="portBinding.hostPort" placeholder="e.g. 80 or 1.2.3.4:80">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">container</span>
@@ -106,11 +105,12 @@
<rd-widget>
<rd-widget-body>
<ul class="nav nav-tabs">
<li class="active interactive"><a data-target="#command" data-toggle="tab">Command</a></li>
<li class="interactive"><a data-target="#volumes" data-toggle="tab">Volumes</a></li>
<li class="interactive"><a data-target="#network" data-toggle="tab">Network</a></li>
<li class="interactive"><a data-target="#security" data-toggle="tab">Security/Host</a></li>
<li class="active clickable"><a data-target="#command" data-toggle="tab">Command</a></li>
<li class="clickable"><a data-target="#volumes" data-toggle="tab">Volumes</a></li>
<li class="clickable"><a data-target="#network" data-toggle="tab">Network</a></li>
<li class="clickable"><a data-target="#security" data-toggle="tab">Security/Host</a></li>
</ul>
<!-- tab-content -->
<div class="tab-content">
<!-- tab-command -->
@@ -181,7 +181,7 @@
<div class="form-group">
<label for="container_env" class="col-sm-1 control-label text-left">Environment variables</label>
<div class="col-sm-11">
<span class="label label-default interactive" ng-click="addEnvironmentVariable()">
<span class="label label-default clickable" ng-click="addEnvironmentVariable()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> environment variable
</span>
</div>
@@ -215,28 +215,22 @@
<!-- volumes -->
<div class="form-group">
<label for="container_volumes" class="col-sm-1 control-label text-left">Volumes</label>
<div class="col-sm-11">
<span class="label label-default interactive" ng-click="addVolume()">
<div class="col-sm-11" ng-if="availableVolumes.length !== 0">
<span class="label label-default clickable" ng-click="addVolume()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> volume
</span>
</div>
<div class="col-sm-11" ng-if="availableVolumes.length === 0" style="margin-top: 5px;">
<span class="small text-muted">You don't have any persistent volumes. Head over the <a ui-sref="volumes">volumes view</a> to create one.</span>
</div>
<!-- volumes-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
<div ng-repeat="volume in formValues.Volumes" style="margin-top: 2px;">
<div class="input-group col-sm-1 input-group-sm">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="volume.readOnly"> Read-only
</label>
</div>
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon"><input type="checkbox" ng-model="volume.isPath" ng-click="resetVolumePath($index)">Path</span>
<select class="selectpicker form-control" ng-model="volume.name" ng-if="!volume.isPath">
<select class="selectpicker form-control" ng-model="volume.name">
<option selected disabled hidden value="">Select a volume</option>
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option>
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:50}}</option>
</select>
<input ng-if="volume.isPath" type="text" class="form-control" ng-model="volume.name" placeholder="e.g. /path/on/host">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">container</span>
@@ -258,7 +252,7 @@
<!-- 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="form-group" ng-if="globalNetworkCount === 0">
<div class="col-sm-12">
<span class="small text-muted">You don't have any shared network. Head over the <a ui-sref="networks">networks view</a> to create one.</span>
</div>
@@ -267,26 +261,13 @@
<div class="form-group">
<label for="container_network" class="col-sm-1 control-label text-left">Network</label>
<div class="col-sm-9">
<select class="selectpicker form-control" ng-model="config.HostConfig.NetworkMode" id="container_network">
<select class="selectpicker form-control" ng-model="config.HostConfig.NetworkMode">
<option selected disabled hidden value="">Select a network</option>
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
</select>
</div>
</div>
<!-- !network-input -->
<!-- container-name-input -->
<div class="form-group" ng-if="config.HostConfig.NetworkMode == 'container'">
<label for="container_network_container" class="col-sm-1 control-label text-left">Container</label>
<div class="col-sm-9">
<select ng-if="(!swarm || swarm && swarm_mode)" ng-options="container|containername for container in runningContainers" class="selectpicker form-control" ng-model="formValues.NetworkContainer">
<option selected disabled hidden value="">Select a container</option>
</select>
<select ng-if="swarm && !swarm_mode" ng-options="container|swarmcontainername for container in runningContainers" class="selectpicker form-control" ng-model="formValues.NetworkContainer">
<option selected disabled hidden value="">Select a container</option>
</select>
</div>
</div>
<!-- !container-name-input -->
<!-- hostname-input -->
<div class="form-group">
<label for="container_hostname" class="col-sm-1 control-label text-left">Hostname</label>
@@ -1,77 +0,0 @@
angular.module('createNetwork', [])
.controller('CreateNetworkController', ['$scope', '$state', 'Messages', 'Network',
function ($scope, $state, Messages, Network) {
$scope.formValues = {
DriverOptions: [],
Subnet: '',
Gateway: ''
};
$scope.config = {
Driver: 'bridge',
CheckDuplicate: true,
Internal: false,
// Force IPAM Driver to 'default', should not be required.
// See: https://github.com/docker/docker/issues/25735
IPAM: {
Driver: 'default',
Config: []
}
};
$scope.addDriverOption = function() {
$scope.formValues.DriverOptions.push({ name: '', value: '' });
};
$scope.removeDriverOption = function(index) {
$scope.formValues.DriverOptions.splice(index, 1);
};
function createNetwork(config) {
$('#createNetworkSpinner').show();
Network.create(config, function (d) {
if (d.message) {
$('#createNetworkSpinner').hide();
Messages.error('Unable to create network', {}, d.message);
} else {
Messages.send("Network created", d.Id);
$('#createNetworkSpinner').hide();
$state.go('networks', {}, {reload: true});
}
}, function (e) {
$('#createNetworkSpinner').hide();
Messages.error("Failure", e, 'Unable to create network');
});
}
function prepareIPAMConfiguration(config) {
if ($scope.formValues.Subnet) {
var ipamConfig = {};
ipamConfig.Subnet = $scope.formValues.Subnet;
if ($scope.formValues.Gateway) {
ipamConfig.Gateway = $scope.formValues.Gateway ;
}
config.IPAM.Config.push(ipamConfig);
}
}
function prepareDriverOptions(config) {
var options = {};
$scope.formValues.DriverOptions.forEach(function (option) {
options[option.name] = option.value;
});
config.Options = options;
}
function prepareConfiguration() {
var config = angular.copy($scope.config);
prepareIPAMConfiguration(config);
prepareDriverOptions(config);
return config;
}
$scope.create = function () {
var config = prepareConfiguration();
createNetwork(config);
};
}]);
@@ -1,95 +0,0 @@
<rd-header>
<rd-header-title title="Create network"></rd-header-title>
<rd-header-content>
<a ui-sref="networks">Networks</a> > Add network
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal">
<!-- name-input -->
<div class="form-group">
<label for="network_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="config.Name" id="network_name" placeholder="e.g. myNetwork">
</div>
</div>
<!-- !name-input -->
<!-- subnet-gateway-inputs -->
<div class="form-group">
<label for="network_subnet" class="col-sm-1 control-label text-left">Subnet</label>
<div class="col-sm-5">
<input type="text" class="form-control" ng-model="formValues.Subnet" id="network_subnet" placeholder="e.g. 172.20.0.0/16">
</div>
<label for="network_gateway" class="col-sm-1 control-label text-left">Gateway</label>
<div class="col-sm-5">
<input type="text" class="form-control" ng-model="formValues.Gateway" id="network_gateway" placeholder="e.g. 172.20.10.11">
</div>
</div>
<!-- !subnet-gateway-inputs -->
<!-- driver-input -->
<div class="form-group">
<label for="network_driver" class="col-sm-1 control-label text-left">Driver</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="config.Driver" id="network_driver" placeholder="e.g. driverName">
</div>
</div>
<!-- !driver-input -->
<!-- driver-options -->
<div class="form-group">
<label for="network_driveropts" class="col-sm-1 control-label text-left">Driver options</label>
<div class="col-sm-11">
<span class="label label-default interactive" ng-click="addDriverOption()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> driver option
</span>
</div>
<!-- driver-options-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
<div ng-repeat="option in formValues.DriverOptions" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span>
<input type="text" class="form-control" ng-model="option.name" placeholder="e.g. com.docker.network.bridge.enable_icc">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="option.value" placeholder="e.g. true">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeDriverOption($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !driver-options-input-list -->
</div>
<!-- !driver-options -->
<!-- internal -->
<div class="form-group">
<div class="col-sm-12">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="config.Internal"> Restrict external access to the network
</label>
</div>
</div>
</div>
<!-- !internal -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12" style="text-align: center;">
<div>
<i id="createNetworkSpinner" class="fa fa-cog fa-3x fa-spin" style="margin-bottom: 5px; display: none;"></i>
</div>
<button type="button" class="btn btn-default btn-lg" ng-disabled="!config.Name" ng-click="create()">Create</button>
<a type="button" class="btn btn-default btn-lg" ui-sref="networks">Cancel</a>
</div>
</div>
@@ -1,215 +0,0 @@
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: [],
Labels: [],
ContainerLabels: [],
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);
};
$scope.addLabel = function() {
$scope.formValues.Labels.push({ name: '', value: ''});
};
$scope.removeLabel = function(index) {
$scope.formValues.Labels.splice(index, 1);
};
$scope.addContainerLabel = function() {
$scope.formValues.ContainerLabels.push({ name: '', value: ''});
};
$scope.removeContainerLabel = function(index) {
$scope.formValues.ContainerLabels.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 prepareLabelsConfig(config, input) {
var labels = {};
input.Labels.forEach(function (label) {
if (label.name && label.value) {
labels[label.name] = label.value;
}
});
config.Labels = labels;
var containerLabels = {};
input.ContainerLabels.forEach(function (label) {
if (label.name && label.value) {
containerLabels[label.name] = label.value;
}
});
config.TaskTemplate.ContainerSpec.Labels = containerLabels;
}
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);
prepareLabelsConfig(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");
});
}]);
@@ -1,338 +0,0 @@
<rd-header>
<rd-header-title title="Create service"></rd-header-title>
<rd-header-content>
<a ui-sref="services">Services</a> > 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 interactive" 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 interactive"><a data-target="#command" data-toggle="tab">Command</a></li>
<li class="interactive"><a data-target="#volumes" data-toggle="tab">Volumes</a></li>
<li class="interactive"><a data-target="#network" data-toggle="tab">Network</a></li>
<li class="interactive"><a data-target="#labels" data-toggle="tab">Labels</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 interactive" 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 interactive" 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 interactive" 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-labels -->
<div class="tab-pane" id="labels">
<form class="form-horizontal" style="margin-top: 15px;">
<!-- labels -->
<div class="form-group">
<label for="service_env" class="col-sm-1 control-label text-left">Labels</label>
<div class="col-sm-11">
<span class="label label-default interactive" ng-click="addLabel()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> label
</span>
</div>
<!-- labels-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
<div ng-repeat="label in formValues.Labels" 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="label.name" placeholder="e.g. com.example.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="label.value" placeholder="e.g. bar">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeLabel($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !labels-input-list -->
</div>
<!-- !labels-->
<!-- container-labels -->
<div class="form-group">
<label for="service_env" class="col-sm-1 control-label text-left">Container labels</label>
<div class="col-sm-11">
<span class="label label-default interactive" ng-click="addContainerLabel()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> container label
</span>
</div>
<!-- container-labels-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
<div ng-repeat="label in formValues.ContainerLabels" 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="label.name" placeholder="e.g. com.example.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="label.value" placeholder="e.g. bar">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeContainerLabel($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !container-labels-input-list -->
</div>
<!-- !container-labels-->
</form>
</div>
<!-- !tab-labels -->
<!-- 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,56 +0,0 @@
angular.module('createVolume', [])
.controller('CreateVolumeController', ['$scope', '$state', 'Volume', 'Messages',
function ($scope, $state, Volume, Messages) {
$scope.formValues = {
DriverOptions: []
};
$scope.config = {
Driver: 'local'
};
$scope.addDriverOption = function() {
$scope.formValues.DriverOptions.push({ name: '', value: '' });
};
$scope.removeDriverOption = function(index) {
$scope.formValues.DriverOptions.splice(index, 1);
};
function createVolume(config) {
$('#createVolumeSpinner').show();
Volume.create(config, function (d) {
if (d.message) {
$('#createVolumeSpinner').hide();
Messages.error('Unable to create volume', {}, d.message);
} else {
Messages.send("Volume created", d.Name);
$('#createVolumeSpinner').hide();
$state.go('volumes', {}, {reload: true});
}
}, function (e) {
$('#createVolumeSpinner').hide();
Messages.error("Failure", e, 'Unable to create volume');
});
}
function prepareDriverOptions(config) {
var options = {};
$scope.formValues.DriverOptions.forEach(function (option) {
options[option.name] = option.value;
});
config.DriverOpts = options;
}
function prepareConfiguration() {
var config = angular.copy($scope.config);
prepareDriverOptions(config);
return config;
}
$scope.create = function () {
var config = prepareConfiguration();
createVolume(config);
};
}]);
@@ -1,72 +0,0 @@
<rd-header>
<rd-header-title title="Create volume"></rd-header-title>
<rd-header-content>
<a ui-sref="volumes">Volumes</a> > Add volume
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal">
<!-- name-input -->
<div class="form-group">
<label for="volume_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="config.Name" id="volume_name" placeholder="e.g. myVolume">
</div>
</div>
<!-- !name-input -->
<!-- driver-input -->
<div class="form-group">
<label for="volume_driver" class="col-sm-1 control-label text-left">Driver</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="config.Driver" id="volume_driver" placeholder="e.g. driverName">
</div>
</div>
<!-- !driver-input -->
<!-- driver-options -->
<div class="form-group">
<label for="volume_driveropts" class="col-sm-1 control-label text-left">Driver options</label>
<div class="col-sm-11">
<span class="label label-default interactive" ng-click="addDriverOption()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> driver option
</span>
</div>
<!-- driver-options-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
<div ng-repeat="option in formValues.DriverOptions" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span>
<input type="text" class="form-control" ng-model="option.name" placeholder="e.g. mountpoint">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="option.value" placeholder="e.g. /path/on/host">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeDriverOption($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !driver-options-input-list -->
</div>
<!-- !driver-options -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12" style="text-align: center;">
<div>
<i id="createVolumeSpinner" class="fa fa-cog fa-3x fa-spin" style="margin-bottom: 5px; display: none;"></i>
</div>
<button type="button" class="btn btn-default btn-lg" ng-click="create()">Create</button>
<a type="button" class="btn btn-default btn-lg" ui-sref="volumes">Cancel</a>
</div>
</div>
+9 -31
View File
@@ -6,7 +6,7 @@
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="swarm_mode || !swarm">
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="!swarm">
<rd-widget>
<rd-widget-header icon="fa-tachometer" title="Node info"></rd-widget-header>
<rd-widget-body classes="no-padding">
@@ -33,7 +33,7 @@
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="swarm && !swarm_mode">
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="swarm">
<rd-widget>
<rd-widget-header icon="fa-tachometer" title="Cluster info"></rd-widget-header>
<rd-widget-body classes="no-padding">
@@ -41,7 +41,7 @@
<tbody>
<tr>
<td>Nodes</td>
<td>{{ infoData.SystemStatus[0][1] == 'primary' ? infoData.SystemStatus[3][1] : infoData.SystemStatus[4][1] }}</td>
<td>{{ infoData.SystemStatus[3][1] }}</td>
</tr>
<tr>
<td>Swarm version</td>
@@ -53,29 +53,7 @@
</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-12 col-md-12 col-xs-12" ng-if="swarm && swarm_mode">
<rd-widget>
<rd-widget-header icon="fa-tachometer" title="Swarm info"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td colspan="2"><span class="small text-muted">This node is part of a Swarm cluster</span></td>
</tr>
<tr >
<td>Node role</td>
<td>{{ infoData.Swarm.ControlAvailable ? 'Manager' : 'Worker' }}</td>
</tr>
<tr ng-if="infoData.Swarm.ControlAvailable">
<td>Nodes in the cluster</td>
<td>{{ infoData.Swarm.Nodes }}</td>
<td>{{ infoData.MemTotal|humansize }}</td>
</tr>
</tbody>
</table>
@@ -90,11 +68,11 @@
<rd-widget>
<rd-widget-body>
<div class="widget-icon blue pull-left">
<i class="fa fa-server"></i>
<i class="fa fa-tasks"></i>
</div>
<div class="pull-right">
<div><i class="fa fa-heartbeat space-right green-icon"></i>{{ containerData.running }} running</div>
<div><i class="fa fa-heartbeat space-right red-icon"></i>{{ containerData.stopped }} stopped</div>
<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>
@@ -110,7 +88,7 @@
<i class="fa fa-clone"></i>
</div>
<div class="pull-right">
<div><i class="fa fa-pie-chart space-right"></i>{{ imageData.size|humansize }}</div>
<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>
@@ -126,7 +104,7 @@
<i class="fa fa-cubes"></i>
</div>
<div class="pull-right" ng-if="infoData.Driver">
<div><i class="fa fa-hdd-o space-right"></i>{{ infoData.Driver }} driver</div>
<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>
+24 -12
View File
@@ -1,6 +1,6 @@
angular.module('dashboard', [])
.controller('DashboardController', ['$scope', '$q', 'Config', 'Container', 'ContainerHelper', 'Image', 'Network', 'Volume', 'Info',
function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume, Info) {
.controller('DashboardController', ['$scope', '$q', 'Config', 'Container', 'Image', 'Network', 'Volume', 'Info',
function ($scope, $q, Config, Container, Image, Network, Volume, Info) {
$scope.containerData = {
total: 0
@@ -14,15 +14,14 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
$scope.volumeData = {
total: 0
};
$scope.swarm_mode = false;
function prepareContainerData(d, containersToHideLabels) {
function prepareContainerData(d) {
var running = 0;
var stopped = 0;
var containers = d;
if (containersToHideLabels) {
containers = ContainerHelper.hideContainers(d, containersToHideLabels);
if (hiddenLabels) {
containers = hideContainers(d);
}
for (var i = 0; i < containers.length; i++) {
@@ -64,12 +63,9 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
function prepareInfoData(d) {
var info = d;
$scope.infoData = info;
if ($scope.swarm && !_.startsWith(info.ServerVersion, 'swarm')) {
$scope.swarm_mode = true;
}
}
function fetchDashboardData(containersToHideLabels) {
function fetchDashboardData() {
$('#loadingViewSpinner').show();
$q.all([
Container.query({all: 1}).$promise,
@@ -78,7 +74,7 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
Network.query({}).$promise,
Info.get({}).$promise
]).then(function (d) {
prepareContainerData(d[0], containersToHideLabels);
prepareContainerData(d[0]);
prepareImageData(d[1]);
prepareVolumeData(d[2]);
prepareNetworkData(d[3]);
@@ -87,8 +83,24 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
});
}
var hideContainers = function (containers) {
return containers.filter(function (container) {
var filterContainer = false;
hiddenLabels.forEach(function(label, index) {
if (_.has(container.Labels, label.name) &&
container.Labels[label.name] === label.value) {
filterContainer = true;
}
});
if (!filterContainer) {
return container;
}
});
};
Config.$promise.then(function (c) {
$scope.swarm = c.swarm;
fetchDashboardData(c.hiddenLabels);
hiddenLabels = c.hiddenLabels;
fetchDashboardData();
});
}]);
+2 -17
View File
@@ -1,6 +1,5 @@
angular.module('dashboard')
.controller('MasterCtrl', ['$scope', '$cookieStore', 'Settings', 'Config', 'Info',
function ($scope, $cookieStore, Settings, Config, Info) {
.controller('MasterCtrl', ['$scope', '$cookieStore', 'Settings', 'Config', function ($scope, $cookieStore, Settings, Config) {
/**
* Sidebar Toggle & Cookie Control
*/
@@ -10,21 +9,7 @@ function ($scope, $cookieStore, Settings, Config, Info) {
return window.innerWidth;
};
$scope.swarm_mode = false;
Config.$promise.then(function (c) {
$scope.logo = c.logo;
$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.config = Config;
$scope.$watch($scope.getWidth, function(newValue, oldValue) {
if (newValue >= mobileView) {
+103 -77
View File
@@ -3,113 +3,139 @@
<a data-toggle="tooltip" title="Refresh" ui-sref="docker" 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>Docker</rd-header-content>
</rd-header>
<div class="row" ng-if="state.loaded">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row">
<div class="col-lg-3 col-md-6 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-code" title="Engine version"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Version</td>
<td>{{ version.Version }}</td>
</tr>
<tr>
<td>API version</td>
<td>{{ version.ApiVersion }}</td>
</tr>
<tr>
<td>Go version</td>
<td>{{ version.GoVersion }}</td>
</tr>
<tr>
<td>OS type</td>
<td>{{ version.Os }}</td>
</tr>
<tr>
<td>OS</td>
<td>{{ info.OperatingSystem }}</td>
</tr>
<tr>
<td>Architecture</td>
<td>{{ version.Arch }}</td>
</tr>
<tr>
<td>Kernel version</td>
<td>{{ version.KernelVersion }}</td>
</tr>
</tbody>
</table>
<rd-widget-body>
<div class="widget-icon pull-left">
<i class="fa fa-code"></i>
</div>
<div class="title">{{ docker.Version }}</div>
<div class="comment">Docker version</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-3 col-md-6 col-xs-12">
<rd-widget>
<rd-widget-body>
<div class="widget-icon pull-left">
<i class="fa fa-code"></i>
</div>
<div class="title">{{ docker.ApiVersion }}</div>
<div class="comment">API version</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-3 col-md-6 col-xs-12">
<rd-widget>
<rd-widget-body>
<div class="widget-icon pull-left">
<i class="fa fa-code"></i>
</div>
<div class="title">{{ docker.GoVersion }}</div>
<div class="comment">Go version</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="state.loaded">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row">
<div class="col-lg-12">
<rd-widget>
<rd-widget-header icon="fa-th" title="Engine 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>
<tr>
<td>Total CPU</td>
<td>Containers</td>
<td>{{ info.Containers }}</td>
</tr>
<tr>
<td>Images</td>
<td>{{ info.Images }}</td>
</tr>
<tr>
<td>Debug</td>
<td>{{ info.Debug }}</td>
</tr>
<tr>
<td>CPUs</td>
<td>{{ info.NCPU }}</td>
</tr>
<tr>
<td>Total memory</td>
<td>Total Memory</td>
<td>{{ info.MemTotal|humansize }}</td>
</tr>
<tr>
<td>Docker root directory</td>
<td>{{ info.DockerRootDir }}</td>
<td>Operating System</td>
<td>{{ info.OperatingSystem }}</td>
</tr>
<tr>
<td>Storage driver</td>
<td>Kernel Version</td>
<td>{{ info.KernelVersion }}</td>
</tr>
<tr>
<td>ID</td>
<td>{{ info.ID }}</td>
</tr>
<tr>
<td>Labels</td>
<td>{{ info.Labels }}</td>
</tr>
<tr>
<td>File Descriptors</td>
<td>{{ info.NFd }}</td>
</tr>
<tr>
<td>Goroutines</td>
<td>{{ info.NGoroutines }}</td>
</tr>
<tr>
<td>Storage Driver</td>
<td>{{ info.Driver }}</td>
</tr>
<tr>
<td>Logging driver</td>
<td>{{ info.LoggingDriver }}</td>
<td>Storage Driver Status</td>
<td>
<p ng-repeat="val in info.DriverStatus">
{{ val[0] }}: {{ val[1] }}
</p>
</td>
</tr>
<tr ng-if="info.CgroupDriver">
<td>Cgroup driver</td>
<td>{{ info.CgroupDriver }}</td>
</tr>
<tr ng-if="info.ExecutionDriver">
<td>Execution driver</td>
<tr>
<td>Execution Driver</td>
<td>{{ info.ExecutionDriver }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="state.loaded && info.Plugins">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-plug" title="Engine plugins"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr ng-if="info.Plugins.Volume">
<td>Volume</td>
<td>{{ info.Plugins.Volume|arraytostr: ', '}}</td>
<tr>
<td>IPv4 Forwarding</td>
<td>{{ info.IPv4Forwarding }}</td>
</tr>
<tr ng-if="info.Plugins.Network">
<td>Network</td>
<td>{{ info.Plugins.Network|arraytostr: ', '}}</td>
<tr>
<td>Index Server Address</td>
<td>{{ info.IndexServerAddress }}</td>
</tr>
<tr ng-if="info.Plugins.Authorization">
<td>Authorization</td>
<td>{{ info.Plugins.Authorization|arraytostr: ', '}}</td>
<tr>
<td>Init Path</td>
<td>{{ info.InitPath }}</td>
</tr>
<tr>
<td>Docker Root Directory</td>
<td>{{ info.DockerRootDir }}</td>
</tr>
<tr>
<td>Init SHA1</td>
<td>{{ info.InitSha1 }}</td>
</tr>
<tr>
<td>Memory Limit</td>
<td>{{ info.MemoryLimit }}</td>
</tr>
<tr>
<td>Swap Limit</td>
<td>{{ info.SwapLimit }}</td>
</tr>
</tbody>
</table>
+15 -20
View File
@@ -1,24 +1,19 @@
angular.module('docker', [])
.controller('DockerController', ['$scope', 'Info', 'Version', 'Messages',
function ($scope, Info, Version, Messages) {
$scope.state = {
loaded: false
};
$scope.info = {};
$scope.version = {};
.controller('DockerController', ['$scope', 'Info', 'Version', 'Settings',
function ($scope, Info, Version, Settings) {
Info.get({}, function (infoData) {
$scope.info = infoData;
Version.get({}, function (versionData) {
$scope.version = versionData;
$scope.state.loaded = true;
$('#loadingViewSpinner').hide();
}, function (e) {
Messages.error("Failure", e, 'Unable to retrieve engine details');
$('#loadingViewSpinner').hide();
});
}, function (e) {
Messages.error("Failure", e, 'Unable to retrieve engine information');
$('#loadingViewSpinner').hide();
$scope.info = {};
$scope.docker = {};
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
Version.get({}, function (d) {
$scope.docker = d;
});
Info.get({}, function (d) {
$scope.info = d;
});
}]);
+1 -4
View File
@@ -49,16 +49,13 @@
</tr>
</thead>
<tbody>
<tr dir-paginate="event in (events | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count)">
<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 ng-if="events" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
<rd-widget>
+1 -2
View File
@@ -4,7 +4,6 @@ function ($scope, Settings, Messages, Events) {
$scope.state = {};
$scope.sortType = 'Time';
$scope.sortReverse = true;
$scope.pagination_count = Settings.pagination_count;
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
@@ -22,7 +21,7 @@ function ($scope, Settings, Messages, Events) {
$('#loadEventsSpinner').hide();
},
function (e) {
Messages.error("Unable to load events", e.data);
$('#loadEventsSpinner').hide();
Messages.error("Failure", e, "Unable to load events");
});
}]);
+11 -8
View File
@@ -3,7 +3,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="images">Images</a> > <a ui-sref="image({id: image.Id})">{{ image.Id }}</a>
Images > <a ui-sref="image({id: image.Id})">{{ image.Id }}</a>
</rd-header-content>
</rd-header>
@@ -13,7 +13,7 @@
<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 space-right">
<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>
@@ -48,7 +48,10 @@
</div>
<label for="image_registry" class="col-sm-1 control-label text-left">Registry</label>
<div class="col-sm-3">
<input type="text" class="form-control" ng-model="config.Registry" id="image_registry" placeholder="optional">
<select class="selectpicker form-control" ng-model="config.Registry">
<option value="">Docker Hub</option>
<option ng-repeat="registry in formValues.AvailableRegistries" ng-value="registry.value">{{ registry.name }}</option>
</select>
</div>
</div>
<!-- !name-and-registry-inputs -->
@@ -82,7 +85,7 @@
<td>ID</td>
<td>
{{ image.Id }}
<button class="btn btn-xs btn-danger" ng-click="removeImage(image.Id)"><i class="fa fa-trash space-right" aria-hidden="true"></i>Delete this image</button>
<button class="btn btn-xs btn-danger" ng-click="removeImage(image.Id)">Delete this image</button>
</td>
</tr>
<tr ng-if="image.Parent">
@@ -130,7 +133,7 @@
<tr ng-if="image.ContainerConfig.ExposedPorts">
<td>EXPOSE</td>
<td>
<span class="label label-default space-right" ng-repeat="port in exposedPorts">
<span class="label label-default tag" ng-repeat="port in exposedPorts">
{{ port }}
</span>
</td>
@@ -138,7 +141,7 @@
<tr ng-if="image.ContainerConfig.Volumes">
<td>VOLUME</td>
<td>
<span class="label label-default space-right" ng-repeat="volume in volumes">
<span class="label label-default tag" ng-repeat="volume in volumes">
{{ volume }}
</span>
</td>
@@ -148,8 +151,8 @@
<td>
<table class="table table-bordered table-condensed">
<tr ng-repeat="var in image.ContainerConfig.Env">
<td>{{ var|key: '=' }}</td>
<td>{{ var|value: '=' }}</td>
<td>{{ var|key }}</td>
<td>{{ var|value }}</td>
</tr>
</table>
</td>
+47 -21
View File
@@ -1,12 +1,17 @@
angular.module('image', [])
.controller('ImageController', ['$scope', '$stateParams', '$state', 'Image', 'ImageHelper', 'Messages',
function ($scope, $stateParams, $state, Image, ImageHelper, Messages) {
.controller('ImageController', ['$scope', '$stateParams', '$state', 'Config', 'Image', 'Messages',
function ($scope, $stateParams, $state, Config, Image, Messages) {
$scope.RepoTags = [];
$scope.config = {
Image: '',
Registry: ''
};
$scope.formValues = {
AvailableRegistries: []
};
// Get RepoTags from the /images/query endpoint instead of /image/json,
// for backwards compatibility with Docker API versions older than 1.21
function getRepoTags(imageId) {
@@ -19,18 +24,31 @@ function ($scope, $stateParams, $state, Image, ImageHelper, Messages) {
});
}
function createImageConfig(imageName, registry) {
var imageNameAndTag = imageName.split(':');
var image = imageNameAndTag[0];
if (registry) {
image = registry + '/' + imageNameAndTag[0];
}
var imageConfig = {
repo: image,
tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest'
};
return imageConfig;
}
$scope.tagImage = function() {
$('#loadingViewSpinner').show();
var image = _.toLower($scope.config.Image);
var registry = _.toLower($scope.config.Registry);
var imageConfig = ImageHelper.createImageConfig(image, registry);
var imageConfig = createImageConfig(image, registry);
Image.tag({id: $stateParams.id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) {
Messages.send('Image successfully tagged');
$('#loadingViewSpinner').hide();
$state.go('image', {id: $stateParams.id}, {reload: true});
}, function(e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to tag image");
Messages.error("Unable to tag image", e.data);
});
};
@@ -38,14 +56,14 @@ function ($scope, $stateParams, $state, Image, ImageHelper, Messages) {
$('#loadingViewSpinner').show();
Image.push({tag: tag}, function (d) {
if (d[d.length-1].error) {
Messages.error("Unable to push image", {}, d[d.length-1].error);
Messages.error("Unable to push image", d[d.length-1].error);
} else {
Messages.send('Image successfully pushed');
}
$('#loadingViewSpinner').hide();
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to push image");
Messages.error("Unable to push image", e.data);
});
};
@@ -54,7 +72,7 @@ function ($scope, $stateParams, $state, Image, ImageHelper, Messages) {
Image.remove({id: id}, function (d) {
if (d[0].message) {
$('#loadingViewSpinner').hide();
Messages.error("Unable to remove image", {}, d[0].message);
Messages.error("Unable to remove image", d[0].message);
} else {
// If last message key is 'Deleted' or if it's 'Untagged' and there is only one tag associated to the image
// then assume the image is gone and send to images page
@@ -68,22 +86,30 @@ function ($scope, $stateParams, $state, Image, ImageHelper, Messages) {
}
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, 'Unable to remove image');
Messages.error("Unable to remove image", e.data);
});
};
$('#loadingViewSpinner').show();
Image.get({id: $stateParams.id}, function (d) {
$scope.image = d;
if (d.RepoTags) {
$scope.RepoTags = d.RepoTags;
} else {
getRepoTags(d.Id);
}
$('#loadingViewSpinner').hide();
$scope.exposedPorts = d.ContainerConfig.ExposedPorts ? Object.keys(d.ContainerConfig.ExposedPorts) : [];
$scope.volumes = d.ContainerConfig.Volumes ? Object.keys(d.ContainerConfig.Volumes) : [];
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve image info");
Config.$promise.then(function (c) {
$scope.formValues.AvailableRegistries = c.registries;
$('#loadingViewSpinner').show();
Image.get({id: $stateParams.id}, function (d) {
$scope.image = d;
if (d.RepoTags) {
$scope.RepoTags = d.RepoTags;
} else {
getRepoTags(d.Id);
}
$('#loadingViewSpinner').hide();
$scope.exposedPorts = d.ContainerConfig.ExposedPorts ? Object.keys(d.ContainerConfig.ExposedPorts) : [];
$scope.volumes = d.ContainerConfig.Volumes ? Object.keys(d.ContainerConfig.Volumes) : [];
}, function (e) {
if (e.status === 404) {
Messages.error("Unable to find image", $stateParams.id);
} else {
Messages.error("Unable to retrieve image info", e.data);
}
});
});
}]);
+9 -23
View File
@@ -22,7 +22,10 @@
</div>
<label for="image_registry" class="col-sm-1 control-label text-left">Registry</label>
<div class="col-sm-3">
<input type="text" class="form-control" ng-model="config.Registry" id="image_registry" placeholder="leave empty to use DockerHub">
<select class="selectpicker form-control" ng-model="config.Registry">
<option value="">Docker Hub</option>
<option ng-repeat="registry in availableRegistries" ng-value="registry.value">{{ registry.name }}</option>
</select>
</div>
</div>
<!-- !name-and-registry-inputs -->
@@ -55,7 +58,7 @@
</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 space-right" aria-hidden="true"></i>Remove</button>
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount">Remove</button>
</div>
<div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
@@ -88,38 +91,21 @@
<span ng-show="sortType == 'VirtualSize' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="images" ng-click="order('Created')">
Created
<span ng-show="sortType == 'Created' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Created' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="image in (state.filteredImages = (images | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
<tr ng-repeat="image in (state.filteredImages = (images | filter:state.filter | orderBy:sortType:sortReverse))">
<td><input type="checkbox" ng-model="image.Checked" ng-change="selectItem(image)" /></td>
<td><a ui-sref="image({id: image.Id})">{{ image.Id|truncate:20}}</a></td>
<td>
<span class="label label-primary image-tag" ng-repeat="tag in (image|repotags)">{{ tag }}</span>
</td>
<td>{{ image.VirtualSize|humansize }}</td>
<td>{{ image.Created|getisodatefromtimestamp }}</td>
</tr>
<tr ng-if="!images">
<td colspan="5" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="images.length == 0">
<td colspan="5" class="text-center text-muted">No images available.</td>
</tr>
</tbody>
</table>
<div ng-if="images" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
<rd-widget>
</div>
</div>
<rd-widget>
</div>
</div>
+11 -11
View File
@@ -1,11 +1,10 @@
angular.module('images', [])
.controller('ImagesController', ['$scope', '$state', 'Config', 'Image', 'Messages', 'Settings',
function ($scope, $state, Config, Image, Messages, Settings) {
.controller('ImagesController', ['$scope', '$state', 'Config', 'Image', 'Messages',
function ($scope, $state, Config, Image, Messages) {
$scope.state = {};
$scope.sortType = 'RepoTags';
$scope.sortReverse = true;
$scope.state.selectedItemCount = 0;
$scope.pagination_count = Settings.pagination_count;
$scope.config = {
Image: '',
@@ -41,21 +40,21 @@ function ($scope, $state, Config, Image, Messages, Settings) {
$scope.pullImage = function() {
$('#pullImageSpinner').show();
var image = _.toLower($scope.config.Image);
var registry = _.toLower($scope.config.Registry);
var registry = $scope.config.Registry;
var imageConfig = createImageConfig(image, registry);
Image.create(imageConfig, function (data) {
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
if (err) {
var detail = data[data.length - 1];
$('#pullImageSpinner').hide();
Messages.error('Error', {}, detail.error);
Messages.error('Error', detail.error);
} else {
$('#pullImageSpinner').hide();
$state.go('images', {}, {reload: true});
}
}, function (e) {
$('#pullImageSpinner').hide();
Messages.error("Failure", e, "Unable to pull image");
Messages.error('Error', 'Unable to pull image ' + image);
});
};
@@ -73,8 +72,8 @@ function ($scope, $state, Config, Image, Messages, Settings) {
counter = counter + 1;
Image.remove({id: i.Id}, function (d) {
if (d[0].message) {
$('#loadImagesSpinner').hide();
Messages.error("Unable to remove image", {}, d[0].message);
$('#loadingViewSpinner').hide();
Messages.error("Unable to remove image", d[0].message);
} else {
Messages.send("Image deleted", i.Id);
var index = $scope.images.indexOf(i);
@@ -82,7 +81,7 @@ function ($scope, $state, Config, Image, Messages, Settings) {
}
complete();
}, function (e) {
Messages.error("Failure", e, 'Unable to remove image');
Messages.error("Unable to remove image", e.data);
complete();
});
}
@@ -96,13 +95,14 @@ function ($scope, $state, Config, Image, Messages, Settings) {
});
$('#loadImagesSpinner').hide();
}, function (e) {
Messages.error("Failure", e.data);
$('#loadImagesSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve images");
$scope.images = [];
});
}
Config.$promise.then(function (c) {
$scope.availableRegistries = c.registries;
fetchImages();
});
}]);
+97 -35
View File
@@ -3,61 +3,123 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="networks">Networks</a> > <a ui-sref="network({id: network.Id})">{{ network.Name }}</a>
Networks > <a ui-sref="network({id: network.Id})">{{ network.Name }}</a>
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<div class="col-lg-9 col-md-9 col-xs-9">
<rd-widget>
<rd-widget-body>
<div class="widget-icon grey pull-left">
<i class="fa fa-sitemap"></i>
</div>
<div class="title">{{ network.Name }}</div>
<div class="comment">Name</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-3 col-md-3 col-xs-3">
<rd-widget>
<rd-widget-body>
<div class="widget-icon grey pull-left">
<i class="fa fa-cogs"></i>
</div>
<div class="title">
<div class="btn-group" role="group" aria-label="...">
<button class="btn btn-default" disabled>Connect container...</button>
<button class="btn btn-danger" ng-click="remove(id)">Remove</button>
</div>
</div>
<div class="comment">
Actions
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<rd-widget>
<rd-widget-header icon="fa-sitemap" title="Network details"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Name</td>
<td>{{ network.Name }}</td>
<td>Id</td>
<td>{{ network.Id }}</td>
</tr>
<tr>
<td>ID</td>
<td>
{{ network.Id }}
<button class="btn btn-xs btn-danger" ng-click="removeNetwork(network.Id)"><i class="fa fa-trash space-right" aria-hidden="true"></i>Delete this network</button>
</td>
<td>Scope</td>
<td>{{ network.Scope }}</td>
</tr>
<tr>
<td>Driver</td>
<td>{{ network.Driver }}</td>
</tr>
<tr>
<td>Scope</td>
<td>{{ network.Scope }}</td>
<td>IPAM</td>
<td>
<table class="table table-striped">
<tr>
<td>Driver</td>
<td>{{ network.IPAM.Driver }}</td>
</tr>
<tr>
<td>Subnet</td>
<td>{{ network.IPAM.Config[0].Subnet }}</td>
</tr>
<tr>
<td>Gateway</td>
<td>{{ network.IPAM.Config[0].Gateway }}</td>
</tr>
</table>
</td>
</tr>
<tr ng-if="network.IPAM.Config[0].Subnet">
<td>Subnet</td>
<td>{{ network.IPAM.Config[0].Subnet }}</td>
<tr>
<td>Containers</td>
<td>
<table class="table table-striped" ng-repeat="(Id, container) in network.Containers">
<tr>
<td>Id</td>
<td><a ui-sref="container({id: Id})">{{ Id }}</a></td>
</tr>
<tr>
<td>EndpointID</td>
<td>{{ container.EndpointID}}</td>
</tr>
<tr>
<td>MacAddress</td>
<td>{{ container.MacAddress}}</td>
</tr>
<tr>
<td>IPv4Address</td>
<td>{{ container.IPv4Address}}</td>
</tr>
<tr>
<td>IPv6Address</td>
<td>{{ container.IPv6Address}}</td>
</tr>
<tr>
<td colspan="2">
<button ng-click="disconnect(network.Id, Id)" class="btn btn-danger">
Disconnect from network
</button>
</td>
</tr>
</table>
</td>
</tr>
<tr 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>
<td>Options</td>
<td>
<table role="table" class="table table-striped">
<tr ng-repeat="(k, v) in network.Options">
<td>{{ k }}</td>
<td>{{ v }}</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
+21 -14
View File
@@ -1,21 +1,28 @@
angular.module('network', [])
.controller('NetworkController', ['$scope', '$state', '$stateParams', 'Network', 'Messages',
function ($scope, $state, $stateParams, Network, Messages) {
.controller('NetworkController', ['$scope', 'Network', 'Messages', '$state', '$stateParams', 'errorMsgFilter',
function ($scope, Network, Messages, $state, $stateParams, errorMsgFilter) {
$scope.removeNetwork = function removeNetwork(networkId) {
$scope.disconnect = function disconnect(networkId, containerId) {
$('#loadingViewSpinner').show();
Network.remove({id: $stateParams.id}, function (d) {
if (d.message) {
$('#loadingViewSpinner').hide();
Messages.send("Error", {}, d.message);
} else {
$('#loadingViewSpinner').hide();
Messages.send("Network removed", $stateParams.id);
$state.go('networks', {});
}
Network.disconnect({id: $stateParams.id}, {Container: containerId}, function (d) {
$('#loadingViewSpinner').hide();
Messages.send("Container disconnected", containerId);
$state.go('network', {id: $stateParams.id}, {reload: true});
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to remove network");
Messages.error("Failure", e.data);
});
};
$scope.remove = function remove(networkId) {
$('#loadingViewSpinner').show();
Network.remove({id: $stateParams.id}, function (d) {
$('#loadingViewSpinner').hide();
Messages.send("Network removed", "");
$state.go('networks', {});
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e.data);
});
};
@@ -24,7 +31,7 @@ function ($scope, $state, $stateParams, Network, Messages) {
$scope.network = d;
$('#loadingViewSpinner').hide();
}, function (e) {
Messages.error("Failure", e.data);
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve network info");
});
}]);
+89 -106
View File
@@ -7,10 +7,11 @@
<rd-header-content>Networks</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-plus" title="Add a network">
<rd-widget-header icon="fa-plus" title="Add a shared network">
</rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
@@ -22,22 +23,39 @@
</div>
</div>
<!-- !name-input -->
<!-- advanced-settings-input -->
<div class="form-group">
<div class="col-sm-12">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="state.advancedSettings"> Show advanced settings
</label>
</div>
</div>
</div>
<!-- !advanced-settings-input -->
<!-- subnet-gateway-inputs -->
<div class="form-group" ng-if="state.advancedSettings">
<label for="network_subnet" class="col-sm-1 control-label text-left">Subnet</label>
<div class="col-sm-5">
<input type="text" class="form-control" ng-model="formValues.Subnet" id="network_subnet" placeholder="e.g. 172.20.0.0/16">
</div>
<label for="network_gateway" class="col-sm-1 control-label text-left">Gateway</label>
<div class="col-sm-5">
<input type="text" class="form-control" ng-model="formValues.Gateway" id="network_gateway" placeholder="e.g. 172.20.10.11">
</div>
</div>
<!-- !subnet-gateway-inputs -->
<!-- tag-note -->
<div class="form-group" ng-if="swarm">
<div class="col-sm-12">
<span class="small text-muted">Note: The network will be created using the overlay driver and will allow containers to communicate across the hosts of your cluster.</span>
</div>
</div>
<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>
@@ -47,103 +65,68 @@
</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 space-right" 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 dir-paginate="network in ( state.filteredNetworks = (networks | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
<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">
<td colspan="8" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="networks.length == 0">
<td colspan="8" class="text-center text-muted">No networks available.</td>
</tr>
</tbody>
</table>
<div ng-if="networks" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
<rd-widget>
</div>
<div class="col-lg-12">
<rd-widget>
<rd-widget-header icon="fa-sitemap" title="Networks">
<div class="pull-right">
<i id="loadNetworksSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-lg-12">
<div class="pull-left">
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount">Remove</button>
</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('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('IPAM.Config[0].Subnet')">
Subnet
<span ng-show="sortType == 'IPAM.Config[0].Subnet' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'IPAM.Config[0].Subnet' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="networks" ng-click="order('IPAM.Config[0].Gateway')">
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.Scope }}</td>
<td>{{ network.IPAM.Config[0].Subnet ? network.IPAM.Config[0].Subnet : '-' }}</td>
<td>{{ network.IPAM.Config[0].Gateway ? network.IPAM.Config[0].Gateway : '-' }}</td>
</tr>
</tbody>
</table>
</div>
</rd-widget-body>
<rd-widget>
</div>
+55 -40
View File
@@ -1,45 +1,22 @@
angular.module('networks', [])
.controller('NetworksController', ['$scope', '$state', 'Network', 'Config', 'Messages', 'Settings',
function ($scope, $state, Network, Config, Messages, Settings) {
.controller('NetworksController', ['$scope', '$state', 'Network', 'Config', 'Messages', 'errorMsgFilter',
function ($scope, $state, Network, Config, Messages, errorMsgFilter) {
$scope.state = {};
$scope.state.selectedItemCount = 0;
$scope.state.advancedSettings = false;
$scope.sortType = 'Name';
$scope.sortType = 'Scope';
$scope.sortReverse = false;
$scope.pagination_count = Settings.pagination_count;
$scope.config = {
Name: ''
$scope.formValues = {
Subnet: '',
Gateway: ''
};
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'
};
$scope.config = {
Name: '',
IPAM: {
Config: []
}
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) {
@@ -55,6 +32,44 @@ function ($scope, $state, Network, Config, Messages, Settings) {
}
};
function prepareIPAMConfiguration(config) {
if ($scope.formValues.Subnet) {
var ipamConfig = {};
ipamConfig.Subnet = $scope.formValues.Subnet;
if ($scope.formValues.Gateway) {
ipamConfig.Gateway = $scope.formValues.Gateway ;
}
config.IPAM.Config.push(ipamConfig);
}
}
function prepareNetworkConfiguration() {
var config = angular.copy($scope.config);
prepareIPAMConfiguration(config);
if ($scope.swarm) {
config.Driver = 'overlay';
}
return config;
}
$scope.createNetwork = function() {
$('#createNetworkSpinner').show();
var config = prepareNetworkConfiguration();
Network.create(config, function (d) {
if (d.Id) {
Messages.send("Network created", d.Id);
$('#createNetworkSpinner').hide();
$state.go('networks', {}, {reload: true});
} else {
$('#createNetworkSpinner').hide();
Messages.error('Unable to create network', errorMsgFilter(d));
}
}, function (e) {
$('#createNetworkSpinner').hide();
Messages.error('Unable to create network', e.data);
});
};
$scope.removeAction = function () {
$('#loadNetworksSpinner').show();
var counter = 0;
@@ -68,16 +83,17 @@ function ($scope, $state, Network, Config, Messages, Settings) {
if (network.Checked) {
counter = counter + 1;
Network.remove({id: network.Id}, function (d) {
if (d.message) {
Messages.send("Error", d.message);
var error = errorMsgFilter(d);
if (error) {
Messages.send("Error", "Unable to remove network with active endpoints");
} else {
Messages.send("Network removed", network.Id);
Messages.send("Network deleted", network.Id);
var index = $scope.networks.indexOf(network);
$scope.networks.splice(index, 1);
}
complete();
}, function (e) {
Messages.error("Failure", e, 'Unable to remove network');
Messages.error("Failure", e.data);
complete();
});
}
@@ -90,9 +106,8 @@ function ($scope, $state, Network, Config, Messages, Settings) {
$scope.networks = d;
$('#loadNetworksSpinner').hide();
}, function (e) {
Messages.error("Failure", e.data);
$('#loadNetworksSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve networks");
$scope.networks = [];
});
}
-239
View File
@@ -1,239 +0,0 @@
<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>
<a ui-sref="services">Services</a> > <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 space-right" 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.EditReplicas">
{{ service.Replicas }}
<a class="interactive" ng-click="service.EditReplicas = true;"><i class="fa fa-arrows-v" aria-hidden="true"></i> Scale</a>
</span>
<span ng-if="service.Mode === 'replicated' && service.EditReplicas">
<input class="input-sm" type="number" ng-model="service.newServiceReplicas" />
<a class="interactive" ng-click="service.EditReplicas = 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 ng-if="!service.EditImage">
{{ service.Image }}
<a href="" data-toggle="tooltip" title="Edit service image" ng-click="service.EditImage = true;"><i class="fa fa-edit"></i></a>
</td>
<td ng-if="service.EditImage">
<input type="text" class="containerNameInput" ng-model="service.newServiceImage">
<a class="interactive" ng-click="service.EditImage = false;"><i class="fa fa-times"></i></a>
<a class="interactive" ng-click="changeServiceImage(service)"><i class="fa fa-check-square-o"></i></a>
</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.EnvironmentVariables">
<td>Environment variables</td>
<td>
<div class="form-group">
<div class="col-sm-11 nopadding">
<span class="label label-default interactive fit-text-size" ng-click="addEnvironmentVariable(service)">
<i class="fa fa-plus-circle" aria-hidden="true"></i> environment variable
</span>
</div>
<!-- environment-variable-input-list -->
<div class="col-sm-11 form-inline nopadding" style="margin-top: 10px;">
<div ng-repeat="var in service.EnvironmentVariables" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon fit-text-size">name</span>
<input type="text" class="form-control" ng-model="var.key" ng-disabled="var.added" placeholder="e.g. FOO">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon fit-text-size">value</span>
<input type="text" class="form-control" ng-model="var.value" ng-change="updateEnvironmentVariable(service, var)" placeholder="e.g. bar">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeEnvironmentVariable(service, $index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !environment-variable-input-list -->
</div>
</td>
</tr>
<tr>
<td>Labels</td>
<td>
<div class="form-group">
<div class="col-sm-11 nopadding">
<span class="label label-default interactive fit-text-size" ng-click="addLabel(service)">
<i class="fa fa-plus-circle" aria-hidden="true"></i> label
</span>
</div>
<!-- labels-input-list -->
<div class="col-sm-11 form-inline nopadding" style="margin-top: 10px;">
<div ng-repeat="label in service.ServiceLabels" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon fit-text-size">name</span>
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo" ng-change="updateLabel(service, label)">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon fit-text-size">value</span>
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar" ng-change="updateLabel(service, label)">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeLabel(service, $index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !labels-input-list -->
</div>
</td>
</tr>
<tr>
<td>Container labels</td>
<td>
<div class="form-group">
<div class="col-sm-11 nopadding">
<span class="label label-default interactive fit-text-size" ng-click="addContainerLabel(service)">
<i class="fa fa-plus-circle" aria-hidden="true"></i> container label
</span>
</div>
<!-- labels-input-list -->
<div class="col-sm-11 form-inline nopadding" style="margin-top: 10px;">
<div ng-repeat="label in service.ServiceContainerLabels" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon fit-text-size">name</span>
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo" ng-change="updateLabel(service, label)">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon fit-text-size">value</span>
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar" ng-change="updateLabel(service, label)">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeContainerLabel(service, $index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !labels-input-list -->
</div>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
<rd-widget-footer ng-if="service.hasChanges">
<div>
<button type="button" class="btn btn-primary" ng-click="updateService(service)">Save changes</button>
<button type="button" class="btn btn-default" ng-click="cancelChanges(service)">Reset</button>
</div>
</rd-widget-footer>
</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('Updated')">
Last update
<span ng-show="sortType == 'Updated' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Updated' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="task in (filteredTasks = ( tasks | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
<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>
<div ng-if="tasks" class="pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
-209
View File
@@ -1,209 +0,0 @@
angular.module('service', [])
.controller('ServiceController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Task', 'Node', 'Messages', 'Settings',
function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Messages, Settings) {
$scope.service = {};
$scope.tasks = [];
$scope.displayNode = false;
$scope.sortType = 'Status';
$scope.sortReverse = false;
$scope.pagination_count = Settings.pagination_count;
var previousServiceValues = {};
$scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.renameService = function renameService(service) {
updateServiceAttribute(service, 'Name', service.newServiceName || service.name);
service.EditName = false;
};
$scope.changeServiceImage = function changeServiceImage(service) {
updateServiceAttribute(service, 'Image', service.newServiceImage || service.image);
service.EditImage = false;
};
$scope.scaleService = function scaleService(service) {
updateServiceAttribute(service, 'Replicas', service.newServiceReplicas || service.Replicas);
service.EditReplicas = false;
};
$scope.addEnvironmentVariable = function addEnvironmentVariable(service) {
service.EnvironmentVariables.push({ key: '', value: '', originalValue: '' });
service.hasChanges = true;
};
$scope.removeEnvironmentVariable = function removeEnvironmentVariable(service, index) {
var removedElement = service.EnvironmentVariables.splice(index, 1);
service.hasChanges = service.hasChanges || removedElement !== null;
};
$scope.updateEnvironmentVariable = function updateEnvironmentVariable(service, variable) {
service.hasChanges = service.hasChanges || variable.value !== variable.originalValue;
};
$scope.addLabel = function addLabel(service) {
service.hasChanges = true;
service.ServiceLabels.push({ key: '', value: '', originalValue: '' });
};
$scope.removeLabel = function removeLabel(service, index) {
var removedElement = service.ServiceLabels.splice(index, 1);
service.hasChanges = service.hasChanges || removedElement !== null;
};
$scope.updateLabel = function updateLabel(service, label) {
service.hasChanges = service.hasChanges || label.value !== label.originalValue;
};
$scope.addContainerLabel = function addContainerLabel(service) {
service.hasChanges = true;
service.ServiceContainerLabels.push({ key: '', value: '', originalValue: '' });
};
$scope.removeContainerLabel = function removeContainerLabel(service, index) {
var removedElement = service.ServiceContainerLabels.splice(index, 1);
service.hasChanges = service.hasChanges || removedElement !== null;
};
$scope.cancelChanges = function changeServiceImage(service) {
Object.keys(previousServiceValues).forEach(function(attribute) {
service[attribute] = previousServiceValues[attribute]; // reset service values
service['newService' + attribute] = previousServiceValues[attribute]; // reset edit fields
});
previousServiceValues = {}; // clear out all changes
// clear out environment variable changes
service.EnvironmentVariables = translateEnvironmentVariables(service.Env);
service.ServiceLabels = translateLabelsToServiceLabels(service.Labels);
service.ServiceContainerLabels = translateLabelsToServiceLabels(service.ContainerLabels);
service.hasChanges = false;
};
$scope.updateService = function updateService(service) {
$('#loadServicesSpinner').show();
var config = ServiceHelper.serviceToConfig(service.Model);
config.Name = service.newServiceName;
config.Labels = translateServiceLabelsToLabels(service.ServiceLabels);
config.TaskTemplate.ContainerSpec.Env = translateEnvironmentVariablesToEnv(service.EnvironmentVariables);
config.TaskTemplate.ContainerSpec.Labels = translateServiceLabelsToLabels(service.ServiceContainerLabels);
config.TaskTemplate.ContainerSpec.Image = service.newServiceImage;
if (service.Mode === 'replicated') {
config.Mode.Replicated.Replicas = service.Replicas;
}
Service.update({ id: service.Id, version: service.Version }, config, function (data) {
$('#loadServicesSpinner').hide();
Messages.send("Service successfully updated", "Service updated");
$state.go('service', {id: service.Id}, {reload: true});
}, function (e) {
$('#loadServicesSpinner').hide();
Messages.error("Failure", e, "Unable to update 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;
service.newServiceImage = service.Image;
service.newServiceReplicas = service.Replicas;
service.EnvironmentVariables = translateEnvironmentVariables(service.Env);
service.ServiceLabels = translateLabelsToServiceLabels(service.Labels);
service.ServiceContainerLabels = translateLabelsToServiceLabels(service.ContainerLabels);
$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");
});
}
function updateServiceAttribute(service, name, newValue) {
// ensure we only capture the original previous value, in case we update the attribute multiple times
if (!previousServiceValues[name]) {
previousServiceValues[name] = service[name];
}
// update the attribute
service[name] = newValue;
service.hasChanges = true;
}
function translateEnvironmentVariables(env) {
if (env) {
var variables = [];
env.forEach(function(variable) {
var idx = variable.indexOf('=');
var keyValue = [variable.slice(0,idx), variable.slice(idx+1)];
var originalValue = (keyValue.length > 1) ? keyValue[1] : '';
variables.push({ key: keyValue[0], value: originalValue, originalValue: originalValue, added: true});
});
return variables;
}
return [];
}
function translateEnvironmentVariablesToEnv(env) {
if (env) {
var variables = [];
env.forEach(function(variable) {
if (variable.key && variable.key !== '' && variable.value && variable.value !== '') {
variables.push(variable.key + '=' + variable.value);
}
});
return variables;
}
return [];
}
function translateLabelsToServiceLabels(Labels) {
var labels = [];
if (Labels) {
Object.keys(Labels).forEach(function(key) {
labels.push({ key: key, value: Labels[key], originalValue: Labels[key], added: true});
});
}
return labels;
}
function translateServiceLabelsToLabels(labels) {
var Labels = {};
if (labels) {
labels.forEach(function(label) {
Labels[label.key] = label.value;
});
}
return Labels;
}
fetchServiceDetails();
}]);
-87
View File
@@ -1,87 +0,0 @@
<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 space-right" 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 dir-paginate="service in (state.filteredServices = ( services | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
<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>
<tr ng-if="!services">
<td colspan="4" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="services.length == 0">
<td colspan="4" class="text-center text-muted">No services available.</td>
</tr>
</tbody>
</table>
<div ng-if="services" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
<rd-widget>
</div>
</div>
@@ -1,84 +0,0 @@
angular.module('services', [])
.controller('ServicesController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Messages', 'Settings',
function ($scope, $stateParams, $state, Service, ServiceHelper, Messages, Settings) {
$scope.state = {};
$scope.state.selectedItemCount = 0;
$scope.sortType = 'Name';
$scope.sortReverse = false;
$scope.pagination_count = Settings.pagination_count;
$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");
$scope.services = [];
});
}
fetchServices();
}]);
+4 -13
View File
@@ -1,7 +1,7 @@
<rd-header>
<rd-header-title title="Container stats"></rd-header-title>
<rd-header-content>
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Stats
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Stats
</rd-header-content>
</rd-header>
@@ -10,7 +10,7 @@
<rd-widget>
<rd-widget-body>
<div class="widget-icon grey pull-left">
<i class="fa fa-server"></i>
<i class="fa fa-tasks"></i>
</div>
<div class="title">{{ container.Name|trimcontainername }}</div>
<div class="comment">
@@ -59,24 +59,15 @@
<table class="table table-striped">
<thead>
<tr>
<th ng-repeat="title in containerTop.Titles">
<a ui-sref="stats({id: container.Id})" ng-click="order(title)">
{{title}}
<span ng-show="sortType == title && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == title && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-repeat="title in containerTop.Titles">{{title}}</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="processInfos in state.filteredProcesses = (containerTop.Processes | orderBy:sortType:sortReverse | itemsPerPage: pagination_count)">
<tr ng-repeat="processInfos in containerTop.Processes">
<td ng-repeat="processInfo in processInfos track by $index">{{processInfo}}</td>
</tr>
</tbody>
</table>
<div ng-if="containerTop.Processes" class="pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</rd-widget-body>
</rd-widget>
</div>
+5 -13
View File
@@ -4,14 +4,6 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
// TODO: Force scale to 0-100 for cpu, fix charts on dashboard,
// TODO: Force memory scale to 0 - max memory
$scope.ps_args = '';
$scope.state = {};
$scope.sortType = 'CMD';
$scope.sortReverse = false;
$scope.pagination_count = Settings.pagination_count;
$scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.getTop = function () {
ContainerTop.get($stateParams.id, {
ps_args: $scope.ps_args
@@ -95,7 +87,7 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
},
{
scaleLabel: function (valueObj) {
return humansizeFilter(parseInt(valueObj.value, 10), 2);
return humansizeFilter(parseInt(valueObj.value, 10));
},
responsive: true
//scaleOverride: true,
@@ -108,7 +100,7 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
datasets: [networkRxDataset, networkTxDataset]
}, {
scaleLabel: function (valueObj) {
return humansizeFilter(parseInt(valueObj.value, 10), 2);
return humansizeFilter(parseInt(valueObj.value, 10));
},
responsive: true
});
@@ -120,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;
}
@@ -131,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);
});
}
@@ -196,7 +188,7 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
Container.get({id: $stateParams.id}, function (d) {
$scope.container = d;
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve container info");
Messages.error("Failure", e.data);
});
$scope.getTop();
}]);
+11 -86
View File
@@ -16,14 +16,13 @@
<tbody>
<tr>
<td>Nodes</td>
<td ng-if="!swarm_mode">{{ swarm.Nodes }}</td>
<td ng-if="swarm_mode">{{ info.Swarm.Nodes }}</td>
<td>{{ swarm.Nodes }}</td>
</tr>
<tr ng-if="!swarm_mode">
<tr>
<td>Images</td>
<td>{{ info.Images }}</td>
</tr>
<tr ng-if="!swarm_mode">
<tr>
<td>Swarm version</td>
<td>{{ docker.Version|swarmversion }}</td>
</tr>
@@ -31,29 +30,27 @@
<td>Docker API version</td>
<td>{{ docker.ApiVersion }}</td>
</tr>
<tr ng-if="!swarm_mode">
<tr>
<td>Strategy</td>
<td>{{ swarm.Strategy }}</td>
</tr>
<tr>
<td>Total CPU</td>
<td ng-if="!swarm_mode">{{ info.NCPU }}</td>
<td ng-if="swarm_mode">{{ totalCPU }}</td>
<td>{{ info.NCPU }}</td>
</tr>
<tr>
<td>Total memory</td>
<td ng-if="!swarm_mode">{{ info.MemTotal|humansize: 2 }}</td>
<td ng-if="swarm_mode">{{ totalMemory|humansize: 2 }}</td>
<td>{{ info.MemTotal|humansize }}</td>
</tr>
<tr ng-if="!swarm_mode">
<tr>
<td>Operating system</td>
<td>{{ info.OperatingSystem }}</td>
</tr>
<tr ng-if="!swarm_mode">
<tr>
<td>Kernel version</td>
<td>{{ info.KernelVersion }}</td>
</tr>
<tr ng-if="!swarm_mode">
<tr>
<td>Go version</td>
<td>{{ docker.GoVersion }}</td>
</tr>
@@ -63,9 +60,8 @@
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" ng-if="!swarm_mode">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-hdd-o" title="Node status"></rd-widget-header>
<rd-widget-body classes="no-padding">
@@ -117,7 +113,7 @@
</tr>
</thead>
<tbody>
<tr dir-paginate="node in (state.filteredNodes = (swarm.Status | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
<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>
@@ -127,77 +123,6 @@
</tr>
</tbody>
</table>
<div class="pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</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 dir-paginate="node in (state.filteredNodes = (nodes | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
<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>
<div class="pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</rd-widget-body>
</rd-widget>
</div>
+53 -74
View File
@@ -1,83 +1,62 @@
angular.module('swarm', [])
.controller('SwarmController', ['$scope', 'Info', 'Version', 'Node', 'Settings',
function ($scope, Info, Version, Node, Settings) {
.controller('SwarmController', ['$scope', 'Info', 'Version', 'Settings',
function ($scope, Info, Version, Settings) {
$scope.sortType = 'Name';
$scope.sortReverse = true;
$scope.info = {};
$scope.docker = {};
$scope.swarm = {};
$scope.swarm_mode = false;
$scope.totalCPU = 0;
$scope.totalMemory = 0;
$scope.pagination_count = Settings.pagination_count;
$scope.sortType = 'Name';
$scope.sortReverse = true;
$scope.info = {};
$scope.docker = {};
$scope.swarm = {};
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$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;
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;
Version.get({}, function (d) {
$scope.docker = d;
});
Info.get({}, function (d) {
$scope.info = d;
extractSwarmInfo(d);
});
} 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 nodes = systemStatus[0][1] === 'primary' ? systemStatus[3][1] : systemStatus[4][1];
var node_count = parseInt(nodes, 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 node_count = parseInt(systemStatus[3][1], 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 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 extractNodesInfo(info, node_count) {
// First information for node1 available at element #4 of SystemStatus
// The next 10 elements are information related to the node
var node_offset = 4;
for (i = 0; i < node_count; i++) {
extractNodeInfo(info, node_offset);
node_offset += 9;
}
}
function extractNodeInfo(info, offset) {
var node = {};
node.name = info[offset][0];
node.ip = info[offset][1];
node.id = info[offset + 1][1];
node.status = info[offset + 2][1];
node.containers = info[offset + 3][1];
node.cpu = info[offset + 4][1].split('/')[1];
node.memory = info[offset + 5][1].split('/')[1];
node.labels = info[offset + 6][1];
node.version = info[offset + 8][1];
$scope.swarm.Status.push(node);
}
}]);
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
View File
@@ -1,50 +0,0 @@
<rd-header>
<rd-header-title title="Task details">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="services">Services</a> > <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
View File
@@ -1,29 +0,0 @@
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();
}]);
-142
View File
@@ -1,142 +0,0 @@
<rd-header>
<rd-header-title title="Application templates list">
<a data-toggle="tooltip" title="Refresh" ui-sref="templates" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>Templates</rd-header-content>
</rd-header>
<div class="row" ng-if="state.selectedTemplate">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-custom-header icon="state.selectedTemplate.logo" title="state.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 state.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">
<a class="small interactive" ng-if="!state.showAdvancedOptions" ng-click="state.showAdvancedOptions = true;">
<i class="fa fa-plus space-right" aria-hidden="true"></i> Show advanced options
</a>
<a class="small interactive" ng-if="state.showAdvancedOptions" ng-click="state.showAdvancedOptions = false;">
<i class="fa fa-minus space-right" aria-hidden="true"></i> Hide advanced options
</a>
</div>
</div>
<div class="form-group" ng-if="state.showAdvancedOptions">
<label for="container_ports" class="col-sm-1 control-label text-left">Port mapping</label>
<div class="col-sm-11" style="margin-top: 5px;">
<span class="label label-default interactive" ng-click="addPortBinding()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> map additional 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.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>
<input type="text" class="form-control" ng-model="portBinding.containerPort" placeholder="e.g. 80">
</div>
<div class="input-group col-sm-1 input-group-sm">
<select class="selectpicker form-control" ng-model="portBinding.protocol">
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removePortBinding($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !port-mapping-input-list -->
</div>
<!-- !port-mapping -->
<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="state.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 dir-paginate="tpl in templates | itemsPerPage: pagination_count" class="container-template hvr-underline-from-center" id="template_{{ tpl.index }}" ng-click="selectTemplate(tpl.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" class="text-center text-muted">
Loading...
</div>
<div ng-if="templates.length == 0" class="text-center text-muted">
No templates available.
</div>
</div>
<div ng-if="templates">
<dir-pagination-controls></dir-pagination-controls>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
@@ -1,237 +0,0 @@
angular.module('templates', [])
.controller('TemplatesController', ['$scope', '$q', '$state', '$filter', 'Config', 'Info', 'Container', 'ContainerHelper', 'Image', 'Volume', 'Network', 'Templates', 'TemplateHelper', 'Messages', 'Settings',
function ($scope, $q, $state, $filter, Config, Info, Container, ContainerHelper, Image, Volume, Network, Templates, TemplateHelper, Messages, Settings) {
$scope.state = {
selectedTemplate: null,
showAdvancedOptions: false
};
$scope.formValues = {
network: "",
name: "",
ports: []
};
$scope.pagination_count = Settings.pagination_count;
var selectedItem = -1;
$scope.addPortBinding = function() {
$scope.formValues.ports.push({ hostPort: '', containerPort: '', protocol: 'tcp' });
};
$scope.removePortBinding = function(index) {
$scope.formValues.ports.splice(index, 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 preparePortBindings(config, ports) {
var bindings = {};
ports.forEach(function (portBinding) {
if (portBinding.containerPort) {
var key = portBinding.containerPort + "/" + portBinding.protocol;
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;
}
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);
}
});
}
preparePortBindings(containerConfig, $scope.formValues.ports);
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.state.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.state.selectedTemplate = null;
} else {
$('#template_' + selectedItem).toggleClass("container-template--selected");
selectedItem = id;
var selectedTemplate = $scope.templates[id];
$scope.state.selectedTemplate = selectedTemplate;
$scope.formValues.ports = selectedTemplate.ports ? TemplateHelper.getPortBindings(selectedTemplate.ports) : [];
}
};
function initTemplates() {
Templates.get(function (data) {
$scope.templates = data.map(function(tpl,index){
tpl.index = index;
return tpl;
});
$('#loadTemplatesSpinner').hide();
}, function (e) {
$('#loadTemplatesSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve apps list");
$scope.templates = [];
});
}
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();
});
}]);
+38 -22
View File
@@ -7,6 +7,40 @@
<rd-header-content>Volumes</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-plus" title="Add a persistent volume">
</rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- name-input -->
<div class="form-group">
<label for="volume_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="config.Name" id="volume_name" placeholder="e.g. mysql-data">
</div>
</div>
<!-- !name-input -->
<!-- tag-note -->
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">Note: The volume will be created in our persisted storage and will be available across all the hosts of your cluster.</span>
</div>
</div>
<!-- !tag-note -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-default btn-sm" ng-disabled="!config.Name" ng-click="createVolume()">Create</button>
<i id="createVolumeSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="col-lg-12">
<rd-widget>
<rd-widget-header icon="fa-cubes" title="Volumes">
@@ -16,8 +50,7 @@
</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 space-right" aria-hidden="true"></i>Remove</button>
<a class="btn btn-default" type="button" ui-sref="actions.create.volume">Add volume</a>
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount">Remove</button>
</div>
<div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
@@ -43,34 +76,17 @@
<span ng-show="sortType == 'Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="volumes" ng-click="order('Mountpoint')">
Mountpoint
<span ng-show="sortType == 'Mountpoint' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Mountpoint' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="volume in (state.filteredVolumes = (volumes | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
<tr ng-repeat="volume in (state.filteredVolumes = (volumes | filter:state.filter | orderBy:sortType:sortReverse))">
<td><input type="checkbox" ng-model="volume.Checked" ng-change="selectItem(volume)"/></td>
<td>{{ volume.Name|truncate:50 }}</td>
<td>{{ volume.Driver }}</td>
<td>{{ volume.Mountpoint }}</td>
</tr>
<tr ng-if="!volumes">
<td colspan="4" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="volumes.length == 0">
<td colspan="4" class="text-center text-muted">No volumes available.</td>
</tr>
</tbody>
</table>
<div ng-if="volumes" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
<rd-widget>
</div>
<rd-widget>
</div>
+36 -15
View File
@@ -1,14 +1,14 @@
angular.module('volumes', [])
.controller('VolumesController', ['$scope', '$state', 'Volume', 'Messages', 'Settings',
function ($scope, $state, Volume, Messages, Settings) {
.controller('VolumesController', ['$scope', '$state', 'Volume', 'Messages', 'errorMsgFilter',
function ($scope, $state, Volume, Messages, errorMsgFilter) {
$scope.state = {};
$scope.state.selectedItemCount = 0;
$scope.sortType = 'Name';
$scope.sortType = 'Driver';
$scope.sortReverse = true;
$scope.config = {
Name: ''
};
$scope.pagination_count = Settings.pagination_count;
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
@@ -23,6 +23,32 @@ function ($scope, $state, Volume, Messages, Settings) {
}
};
function prepareVolumeConfiguration() {
var config = angular.copy($scope.config);
config.Driver = 'local-persist';
config.DriverOpts = {};
config.DriverOpts.mountpoint = '/volume/' + config.Name;
return config;
}
$scope.createVolume = function() {
$('#createVolumeSpinner').show();
var config = prepareVolumeConfiguration();
Volume.create(config, function (d) {
if (d.Name) {
Messages.send("Volume created", d.Name);
$('#createVolumeSpinner').hide();
$state.go('volumes', {}, {reload: true});
} else {
$('#createVolumeSpinner').hide();
Messages.error('Unable to create volume', errorMsgFilter(d));
}
}, function (e) {
$('#createVolumeSpinner').hide();
Messages.error('Unable to create volume', e.data);
});
};
$scope.removeAction = function () {
$('#loadVolumesSpinner').show();
var counter = 0;
@@ -36,16 +62,12 @@ function ($scope, $state, Volume, Messages, Settings) {
if (volume.Checked) {
counter = counter + 1;
Volume.remove({name: volume.Name}, function (d) {
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);
}
Messages.send("Volume deleted", volume.Name);
var index = $scope.volumes.indexOf(volume);
$scope.volumes.splice(index, 1);
complete();
}, function (e) {
Messages.error("Failure", e, "Unable to remove volume");
Messages.error("Failure", e.data);
complete();
});
}
@@ -55,12 +77,11 @@ function ($scope, $state, Volume, Messages, Settings) {
function fetchVolumes() {
$('#loadVolumesSpinner').show();
Volume.query({}, function (d) {
$scope.volumes = d.Volumes || [];
$scope.volumes = _.uniqBy(d.Volumes, 'Name');
$('#loadVolumesSpinner').hide();
}, function (e) {
Messages.error("Failure", e.data);
$('#loadVolumesSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve volumes");
$scope.volumes = [];
});
}
fetchVolumes();
+1 -1
View File
@@ -1,5 +1,5 @@
angular
.module('portainer')
.module('uifordocker')
.directive('rdHeaderContent', function rdHeaderContent() {
var directive = {
requires: '^rdHeader',
+1 -1
View File
@@ -1,5 +1,5 @@
angular
.module('portainer')
.module('uifordocker')
.directive('rdHeaderTitle', function rdHeaderTitle() {
var directive = {
requires: '^rdHeader',
+1 -1
View File
@@ -1,5 +1,5 @@
angular
.module('portainer')
.module('uifordocker')
.directive('rdHeader', function rdHeader() {
var directive = {
scope: {
+1 -1
View File
@@ -1,5 +1,5 @@
angular
.module('portainer')
.module('uifordocker')
.directive('rdLoading', function rdLoading() {
var directive = {
restrict: 'AE',
+1 -1
View File
@@ -1,5 +1,5 @@
angular
.module('portainer')
.module('uifordocker')
.directive('rdWidgetBody', function rdWidgetBody() {
var directive = {
requires: '^rdWidget',
-15
View File
@@ -1,15 +0,0 @@
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="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 -1
View File
@@ -1,5 +1,5 @@
angular
.module('portainer')
.module('uifordocker')
.directive('rdWidgetFooter', function rdWidgetFooter() {
var directive = {
requires: '^rdWidget',
+1 -1
View File
@@ -1,5 +1,5 @@
angular
.module('portainer')
.module('uifordocker')
.directive('rdWidgetHeader', function rdWidgetTitle() {
var directive = {
requires: '^rdWidget',
+1 -1
View File
@@ -1,5 +1,5 @@
angular
.module('portainer')
.module('uifordocker')
.directive('rdWidgetTaskbar', function rdWidgetTaskbar() {
var directive = {
requires: '^rdWidget',
+1 -1
View File
@@ -1,5 +1,5 @@
angular
.module('portainer')
.module('uifordocker')
.directive('rdWidget', function rdWidget() {
var directive = {
scope: {
+21 -47
View File
@@ -1,4 +1,4 @@
angular.module('portainer.filters', [])
angular.module('uifordocker.filters', [])
.filter('truncate', function () {
'use strict';
return function (text, length, end) {
@@ -18,24 +18,6 @@ angular.module('portainer.filters', [])
}
};
})
.filter('taskstatusbadge', function () {
'use strict';
return function (text) {
var status = _.toLower(text);
if (status.indexOf('new') !== -1 || status.indexOf('allocated') !== -1 ||
status.indexOf('assigned') !== -1 || status.indexOf('accepted') !== -1) {
return 'info';
} else if (status.indexOf('pending') !== -1) {
return 'warning';
} else if (status.indexOf('shutdown') !== -1 || status.indexOf('failed') !== -1 ||
status.indexOf('rejected') !== -1) {
return 'danger';
} else if (status.indexOf('complete') !== -1) {
return 'primary';
}
return 'success';
};
})
.filter('containerstatusbadge', function () {
'use strict';
return function (text) {
@@ -123,13 +105,15 @@ angular.module('portainer.filters', [])
})
.filter('humansize', function () {
'use strict';
return function (bytes, round) {
if (!round) {
round = 1;
}
if (bytes || bytes === 0) {
return filesize(bytes, {base: 10, round: round});
return function (bytes) {
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
if (bytes === 0) {
return 'n/a';
}
var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)), 10);
var value = bytes / Math.pow(1024, i);
var decimalPlaces = (i < 1) ? 0 : (i - 1);
return value.toFixed(decimalPlaces) + ' ' + sizes[[i]];
};
})
.filter('containername', function () {
@@ -192,34 +176,24 @@ angular.module('portainer.filters', [])
})
.filter('key', function () {
'use strict';
return function (pair, separator) {
return pair.slice(0, pair.indexOf(separator));
return function (pair) {
return pair.slice(0, pair.indexOf('='));
};
})
.filter('value', function () {
'use strict';
return function (pair, separator) {
return pair.slice(pair.indexOf(separator) + 1);
return function (pair) {
return pair.slice(pair.indexOf('=') + 1);
};
})
.filter('emptyobject', function () {
'use strict';
return function (obj) {
return _.isEmpty(obj);
};
})
.filter('ipaddress', function () {
'use strict';
return function (ip) {
return ip.slice(0, ip.indexOf('/'));
};
})
.filter('arraytostr', function () {
'use strict';
return function (arr, separator) {
if (arr) {
return _.join(arr, separator);
.filter('errorMsg', function () {
return function (object) {
var idx = 0;
var msg = '';
while (object[idx] && typeof(object[idx]) === 'string') {
msg += object[idx];
idx++;
}
return '';
return msg;
};
});
-92
View File
@@ -1,92 +0,0 @@
angular.module('portainer.helpers', [])
.factory('ImageHelper', [function ImageHelperFactory() {
'use strict';
return {
createImageConfig: function(imageName, registry) {
var imageNameAndTag = imageName.split(':');
var image = imageNameAndTag[0];
if (registry) {
image = registry + '/' + imageNameAndTag[0];
}
var imageConfig = {
repo: image,
tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest'
};
return imageConfig;
}
};
}])
.factory('ContainerHelper', [function ContainerHelperFactory() {
'use strict';
return {
hideContainers: function(containers, containersToHideLabels) {
return containers.filter(function (container) {
var filterContainer = false;
containersToHideLabels.forEach(function(label, index) {
if (_.has(container.Labels, label.name) &&
container.Labels[label.name] === label.value) {
filterContainer = true;
}
});
if (!filterContainer) {
return container;
}
});
}
};
}])
.factory('ServiceHelper', [function ServiceHelperFactory() {
'use strict';
return {
serviceToConfig: function(service) {
return {
Name: service.Spec.Name,
Labels: service.Spec.Labels,
TaskTemplate: service.Spec.TaskTemplate,
Mode: service.Spec.Mode,
UpdateConfig: service.Spec.UpdateConfig,
Networks: service.Spec.Networks,
EndpointSpec: service.Spec.EndpointSpec
};
}
};
}])
.factory('TemplateHelper', [function TemplateHelperFactory() {
'use strict';
return {
getPortBindings: function(ports) {
var bindings = [];
ports.forEach(function (port) {
var portAndProtocol = _.split(port, '/');
var binding = {
containerPort: portAndProtocol[0],
protocol: portAndProtocol[1]
};
bindings.push(binding);
});
return bindings;
},
//Not used atm, may prove useful later
getVolumeBindings: function(volumes) {
var bindings = [];
volumes.forEach(function (volume) {
bindings.push({ containerPath: volume });
});
return bindings;
},
//Not used atm, may prove useful later
getEnvBindings: function(env) {
var bindings = [];
env.forEach(function (envvar) {
var binding = {
name: envvar.name
};
if (envvar.set) {
binding.value = envvar.set;
}
bindings.push(binding);
});
return bindings;
}
};
}]);
+7 -56
View File
@@ -1,18 +1,3 @@
function isJSONArray(jsonString) {
return Object.prototype.toString.call(jsonString) === '[object Array]';
}
function isJSON(jsonString) {
try {
var o = JSON.parse(jsonString);
if (o && typeof o === "object") {
return o;
}
}
catch (e) { }
return false;
}
// The Docker API often returns a list of JSON object.
// This handler wrap the JSON objects in an array.
// Used by the API in: Image push, Image create, Events query.
@@ -21,48 +6,14 @@ function jsonObjectsToArrayHandler(data) {
return angular.fromJson(str);
}
// The Docker API often returns an empty string or a valid JSON object on success (Docker 1.9 -> Docker 1.12).
// On error, it returns either an error message as a string (Docker < 1.12) or a JSON object with the field message
// container the error (Docker = 1.12)
// This handler ensure a valid JSON object is returned in any case.
// Used by the API in: container deletion, network deletion, network creation, volume creation,
// container exec, exec resize.
function genericHandler(data) {
var response = {};
// No data is returned when deletion is successful (Docker 1.9 -> 1.12)
if (!data) {
return response;
}
// A string is returned on failure (Docker < 1.12)
else if (!isJSON(data)) {
response.message = data;
}
// Docker 1.12 returns a valid JSON object when an error occurs
else {
response = angular.fromJson(data);
}
return response;
}
// Image delete API returns an array on success (Docker 1.9 -> Docker 1.12).
// On error, it returns either an error message as a string (Docker < 1.12) or a JSON object with the field message
// container the error (Docker = 1.12).
// This handler returns the original array on success or a newly created array containing
// only one JSON object with the field message filled with the error message on failure.
// Image delete API returns an array on success and an object on error.
// This handler creates an array from an object in case of error.
function deleteImageHandler(data) {
// 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);
var response = angular.fromJson(data);
if (!Array.isArray(response)) {
var arr = [];
arr.push(response);
return arr;
}
return response;
}
+31 -87
View File
@@ -1,4 +1,4 @@
angular.module('portainer.services', ['ngResource', 'ngSanitize'])
angular.module('uifordocker.services', ['ngResource', 'ngSanitize'])
.factory('Container', ['$resource', 'Settings', function ContainerFactory($resource, Settings) {
'use strict';
// Resource for interacting with the docker containers
@@ -8,70 +8,46 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
}, {
query: {method: 'GET', params: {all: 0, action: 'json'}, isArray: true},
get: {method: 'GET', params: {action: 'json'}},
start: {method: 'POST', params: {id: '@id', action: 'start'}},
stop: {method: 'POST', params: {id: '@id', t: 5, action: 'stop'}},
restart: {method: 'POST', params: {id: '@id', t: 5, action: 'restart'}},
kill: {method: 'POST', params: {id: '@id', action: 'kill'}},
pause: {method: 'POST', params: {id: '@id', action: 'pause'}},
unpause: {method: 'POST', params: {id: '@id', action: 'unpause'}},
changes: {method: 'GET', params: {action: 'changes'}, isArray: true},
create: {method: 'POST', params: {action: 'create'}},
remove: {method: 'DELETE', params: {id: '@id', v: 0}},
rename: {method: 'POST', params: {id: '@id', action: 'rename'}, isArray: false},
stats: {method: 'GET', params: {id: '@id', stream: false, action: 'stats'}, timeout: 5000},
start: {
method: 'POST', params: {id: '@id', action: 'start'},
transformResponse: genericHandler
},
create: {
method: 'POST', params: {action: 'create'},
transformResponse: genericHandler
},
remove: {
method: 'DELETE', params: {id: '@id', v: 0},
transformResponse: genericHandler
},
rename: {
method: 'POST', params: {id: '@id', action: 'rename', name: '@name'},
transformResponse: genericHandler
},
exec: {
method: 'POST', params: {id: '@id', action: 'exec'},
transformResponse: genericHandler
}
exec: {method: 'POST', params: {id: '@id', action: 'exec'}}
});
}])
.factory('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
}
resize: {method: 'POST', params: {id: '@id', action: 'resize', h: '@height', w: '@width'}}
});
}])
.factory('ContainerCommit', ['$resource', '$http', 'Settings', function ContainerCommitFactory($resource, $http, Settings) {
'use strict';
// http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#create-a-new-image-from-a-container-s-changes
return $resource(Settings.url + '/commit', {}, {
commit: {method: 'POST', params: {container: '@id', repo: '@repo', tag: '@tag'}}
});
return {
commit: function (params, callback) {
$http({
method: 'POST',
url: Settings.url + '/commit',
params: {
'container': params.id,
'tag': params.tag || null,
'repo': params.repo || null
},
data: params.config
}).success(callback).error(function (data, status, headers, config) {
console.log(error, data);
});
}
};
}])
.factory('ContainerLogs', ['$resource', '$http', 'Settings', function ContainerLogsFactory($resource, $http, Settings) {
'use strict';
@@ -150,22 +126,6 @@ angular.module('portainer.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
@@ -187,8 +147,8 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
return $resource(Settings.url + '/networks/:id/:action', {id: '@id'}, {
query: {method: 'GET', isArray: true},
get: {method: 'GET'},
create: {method: 'POST', params: {action: 'create'}, transformResponse: genericHandler},
remove: { method: 'DELETE', transformResponse: genericHandler },
create: {method: 'POST', params: {action: 'create'}},
remove: {method: 'DELETE'},
connect: {method: 'POST', params: {action: 'connect'}},
disconnect: {method: 'POST', params: {action: 'disconnect'}}
});
@@ -199,21 +159,14 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
return $resource(Settings.url + '/volumes/:name/:action', {name: '@name'}, {
query: {method: 'GET'},
get: {method: 'GET'},
create: {method: 'POST', params: {action: 'create'}, transformResponse: genericHandler},
remove: {
method: 'DELETE', transformResponse: genericHandler
}
create: {method: 'POST', params: {action: 'create'}},
remove: {method: 'DELETE'}
});
}])
.factory('Config', ['$resource', 'CONFIG_ENDPOINT', function ConfigFactory($resource, CONFIG_ENDPOINT) {
return $resource(CONFIG_ENDPOINT).get();
}])
.factory('Templates', ['$resource', 'TEMPLATES_ENDPOINT', function TemplatesFactory($resource, TEMPLATES_ENDPOINT) {
return $resource(TEMPLATES_ENDPOINT, {}, {
get: {method: 'GET', isArray: true}
});
}])
.factory('Settings', ['DOCKER_ENDPOINT', 'DOCKER_PORT', 'UI_VERSION', 'PAGINATION_MAX_ITEMS', function SettingsFactory(DOCKER_ENDPOINT, DOCKER_PORT, UI_VERSION, PAGINATION_MAX_ITEMS) {
.factory('Settings', ['DOCKER_ENDPOINT', 'DOCKER_PORT', 'UI_VERSION', function SettingsFactory(DOCKER_ENDPOINT, DOCKER_PORT, UI_VERSION) {
'use strict';
var url = DOCKER_ENDPOINT;
if (DOCKER_PORT) {
@@ -225,8 +178,7 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
endpoint: DOCKER_ENDPOINT,
uiVersion: UI_VERSION,
url: url,
firstLoad: firstLoad,
pagination_count: PAGINATION_MAX_ITEMS
firstLoad: firstLoad
};
}])
.factory('Messages', ['$rootScope', '$sanitize', function MessagesFactory($rootScope, $sanitize) {
@@ -244,18 +196,10 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
}
});
},
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;
}
error: function (title, text) {
$.gritter.add({
title: $sanitize(title),
text: $sanitize(msg),
text: $sanitize(text),
time: 10000,
before_open: function () {
if ($('.gritter-item-wrapper').length === 4) {
+1 -59
View File
@@ -8,66 +8,17 @@ function ImageViewModel(data) {
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';
}
this.Labels = data.Spec.Labels;
if (data.Spec.TaskTemplate.ContainerSpec) {
this.ContainerLabels = data.Spec.TaskTemplate.ContainerSpec.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.Status = data.Status;
this.Names = data.Names;
// Unavailable in Docker < 1.10
if (data.NetworkSettings && !_.isEmpty(data.NetworkSettings.Networks)) {
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.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) {
@@ -106,9 +57,6 @@ function createEventDetails(event) {
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';
@@ -163,12 +111,6 @@ function createEventDetails(event) {
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';
}
+109 -113
View File
@@ -1,7 +1,98 @@
.container > hr {
margin: 60px 0;
}
.jumbotron {
margin: 80px 0;
text-align: center;
}
.jumbotron h1 {
font-size: 100px;
line-height: 1;
}
.jumbotron .lead {
font-size: 24px;
line-height: 1.25;
}
.jumbotron .btn {
padding: 14px 24px;
font-size: 21px;
}
.marketing {
margin: 60px 0;
}
.marketing p + h4 {
margin-top: 28px;
}
.masthead .nav {
margin: 0;
margin: 0 0 2em 0;
width: 100%;
}
.masthead .nav.well {
padding: 0;
}
.masthead .nav li {
display: table-cell;
float: none;
width: 1%;
}
.masthead .nav li a {
font-weight: bold;
text-align: center;
border-right: 1px solid rgba(0,0,0,.1);
border-left: 1px solid rgba(255,255,255,.75);
}
.masthead .nav li:first-child a {
border-left: 0;
border-radius: 3px 0 0 3px;
}
.masthead .nav li:last-child a {
border-right: 0;
border-radius: 0 3px 3px 0;
}
.btn-group button {
margin: 3px;
}
.detail {
width: 80%;
margin: 0 auto;
}
.center {
width: 100%;
margin: 0 auto;
}
.btn-remove {
max-width: 70%;
margin: 0 auto;
}
.actions {
margin: 0 auto;
}
.container-bottom {
height: 50px;
}
.well {
padding: 10px 15px 0 15px;
}
.messages {
max-height: 50px;
overflow-x: hidden;
@@ -15,6 +106,14 @@
border-width: 0 0 0 1em;
}
.inline-four .form-control {
max-width: 25%;
}
.dropdown {
cursor: pointer;
}
.logo {
display: inline;
width: 100%;
@@ -63,7 +162,11 @@ input[type="radio"] {
vertical-align: middle;
}
.space-right {
.clickable {
cursor: pointer;
}
.text-icon {
margin-right: 5px;
}
@@ -79,17 +182,16 @@ input[type="radio"] {
color: white;
}
.widget .widget-body table tbody .image-tag {
font-size: 90% !important;
.image-tag {
margin-right: 5px;
}
.widget .widget-body table tbody .fit-text-size {
font-size: 90% !important;
.label.tag {
margin-right: 5px;
}
.nopadding {
padding: 0 !important;
.widget .widget-body table tbody .image-tag {
font-size: 90% !important;
}
.terminal-container {
@@ -100,109 +202,3 @@ input[type="radio"] {
.interactive {
cursor: pointer;
}
.template-list {
display: flex;
flex-wrap: wrap;
}
.custom-header-ico {
max-width: 32px;
max-height: 32px;
margin-right: 2px;
}
.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;
}
}
/* Underline From Center */
.hvr-underline-from-center {
display: inline-block;
vertical-align: middle;
-webkit-transform: translateZ(0);
transform: translateZ(0);
box-shadow: 0 0 1px rgba(0, 0, 0, 0);
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
-moz-osx-font-smoothing: grayscale;
position: relative;
overflow: hidden;
}
.hvr-underline-from-center:before {
content: "";
position: absolute;
z-index: -1;
left: 50%;
right: 50%;
bottom: 0;
background: #85898b;
height: 2px;
-webkit-transition-property: left, right;
transition-property: left, right;
-webkit-transition-duration: 0.3s;
transition-duration: 0.3s;
-webkit-transition-timing-function: ease-out;
transition-timing-function: ease-out;
}
.hvr-underline-from-center:hover:before, .hvr-underline-from-center:focus:before, .hvr-underline-from-center:active:before {
left: 0;
right: 0;
}
.container-template {
font-size: 1em;
width: 256px;
height: 128px;
margin: 15px;
padding: 5px;
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;
margin-bottom: 5px;
}
.pagination-controls {
margin-left: 10px;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 880 B

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 880 B

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 180 KiB

+10 -12
View File
@@ -1,18 +1,18 @@
{
"name": "portainer",
"version": "1.10.2",
"homepage": "https://github.com/portainer/portainer",
"name": "uifordocker",
"version": "1.7.0",
"homepage": "https://github.com/kevana/ui-for-docker",
"authors": [
"Anthony Lapenna <anthony.lapenna at gmail dot com>"
"Michael Crosby <crosbymichael@gmail.com>",
"Kevan Ahlquist <ahlquistkd@gmail.com>",
"Anthony Lapenna <anthony.lapenna@cloudinovasi.id>"
],
"description": "A web interface for the Docker Remote API.",
"keywords": [
"docker",
"ui",
"api",
"portainer",
"uifordocker",
"dockerui"
"dockerui",
"docker",
"api"
],
"license": "MIT",
"ignore": [
@@ -32,16 +32,14 @@
"angular-mocks": "~1.5.0",
"angular-resource": "~1.5.0",
"angular-ui-select": "~0.17.1",
"angular-utils-pagination": "~0.11.1",
"bootstrap": "~3.3.6",
"font-awesome": "~4.6.3",
"filesize": "~3.3.0",
"jquery": "1.11.1",
"jquery.gritter": "1.7.4",
"lodash": "4.12.0",
"rdash-ui": "1.0.*",
"moment": "~2.14.1",
"xterm.js": "~2.0.1"
"xterm.js": "~1.0.0"
},
"resolutions": {
"angular": "1.5.5"
-36
View File
@@ -1,36 +0,0 @@
#!/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-unix && mkdir -pv /tmp/portainer-build-unix/portainer
mv dist/* /tmp/portainer-build-unix/portainer
cd /tmp/portainer-build-unix && tar cvpfz portainer-${VERSION}-linux-amd64.tar.gz portainer
cd -
grunt release-win
rm -rf /tmp/portainer-build-win && mkdir -pv /tmp/portainer-build-win/portainer
mv dist/* /tmp/portainer-build-win/portainer
cd /tmp/portainer-build-win
tar cvpfz portainer-${VERSION}-windows-amd64.tar.gz portainer
cd -
grunt release-arm
rm -rf /tmp/portainer-build-arm && mkdir -pv /tmp/portainer-build-arm/portainer
mv dist/* /tmp/portainer-build-arm/portainer
cd /tmp/portainer-build-arm
tar cvpfz portainer-${VERSION}-linux-arm.tar.gz portainer
cd -
grunt release-macos
rm -rf /tmp/portainer-build-darwin && mkdir -pv /tmp/portainer-build-darwin/portainer
mv dist/* /tmp/portainer-build-darwin/portainer
cd /tmp/portainer-build-darwin
tar cvpfz portainer-${VERSION}-darwin-amd64.tar.gz portainer
exit 0
-11
View File
@@ -1,11 +0,0 @@
FROM centurylink/ca-certs
COPY dist /
VOLUME /data
WORKDIR /
EXPOSE 9000
ENTRYPOINT ["/portainer"]
@@ -1,9 +0,0 @@
FROM microsoft/windowsservercore
COPY dist /
WORKDIR /
EXPOSE 9000
ENTRYPOINT ["/portainer.exe"]
-9
View File
@@ -1,9 +0,0 @@
FROM microsoft/nanoserver
COPY dist /
WORKDIR /
EXPOSE 9000
ENTRYPOINT ["/portainer.exe"]
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

+4
View File
@@ -0,0 +1,4 @@
FROM nginx:latest
COPY default.conf /etc/nginx/conf.d/default.conf
COPY users.htpasswd /etc/nginx/users.htpasswd
+17
View File
@@ -0,0 +1,17 @@
upstream dockerui {
server dockerui:9000;
}
server {
listen 80;
server_name localhost;
location / {
auth_basic "Docker UI";
auth_basic_user_file /etc/nginx/users.htpasswd;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://dockerui;
}
}
@@ -0,0 +1,10 @@
dockerui:
image: cloudinovasi/ui-for-docker
command: -e http://<SWARM_HOST>:<SWARM_PORT>
nginx:
build: .
links:
- dockerui
ports:
- 80:80
+1
View File
@@ -0,0 +1 @@
user:{PLAIN}password
+26 -114
View File
@@ -16,7 +16,7 @@ module.exports = function (grunt) {
grunt.registerTask('default', ['jshint', 'build', 'karma:unit']);
grunt.registerTask('build', [
'clean:app',
'if:unixBinaryNotExist',
'if:binaryNotExist',
'html2js',
'concat',
'clean:tmpl',
@@ -25,43 +25,7 @@ module.exports = function (grunt) {
]);
grunt.registerTask('release', [
'clean:all',
'if:unixBinaryNotExist',
'html2js',
'uglify',
'clean:tmpl',
'jshint',
//'karma:unit',
'concat:index',
'recess:min',
'copy'
]);
grunt.registerTask('release-win', [
'clean:all',
'if:windowsBinaryNotExist',
'html2js',
'uglify',
'clean:tmpl',
'jshint',
//'karma:unit',
'concat:index',
'recess:min',
'copy'
]);
grunt.registerTask('release-arm', [
'clean:all',
'if:unixArmBinaryNotExist',
'html2js',
'uglify',
'clean:tmpl',
'jshint',
//'karma:unit',
'concat:index',
'recess:min',
'copy'
]);
grunt.registerTask('release-macos', [
'clean:all',
'if:darwinBinaryNotExist',
'if:binaryNotExist',
'html2js',
'uglify',
'clean:tmpl',
@@ -73,11 +37,10 @@ module.exports = function (grunt) {
]);
grunt.registerTask('lint', ['jshint']);
grunt.registerTask('test-watch', ['karma:watch']);
grunt.registerTask('run', ['if:unixBinaryNotExist', 'build', 'shell:buildImage', 'shell:run']);
grunt.registerTask('run-swarm', ['if:unixBinaryNotExist', 'build', 'shell:buildImage', 'shell:runSwarm', 'watch:buildSwarm']);
grunt.registerTask('run-swarm-local', ['if:unixBinaryNotExist', 'build', 'shell:buildImage', 'shell:runSwarmLocal', 'watch:buildSwarm']);
grunt.registerTask('run-dev', ['if:unixBinaryNotExist', 'shell:buildImage', 'shell:run', 'watch:build']);
grunt.registerTask('run-ssl', ['if:unixBinaryNotExist', 'shell:buildImage', 'shell:runSsl', 'watch:buildSsl']);
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)
@@ -108,9 +71,8 @@ module.exports = function (grunt) {
'bower_components/bootstrap/dist/js/bootstrap.min.js',
'bower_components/Chart.js/Chart.min.js',
'bower_components/lodash/dist/lodash.min.js',
'bower_components/filesize/lib/filesize.min.js',
'bower_components/moment/min/moment.min.js',
'bower_components/xterm.js/dist/xterm.js',
'bower_components/xterm.js/src/xterm.js',
'assets/js/jquery.gritter.js', // Using custom version to fix error in minified build due to "use strict"
'assets/js/legend.js' // Not a bower package
],
@@ -125,12 +87,12 @@ module.exports = function (grunt) {
'bower_components/font-awesome/css/font-awesome.min.css',
'bower_components/rdash-ui/dist/css/rdash.min.css',
'bower_components/angular-ui-select/dist/select.min.css',
'bower_components/xterm.js/dist/xterm.css'
'bower_components/xterm.js/src/xterm.css'
]
},
clean: {
all: ['<%= distdir %>/*'],
app: ['<%= distdir %>/*', '!<%= distdir %>/portainer'],
app: ['<%= distdir %>/*', '!<%= distdir %>/ui-for-docker'],
tmpl: ['<%= distdir %>/templates']
},
copy: {
@@ -196,7 +158,6 @@ module.exports = function (grunt) {
'bower_components/angular-ui-router/release/angular-ui-router.min.js',
'bower_components/angular-resource/angular-resource.min.js',
'bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js',
'bower_components/angular-utils-pagination/dirPagination.js',
'bower_components/angular-ui-select/dist/select.min.js'],
dest: '<%= distdir %>/js/angular.js'
}
@@ -291,66 +252,35 @@ module.exports = function (grunt) {
},
shell: {
buildImage: {
command: 'docker build --rm -t portainer -f build/linux/Dockerfile .'
command: 'docker build --rm -t ui-for-docker .'
},
buildBinary: {
command: [
'docker run --rm -v $(pwd)/api:/src centurylink/golang-builder',
'shasum api/portainer > portainer-checksum.txt',
'shasum api/ui-for-docker > ui-for-docker-checksum.txt',
'mkdir -p dist',
'mv api/portainer dist/'
].join(' && ')
},
buildUnixArmBinary: {
command: [
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="linux" -e BUILD_GOARCH="arm" centurylink/golang-builder-cross',
'shasum api/portainer-linux-arm > portainer-checksum.txt',
'mkdir -p dist',
'mv api/portainer-linux-arm dist/portainer'
].join(' && ')
},
buildDarwinBinary: {
command: [
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="darwin" -e BUILD_GOARCH="amd64" centurylink/golang-builder-cross',
'shasum api/portainer-darwin-amd64 > portainer-checksum.txt',
'mkdir -p dist',
'mv api/portainer-darwin-amd64 dist/portainer'
].join(' && ')
},
buildWindowsBinary: {
command: [
'docker run --rm -v $(pwd)/api:/src -e BUILD_GOOS="windows" -e BUILD_GOARCH="amd64" centurylink/golang-builder-cross',
'shasum api/portainer-windows-amd64 > portainer-checksum.txt',
'mkdir -p dist',
'mv api/portainer-windows-amd64 dist/portainer.exe'
'mv api/ui-for-docker dist/'
].join(' && ')
},
run: {
command: [
'docker stop portainer',
'docker rm portainer',
'docker run --privileged -d -p 9000:9000 -v /tmp/portainer:/data -v /var/run/docker.sock:/var/run/docker.sock --name portainer portainer -d /data'
'docker stop ui-for-docker',
'docker rm ui-for-docker',
'docker run --privileged -d -p 9000:9000 -v /tmp/docker-ui:/data -v /var/run/docker.sock:/var/run/docker.sock --name ui-for-docker ui-for-docker -d /data'
].join(';')
},
runSwarm: {
command: [
'docker stop portainer',
'docker rm portainer',
'docker run -d -p 9000:9000 -v /tmp/portainer:/data --name portainer portainer -H tcp://10.0.7.10:2375 --swarm -d /data'
'docker stop ui-for-docker',
'docker rm ui-for-docker',
'docker run -d -p 9000:9000 -v /tmp/docker-ui:/data --name ui-for-docker ui-for-docker -H tcp://10.0.7.10:4000 --swarm -d /data'
].join(';')
},
runSwarmLocal: {
command: [
'docker stop portainer',
'docker rm portainer',
'docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock --name portainer portainer --swarm'
].join(';')
},
runSsl: {
command: [
'docker stop portainer',
'docker rm portainer',
'docker run -d -p 9000:9000 -v /tmp/portainer:/data -v /tmp/docker-ssl:/certs --name portainer portainer -H tcp://10.0.7.10:2376 -d /data --tlsverify'
'docker stop ui-for-docker',
'docker rm ui-for-docker',
'docker run -d -p 9000:9000 -v /tmp/docker-ui:/data -v /tmp/docker-ssl:/certs --name ui-for-docker ui-for-docker -H tcp://10.0.7.10:2376 -d /data --tlsverify'
].join(';')
},
cleanImages: {
@@ -358,29 +288,11 @@ module.exports = function (grunt) {
}
},
'if': {
unixBinaryNotExist: {
options: {
executable: 'dist/portainer'
},
ifFalse: ['shell:buildBinary']
},
unixArmBinaryNotExist: {
options: {
executable: 'dist/portainer'
},
ifFalse: ['shell:buildUnixArmBinary']
},
darwinBinaryNotExist: {
options: {
executable: 'dist/portainer'
},
ifFalse: ['shell:buildDarwinBinary']
},
windowsBinaryNotExist: {
options: {
executable: 'dist/portainer.exe'
},
ifFalse: ['shell:buildWindowsBinary']
binaryNotExist: {
options: {
executable: 'dist/ui-for-docker'
},
ifFalse: ['shell:buildBinary']
}
}
});
+8 -17
View File
@@ -2,7 +2,7 @@
<html lang="en" ng-app="<%= pkg.name %>">
<head>
<meta charset="utf-8">
<title>Portainer</title>
<title>CloudInovasi UI for Docker</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="<%= pkg.author %>">
@@ -32,8 +32,8 @@
<ul class="sidebar">
<li class="sidebar-main">
<a ng-click="toggleSidebar()">
<img ng-if="logo" ng-src="{{ logo }}" class="img-responsive logo">
<img ng-if="!logo" src="images/logo.png" class="img-responsive logo" alt="Portainer">
<img ng-if="config.logo" ng-src="{{ config.logo }}" class="img-responsive logo" alt="CloudInovasi UI">
<img ng-if="!config.logo" src="images/logo.png" class="img-responsive logo" alt="CloudInovasi UI">
<span class="menu-icon glyphicon glyphicon-transfer"></span>
</a>
</li>
@@ -41,12 +41,6 @@
<li class="sidebar-list">
<a ui-sref="index">Dashboard <span class="menu-icon fa fa-tachometer"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="templates">App Templates <span class="menu-icon fa fa-rocket"></span></a>
</li>
<li class="sidebar-list" 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>
@@ -59,22 +53,19 @@
<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="!swarm">
<li class="sidebar-list" ng-if="!config.swarm">
<a ui-sref="events">Events <span class="menu-icon fa fa-history"></span></a>
</li>
<li class="sidebar-list" ng-if="(swarm && !swarm_mode) || (swarm_mode && swarm_manager)">
<li class="sidebar-list" ng-if="config.swarm">
<a ui-sref="swarm">Swarm <span class="menu-icon fa fa-object-group"></span></a>
</li>
<li class="sidebar-list" ng-if="!swarm">
<a ui-sref="docker">Docker <span class="menu-icon fa fa-th"></span></a>
<li class="sidebar-list" ng-if="!config.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/portainer/portainer" target="_blank">
<i class="fa fa-github" aria-hidden="true"></i>
Portainer {{ uiVersion }}
</a>
<a href="https://github.com/cloud-inovasi/cloudinovasi-ui" target="_blank">CloudInovasi UI {{ uiVersion }}</a>
</div>
</div>
</div>
+7 -7
View File
@@ -1,19 +1,19 @@
{
"author": "Portainer.io",
"name": "portainer",
"homepage": "http://portainer.io",
"version": "1.10.2",
"author": "Michael Crosby & Kevan Ahlquist",
"name": "uifordocker",
"homepage": "https://github.com/kevana/ui-for-docker",
"version": "1.7.0",
"repository": {
"type": "git",
"url": "git@github.com:portainer/portainer.git"
"url": "git@github.com:kevana/ui-for-docker.git"
},
"bugs": {
"url": "https://github.com/portainer/portainer/issues"
"url": "https://github.com/kevana/ui-for-docker/issues"
},
"licenses": [
{
"type": "MIT",
"url": "https://raw.githubusercontent.com/portainer/portainer/develop/LICENSE"
"url": "https://raw.githubusercontent.com/kevana/ui-for-docker/master/LICENSE"
}
],
"engines": {
@@ -1,7 +1,7 @@
describe('ContainerController', function () {
var $scope, $httpBackend, mockContainer, $routeParams;
beforeEach(module('portainer'));
beforeEach(module('dockerui'));
beforeEach(inject(function ($rootScope, $controller, _$routeParams_) {
@@ -22,8 +22,8 @@ describe('ContainerController', function () {
$httpBackend.expectGET('dockerapi/containers/json').respond({
'Created': 1421817232,
'id': 'b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f',
'Image': 'portainer:latest',
'Name': '/portainer'
'Image': 'dockerui:latest',
'Name': '/dockerui'
});
}
@@ -33,8 +33,8 @@ describe('ContainerController', function () {
$scope.container = {
'Created': 1421817232,
'id': 'b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f',
'Image': 'portainer:latest',
'Name': '/portainer'
'Image': 'dockerui:latest',
'Name': '/dockerui'
};
$scope.container.newContainerName = "newName";
@@ -1,7 +1,7 @@
describe("ContainerTopController", function () {
var $scope, $httpBackend, $routeParams;
beforeEach(angular.mock.module('portainer'));
beforeEach(angular.mock.module('dockerui'));
beforeEach(inject(function (_$rootScope_, _$httpBackend_, $controller, _$routeParams_) {
$scope = _$rootScope_.$new();
@@ -1,7 +1,7 @@
describe('NetworkController', function () {
var $scope, $httpBackend, $routeParams;
beforeEach(module('portainer'));
beforeEach(module('dockerui'));
beforeEach(inject(function (_$httpBackend_, $controller, _$routeParams_) {
$scope = {};
$httpBackend = _$httpBackend_;
@@ -1,7 +1,7 @@
describe('NetworksController', function () {
var $scope, $httpBackend, $routeParams;
beforeEach(module('portainer'));
beforeEach(module('dockerui'));
beforeEach(inject(function (_$httpBackend_, $controller, _$routeParams_) {
$scope = {};
$httpBackend = _$httpBackend_;
@@ -1,7 +1,7 @@
describe('startContainerController', function () {
var scope, $location, createController, mockContainer, $httpBackend;
beforeEach(angular.mock.module('portainer'));
beforeEach(angular.mock.module('dockerui'));
beforeEach(inject(function ($rootScope, $controller, _$location_) {
$location = _$location_;
@@ -20,11 +20,11 @@ describe('startContainerController', function () {
}));
function expectGetContainers() {
$httpBackend.expectGET('dockerapi/containers/json?all=1').respond([{
'Command': './portainer -e /docker.sock',
'Command': './dockerui -e /docker.sock',
'Created': 1421817232,
'Id': 'b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f',
'Image': 'portainer:latest',
'Names': ['/portainer'],
'Image': 'dockerui:latest',
'Names': ['/dockerui'],
'Ports': [{
'IP': '0.0.0.0',
'PrivatePort': 9000,
@@ -1,7 +1,7 @@
describe("StatsController", function () {
var $scope, $httpBackend, $routeParams;
beforeEach(angular.mock.module('portainer'));
beforeEach(angular.mock.module('dockerui'));
beforeEach(inject(function (_$rootScope_, _$httpBackend_, $controller, _$routeParams_) {
$scope = _$rootScope_.$new();
@@ -1,7 +1,7 @@
describe('VolumesController', function () {
var $scope, $httpBackend, $routeParams;
beforeEach(module('portainer'));
beforeEach(module('dockerui'));
beforeEach(inject(function (_$httpBackend_, $controller, _$routeParams_) {
$scope = {};
$httpBackend = _$httpBackend_;
+226 -1
View File
@@ -1,5 +1,5 @@
describe('filters', function () {
beforeEach(module('portainer.filters'));
beforeEach(module('dockerui.filters'));
describe('truncate', function () {
it('should truncate the string to 10 characters ending in "..." by default', inject(function (truncateFilter) {
@@ -82,6 +82,32 @@ describe('filters', function () {
}));
});
describe('humansize', function () {
it('should return n/a when size is zero', inject(function (humansizeFilter) {
expect(humansizeFilter(0)).toBe('n/a');
}));
it('should handle Bytes values', inject(function (humansizeFilter) {
expect(humansizeFilter(512)).toBe('512 Bytes');
}));
it('should handle KB values', inject(function (humansizeFilter) {
expect(humansizeFilter(5 * 1024)).toBe('5 KB');
}));
it('should handle MB values', inject(function (humansizeFilter) {
expect(humansizeFilter(5 * 1024 * 1024)).toBe('5.0 MB');
}));
it('should handle GB values', inject(function (humansizeFilter) {
expect(humansizeFilter(5 * 1024 * 1024 * 1024)).toBe('5.00 GB');
}));
it('should handle TB values', inject(function (humansizeFilter) {
expect(humansizeFilter(5 * 1024 * 1024 * 1024 * 1024)).toBe('5.000 TB');
}));
});
describe('containername', function () {
it('should strip the leading slash from container name', inject(function (containernameFilter) {
var container = {
@@ -107,4 +133,203 @@ describe('filters', function () {
expect(repotagFilter(image)).toBe('ubuntu:latest');
}));
});
describe('errorMsgFilter', function () {
it('should convert the $resource object to a string message',
inject(function (errorMsgFilter) {
var response = {
'0': 'C',
'1': 'o',
'2': 'n',
'3': 'f',
'4': 'l',
'5': 'i',
'6': 'c',
'7': 't',
'8': ',',
'9': ' ',
'10': 'T',
'11': 'h',
'12': 'e',
'13': ' ',
'14': 'n',
'15': 'a',
'16': 'm',
'17': 'e',
'18': ' ',
'19': 'u',
'20': 'b',
'21': 'u',
'22': 'n',
'23': 't',
'24': 'u',
'25': '-',
'26': 's',
'27': 'l',
'28': 'e',
'29': 'e',
'30': 'p',
'31': '-',
'32': 'r',
'33': 'u',
'34': 'n',
'35': 't',
'36': 'i',
'37': 'm',
'38': 'e',
'39': ' ',
'40': 'i',
'41': 's',
'42': ' ',
'43': 'a',
'44': 'l',
'45': 'r',
'46': 'e',
'47': 'a',
'48': 'd',
'49': 'y',
'50': ' ',
'51': 'a',
'52': 's',
'53': 's',
'54': 'i',
'55': 'g',
'56': 'n',
'57': 'e',
'58': 'd',
'59': ' ',
'60': 't',
'61': 'o',
'62': ' ',
'63': 'b',
'64': '6',
'65': '9',
'66': 'e',
'67': '5',
'68': '3',
'69': 'a',
'70': '6',
'71': '2',
'72': '2',
'73': 'c',
'74': '8',
'75': '.',
'76': ' ',
'77': 'Y',
'78': 'o',
'79': 'u',
'80': ' ',
'81': 'h',
'82': 'a',
'83': 'v',
'84': 'e',
'85': ' ',
'86': 't',
'87': 'o',
'88': ' ',
'89': 'd',
'90': 'e',
'91': 'l',
'92': 'e',
'93': 't',
'94': 'e',
'95': ' ',
'96': '(',
'97': 'o',
'98': 'r',
'99': ' ',
'100': 'r',
'101': 'e',
'102': 'n',
'103': 'a',
'104': 'm',
'105': 'e',
'106': ')',
'107': ' ',
'108': 't',
'109': 'h',
'110': 'a',
'111': 't',
'112': ' ',
'113': 'c',
'114': 'o',
'115': 'n',
'116': 't',
'117': 'a',
'118': 'i',
'119': 'n',
'120': 'e',
'121': 'r',
'122': ' ',
'123': 't',
'124': 'o',
'125': ' ',
'126': 'b',
'127': 'e',
'128': ' ',
'129': 'a',
'130': 'b',
'131': 'l',
'132': 'e',
'133': ' ',
'134': 't',
'135': 'o',
'136': ' ',
'137': 'a',
'138': 's',
'139': 's',
'140': 'i',
'141': 'g',
'142': 'n',
'143': ' ',
'144': 'u',
'145': 'b',
'146': 'u',
'147': 'n',
'148': 't',
'149': 'u',
'150': '-',
'151': 's',
'152': 'l',
'153': 'e',
'154': 'e',
'155': 'p',
'156': '-',
'157': 'r',
'158': 'u',
'159': 'n',
'160': 't',
'161': 'i',
'162': 'm',
'163': 'e',
'164': ' ',
'165': 't',
'166': 'o',
'167': ' ',
'168': 'a',
'169': ' ',
'170': 'c',
'171': 'o',
'172': 'n',
'173': 't',
'174': 'a',
'175': 'i',
'176': 'n',
'177': 'e',
'178': 'r',
'179': ' ',
'180': 'a',
'181': 'g',
'182': 'a',
'183': 'i',
'184': 'n',
'185': '.',
'186': '\n',
'$promise': {},
'$resolved': true
};
var message = 'Conflict, The name ubuntu-sleep-runtime is already assigned to b69e53a622c8. You have to delete (or rename) that container to be able to assign ubuntu-sleep-runtime to a container again.\n';
expect(errorMsgFilter(response)).toBe(message);
}));
});
});
+1 -1
View File
@@ -7,7 +7,7 @@ files = [
JASMINE_ADAPTER,
'dist/angular.js',
'dist/vendor.js',
'dist/portainer.js',
'dist/dockerui.js',
'bower_components/angular-mocks/angular-mocks.js',
'test/unit/**/*.spec.js'
];