Merge branch 'release/1.8.0'

pull/202/head 1.8.0
Anthony Lapenna 2016-09-04 15:11:03 +12:00
commit 4237f452df
64 changed files with 1014 additions and 820 deletions

2
.gitignore vendored
View File

@ -7,4 +7,4 @@ bower_components
*.iml
dist
dist/*
ui-for-docker-checksum.txt
portainer-checksum.txt

2
.godir
View File

@ -1 +1 @@
dockerui
portainer

View File

@ -1,8 +1,9 @@
FROM scratch
FROM centurylink/ca-certs
COPY dist /
VOLUME /data
EXPOSE 9000
ENTRYPOINT ["/ui-for-docker"]
ENTRYPOINT ["/portainer"]

View File

@ -1,4 +1,4 @@
Cloudinovasi-ui: Copyright (c) 2016 Cloudinovasi
Portainer: Copyright (c) 2016 CloudInovasi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -16,7 +16,7 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (anthonylapenna at cloudinovasi dot id)

View File

@ -1 +1 @@
web: dockerui -p ":$PORT" -e "$DOCKER_ENDPOINT"
web: portainer -p ":$PORT" -e "$DOCKER_ENDPOINT"

View File

@ -1,48 +1,42 @@
# Cloudinovasi UI for Docker
# Portainer
A fork of the amazing UI for Docker by Michael Crosby and Kevan Ahlquist (https://github.com/kevana/ui-for-docker) using the rdash-angular theme (https://github.com/rdash/rdash-angular).
Portainer is a web interface for the Docker remote API.
![Dashboard](/dashboard.png)
UI For Docker is a web interface for the Docker Remote API. The goal is to provide a pure client side implementation so it is effortless to connect and manage docker.
## Goals
* Minimal dependencies - I really want to keep this project a pure html/js app.
* Consistency - The web UI should be consistent with the commands found on the docker CLI.
## Supported Docker versions
The current Docker version support policy is the following: `N` to `N-2` included where `N` is the latest version.
The following Docker versions are supported:
At the moment, the following versions are supported: 1.9, 1.10 & 1.11.
* full support for Docker 1.10, 1.11 and 1.12
* partial support for Docker 1.9 (some features won't be available)
## Run
### Quickstart
1. Run: `docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui`
1. Run: `docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/portainer`
2. Open your browser to `http://<dockerd host ip>:9000`
Bind mounting the Unix socket into the UI For Docker container is much more secure than exposing your docker daemon over TCP.
Bind mounting the Unix socket into the Portainer container is much more secure than exposing your docker daemon over TCP.
The `--privileged` flag is required for hosts using SELinux.
### Specify socket to connect to Docker daemon
By default UI For Docker connects to the Docker daemon with`/var/run/docker.sock`. For this to work you need to bind mount the unix socket into the container with `-v /var/run/docker.sock:/var/run/docker.sock`.
By default Portainer connects to the Docker daemon with`/var/run/docker.sock`. For this to work you need to bind mount the unix socket into the container with `-v /var/run/docker.sock:/var/run/docker.sock`.
You can use the `--host`, `-H` flags to change this socket:
```
# Connect to a tcp socket:
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H tcp://127.0.0.1:2375
$ docker run -d -p 9000:9000 cloudinovasi/portainer -H tcp://127.0.0.1:2375
```
```
# Connect to another unix socket:
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H unix:///path/to/docker.sock
$ docker run -d -p 9000:9000 cloudinovasi/portainer -H unix:///path/to/docker.sock
```
### Swarm support
@ -53,17 +47,17 @@ You can access a specific view for you Swarm cluster by defining the `--swarm` f
```
# Connect to a tcp socket and enable Swarm:
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H tcp://<SWARM_HOST>:<SWARM_PORT> --swarm
$ docker run -d -p 9000:9000 cloudinovasi/portainer -H tcp://<SWARM_HOST>:<SWARM_PORT> --swarm
```
*NOTE*: Due to Swarm not exposing information in a machine readable way, the app is bound to a specific version of Swarm at the moment.
### 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:
### Change address/port Portainer is served on
Portainer listens on port 9000 by default. If you run Portainer inside a container then you can bind the container's internal port to any external address and port:
```
# 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
# Expose Portainer on 10.20.30.1:80
$ docker run -d -p 10.20.30.1:80:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/portainer
```
### Access a Docker engine protected via TLS
@ -73,13 +67,13 @@ Ensure that you have access to the CA, the cert and the public key used to acces
These files will need to be named `ca.pem`, `cert.pem` and `key.pem` respectively. Store them somewhere on your disk and mount a volume containing these files inside the UI container:
```
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -v /path/to/certs:/certs -H https://my-docker-host.domain:2376 --tlsverify
$ docker run -d -p 9000:9000 cloudinovasi/portainer -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
$ docker run -d -p 9000:9000 cloudinovasi/portainer -v /path/to/certs:/certs -H https://my-docker-host.domain:2376 --tlsverify --tlscacert /certs/myCa.pem --tlscert /certs/myCert.pem --tlskey /certs/myKey.pem
```
*Note*: Replace `/path/to/certs` to the path to the certificate files on your disk.
@ -91,10 +85,10 @@ You can use the `--logo` flag to specify an URL to your own logo.
For example, using the Docker logo:
```
$ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui --logo "https://www.docker.com/sites/all/themes/docker/assets/images/brand-full.svg"
$ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/portainer --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.
The custom logo will replace the Portainer logo in the UI.
### Hide containers with specific labels
@ -109,45 +103,53 @@ $ 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
$ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/portainer -l owner=acme
```
### Reverse proxy configuration
Has been tested with Nginx 1.11.
Use the following configuration to host the UI at `myhost.mydomain.com/dockerui`:
Use the following configuration to host the UI at `myhost.mydomain.com/portainer`:
```nginx
upstream cloudinovasi-ui {
upstream portainer {
server ADDRESS:PORT;
}
server {
listen 80;
location /dockerui/ {
location /portainer/ {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://cloudinovasi-ui/;
proxy_pass http://portainer/;
}
location /dockerui/ws/ {
location /portainer/ws/ {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_pass http://cloudinovasi-ui/ws/;
proxy_pass http://portainer/ws/;
}
}
```
Replace `ADDRESS:PORT` with the CloudInovasi UI container details.
Replace `ADDRESS:PORT` with the Portainer container details.
### Host your own apps
You can specify an URL to your own templates (**Apps**) definitions using the `--templates` or `-t` flags.
By default, CloudInovasi templates will be used (https://raw.githubusercontent.com/cloud-inovasi/ui-templates/master/templates.json).
For more information about hosting your own template definition and the format, see: https://github.com/cloud-inovasi/ui-templates
### Available options
The following options are available for the `ui-for-docker` binary:
The following options are available for the `portainer` binary:
* `--host`, `-H`: Docker daemon endpoint (default: `"unix:///var/run/docker.sock"`)
* `--bind`, `-p`: Address and port to serve UI For Docker (default: `":9000"`)
* `--bind`, `-p`: Address and port to serve Portainer (default: `":9000"`)
* `--data`, `-d`: Path to the data folder (default: `"."`)
* `--assets`, `-a`: Path to the assets (default: `"."`)
* `--swarm`, `-s`: Swarm cluster support (default: `false`)
@ -157,3 +159,4 @@ The following options are available for the `ui-for-docker` binary:
* `--tlskey`: Path to the TLS key (default `/certs/key.pem`)
* `--hide-label`, `-l`: Hide containers with a specific label in the UI
* `--logo`: URL to a picture to be displayed as a logo in the UI
* `--templates`, `-t`: URL to templates (apps) definitions

View File

@ -9,11 +9,12 @@ import (
type (
api struct {
endpoint *url.URL
bindAddress string
assetPath string
dataPath string
tlsConfig *tls.Config
endpoint *url.URL
bindAddress string
assetPath string
dataPath string
tlsConfig *tls.Config
templatesURL string
}
apiConfig struct {
@ -26,6 +27,7 @@ type (
TLSCACertPath string
TLSCertPath string
TLSKeyPath string
TemplatesURL string
}
)
@ -48,10 +50,11 @@ func newAPI(apiConfig apiConfig) *api {
}
return &api{
endpoint: endpointURL,
bindAddress: apiConfig.BindAddress,
assetPath: apiConfig.AssetPath,
dataPath: apiConfig.DataPath,
tlsConfig: tlsConfig,
endpoint: endpointURL,
bindAddress: apiConfig.BindAddress,
assetPath: apiConfig.AssetPath,
dataPath: apiConfig.DataPath,
tlsConfig: tlsConfig,
templatesURL: apiConfig.TemplatesURL,
}
}

View File

@ -25,6 +25,9 @@ func (a *api) newHandler(settings *Settings) http.Handler {
mux.HandleFunc("/settings", func(w http.ResponseWriter, r *http.Request) {
settingsHandler(w, r, settings)
})
mux.HandleFunc("/templates", func(w http.ResponseWriter, r *http.Request) {
templatesHandler(w, r, a.templatesURL)
})
return CSRFHandler(newCSRFWrapper(mux))
}

View File

@ -1,4 +1,4 @@
package main // import "github.com/cloudinovasi/ui-for-docker"
package main // import "github.com/cloudinovasi/portainer"
import (
"gopkg.in/alecthomas/kingpin.v2"
@ -6,10 +6,10 @@ import (
// main is the entry point of the program
func main() {
kingpin.Version("1.7.0")
kingpin.Version("1.8.0")
var (
endpoint = kingpin.Flag("host", "Dockerd endpoint").Default("unix:///var/run/docker.sock").Short('H').String()
addr = kingpin.Flag("bind", "Address and port to serve UI For Docker").Default(":9000").Short('p').String()
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()
@ -19,6 +19,7 @@ func main() {
swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool()
labels = pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l'))
logo = kingpin.Flag("logo", "URL for the logo displayed in the UI").String()
templates = kingpin.Flag("templates", "URL to the templates (apps) definitions").Default("https://raw.githubusercontent.com/cloud-inovasi/ui-templates/master/templates.json").Short('t').String()
)
kingpin.Parse()
@ -32,6 +33,7 @@ func main() {
TLSCACertPath: *tlscacert,
TLSCertPath: *tlscert,
TLSKeyPath: *tlskey,
TemplatesURL: *templates,
}
settings := &Settings{

View File

@ -12,7 +12,7 @@ type Settings struct {
Logo string `json:"logo"`
}
// configurationHandler defines a handler function used to encode the configuration in JSON
// settingsHandler defines a handler function used to encode the configuration in JSON
func settingsHandler(w http.ResponseWriter, r *http.Request, s *Settings) {
json.NewEncoder(w).Encode(*s)
}

27
api/templates.go Normal file
View File

@ -0,0 +1,27 @@
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
)
// templatesHandler defines a handler function used to retrieve the templates from a URL and put them in the response
func templatesHandler(w http.ResponseWriter, r *http.Request, templatesURL string) {
resp, err := http.Get(templatesURL)
if err != nil {
http.Error(w, fmt.Sprintf("Error making request to %s: %s", templatesURL, err.Error()), http.StatusInternalServerError)
log.Print(err)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
http.Error(w, "Error reading body from templates URL", http.StatusInternalServerError)
log.Print(err)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(body)
}

View File

@ -1,12 +1,13 @@
angular.module('uifordocker', [
'uifordocker.templates',
angular.module('portainer', [
'portainer.templates',
'ui.bootstrap',
'ui.router',
'ui.select',
'ngCookies',
'ngSanitize',
'uifordocker.services',
'uifordocker.filters',
'portainer.services',
'portainer.helpers',
'portainer.filters',
'dashboard',
'container',
'containerConsole',
@ -22,6 +23,7 @@ angular.module('uifordocker', [
'network',
'networks',
'createNetwork',
'templates',
'volumes',
'createVolume'])
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', function ($stateProvider, $urlRouterProvider, $httpProvider) {
@ -118,6 +120,11 @@ angular.module('uifordocker', [
templateUrl: 'app/components/network/network.html',
controller: 'NetworkController'
})
.state('templates', {
url: '/templates/',
templateUrl: 'app/components/templates/templates.html',
controller: 'TemplatesController'
})
.state('volumes', {
url: '/volumes/',
templateUrl: 'app/components/volumes/volumes.html',
@ -156,4 +163,5 @@ angular.module('uifordocker', [
.constant('DOCKER_ENDPOINT', 'dockerapi')
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is requred. If you have a port, prefix it with a ':' i.e. :4243
.constant('CONFIG_ENDPOINT', 'settings')
.constant('UI_VERSION', 'v1.7.0');
.constant('TEMPLATES_ENDPOINT', 'templates')
.constant('UI_VERSION', 'v1.8.0');

View File

@ -7,68 +7,19 @@
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-6 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-body>
<div class="widget-icon grey pull-left">
<i class="fa fa-tasks"></i>
</div>
<div ng-if="!container.edit">
<div class="title">{{ container.Name|trimcontainername }}</div>
<div class="comment">
Name <a href="" ng-click="container.edit = true;"><i class="fa fa-edit"></i></a>
</div>
</div>
<div ng-if="container.edit">
<div class="title"><input type="text" class="containerNameInput" ng-model="container.newContainerName"></div>
<div class="comment">
Name
<a href="" ng-click="container.edit = false;"><i class="fa fa-times"></i></a>
<a href="" ng-click="renameContainer()"><i class="fa fa-check-square-o"></i></a>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-6 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-body>
<div ng-class="{true: 'widget-icon green pull-left', false: 'widget-icon red pull-left'}[container.State.Running]">
<i class="fa fa-heartbeat"></i>
</div>
<div class="title">{{ container.State|getstatetext }}</div>
<div class="comment">State</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-body>
<div class="widget-icon grey pull-left">
<i class="fa fa-cogs"></i>
</div>
<div class="title">
<div class="btn-group" role="group" aria-label="...">
<button class="btn btn-primary" ng-click="commit()">Commit</button>
<button class="btn btn-primary" ng-click="start()" ng-disabled="container.State.Running">Start</button>
<button class="btn btn-primary" ng-click="stop()" ng-disabled="!container.State.Running">Stop</button>
<button class="btn btn-primary" ng-click="kill()" ng-disabled="!container.State.Running">Kill</button>
<button class="btn btn-primary" ng-click="restart()">Restart</button>
<button class="btn btn-primary" ng-click="pause()" ng-disabled="!container.State.Running && !container.State.Paused">Pause</button>
<button class="btn btn-primary" ng-click="unpause()" ng-disabled="!container.State.Paused">Unpause</button>
<button class="btn btn-danger" ng-click="remove()" ng-disabled="container.State.Running">Remove</button>
</div>
<div class="btn-group" role="group" aria-label="...">
<a class="btn btn-default" type="button" ui-sref="stats({id: container.Id})">Stats</a>
<a class="btn btn-default" type="button" ui-sref="logs({id: container.Id})">Logs</a>
<a class="btn btn-default" type="button" ui-sref="console({id: container.Id})">Console</a>
</div>
</div>
<div class="comment">
Actions
<rd-widget-header icon="fa-cogs" title="Actions"></rd-widget-header>
<rd-widget-body classes="padding">
<div class="btn-group" role="group" aria-label="...">
<button class="btn btn-primary" ng-click="start()" ng-if="!container.State.Running"><i class="fa fa-play btn-ico" aria-hidden="true"></i>Start</button>
<button class="btn btn-danger" ng-click="stop()" ng-if="container.State.Running"><i class="fa fa-stop btn-ico" aria-hidden="true"></i>Stop</button>
<button class="btn btn-danger" ng-click="kill()" ng-if="container.State.Running"><i class="fa fa-bomb btn-ico" aria-hidden="true"></i>Kill</button>
<button class="btn btn-primary" ng-click="restart()" ng-if="container.State.Running"><i class="fa fa-refresh btn-ico" aria-hidden="true"></i>Restart</button>
<button class="btn btn-primary" ng-click="pause()" ng-if="container.State.Running && !container.State.Paused"><i class="fa fa-pause btn-ico" aria-hidden="true"></i>Pause</button>
<button class="btn btn-primary" ng-click="unpause()" ng-if="container.State.Paused"><i class="fa fa-play btn-ico" aria-hidden="true"></i>Resume</button>
<button class="btn btn-danger" ng-click="remove()" ng-disabled="container.State.Running"><i class="fa fa-trash btn-ico" aria-hidden="true"></i>Remove</button>
</div>
</rd-widget-body>
</rd-widget>
@ -76,44 +27,144 @@
</div>
<div class="row">
<div class="col-lg-9">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Container status"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Created</td>
<td>{{ container.Created|getisodate }}</td>
<td>Name</td>
<td ng-if="!container.edit">
{{ container.Name|trimcontainername }}
<a href="" data-toggle="tooltip" title="Edit container name" ng-click="container.edit = true;"><i class="fa fa-edit"></i></a>
</td>
<td ng-if="container.edit">
<input type="text" class="containerNameInput" ng-model="container.newContainerName">
<a href="" ng-click="container.edit = false;"><i class="fa fa-times"></i></a>
<a href="" ng-click="renameContainer()"><i class="fa fa-check-square-o"></i></a>
</td>
</tr>
<tr ng-if="container.NetworkSettings.IPAddress">
<td>IP address</td>
<td>{{ container.NetworkSettings.IPAddress }}</td>
</tr>
<tr>
<td>Path</td>
<td>{{ container.Path }}</td>
</tr>
<tr>
<td>Args</td>
<td>{{ container.Args.join(' ') || 'None' }}</td>
</tr>
<tr>
<td>Exposed Ports</td>
<td>Status</td>
<td>
<ul>
<li ng-repeat="(k, v) in container.Config.ExposedPorts">{{ k }}</li>
</ul>
<i ng-class="{true: 'fa fa-heartbeat text-icon green-icon', false: 'fa fa-heartbeat text-icon red-icon'}[container.State.Running]"></i>
{{ container.State|getstatetext }} since {{ activityTime }}<span ng-if="!container.State.Running"> with exit code {{ container.State.ExitCode }}</span>
</td>
</tr>
<tr ng-if="container.State.Running">
<td>Start time</td>
<td>{{ container.State.StartedAt|getisodate }}</td>
</tr>
<tr ng-if="!container.State.Running">
<td>Finished</td>
<td>{{ container.State.FinishedAt|getisodate }}</td>
</tr>
<tr>
<td colspan="2">
<div class="btn-group" role="group" aria-label="...">
<a class="btn btn-outline-secondary" type="button" ui-sref="stats({id: container.Id})"><i class="fa fa-area-chart btn-ico" aria-hidden="true"></i>Stats</a>
<a class="btn btn-outline-secondary" type="button" ui-sref="logs({id: container.Id})"><i class="fa fa-exclamation-circle btn-ico" aria-hidden="true"></i>Logs</a>
<a class="btn btn-outline-secondary" type="button" ui-sref="console({id: container.Id})"><i class="fa fa-terminal btn-ico" aria-hidden="true"></i>Console</a>
</div>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-clone" title="Create image"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- tag-description -->
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">
You can create an image from this container, this allows you to backup important data or save
helpful configurations. You'll be able to spin up another container based on this image afterward.
</span>
</div>
</div>
<!-- !tag-description -->
<!-- name-and-registry-inputs -->
<div class="form-group">
<label for="image_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-7">
<input type="text" class="form-control" ng-model="config.Image" id="image_name" placeholder="e.g. myImage:myTag">
</div>
<label for="image_registry" class="col-sm-1 control-label text-left">Registry</label>
<div class="col-sm-3">
<input type="text" class="form-control" ng-model="config.Registry" id="image_registry" placeholder="optional">
</div>
</div>
<!-- !name-and-registry-inputs -->
<!-- tag-note -->
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">Note: if you don't specify the tag in the image name, <span class="label label-default">latest</span> will be used.</span>
</div>
</div>
<!-- !tag-note -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-default btn-sm" ng-disabled="!config.Image" ng-click="commit()">Create</button>
<i id="createImageSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Container details"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Image</td>
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
</tr>
<tr ng-if="portBindings.length > 0">
<td>Port configuration</td>
<td>
<div ng-repeat="portMapping in portBindings">
{{ portMapping.container }} <i class="fa fa-long-arrow-right"></i> {{ portMapping.host }}
</div>
</td>
</tr>
<tr>
<td>Environment</td>
<td>
<ul>
<li ng-repeat="k in container.Config.Env">{{ k }}</li>
</ul>
</td>
<td>CMD</td>
<td><code>{{ container.Config.Cmd|command }}</code></td>
</tr>
<tr>
<td>ENV</td>
<td>
<table class="table table-bordered table-condensed">
<tr ng-repeat="var in container.Config.Env">
<td>{{ var|key: '=' }}</td>
<td>{{ var|value: '=' }}</td>
</tr>
</table>
</td>
</tr>
<tr ng-if="!(container.Config.Labels | emptyobject)">
<td>Labels</td>
<td>
<table role="table" class="table">
<table class="table table-bordered table-condensed">
<tr ng-repeat="(k, v) in container.Config.Labels">
<td>{{ k }}</td>
<td>{{ v }}</td>
@ -121,77 +172,33 @@
</table>
</td>
</tr>
<tr>
<td>Publish all ports</td>
<td>{{ container.HostConfig.PublishAllPorts }}</td>
</tr>
<tr>
<td>Ports</td>
<td>
<ul>
<li ng-repeat="(containerport, hostports) in container.NetworkSettings.Ports">
{{ containerport }} =>
<span class="label label-default" style="margin-right: 5px;" ng-repeat="(k,v) in hostports">{{ v.HostIp }}:{{ v.HostPort }}</span>
</li>
</ul>
</td>
</tr>
<tr>
<td>Hostname</td>
<td>{{ container.Config.Hostname }}</td>
</tr>
<tr>
<td>IPAddress</td>
<td>{{ container.NetworkSettings.IPAddress }}</td>
</tr>
<tr>
<td>Cmd</td>
<td>{{ container.Config.Cmd }}</td>
</tr>
<tr>
<td>Entrypoint</td>
<td>{{ container.Config.Entrypoint.join(' ') }}</td>
</tr>
<tr>
<td>Bindings</td>
<td>
<ul>
<li ng-repeat="b in container.HostConfig.Binds">{{ b }}</li>
</ul>
</td>
</tr>
<tr>
<td>Volumes</td>
<td>{{ container.Volumes }}</td>
</tr>
<tr>
<td>SysInitpath</td>
<td>{{ container.SysInitPath }}</td>
</tr>
<tr>
<td>Image</td>
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-3">
</div>
<div class="row" ng-if="container.HostConfig.Binds.length > 0">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Container state details"></rd-widget-header>
<rd-widget-header icon="fa-cubes" title="Volumes"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<thead>
<tr>
<th>Host</th>
<th>Container</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="(key, val) in container.State">
<td>{{key}}</td>
<td ng-if="key === 'StartedAt' || key === 'FinishedAt'">{{val|getisodate}}</td>
<td ng-if="key !== 'StartedAt' && key !== 'FinishedAt'">{{val}}</td>
<tr ng-repeat="vol in container.HostConfig.Binds">
<td>{{ vol|key: ':' }}</td>
<td>{{ vol|value: ':' }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
<div>
</div>

View File

@ -1,13 +1,11 @@
angular.module('container', [])
.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: {}
.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ImageHelper', 'Messages',
function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ImageHelper, Messages) {
$scope.activityTime = 0;
$scope.portBindings = [];
$scope.config = {
Image: '',
Registry: ''
};
var update = function () {
@ -17,74 +15,38 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
$scope.container.edit = false;
$scope.container.newContainerName = $filter('trimcontainername')(d.Name);
// fill up env
if (d.Config.Env) {
$scope.newCfg.Env = d.Config.Env.map(function (entry) {
return {name: entry.split('=')[0], value: entry.split('=')[1]};
if (d.State.Running) {
$scope.activityTime = moment.duration(moment(d.State.StartedAt).utc().diff(moment().utc())).humanize();
} else {
$scope.activityTime = moment.duration(moment().utc().diff(moment(d.State.FinishedAt).utc())).humanize();
}
$scope.portBindings = [];
if (d.NetworkSettings.Ports) {
angular.forEach(Object.keys(d.NetworkSettings.Ports), function(portMapping) {
if (d.NetworkSettings.Ports[portMapping]) {
var mapping = {};
mapping.container = portMapping;
mapping.host = d.NetworkSettings.Ports[portMapping][0].HostIp + ':' + d.NetworkSettings.Ports[portMapping][0].HostPort;
$scope.portBindings.push(mapping);
}
});
}
// fill up ports
$scope.newCfg.Ports = {};
angular.forEach(d.Config.ExposedPorts, function(i, port) {
if (d.HostConfig.PortBindings && port in d.HostConfig.PortBindings) {
$scope.newCfg.Ports[port] = d.HostConfig.PortBindings[port];
}
else {
$scope.newCfg.Ports[port] = [];
}
});
// fill up bindings
$scope.newCfg.Binds = [];
var defaultBinds = {};
angular.forEach(d.Config.Volumes, function(value, vol) {
defaultBinds[vol] = { ContPath: vol, HostPath: '', ReadOnly: false, DefaultBind: true };
});
angular.forEach(d.HostConfig.Binds, function(binding, i) {
var mountpoint = binding.split(':')[0];
var vol = binding.split(':')[1] || '';
var ro = binding.split(':').length > 2 && binding.split(':')[2] === 'ro';
var defaultBind = false;
if (vol === '') {
vol = mountpoint;
mountpoint = '';
}
if (vol in defaultBinds) {
delete defaultBinds[vol];
defaultBind = true;
}
$scope.newCfg.Binds.push({ ContPath: vol, HostPath: mountpoint, ReadOnly: ro, DefaultBind: defaultBind });
});
angular.forEach(defaultBinds, function(bind) {
$scope.newCfg.Binds.push(bind);
});
$('#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,
HostConfig: $scope.container.HostConfig
}, function (d) {
Container.start({id: $scope.container.Id}, {}, function (d) {
update();
Messages.send("Container started", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", "Container failed to start." + e.data);
Messages.error("Failure", e, "Unable to start container");
});
};
@ -95,7 +57,7 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
Messages.send("Container stopped", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", "Container failed to stop." + e.data);
Messages.error("Failure", e, "Unable to stop container");
});
};
@ -106,20 +68,26 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
Messages.send("Container killed", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", "Container failed to die." + e.data);
Messages.error("Failure", e, "Unable to kill container");
});
};
$scope.commit = function () {
$('#loadingViewSpinner').show();
ContainerCommit.commit({id: $stateParams.id, repo: $scope.container.Config.Image}, function (d) {
$('#createImageSpinner').show();
var image = _.toLower($scope.config.Image);
var registry = _.toLower($scope.config.Registry);
var imageConfig = ImageHelper.createImageConfig(image, registry);
ContainerCommit.commit({id: $stateParams.id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) {
$('#createImageSpinner').hide();
update();
Messages.send("Container commited", $stateParams.id);
}, function (e) {
$('#createImageSpinner').hide();
update();
Messages.error("Failure", "Container failed to commit." + e.data);
Messages.error("Failure", e, "Unable to commit container");
});
};
$scope.pause = function () {
$('#loadingViewSpinner').show();
Container.pause({id: $stateParams.id}, function (d) {
@ -127,7 +95,7 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
Messages.send("Container paused", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", "Container failed to pause." + e.data);
Messages.error("Failure", e, "Unable to pause container");
});
};
@ -138,19 +106,24 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
Messages.send("Container unpaused", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", "Container failed to unpause." + e.data);
Messages.error("Failure", e, "Unable to unpause container");
});
};
$scope.remove = function () {
$('#loadingViewSpinner').show();
Container.remove({id: $stateParams.id}, function (d) {
update();
$state.go('containers', {}, {reload: true});
Messages.send("Container removed", $stateParams.id);
if (d.message) {
$('#loadingViewSpinner').hide();
Messages.send("Error", d.message);
}
else {
$state.go('containers', {}, {reload: true});
Messages.send("Container removed", $stateParams.id);
}
}, function (e) {
update();
Messages.error("Failure", "Container failed to remove." + e.data);
Messages.error("Failure", e, "Unable to remove container");
});
};
@ -161,48 +134,24 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
Messages.send("Container restarted", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", "Container failed to restart." + e.data);
});
};
$scope.hasContent = function (data) {
return data !== null && data !== undefined;
};
$scope.getChanges = function () {
$('#loadingViewSpinner').show();
Container.changes({id: $stateParams.id}, function (d) {
$scope.changes = d;
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to restart container");
});
};
$scope.renameContainer = function () {
// #FIXME fix me later to handle http status to show the correct error message
Container.rename({id: $stateParams.id, 'name': $scope.container.newContainerName}, function (data) {
if (data.name) {
$scope.container.Name = data.name;
Messages.send("Container renamed", $stateParams.id);
} else {
Container.rename({id: $stateParams.id, 'name': $scope.container.newContainerName}, function (d) {
if (d.message) {
$scope.container.newContainerName = $scope.container.Name;
Messages.error("Failure", "Container failed to rename.");
Messages.error("Unable to rename container", {}, d.message);
} else {
$scope.container.Name = $scope.container.newContainerName;
Messages.send("Container successfully renamed", d.name);
}
}, function (e) {
Messages.error("Failure", e, 'Unable to rename container');
});
$scope.container.edit = false;
};
$scope.addEntry = function (array, entry) {
array.push(entry);
};
$scope.rmEntry = function (array, entry) {
var idx = array.indexOf(entry);
array.splice(idx, 1);
};
$scope.toggleEdit = function() {
$scope.edit = !$scope.edit;
};
update();
$scope.getChanges();
}]);

View File

@ -1,6 +1,6 @@
angular.module('containerConsole', [])
.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Settings', 'Container', 'Exec', '$timeout', 'Messages', 'errorMsgFilter',
function ($scope, $stateParams, Settings, Container, Exec, $timeout, Messages, errorMsgFilter) {
.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Settings', 'Container', 'Exec', '$timeout', 'Messages',
function ($scope, $stateParams, Settings, Container, Exec, $timeout, Messages) {
$scope.state = {};
$scope.state.command = "bash";
$scope.connected = false;
@ -33,7 +33,10 @@ function ($scope, $stateParams, Settings, Container, Exec, $timeout, Messages, e
};
Container.exec(execConfig, function(d) {
if (d.Id) {
if (d.message) {
$('#loadConsoleSpinner').hide();
Messages.error("Error", {}, d.message);
} else {
var execId = d.Id;
resizeTTY(execId, termHeight, termWidth);
var url = window.location.href.split('#')[0] + 'ws/exec?id=' + execId;
@ -43,13 +46,10 @@ function ($scope, $stateParams, Settings, Container, Exec, $timeout, Messages, e
url = url.replace('http://', 'ws://');
}
initTerm(url, termHeight, termWidth);
} else {
$('#loadConsoleSpinner').hide();
Messages.error('Error', errorMsgFilter(d));
}
}, function (e) {
$('#loadConsoleSpinner').hide();
Messages.error("Failure", e.data);
Messages.error("Failure", e, 'Unable to start an exec instance');
});
};
@ -66,10 +66,11 @@ function ($scope, $stateParams, Settings, Container, Exec, $timeout, Messages, e
function resizeTTY(execId, height, width) {
$timeout(function() {
Exec.resize({id: execId, height: height, width: width}, function (d) {
var error = errorMsgFilter(d);
if (error) {
Messages.error('Error', 'Unable to resize TTY');
if (d.message) {
Messages.error('Error', {}, 'Unable to resize TTY');
}
}, function (e) {
Messages.error("Failure", {}, 'Unable to resize TTY');
});
}, 2000);

View File

@ -13,12 +13,8 @@ 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() {

View File

@ -52,6 +52,13 @@
<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
@ -61,23 +68,16 @@
</th>
<th ng-if="swarm">
<a ui-sref="containers" ng-click="order('Host')">
Host
Host IP
<span ng-show="sortType == 'Host' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Host' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="containers" ng-click="order('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 ui-sref="containers" ng-click="order('Ports')">
Exposed Ports
<span ng-show="sortType == 'Ports' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Ports' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
@ -88,10 +88,15 @@
<td><span class="label label-{{ container.Status|containerstatusbadge }}">{{ container.Status|containerstatus }}</span></td>
<td ng-if="swarm"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername}}</a></td>
<td ng-if="!swarm"><a ui-sref="container({id: container.Id})">{{ container|containername}}</a></td>
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
<td ng-if="state.displayIP">{{ container.IP ? container.IP : '-' }}</td>
<td ng-if="swarm">{{ container.hostIP }}</td>
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
<td>{{ container.Command|truncate:60 }}</td>
<td>
<a ng-if="container.Ports.length > 0" ng-repeat="p in container.Ports" class="image-tag" ng-href="http://{{p.host}}:{{p.public}}" target="_blank">
<i class="fa fa-external-link" aria-hidden="true"></i> {{ p.private }}
</a>
<span ng-if="container.Ports.length == 0" >-</span>
</td>
</tr>
</tbody>
</table>

View File

@ -1,12 +1,12 @@
angular.module('containers', [])
.controller('ContainersController', ['$scope', 'Container', 'Info', 'Settings', 'Messages', 'Config', 'errorMsgFilter',
function ($scope, Container, Info, Settings, Messages, Config, errorMsgFilter) {
.controller('ContainersController', ['$scope', 'Container', 'ContainerHelper', 'Info', 'Settings', 'Messages', 'Config',
function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config) {
$scope.state = {};
$scope.state.displayAll = Settings.displayAll;
$scope.state.displayIP = false;
$scope.sortType = 'State';
$scope.sortReverse = true;
$scope.sortReverse = false;
$scope.state.selectedItemCount = 0;
$scope.order = function (sortType) {
@ -14,13 +14,13 @@ function ($scope, Container, Info, Settings, Messages, Config, errorMsgFilter) {
$scope.sortType = sortType;
};
var update = function (data) {
var update = function (data, containersToHideLabels) {
$('#loadContainersSpinner').show();
$scope.state.selectedItemCount = 0;
Container.query(data, function (d) {
var containers = d;
if (hiddenLabels) {
containers = hideContainers(d);
if (containersToHideLabels) {
containers = ContainerHelper.hideContainers(d, containersToHideLabels);
}
$scope.containers = containers.map(function (container) {
var model = new ContainerViewModel(container);
@ -50,37 +50,25 @@ function ($scope, Container, Info, Settings, Messages, Config, errorMsgFilter) {
if (c.Checked) {
counter = counter + 1;
if (action === Container.start) {
Container.get({id: c.Id}, function (d) {
c = d;
action({id: c.Id, HostConfig: c.HostConfig || {}}, function (d) {
Messages.send("Container " + msg, c.Id);
complete();
}, function (e) {
Messages.error("Failure", e.data);
complete();
});
action({id: c.Id}, {}, function (d) {
Messages.send("Container " + msg, c.Id);
complete();
}, function (e) {
if (e.status === 404) {
$('.detail').hide();
Messages.error("Not found", "Container not found.");
} else {
Messages.error("Failure", e.data);
}
Messages.error("Failure", e, "Unable to start container");
complete();
});
}
else if (action === Container.remove) {
action({id: c.Id}, function (d) {
var error = errorMsgFilter(d);
if (error) {
Messages.send("Error", "Unable to remove running container");
if (d.message) {
Messages.send("Error", d.message);
}
else {
Messages.send("Container " + msg, c.Id);
}
complete();
}, function (e) {
Messages.error("Failure", e.data);
Messages.error("Failure", e, 'Unable to remove container');
complete();
});
}
@ -89,7 +77,7 @@ function ($scope, Container, Info, Settings, Messages, Config, errorMsgFilter) {
Messages.send("Container " + msg, c.Id);
complete();
}, function (e) {
Messages.error("Failure", e.data);
Messages.error("Failure", e, 'An error occured');
complete();
});
@ -142,21 +130,6 @@ function ($scope, Container, Info, Settings, Messages, Config, errorMsgFilter) {
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;
@ -174,15 +147,15 @@ function ($scope, Container, Info, Settings, Messages, Config, errorMsgFilter) {
$scope.swarm = false;
Config.$promise.then(function (c) {
hiddenLabels = c.hiddenLabels;
var containersToHideLabels = c.hiddenLabels;
$scope.swarm = c.swarm;
if (c.swarm) {
Info.get({}, function (d) {
$scope.swarm_hosts = retrieveSwarmHostsInfo(d);
update({all: Settings.displayAll ? 1 : 0});
update({all: Settings.displayAll ? 1 : 0}, containersToHideLabels);
});
} else {
update({all: Settings.displayAll ? 1 : 0});
update({all: Settings.displayAll ? 1 : 0}, containersToHideLabels);
}
});
}]);

View File

@ -1,6 +1,6 @@
angular.module('createContainer', [])
.controller('CreateContainerController', ['$scope', '$state', 'Config', 'Container', 'Image', 'Volume', 'Network', 'Messages', 'errorMsgFilter',
function ($scope, $state, Config, Container, Image, Volume, Network, Messages, errorMsgFilter) {
.controller('CreateContainerController', ['$scope', '$state', 'Config', 'Container', 'Image', 'Volume', 'Network', 'Messages',
function ($scope, $state, Config, Container, Image, Volume, Network, Messages) {
$scope.state = {
alwaysPull: true
@ -61,7 +61,7 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, e
Volume.query({}, function (d) {
$scope.availableVolumes = d.Volumes;
}, function (e) {
Messages.error("Failure", e.data);
Messages.error("Failure", e, "Unable to retrieve volumes");
});
Network.query({}, function (d) {
@ -79,48 +79,51 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, e
}
$scope.availableNetworks = networks;
}, function (e) {
Messages.error("Failure", e.data);
Messages.error("Failure", e, "Unable to retrieve networks");
});
});
// TODO: centralize, already present in templatesController
function createContainer(config) {
$('#createContainerSpinner').show();
Container.create(config, function (d) {
if (d.Id) {
var reqBody = config.HostConfig || {};
reqBody.id = d.Id;
Container.start(reqBody, function (cd) {
$('#createContainerSpinner').hide();
Messages.send('Container Started', d.Id);
$state.go('containers', {}, {reload: true});
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('Error', errorMsgFilter(e));
Messages.error("Failure", e, 'Unable to start container');
});
} else {
$('#createContainerSpinner').hide();
Messages.error('Error', errorMsgFilter(d));
}
}, function (e) {
$('#createContainerSpinner').hide();
Messages.error('Error', errorMsgFilter(e));
Messages.error("Failure", e, 'Unable to create container');
});
}
// 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('Error', 'Unable to pull image ' + image);
Messages.error('Failure', e, 'Unable to pull image');
});
}
@ -223,7 +226,7 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, e
$scope.create = function () {
var config = prepareConfiguration();
$('#createContainerSpinner').show();
if ($scope.state.alwaysPull) {
pullImageAndCreateContainer(config);
} else {

View File

@ -1,6 +1,6 @@
angular.module('createNetwork', [])
.controller('CreateNetworkController', ['$scope', '$state', 'Messages', 'Network', 'errorMsgFilter',
function ($scope, $state, Messages, Network, errorMsgFilter) {
.controller('CreateNetworkController', ['$scope', '$state', 'Messages', 'Network',
function ($scope, $state, Messages, Network) {
$scope.formValues = {
DriverOptions: [],
Subnet: '',
@ -27,17 +27,17 @@ function ($scope, $state, Messages, Network, errorMsgFilter) {
function createNetwork(config) {
$('#createNetworkSpinner').show();
Network.create(config, function (d) {
if (d.Id) {
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});
} else {
$('#createNetworkSpinner').hide();
Messages.error('Unable to create network', errorMsgFilter(d));
}
}, function (e) {
$('#createNetworkSpinner').hide();
Messages.error('Unable to create network', e.data);
Messages.error("Failure", e, 'Unable to create network');
});
}

View File

@ -1,6 +1,6 @@
angular.module('createVolume', [])
.controller('CreateVolumeController', ['$scope', '$state', 'Volume', 'Messages', 'errorMsgFilter',
function ($scope, $state, Volume, Messages, errorMsgFilter) {
.controller('CreateVolumeController', ['$scope', '$state', 'Volume', 'Messages',
function ($scope, $state, Volume, Messages) {
$scope.formValues = {
DriverOptions: []
@ -21,17 +21,17 @@ function ($scope, $state, Volume, Messages, errorMsgFilter) {
function createVolume(config) {
$('#createVolumeSpinner').show();
Volume.create(config, function (d) {
if (d.Name) {
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});
} else {
$('#createVolumeSpinner').hide();
Messages.error('Unable to create volume', errorMsgFilter(d));
}
}, function (e) {
$('#createVolumeSpinner').hide();
Messages.error('Unable to create volume', e.data);
Messages.error("Failure", e, 'Unable to create volume');
});
}

View File

@ -1,6 +1,6 @@
angular.module('dashboard', [])
.controller('DashboardController', ['$scope', '$q', 'Config', 'Container', 'Image', 'Network', 'Volume', 'Info',
function ($scope, $q, Config, Container, Image, Network, Volume, Info) {
.controller('DashboardController', ['$scope', '$q', 'Config', 'Container', 'ContainerHelper', 'Image', 'Network', 'Volume', 'Info',
function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume, Info) {
$scope.containerData = {
total: 0
@ -15,13 +15,13 @@ function ($scope, $q, Config, Container, Image, Network, Volume, Info) {
total: 0
};
function prepareContainerData(d) {
function prepareContainerData(d, containersToHideLabels) {
var running = 0;
var stopped = 0;
var containers = d;
if (hiddenLabels) {
containers = hideContainers(d);
if (containersToHideLabels) {
containers = ContainerHelper.hideContainers(d, containersToHideLabels);
}
for (var i = 0; i < containers.length; i++) {
@ -65,7 +65,7 @@ function ($scope, $q, Config, Container, Image, Network, Volume, Info) {
$scope.infoData = info;
}
function fetchDashboardData() {
function fetchDashboardData(containersToHideLabels) {
$('#loadingViewSpinner').show();
$q.all([
Container.query({all: 1}).$promise,
@ -74,7 +74,7 @@ function ($scope, $q, Config, Container, Image, Network, Volume, Info) {
Network.query({}).$promise,
Info.get({}).$promise
]).then(function (d) {
prepareContainerData(d[0]);
prepareContainerData(d[0], containersToHideLabels);
prepareImageData(d[1]);
prepareVolumeData(d[2]);
prepareNetworkData(d[3]);
@ -83,24 +83,8 @@ function ($scope, $q, Config, Container, Image, Network, Volume, Info) {
});
}
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;
hiddenLabels = c.hiddenLabels;
fetchDashboardData();
fetchDashboardData(c.hiddenLabels);
});
}]);

View File

@ -21,7 +21,7 @@ function ($scope, Settings, Messages, Events) {
$('#loadEventsSpinner').hide();
},
function (e) {
Messages.error("Unable to load events", e.data);
$('#loadEventsSpinner').hide();
Messages.error("Failure", e, "Unable to load events");
});
}]);

View File

@ -148,8 +148,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>

View File

@ -1,8 +1,7 @@
angular.module('image', [])
.controller('ImageController', ['$scope', '$stateParams', '$state', 'Image', 'Messages',
function ($scope, $stateParams, $state, Image, Messages) {
.controller('ImageController', ['$scope', '$stateParams', '$state', 'Image', 'ImageHelper', 'Messages',
function ($scope, $stateParams, $state, Image, ImageHelper, Messages) {
$scope.RepoTags = [];
$scope.config = {
Image: '',
Registry: ''
@ -20,31 +19,18 @@ function ($scope, $stateParams, $state, Image, 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 = createImageConfig(image, registry);
var imageConfig = ImageHelper.createImageConfig(image, registry);
Image.tag({id: $stateParams.id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) {
Messages.send('Image successfully tagged');
$('#loadingViewSpinner').hide();
$state.go('image', {id: $stateParams.id}, {reload: true});
}, function(e) {
$('#loadingViewSpinner').hide();
Messages.error("Unable to tag image", e.data);
Messages.error("Failure", e, "Unable to tag image");
});
};
@ -52,14 +38,14 @@ function ($scope, $stateParams, $state, Image, 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("Unable to push image", e.data);
Messages.error("Failure", e, "Unable to push image");
});
};
@ -68,7 +54,7 @@ function ($scope, $stateParams, $state, Image, 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
@ -82,7 +68,7 @@ function ($scope, $stateParams, $state, Image, Messages) {
}
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Unable to remove image", e.data);
Messages.error("Failure", e, 'Unable to remove image');
});
};
@ -98,10 +84,6 @@ function ($scope, $stateParams, $state, Image, Messages) {
$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);
}
Messages.error("Failure", e, "Unable to retrieve image info");
});
}]);

View File

@ -47,14 +47,14 @@ function ($scope, $state, Config, Image, Messages) {
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('Error', 'Unable to pull image ' + image);
Messages.error("Failure", e, "Unable to pull image");
});
};
@ -73,7 +73,7 @@ function ($scope, $state, Config, Image, Messages) {
Image.remove({id: i.Id}, function (d) {
if (d[0].message) {
$('#loadingViewSpinner').hide();
Messages.error("Unable to remove image", d[0].message);
Messages.error("Unable to remove image", {}, d[0].message);
} else {
Messages.send("Image deleted", i.Id);
var index = $scope.images.indexOf(i);
@ -81,7 +81,7 @@ function ($scope, $state, Config, Image, Messages) {
}
complete();
}, function (e) {
Messages.error("Unable to remove image", e.data);
Messages.error("Failure", e, 'Unable to remove image');
complete();
});
}
@ -95,8 +95,8 @@ function ($scope, $state, Config, Image, Messages) {
});
$('#loadImagesSpinner').hide();
}, function (e) {
Messages.error("Failure", e.data);
$('#loadImagesSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve images");
});
}
@ -104,5 +104,4 @@ function ($scope, $state, Config, Image, Messages) {
$scope.availableRegistries = c.registries;
fetchImages();
});
}]);

View File

@ -1,6 +1,6 @@
angular.module('network', [])
.controller('NetworkController', ['$scope', 'Network', 'Messages', '$state', '$stateParams', 'errorMsgFilter',
function ($scope, Network, Messages, $state, $stateParams, errorMsgFilter) {
.controller('NetworkController', ['$scope', 'Network', 'Messages', '$state', '$stateParams',
function ($scope, Network, Messages, $state, $stateParams) {
$scope.disconnect = function disconnect(networkId, containerId) {
$('#loadingViewSpinner').show();
@ -10,19 +10,24 @@ function ($scope, Network, Messages, $state, $stateParams, errorMsgFilter) {
$state.go('network', {id: $stateParams.id}, {reload: true});
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e.data);
Messages.error("Failure", e, "Unable to disconnect container");
});
};
$scope.remove = function remove(networkId) {
$('#loadingViewSpinner').show();
Network.remove({id: $stateParams.id}, function (d) {
$('#loadingViewSpinner').hide();
Messages.send("Network removed", "");
$state.go('networks', {});
if (d.message) {
$('#loadingViewSpinner').hide();
Messages.send("Error", {}, d.message);
} else {
$('#loadingViewSpinner').hide();
Messages.send("Network removed", $stateParams.id);
$state.go('networks', {});
}
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e.data);
Messages.error("Failure", e, "Unable to remove network");
});
};
@ -31,7 +36,7 @@ function ($scope, Network, Messages, $state, $stateParams, errorMsgFilter) {
$scope.network = d;
$('#loadingViewSpinner').hide();
}, function (e) {
Messages.error("Failure", e.data);
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve network info");
});
}]);

View File

@ -1,6 +1,6 @@
angular.module('networks', [])
.controller('NetworksController', ['$scope', '$state', 'Network', 'Config', 'Messages', 'errorMsgFilter',
function ($scope, $state, Network, Config, Messages, errorMsgFilter) {
.controller('NetworksController', ['$scope', '$state', 'Network', 'Config', 'Messages',
function ($scope, $state, Network, Config, Messages) {
$scope.state = {};
$scope.state.selectedItemCount = 0;
$scope.state.advancedSettings = false;
@ -45,17 +45,16 @@ function ($scope, $state, Network, Config, Messages, errorMsgFilter) {
if (network.Checked) {
counter = counter + 1;
Network.remove({id: network.Id}, function (d) {
var error = errorMsgFilter(d);
if (error) {
Messages.send("Error", "Unable to remove network with active endpoints");
if (d.message) {
Messages.send("Error", d.message);
} else {
Messages.send("Network deleted", network.Id);
Messages.send("Network removed", network.Id);
var index = $scope.networks.indexOf(network);
$scope.networks.splice(index, 1);
}
complete();
}, function (e) {
Messages.error("Failure", e.data);
Messages.error("Failure", e, 'Unable to remove network');
complete();
});
}
@ -68,8 +67,8 @@ function ($scope, $state, Network, Config, Messages, errorMsgFilter) {
$scope.networks = d;
$('#loadNetworksSpinner').hide();
}, function (e) {
Messages.error("Failure", e.data);
$('#loadNetworksSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve networks");
});
}

View File

@ -112,7 +112,7 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
return d[key];
});
if (arr.join('').indexOf('no such id') !== -1) {
Messages.error('Unable to retrieve stats', 'Is this container running?');
Messages.error('Unable to retrieve stats', {}, 'Is this container running?');
return;
}
@ -123,7 +123,7 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
updateNetworkChart(d);
timeout = $timeout(updateStats, 5000);
}, function () {
Messages.error('Unable to retrieve stats', 'Is this container running?');
Messages.error('Unable to retrieve stats', {}, 'Is this container running?');
timeout = $timeout(updateStats, 5000);
});
}
@ -188,7 +188,7 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
Container.get({id: $stateParams.id}, function (d) {
$scope.container = d;
}, function (e) {
Messages.error("Failure", e.data);
Messages.error("Failure", e, "Unable to retrieve container info");
});
$scope.getTop();
}]);

View File

@ -0,0 +1,81 @@
<rd-header>
<rd-header-title title="Application templates list">
<a data-toggle="tooltip" title="Refresh" ui-sref="templates" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>Templates</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-rocket" title="Available templates">
<div class="pull-right">
<i id="loadTemplatesSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
</div>
</rd-widget-header>
<rd-widget-body classes="padding">
<div class="template-list">
<div ng-repeat="tpl in templates" class="container-template hvr-grow" id="template_{{ $index }}" ng-click="selectTemplate($index)">
<img class="logo" ng-src="{{ tpl.logo }}" />
<div class="title">{{ tpl.title }}</div>
<div class="description">{{ tpl.description }}</div>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="selectedTemplate">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-cogs" title="Configuration"></rd-widget-header>
<rd-widget-body classes="padding">
<form class="form-horizontal">
<div class="form-group" ng-if="globalNetworkCount === 0">
<div class="col-sm-12">
<span class="small text-muted">When using Swarm, we recommend deploying containers in a shared network. Looks like you don't have any shared network, head over the <a ui-sref="networks">networks view</a> to create one.</span>
</div>
</div>
<!-- name-and-network-inputs -->
<div class="form-group">
<label for="image_registry" class="col-sm-2 control-label text-left">Name</label>
<div class="col-sm-4">
<input type="text" class="form-control" ng-model="formValues.name" placeholder="e.g. web (optional)">
</div>
<label for="container_network" class="col-sm-2 control-label text-right">Network</label>
<div class="col-sm-4">
<select class="selectpicker form-control" ng-options="net.Name for net in availableNetworks" ng-model="formValues.network">
<option disabled hidden value="">Select a network</option>
</select>
</div>
</div>
<!-- !name-and-network-inputs -->
<div ng-repeat="var in selectedTemplate.env" ng-if="!var.set" class="form-group">
<label for="field_{{ $index }}" class="col-sm-2 control-label text-left">{{ var.label }}</label>
<div class="col-sm-10">
<select ng-if="!swarm && var.type === 'container'" ng-options="container|containername for container in runningContainers" class="selectpicker form-control" ng-model="var.value">
<option selected disabled hidden value="">Select a container</option>
</select>
<select ng-if="swarm && var.type === 'container'" ng-options="container|swarmcontainername for container in runningContainers" class="selectpicker form-control" ng-model="var.value">
<option selected disabled hidden value="">Select a container</option>
</select>
<input ng-if="!var.type || !var.type === 'container'" type="text" class="form-control" ng-model="var.value" id="field_{{ $index }}">
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="selectedTemplate">
<div class="col-lg-12 col-md-12 col-xs-12" style="text-align: center;">
<div>
<i id="createContainerSpinner" class="fa fa-cog fa-3x fa-spin" style="margin-bottom: 5px; display: none;"></i>
</div>
<button type="button" class="btn btn-default btn-lg" ng-disabled="!formValues.network" ng-click="createTemplate()">Create</button>
</div>
</div>

View File

@ -0,0 +1,199 @@
angular.module('templates', [])
.controller('TemplatesController', ['$scope', '$q', '$state', '$filter', 'Config', 'Container', 'ContainerHelper', 'Image', 'Volume', 'Network', 'Templates', 'Messages',
function ($scope, $q, $state, $filter, Config, Container, ContainerHelper, Image, Volume, Network, Templates, Messages) {
$scope.templates = [];
$scope.selectedTemplate = null;
$scope.formValues = {
network: "",
name: ""
};
var selectedItem = -1;
// TODO: centralize, already present in createContainerController
function createContainer(config) {
Container.create(config, function (d) {
if (d.message) {
$('#createContainerSpinner').hide();
Messages.error('Error', {}, d.message);
} else {
Container.start({id: d.Id}, {}, function (cd) {
if (cd.message) {
$('#createContainerSpinner').hide();
Messages.error('Error', {}, cd.message);
} else {
$('#createContainerSpinner').hide();
Messages.send('Container Started', d.Id);
$state.go('containers', {}, {reload: true});
}
}, function (e) {
$('#createContainerSpinner').hide();
Messages.error("Failure", e, 'Unable to start container');
});
}
}, function (e) {
$('#createContainerSpinner').hide();
Messages.error("Failure", e, 'Unable to create container');
});
}
// TODO: centralize, already present in createContainerController
function pullImageAndCreateContainer(imageConfig, containerConfig) {
Image.create(imageConfig, function (data) {
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
if (err) {
var detail = data[data.length - 1];
$('#createContainerSpinner').hide();
Messages.error("Error", {}, detail.error);
} else {
createContainer(containerConfig);
}
}, function (e) {
$('#createContainerSpinner').hide();
Messages.error("Failure", e, "Unable to pull image");
});
}
function getInitialConfiguration() {
return {
Env: [],
OpenStdin: false,
Tty: false,
ExposedPorts: {},
HostConfig: {
RestartPolicy: {
Name: 'no'
},
PortBindings: {},
Binds: [],
NetworkMode: $scope.formValues.network.Name,
Privileged: false
},
Volumes: {},
name: $scope.formValues.name
};
}
function createConfigFromTemplate(template) {
var containerConfig = getInitialConfiguration();
containerConfig.Image = template.image;
if (template.env) {
template.env.forEach(function (v) {
if (v.value || v.set) {
var val;
if (v.type && v.type === 'container') {
if ($scope.swarm && $scope.formValues.network.Scope === 'global') {
val = $filter('swarmcontainername')(v.value);
} else {
var container = v.value;
val = container.NetworkSettings.Networks[Object.keys(container.NetworkSettings.Networks)[0]].IPAddress;
}
} else {
val = v.set ? v.set : v.value;
}
containerConfig.Env.push(v.name + "=" + val);
}
});
}
if (template.ports) {
template.ports.forEach(function (p) {
containerConfig.ExposedPorts[p] = {};
containerConfig.HostConfig.PortBindings[p] = [{ HostPort: ""}];
});
}
return containerConfig;
}
function prepareVolumeQueries(template, containerConfig) {
var volumeQueries = [];
if (template.volumes) {
template.volumes.forEach(function (vol) {
volumeQueries.push(
Volume.create({}, function (d) {
if (d.message) {
Messages.error("Unable to create volume", {}, d.message);
} else {
Messages.send("Volume created", d.Name);
containerConfig.Volumes[vol] = {};
containerConfig.HostConfig.Binds.push(d.Name + ':' + vol);
}
}, function (e) {
Messages.error("Failure", e, "Unable to create volume");
}).$promise
);
});
}
return volumeQueries;
}
$scope.createTemplate = function() {
$('#createContainerSpinner').show();
var template = $scope.selectedTemplate;
var containerConfig = createConfigFromTemplate(template);
var imageConfig = {
fromImage: template.image.split(':')[0],
tag: template.image.split(':')[1] ? template.image.split(':')[1] : 'latest'
};
var createVolumeQueries = prepareVolumeQueries(template, containerConfig);
$q.all(createVolumeQueries).then(function (d) {
pullImageAndCreateContainer(imageConfig, containerConfig);
});
};
$scope.selectTemplate = function(id) {
$('#template_' + id).toggleClass("container-template--selected");
if (selectedItem === id) {
selectedItem = -1;
$scope.selectedTemplate = null;
} else {
$('#template_' + selectedItem).toggleClass("container-template--selected");
selectedItem = id;
$scope.selectedTemplate = $scope.templates[id];
}
};
function initTemplates() {
Templates.get(function (data) {
$scope.templates = data;
$('#loadTemplatesSpinner').hide();
}, function (e) {
$('#loadTemplatesSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve apps list");
});
}
Config.$promise.then(function (c) {
$scope.swarm = c.swarm;
var containersToHideLabels = c.hiddenLabels;
Network.query({}, function (d) {
var networks = d;
if ($scope.swarm) {
networks = d.filter(function (network) {
if (network.Scope === 'global') {
return network;
}
});
$scope.globalNetworkCount = networks.length;
networks.push({Scope: "local", Name: "bridge"});
networks.push({Scope: "local", Name: "host"});
networks.push({Scope: "local", Name: "none"});
} else {
$scope.formValues.network = _.find(networks, function(o) { return o.Name === "bridge"; });
}
$scope.availableNetworks = networks;
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve networks");
});
Container.query({all: 0}, function (d) {
var containers = d;
if (containersToHideLabels) {
containers = ContainerHelper.hideContainers(d, containersToHideLabels);
}
$scope.runningContainers = containers;
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve running containers");
});
initTemplates();
});
}]);

View File

@ -1,6 +1,6 @@
angular.module('volumes', [])
.controller('VolumesController', ['$scope', '$state', 'Volume', 'Messages', 'errorMsgFilter',
function ($scope, $state, Volume, Messages, errorMsgFilter) {
.controller('VolumesController', ['$scope', '$state', 'Volume', 'Messages',
function ($scope, $state, Volume, Messages) {
$scope.state = {};
$scope.state.selectedItemCount = 0;
$scope.sortType = 'Name';
@ -41,7 +41,7 @@ function ($scope, $state, Volume, Messages, errorMsgFilter) {
$scope.volumes.splice(index, 1);
complete();
}, function (e) {
Messages.error("Failure", e.data);
Messages.error("Failure", e, "Unable to remove volume");
complete();
});
}
@ -54,8 +54,8 @@ function ($scope, $state, Volume, Messages, errorMsgFilter) {
$scope.volumes = d.Volumes;
$('#loadVolumesSpinner').hide();
}, function (e) {
Messages.error("Failure", e.data);
$('#loadVolumesSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve volumes");
});
}
fetchVolumes();

View File

@ -1,5 +1,5 @@
angular
.module('uifordocker')
.module('portainer')
.directive('rdHeaderContent', function rdHeaderContent() {
var directive = {
requires: '^rdHeader',

View File

@ -1,5 +1,5 @@
angular
.module('uifordocker')
.module('portainer')
.directive('rdHeaderTitle', function rdHeaderTitle() {
var directive = {
requires: '^rdHeader',

View File

@ -1,5 +1,5 @@
angular
.module('uifordocker')
.module('portainer')
.directive('rdHeader', function rdHeader() {
var directive = {
scope: {

View File

@ -1,5 +1,5 @@
angular
.module('uifordocker')
.module('portainer')
.directive('rdLoading', function rdLoading() {
var directive = {
restrict: 'AE',

View File

@ -1,5 +1,5 @@
angular
.module('uifordocker')
.module('portainer')
.directive('rdWidgetBody', function rdWidgetBody() {
var directive = {
requires: '^rdWidget',

View File

@ -1,5 +1,5 @@
angular
.module('uifordocker')
.module('portainer')
.directive('rdWidgetFooter', function rdWidgetFooter() {
var directive = {
requires: '^rdWidget',

View File

@ -1,5 +1,5 @@
angular
.module('uifordocker')
.module('portainer')
.directive('rdWidgetHeader', function rdWidgetTitle() {
var directive = {
requires: '^rdWidget',

View File

@ -1,5 +1,5 @@
angular
.module('uifordocker')
.module('portainer')
.directive('rdWidgetTaskbar', function rdWidgetTaskbar() {
var directive = {
requires: '^rdWidget',

View File

@ -1,5 +1,5 @@
angular
.module('uifordocker')
.module('portainer')
.directive('rdWidget', function rdWidget() {
var directive = {
scope: {

View File

@ -1,4 +1,4 @@
angular.module('uifordocker.filters', [])
angular.module('portainer.filters', [])
.filter('truncate', function () {
'use strict';
return function (text, length, end) {
@ -176,24 +176,19 @@ angular.module('uifordocker.filters', [])
})
.filter('key', function () {
'use strict';
return function (pair) {
return pair.slice(0, pair.indexOf('='));
return function (pair, separator) {
return pair.slice(0, pair.indexOf(separator));
};
})
.filter('value', function () {
'use strict';
return function (pair) {
return pair.slice(pair.indexOf('=') + 1);
return function (pair, separator) {
return pair.slice(pair.indexOf(separator) + 1);
};
})
.filter('errorMsg', function () {
return function (object) {
var idx = 0;
var msg = '';
while (object[idx] && typeof(object[idx]) === 'string') {
msg += object[idx];
idx++;
}
return msg;
.filter('emptyobject', function () {
'use strict';
return function (obj) {
return _.isEmpty(obj);
};
});

37
app/shared/helpers.js Normal file
View File

@ -0,0 +1,37 @@
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;
}
});
}
};
}]);

View File

@ -1,3 +1,18 @@
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.
@ -6,14 +21,48 @@ function jsonObjectsToArrayHandler(data) {
return angular.fromJson(str);
}
// Image delete API returns an array on success and an object on error.
// This handler creates an array from an object in case of error.
function deleteImageHandler(data) {
var response = angular.fromJson(data);
if (!Array.isArray(response)) {
var arr = [];
arr.push(response);
return arr;
// The Docker API often returns an empty string or a valid JSON object on success (Docker 1.9 -> Docker 1.12).
// On error, it returns either an error message as a string (Docker < 1.12) or a JSON object with the field message
// container the error (Docker = 1.12)
// This handler ensure a valid JSON object is returned in any case.
// Used by the API in: container deletion, network deletion, network creation, volume creation,
// container exec, exec resize.
function genericHandler(data) {
var response = {};
// No data is returned when deletion is successful (Docker 1.9 -> 1.12)
if (!data) {
return response;
}
// A string is returned on failure (Docker < 1.12)
else if (!isJSON(data)) {
response.message = data;
}
// Docker 1.12 returns a valid JSON object when an error occurs
else {
response = angular.fromJson(data);
}
return response;
}
// Image delete API returns an array on success (Docker 1.9 -> Docker 1.12).
// On error, it returns either an error message as a string (Docker < 1.12) or a JSON object with the field message
// container the error (Docker = 1.12).
// This handler returns the original array on success or a newly created array containing
// only one JSON object with the field message filled with the error message on failure.
function deleteImageHandler(data) {
var response = [];
// A string is returned on failure (Docker < 1.12)
if (!isJSON(data)) {
response.push({message: data});
}
// A JSON object is returned on failure (Docker = 1.12)
else if (!isJSONArray) {
var json = angular.fromJson(data);
response.push(json);
}
// An array is returned on success (Docker 1.9 -> 1.12)
else {
response = angular.fromJson(data);
}
return response;
}

View File

@ -1,4 +1,4 @@
angular.module('uifordocker.services', ['ngResource', 'ngSanitize'])
angular.module('portainer.services', ['ngResource', 'ngSanitize'])
.factory('Container', ['$resource', 'Settings', function ContainerFactory($resource, Settings) {
'use strict';
// Resource for interacting with the docker containers
@ -8,46 +8,51 @@ angular.module('uifordocker.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},
exec: {method: 'POST', params: {id: '@id', action: 'exec'}}
start: {
method: 'POST', params: {id: '@id', action: 'start'},
transformResponse: genericHandler
},
create: {
method: 'POST', params: {action: 'create'},
transformResponse: genericHandler
},
remove: {
method: 'DELETE', params: {id: '@id', v: 0},
transformResponse: genericHandler
},
rename: {
method: 'POST', params: {id: '@id', action: 'rename', name: '@name'},
transformResponse: genericHandler
},
exec: {
method: 'POST', params: {id: '@id', action: 'exec'},
transformResponse: genericHandler
}
});
}])
.factory('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'}}
resize: {
method: 'POST', params: {id: '@id', action: 'resize', h: '@height', w: '@width'},
transformResponse: genericHandler
}
});
}])
.factory('ContainerCommit', ['$resource', '$http', 'Settings', function ContainerCommitFactory($resource, $http, Settings) {
'use strict';
// http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#create-a-new-image-from-a-container-s-changes
return {
commit: function (params, callback) {
$http({
method: 'POST',
url: Settings.url + '/commit',
params: {
'container': params.id,
'tag': params.tag || null,
'repo': params.repo || null
},
data: params.config
}).success(callback).error(function (data, status, headers, config) {
console.log(error, data);
});
}
};
return $resource(Settings.url + '/commit', {}, {
commit: {method: 'POST', params: {container: '@id', repo: '@repo', tag: '@tag'}}
});
}])
.factory('ContainerLogs', ['$resource', '$http', 'Settings', function ContainerLogsFactory($resource, $http, Settings) {
'use strict';
@ -147,8 +152,8 @@ angular.module('uifordocker.services', ['ngResource', 'ngSanitize'])
return $resource(Settings.url + '/networks/:id/:action', {id: '@id'}, {
query: {method: 'GET', isArray: true},
get: {method: 'GET'},
create: {method: 'POST', params: {action: 'create'}},
remove: {method: 'DELETE'},
create: {method: 'POST', params: {action: 'create'}, transformResponse: genericHandler},
remove: { method: 'DELETE', transformResponse: genericHandler },
connect: {method: 'POST', params: {action: 'connect'}},
disconnect: {method: 'POST', params: {action: 'disconnect'}}
});
@ -159,13 +164,18 @@ angular.module('uifordocker.services', ['ngResource', 'ngSanitize'])
return $resource(Settings.url + '/volumes/:name/:action', {name: '@name'}, {
query: {method: 'GET'},
get: {method: 'GET'},
create: {method: 'POST', params: {action: 'create'}},
create: {method: 'POST', params: {action: 'create'}, transformResponse: genericHandler},
remove: {method: 'DELETE'}
});
}])
.factory('Config', ['$resource', 'CONFIG_ENDPOINT', function ConfigFactory($resource, CONFIG_ENDPOINT) {
return $resource(CONFIG_ENDPOINT).get();
}])
.factory('Templates', ['$resource', 'TEMPLATES_ENDPOINT', function TemplatesFactory($resource, TEMPLATES_ENDPOINT) {
return $resource(TEMPLATES_ENDPOINT, {}, {
get: {method: 'GET', isArray: true}
});
}])
.factory('Settings', ['DOCKER_ENDPOINT', 'DOCKER_PORT', 'UI_VERSION', function SettingsFactory(DOCKER_ENDPOINT, DOCKER_PORT, UI_VERSION) {
'use strict';
var url = DOCKER_ENDPOINT;
@ -196,10 +206,17 @@ angular.module('uifordocker.services', ['ngResource', 'ngSanitize'])
}
});
},
error: function (title, text) {
error: function (title, e, fallbackText) {
console.log(JSON.stringify(e, null, 4));
var msg = fallbackText;
if (e.data && e.data.message) {
msg = e.data.message;
} else if (e.message) {
msg = e.message;
}
$.gritter.add({
title: $sanitize(title),
text: $sanitize(text),
text: $sanitize(msg),
time: 10000,
before_open: function () {
if ($('.gritter-item-wrapper').length === 4) {

View File

@ -19,6 +19,13 @@ function ContainerViewModel(data) {
this.Image = data.Image;
this.Command = data.Command;
this.Checked = false;
this.Ports = [];
for (var i = 0; i < data.Ports.length; ++i) {
var p = data.Ports[i];
if (p.PublicPort) {
this.Ports.push({ host: p.IP, private: p.PrivatePort, public: p.PublicPort });
}
}
}
function createEventDetails(event) {

View File

@ -202,3 +202,56 @@ input[type="radio"] {
.interactive {
cursor: pointer;
}
.action-group {
margin: 10px;
}
.btn-ico {
margin-right: 5px;
}
.template-list {
display: flex;
flex-wrap: wrap;
}
.container-template {
font-size: 1em;
width: 256px;
height: 128px;
margin: 10px;
padding: 10px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-radius: 10px;
cursor: pointer;
border: 2px solid #f6f6f6;
color: #30426a;
}
.container-template--selected {
background-color: #f6f6f6;
color: #2d3e63;
}
.container-template:hover {
background-color: #f6f6f6;
color: #2d3e63;
}
.container-template .logo {
max-width: 48px;
max-height: 48px;
}
.container-template .title {
text-align: center;
}
.container-template .description {
text-align: center;
font-size: 0.8em;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -1,5 +1,5 @@
upstream dockerui {
server dockerui:9000;
upstream portainer {
server portainer:9000;
}
server {
@ -12,6 +12,6 @@ server {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://dockerui;
proxy_pass http://portainer;
}
}

View File

@ -1,10 +1,10 @@
dockerui:
image: cloudinovasi/ui-for-docker
portainer:
image: cloudinovasi/portainer
command: -e http://<SWARM_HOST>:<SWARM_PORT>
nginx:
build: .
links:
- dockerui
- portainer
ports:
- 80:80

View File

@ -87,12 +87,13 @@ module.exports = function (grunt) {
'bower_components/font-awesome/css/font-awesome.min.css',
'bower_components/rdash-ui/dist/css/rdash.min.css',
'bower_components/angular-ui-select/dist/select.min.css',
'bower_components/xterm.js/src/xterm.css'
'bower_components/xterm.js/src/xterm.css',
'bower_components/Hover/css/hover-min.css'
]
},
clean: {
all: ['<%= distdir %>/*'],
app: ['<%= distdir %>/*', '!<%= distdir %>/ui-for-docker'],
app: ['<%= distdir %>/*', '!<%= distdir %>/portainer'],
tmpl: ['<%= distdir %>/templates']
},
copy: {
@ -252,35 +253,35 @@ module.exports = function (grunt) {
},
shell: {
buildImage: {
command: 'docker build --rm -t ui-for-docker .'
command: 'docker build --rm -t portainer .'
},
buildBinary: {
command: [
'docker run --rm -v $(pwd)/api:/src centurylink/golang-builder',
'shasum api/ui-for-docker > ui-for-docker-checksum.txt',
'shasum api/portainer > portainer-checksum.txt',
'mkdir -p dist',
'mv api/ui-for-docker dist/'
'mv api/portainer dist/'
].join(' && ')
},
run: {
command: [
'docker stop ui-for-docker',
'docker rm ui-for-docker',
'docker run --privileged -d -p 9000:9000 -v /tmp/docker-ui:/data -v /var/run/docker.sock:/var/run/docker.sock --name ui-for-docker ui-for-docker -d /data'
'docker stop portainer',
'docker rm portainer',
'docker run --privileged -d -p 9000:9000 -v /tmp/portainer:/data -v /var/run/docker.sock:/var/run/docker.sock --name portainer portainer -d /data'
].join(';')
},
runSwarm: {
command: [
'docker stop ui-for-docker',
'docker rm ui-for-docker',
'docker run -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'
'docker stop portainer',
'docker rm portainer',
'docker run -d -p 9000:9000 -v /tmp/portainer:/data --name portainer portainer -H tcp://10.0.7.10:4000 --swarm -d /data'
].join(';')
},
runSsl: {
command: [
'docker stop ui-for-docker',
'docker rm ui-for-docker',
'docker run -d -p 9000:9000 -v /tmp/docker-ui:/data -v /tmp/docker-ssl:/certs --name ui-for-docker ui-for-docker -H tcp://10.0.7.10:2376 -d /data --tlsverify'
'docker stop portainer',
'docker rm portainer',
'docker run -d -p 9000:9000 -v /tmp/portainer:/data -v /tmp/docker-ssl:/certs --name portainer portainer -H tcp://10.0.7.10:2376 -d /data --tlsverify'
].join(';')
},
cleanImages: {
@ -290,7 +291,7 @@ module.exports = function (grunt) {
'if': {
binaryNotExist: {
options: {
executable: 'dist/ui-for-docker'
executable: 'dist/portainer'
},
ifFalse: ['shell:buildBinary']
}

View File

@ -2,7 +2,7 @@
<html lang="en" ng-app="<%= pkg.name %>">
<head>
<meta charset="utf-8">
<title>CloudInovasi UI for Docker</title>
<title>Portainer</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="<%= pkg.author %>">
@ -32,8 +32,8 @@
<ul class="sidebar">
<li class="sidebar-main">
<a ng-click="toggleSidebar()">
<img ng-if="config.logo" ng-src="{{ config.logo }}" class="img-responsive logo" alt="CloudInovasi UI">
<img ng-if="!config.logo" src="images/logo.png" class="img-responsive logo" alt="CloudInovasi UI">
<img ng-if="config.logo" ng-src="{{ config.logo }}" class="img-responsive logo">
<img ng-if="!config.logo" src="images/logo.png" class="img-responsive logo" alt="Portainer">
<span class="menu-icon glyphicon glyphicon-transfer"></span>
</a>
</li>
@ -41,6 +41,9 @@
<li class="sidebar-list">
<a ui-sref="index">Dashboard <span class="menu-icon fa fa-tachometer"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="templates">App Templates <span class="menu-icon fa fa-rocket"></span></a>
</li>
<li class="sidebar-list">
<a ui-sref="containers">Containers <span class="menu-icon fa fa-server"></span></a>
</li>
@ -65,7 +68,7 @@
</ul>
<div class="sidebar-footer">
<div class="col-xs-12">
<a href="https://github.com/cloud-inovasi/cloudinovasi-ui" target="_blank">CloudInovasi UI {{ uiVersion }}</a>
<a href="https://github.com/cloud-inovasi/portainer" target="_blank">Portainer {{ uiVersion }}</a>
</div>
</div>
</div>

View File

@ -1,19 +1,19 @@
{
"author": "Michael Crosby & Kevan Ahlquist",
"name": "uifordocker",
"homepage": "https://github.com/kevana/ui-for-docker",
"version": "1.7.0",
"author": "Cloud Inovasi",
"name": "portainer",
"homepage": "https://github.com/cloud-inovasi/portainer",
"version": "1.8.0",
"repository": {
"type": "git",
"url": "git@github.com:kevana/ui-for-docker.git"
"url": "git@github.com:cloud-inovasi/portainer.git"
},
"bugs": {
"url": "https://github.com/kevana/ui-for-docker/issues"
"url": "https://github.com/cloud-inovasi/portainer/issues"
},
"licenses": [
{
"type": "MIT",
"url": "https://raw.githubusercontent.com/kevana/ui-for-docker/master/LICENSE"
"url": "https://raw.githubusercontent.com/cloud-inovasi/portainer/develop/LICENSE"
}
],
"engines": {

View File

@ -1,7 +1,7 @@
describe('ContainerController', function () {
var $scope, $httpBackend, mockContainer, $routeParams;
beforeEach(module('dockerui'));
beforeEach(module('portainer'));
beforeEach(inject(function ($rootScope, $controller, _$routeParams_) {
@ -22,8 +22,8 @@ describe('ContainerController', function () {
$httpBackend.expectGET('dockerapi/containers/json').respond({
'Created': 1421817232,
'id': 'b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f',
'Image': 'dockerui:latest',
'Name': '/dockerui'
'Image': 'portainer:latest',
'Name': '/portainer'
});
}
@ -33,8 +33,8 @@ describe('ContainerController', function () {
$scope.container = {
'Created': 1421817232,
'id': 'b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f',
'Image': 'dockerui:latest',
'Name': '/dockerui'
'Image': 'portainer:latest',
'Name': '/portainer'
};
$scope.container.newContainerName = "newName";

View File

@ -1,7 +1,7 @@
describe("ContainerTopController", function () {
var $scope, $httpBackend, $routeParams;
beforeEach(angular.mock.module('dockerui'));
beforeEach(angular.mock.module('portainer'));
beforeEach(inject(function (_$rootScope_, _$httpBackend_, $controller, _$routeParams_) {
$scope = _$rootScope_.$new();

View File

@ -1,7 +1,7 @@
describe('NetworkController', function () {
var $scope, $httpBackend, $routeParams;
beforeEach(module('dockerui'));
beforeEach(module('portainer'));
beforeEach(inject(function (_$httpBackend_, $controller, _$routeParams_) {
$scope = {};
$httpBackend = _$httpBackend_;

View File

@ -1,7 +1,7 @@
describe('NetworksController', function () {
var $scope, $httpBackend, $routeParams;
beforeEach(module('dockerui'));
beforeEach(module('portainer'));
beforeEach(inject(function (_$httpBackend_, $controller, _$routeParams_) {
$scope = {};
$httpBackend = _$httpBackend_;

View File

@ -1,7 +1,7 @@
describe('startContainerController', function () {
var scope, $location, createController, mockContainer, $httpBackend;
beforeEach(angular.mock.module('dockerui'));
beforeEach(angular.mock.module('portainer'));
beforeEach(inject(function ($rootScope, $controller, _$location_) {
$location = _$location_;
@ -20,11 +20,11 @@ describe('startContainerController', function () {
}));
function expectGetContainers() {
$httpBackend.expectGET('dockerapi/containers/json?all=1').respond([{
'Command': './dockerui -e /docker.sock',
'Command': './portainer -e /docker.sock',
'Created': 1421817232,
'Id': 'b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f',
'Image': 'dockerui:latest',
'Names': ['/dockerui'],
'Image': 'portainer:latest',
'Names': ['/portainer'],
'Ports': [{
'IP': '0.0.0.0',
'PrivatePort': 9000,

View File

@ -1,7 +1,7 @@
describe("StatsController", function () {
var $scope, $httpBackend, $routeParams;
beforeEach(angular.mock.module('dockerui'));
beforeEach(angular.mock.module('portainer'));
beforeEach(inject(function (_$rootScope_, _$httpBackend_, $controller, _$routeParams_) {
$scope = _$rootScope_.$new();

View File

@ -1,7 +1,7 @@
describe('VolumesController', function () {
var $scope, $httpBackend, $routeParams;
beforeEach(module('dockerui'));
beforeEach(module('portainer'));
beforeEach(inject(function (_$httpBackend_, $controller, _$routeParams_) {
$scope = {};
$httpBackend = _$httpBackend_;

View File

@ -1,5 +1,5 @@
describe('filters', function () {
beforeEach(module('dockerui.filters'));
beforeEach(module('portainer.filters'));
describe('truncate', function () {
it('should truncate the string to 10 characters ending in "..." by default', inject(function (truncateFilter) {
@ -133,203 +133,4 @@ 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);
}));
});
});

View File

@ -7,7 +7,7 @@ files = [
JASMINE_ADAPTER,
'dist/angular.js',
'dist/vendor.js',
'dist/dockerui.js',
'dist/portainer.js',
'bower_components/angular-mocks/angular-mocks.js',
'test/unit/**/*.spec.js'
];