├── .dockerignore ├── .gitignore ├── .godir ├── Dockerfile ├── LICENSE ├── Procfile ├── README.md ├── api ├── api.go ├── csrf.go ├── flags.go ├── handler.go ├── ssl.go └── unix_handler.go ├── app ├── app.js ├── components │ ├── builder │ │ ├── builder.html │ │ └── builderController.js │ ├── container │ │ ├── container.html │ │ └── containerController.js │ ├── containerLogs │ │ ├── containerLogsController.js │ │ └── containerlogs.html │ ├── containerTop │ │ ├── containerTop.html │ │ └── containerTopController.js │ ├── containers │ │ ├── containers.html │ │ └── containersController.js │ ├── containersNetwork │ │ ├── containersNetwork.html │ │ └── containersNetworkController.js │ ├── dashboard │ │ ├── dashboard.html │ │ └── dashboardController.js │ ├── events │ │ ├── events.html │ │ └── eventsController.js │ ├── footer │ │ ├── footerController.js │ │ └── statusbar.html │ ├── image │ │ ├── image.html │ │ └── imageController.js │ ├── images │ │ ├── images.html │ │ └── imagesController.js │ ├── info │ │ ├── info.html │ │ └── infoController.js │ ├── masthead │ │ ├── masthead.html │ │ └── mastheadController.js │ ├── network │ │ ├── network.html │ │ └── networkController.js │ ├── networks │ │ ├── networks.html │ │ └── networksController.js │ ├── pullImage │ │ ├── pullImage.html │ │ └── pullImageController.js │ ├── sidebar │ │ ├── sidebar.html │ │ └── sidebarController.js │ ├── startContainer │ │ ├── startContainerController.js │ │ └── startcontainer.html │ ├── stats │ │ ├── stats.html │ │ └── statsController.js │ └── volumes │ │ ├── volumes.html │ │ └── volumesController.js └── shared │ ├── filters.js │ ├── services.js │ └── viewmodel.js ├── assets ├── css │ └── app.css ├── ico │ ├── apple-touch-icon-precomposed.png │ └── favicon.ico └── js │ ├── .jshintrc │ ├── jquery.gritter.js │ └── legend.js ├── bower.json ├── container.png ├── containers.png ├── examples ├── nginx-basic-auth │ ├── Dockerfile │ ├── default.conf │ ├── docker-compose.yml │ └── users.htpasswd └── swarm │ ├── Dockerfile │ ├── README.md │ └── docker-compose.yml ├── gruntFile.js ├── index.html ├── package.json └── test └── unit ├── app ├── components │ ├── containerController.spec.js │ ├── containerTopController.spec.js │ ├── networkController.spec.js │ ├── networksController.spec.js │ ├── startContainerController.spec.js │ ├── statsController.spec.js │ └── volumesController.spec.js └── shared │ └── filters.spec.js └── karma.conf.js /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !dist 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs/* 2 | !.gitkeep 3 | *.esproj/* 4 | node_modules 5 | bower_components 6 | .idea 7 | dist 8 | dockerui 9 | *.iml 10 | -------------------------------------------------------------------------------- /.godir: -------------------------------------------------------------------------------- 1 | dockerui 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | COPY dist / 4 | 5 | VOLUME /data 6 | 7 | EXPOSE 9000 8 | ENTRYPOINT ["/ui-for-docker"] 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: dockerui -p ":$PORT" -e "$DOCKER_ENDPOINT" 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## UI For Docker 2 | 3 | >This repo is deprecated. Development continues at Portainer: [github.com/portainer/portainer](https://github.com/portainer/portainer) 4 | 5 | 6 | [![Join the chat at https://gitter.im/kevana/ui-for-docker](https://badges.gitter.im/kevana/ui-for-docker.svg)](https://gitter.im/kevana/ui-for-docker?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 7 | 8 | ![Containers](/containers.png) 9 | 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. 10 | 11 | ![Container](/container.png) 12 | 13 | 14 | ### Goals 15 | * Minimal dependencies - I really want to keep this project a pure html/js app. 16 | * Consistency - The web UI should be consistent with the commands found on the docker CLI. 17 | 18 | ### Quickstart 19 | 1. Run: `docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock uifd/ui-for-docker` 20 | 21 | 2. Open your browser to `http://:9000` 22 | 23 | 24 | 25 | Bind mounting the Unix socket into the UI For Docker container is much more secure than exposing your docker daemon over TCP. The `--privileged` flag is required for hosts using SELinux. You should still secure your UI For Docker instance behind some type of auth. Directions for using Nginx auth are [here](https://github.com/kevana/ui-for-docker/wiki/UI-for-Docker-with-Nginx-HTTP-Auth). 26 | 27 | ### Specify socket to connect to Docker daemon 28 | 29 | 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`. 30 | 31 | You can use the `-H` flag to change this socket: 32 | 33 | # Connect to a tcp socket: 34 | $ docker run -d -p 9000:9000 --privileged uifd/ui-for-docker -H tcp://127.0.0.1:2375 35 | 36 | ### Change address/port UI For Docker is served on 37 | 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: 38 | 39 | # Expose UI For Docker on 10.20.30.1:80 40 | $ docker run -d -p 10.20.30.1:80:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock uifd/ui-for-docker 41 | 42 | ### Access a Docker engine protected via TLS 43 | 44 | Ensure that you have access to the CA, the TLS certificate and the TLS key used to access your Docker engine. 45 | 46 | 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: 47 | 48 | ``` 49 | $ docker run -d -p 9000:9000 uifd/ui-for-docker -v /path/to/certs:/certs -H tcp://my-docker-host.domain:2376 -tlsverify 50 | ``` 51 | 52 | If you want to specify different names for the CA, certificate and public key respectively you can use the `-tlscacert`, `-tlscert` and `-tlskey`: 53 | 54 | ``` 55 | $ docker run -d -p 9000:9000 uifd/ui-for-docker -v /path/to/certs:/certs -H tcp://my-docker-host.domain:2376 -tlsverify -tlscacert /certs/myCA.pem -tlscert /certs/myCert.pem -tlskey /certs/myKey.pem 56 | ``` 57 | 58 | *Note*: Replace `/path/to/certs` to the path to the certificate files on your disk. 59 | 60 | ### Check the [wiki](https://github.com/kevana/ui-for-docker/wiki) for more info about using UI For Docker 61 | 62 | ### Stack 63 | * [Angular.js](https://github.com/angular/angular.js) 64 | * [Bootstrap](http://getbootstrap.com/) 65 | * [Gritter](https://github.com/jboesch/Gritter) 66 | * [Spin.js](https://github.com/fgnass/spin.js/) 67 | * [Golang](https://golang.org/) 68 | * [Vis.js](http://visjs.org/) 69 | 70 | 71 | ### Todo: 72 | * Full repository support 73 | * Search 74 | * Push files to a container 75 | * Unit tests 76 | 77 | 78 | ### License - MIT 79 | The UI For Docker code is licensed under the MIT license. 80 | 81 | 82 | **UI For Docker:** 83 | Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com) 84 | 85 | Permission is hereby granted, free of charge, to any person 86 | obtaining a copy of this software and associated documentation 87 | files (the "Software"), to deal in the Software without 88 | restriction, including without limitation the rights to use, copy, 89 | modify, merge, publish, distribute, sublicense, and/or sell copies 90 | of the Software, and to permit persons to whom the Software is 91 | furnished to do so, subject to the following conditions: 92 | 93 | The above copyright notice and this permission notice shall be 94 | included in all copies or substantial portions of the Software. 95 | 96 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 97 | EXPRESS OR IMPLIED, 98 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 99 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 100 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 101 | HOLDERS BE LIABLE FOR ANY CLAIM, 102 | DAMAGES OR OTHER LIABILITY, 103 | WHETHER IN AN ACTION OF CONTRACT, 104 | TORT OR OTHERWISE, 105 | ARISING FROM, OUT OF OR IN CONNECTION WITH 106 | THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 107 | -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | package main // import "github.com/kevana/ui-for-docker" 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | func main() { 10 | var ( 11 | endpoint = flag.String("H", "unix:///var/run/docker.sock", "Dockerd endpoint") 12 | addr = flag.String("p", ":9000", "Address and port to serve UI For Docker") 13 | assets = flag.String("a", ".", "Path to the assets") 14 | data = flag.String("d", ".", "Path to the data") 15 | tlsverify = flag.Bool("tlsverify", false, "TLS support") 16 | tlscacert = flag.String("tlscacert", "/certs/ca.pem", "Path to the CA") 17 | tlscert = flag.String("tlscert", "/certs/cert.pem", "Path to the TLS certificate file") 18 | tlskey = flag.String("tlskey", "/certs/key.pem", "Path to the TLS key") 19 | ) 20 | flag.Parse() 21 | 22 | tlsFlags := newTLSFlags(*tlsverify, *tlscacert, *tlscert, *tlskey) 23 | 24 | handler := newHandler(*assets, *data, *endpoint, tlsFlags) 25 | if err := http.ListenAndServe(*addr, handler); err != nil { 26 | log.Fatal(err) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /api/csrf.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gorilla/csrf" 5 | "github.com/gorilla/securecookie" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | ) 10 | 11 | const keyFile = "authKey.dat" 12 | 13 | // newAuthKey reuses an existing CSRF authkey if present or generates a new one 14 | func newAuthKey(path string) []byte { 15 | var authKey []byte 16 | authKeyPath := path + "/" + keyFile 17 | data, err := ioutil.ReadFile(authKeyPath) 18 | if err != nil { 19 | log.Print("Unable to find an existing CSRF auth key. Generating a new key.") 20 | authKey = securecookie.GenerateRandomKey(32) 21 | err := ioutil.WriteFile(authKeyPath, authKey, 0644) 22 | if err != nil { 23 | log.Fatal("Unable to persist CSRF auth key.") 24 | log.Fatal(err) 25 | } 26 | } else { 27 | authKey = data 28 | } 29 | return authKey 30 | } 31 | 32 | // newCSRF initializes a new CSRF handler 33 | func newCSRFHandler(keyPath string) func(h http.Handler) http.Handler { 34 | authKey := newAuthKey(keyPath) 35 | return csrf.Protect( 36 | authKey, 37 | csrf.HttpOnly(false), 38 | csrf.Secure(false), 39 | ) 40 | } 41 | 42 | // newCSRFWrapper wraps a http.Handler to add the CSRF token 43 | func newCSRFWrapper(h http.Handler) http.Handler { 44 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 45 | w.Header().Set("X-CSRF-Token", csrf.Token(r)) 46 | h.ServeHTTP(w, r) 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /api/flags.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // TLSFlags defines all the flags associated to the SSL configuration 4 | type TLSFlags struct { 5 | tls bool 6 | caPath string 7 | certPath string 8 | keyPath string 9 | } 10 | 11 | // newTLSFlags creates a new TLSFlags from command flags 12 | func newTLSFlags(tls bool, cacert string, cert string, key string) TLSFlags { 13 | return TLSFlags{ 14 | tls: tls, 15 | caPath: cacert, 16 | certPath: cert, 17 | keyPath: key, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /api/handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "net/http/httputil" 7 | "net/url" 8 | "os" 9 | ) 10 | 11 | // newHandler creates a new http.Handler with CSRF protection 12 | func newHandler(dir string, d string, e string, tlsFlags TLSFlags) http.Handler { 13 | var ( 14 | mux = http.NewServeMux() 15 | fileHandler = http.FileServer(http.Dir(dir)) 16 | ) 17 | 18 | u, perr := url.Parse(e) 19 | if perr != nil { 20 | log.Fatal(perr) 21 | } 22 | 23 | handler := newAPIHandler(u, tlsFlags) 24 | CSRFHandler := newCSRFHandler(d) 25 | 26 | mux.Handle("/dockerapi/", http.StripPrefix("/dockerapi", handler)) 27 | mux.Handle("/", fileHandler) 28 | return CSRFHandler(newCSRFWrapper(mux)) 29 | } 30 | 31 | // newAPIHandler initializes a new http.Handler based on the URL scheme 32 | func newAPIHandler(u *url.URL, tlsFlags TLSFlags) http.Handler { 33 | var handler http.Handler 34 | if u.Scheme == "tcp" { 35 | if tlsFlags.tls { 36 | handler = newTCPHandlerWithTLS(u, tlsFlags) 37 | } else { 38 | handler = newTCPHandler(u) 39 | } 40 | } else if u.Scheme == "unix" { 41 | socketPath := u.Path 42 | if _, err := os.Stat(socketPath); err != nil { 43 | if os.IsNotExist(err) { 44 | log.Fatalf("Unix socket %s does not exist", socketPath) 45 | } 46 | log.Fatal(err) 47 | } 48 | handler = newUnixHandler(socketPath) 49 | } else { 50 | log.Fatalf("Bad Docker enpoint: %s. Only unix:// and tcp:// are supported.", u) 51 | } 52 | return handler 53 | } 54 | 55 | // newUnixHandler initializes a new UnixHandler 56 | func newUnixHandler(e string) http.Handler { 57 | return &unixHandler{e} 58 | } 59 | 60 | // newTCPHandler initializes a HTTP reverse proxy 61 | func newTCPHandler(u *url.URL) http.Handler { 62 | u.Scheme = "http" 63 | return httputil.NewSingleHostReverseProxy(u) 64 | } 65 | 66 | // newTCPHandlerWithL initializes a HTTPS reverse proxy with a TLS configuration 67 | func newTCPHandlerWithTLS(u *url.URL, tlsFlags TLSFlags) http.Handler { 68 | u.Scheme = "https" 69 | var tlsConfig = newTLSConfig(tlsFlags) 70 | proxy := httputil.NewSingleHostReverseProxy(u) 71 | proxy.Transport = &http.Transport{ 72 | TLSClientConfig: tlsConfig, 73 | } 74 | return proxy 75 | } 76 | -------------------------------------------------------------------------------- /api/ssl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "io/ioutil" 7 | "log" 8 | ) 9 | 10 | // newTLSConfig initializes a tls.Config from the TLS flags 11 | func newTLSConfig(tlsFlags TLSFlags) *tls.Config { 12 | cert, err := tls.LoadX509KeyPair(tlsFlags.certPath, tlsFlags.keyPath) 13 | if err != nil { 14 | log.Fatal(err) 15 | } 16 | caCert, err := ioutil.ReadFile(tlsFlags.caPath) 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | caCertPool := x509.NewCertPool() 21 | caCertPool.AppendCertsFromPEM(caCert) 22 | tlsConfig := &tls.Config{ 23 | Certificates: []tls.Certificate{cert}, 24 | RootCAs: caCertPool, 25 | } 26 | return tlsConfig 27 | } 28 | -------------------------------------------------------------------------------- /api/unix_handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net" 7 | "net/http" 8 | "net/http/httputil" 9 | ) 10 | 11 | // unixHandler defines a handler holding the path to a socket under UNIX 12 | type unixHandler struct { 13 | path string 14 | } 15 | 16 | // ServeHTTP implementation for unixHandler 17 | func (h *unixHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 18 | conn, err := net.Dial("unix", h.path) 19 | if err != nil { 20 | w.WriteHeader(http.StatusInternalServerError) 21 | log.Println(err) 22 | return 23 | } 24 | c := httputil.NewClientConn(conn, nil) 25 | defer c.Close() 26 | 27 | res, err := c.Do(r) 28 | if err != nil { 29 | w.WriteHeader(http.StatusInternalServerError) 30 | log.Println(err) 31 | return 32 | } 33 | defer res.Body.Close() 34 | 35 | copyHeader(w.Header(), res.Header) 36 | if _, err := io.Copy(w, res.Body); err != nil { 37 | log.Println(err) 38 | } 39 | } 40 | 41 | func copyHeader(dst, src http.Header) { 42 | for k, vv := range src { 43 | for _, v := range vv { 44 | dst.Add(k, v) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | angular.module('uifordocker', [ 2 | 'uifordocker.templates', 3 | 'ngRoute', 4 | 'uifordocker.services', 5 | 'uifordocker.filters', 6 | 'masthead', 7 | 'footer', 8 | 'dashboard', 9 | 'container', 10 | 'containers', 11 | 'containersNetwork', 12 | 'images', 13 | 'image', 14 | 'pullImage', 15 | 'startContainer', 16 | 'sidebar', 17 | 'info', 18 | 'builder', 19 | 'containerLogs', 20 | 'containerTop', 21 | 'events', 22 | 'stats', 23 | 'network', 24 | 'networks', 25 | 'volumes']) 26 | .config(['$routeProvider', '$httpProvider', function ($routeProvider, $httpProvider) { 27 | 'use strict'; 28 | 29 | $httpProvider.defaults.xsrfCookieName = 'csrfToken'; 30 | $httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token'; 31 | 32 | $routeProvider.when('/', { 33 | templateUrl: 'app/components/dashboard/dashboard.html', 34 | controller: 'DashboardController' 35 | }); 36 | $routeProvider.when('/containers/', { 37 | templateUrl: 'app/components/containers/containers.html', 38 | controller: 'ContainersController' 39 | }); 40 | $routeProvider.when('/containers/:id/', { 41 | templateUrl: 'app/components/container/container.html', 42 | controller: 'ContainerController' 43 | }); 44 | $routeProvider.when('/containers/:id/logs/', { 45 | templateUrl: 'app/components/containerLogs/containerlogs.html', 46 | controller: 'ContainerLogsController' 47 | }); 48 | $routeProvider.when('/containers/:id/top', { 49 | templateUrl: 'app/components/containerTop/containerTop.html', 50 | controller: 'ContainerTopController' 51 | }); 52 | $routeProvider.when('/containers/:id/stats', { 53 | templateUrl: 'app/components/stats/stats.html', 54 | controller: 'StatsController' 55 | }); 56 | $routeProvider.when('/containers_network', { 57 | templateUrl: 'app/components/containersNetwork/containersNetwork.html', 58 | controller: 'ContainersNetworkController' 59 | }); 60 | $routeProvider.when('/images/', { 61 | templateUrl: 'app/components/images/images.html', 62 | controller: 'ImagesController' 63 | }); 64 | $routeProvider.when('/images/:id*/', { 65 | templateUrl: 'app/components/image/image.html', 66 | controller: 'ImageController' 67 | }); 68 | $routeProvider.when('/info', {templateUrl: 'app/components/info/info.html', controller: 'InfoController'}); 69 | $routeProvider.when('/events', { 70 | templateUrl: 'app/components/events/events.html', 71 | controller: 'EventsController' 72 | }); 73 | $routeProvider.otherwise({redirectTo: '/'}); 74 | 75 | // The Docker API likes to return plaintext errors, this catches them and disp 76 | $httpProvider.interceptors.push(function() { 77 | return { 78 | 'response': function(response) { 79 | if (typeof(response.data) === 'string' && 80 | (response.data.startsWith('Conflict.') || response.data.startsWith('conflict:'))) { 81 | $.gritter.add({ 82 | title: 'Error', 83 | text: $('
').text(response.data).html(), 84 | time: 10000 85 | }); 86 | } 87 | var csrfToken = response.headers('X-Csrf-Token'); 88 | if (csrfToken) { 89 | document.cookie = 'csrfToken=' + csrfToken; 90 | } 91 | return response; 92 | } 93 | }; 94 | }); 95 | }]) 96 | // This is your docker url that the api will use to make requests 97 | // You need to set this to the api endpoint without the port i.e. http://192.168.1.9 98 | .constant('DOCKER_ENDPOINT', 'dockerapi') 99 | .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 100 | .constant('UI_VERSION', 'v0.11.0'); 101 | -------------------------------------------------------------------------------- /app/components/builder/builder.html: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /app/components/builder/builderController.js: -------------------------------------------------------------------------------- 1 | angular.module('builder', []) 2 | .controller('BuilderController', ['$scope', 3 | function ($scope) { 4 | $scope.template = 'app/components/builder/builder.html'; 5 | }]); 6 | -------------------------------------------------------------------------------- /app/components/containerLogs/containerLogsController.js: -------------------------------------------------------------------------------- 1 | angular.module('containerLogs', []) 2 | .controller('ContainerLogsController', ['$scope', '$routeParams', '$location', '$anchorScroll', 'ContainerLogs', 'Container', 'ViewSpinner', 3 | function ($scope, $routeParams, $location, $anchorScroll, ContainerLogs, Container, ViewSpinner) { 4 | $scope.stdout = ''; 5 | $scope.stderr = ''; 6 | $scope.showTimestamps = false; 7 | $scope.tailLines = 2000; 8 | 9 | ViewSpinner.spin(); 10 | Container.get({id: $routeParams.id}, function (d) { 11 | $scope.container = d; 12 | ViewSpinner.stop(); 13 | }, function (e) { 14 | if (e.status === 404) { 15 | Messages.error("Not found", "Container not found."); 16 | } else { 17 | Messages.error("Failure", e.data); 18 | } 19 | ViewSpinner.stop(); 20 | }); 21 | 22 | function getLogs() { 23 | ViewSpinner.spin(); 24 | ContainerLogs.get($routeParams.id, { 25 | stdout: 1, 26 | stderr: 0, 27 | timestamps: $scope.showTimestamps, 28 | tail: $scope.tailLines 29 | }, function (data, status, headers, config) { 30 | // Replace carriage returns with newlines to clean up output 31 | data = data.replace(/[\r]/g, '\n'); 32 | // Strip 8 byte header from each line of output 33 | data = data.substring(8); 34 | data = data.replace(/\n(.{8})/g, '\n'); 35 | $scope.stdout = data; 36 | ViewSpinner.stop(); 37 | }); 38 | 39 | ContainerLogs.get($routeParams.id, { 40 | stdout: 0, 41 | stderr: 1, 42 | timestamps: $scope.showTimestamps, 43 | tail: $scope.tailLines 44 | }, function (data, status, headers, config) { 45 | // Replace carriage returns with newlines to clean up output 46 | data = data.replace(/[\r]/g, '\n'); 47 | // Strip 8 byte header from each line of output 48 | data = data.substring(8); 49 | data = data.replace(/\n(.{8})/g, '\n'); 50 | $scope.stderr = data; 51 | ViewSpinner.stop(); 52 | }); 53 | } 54 | 55 | // initial call 56 | getLogs(); 57 | var logIntervalId = window.setInterval(getLogs, 5000); 58 | 59 | $scope.$on("$destroy", function () { 60 | // clearing interval when view changes 61 | clearInterval(logIntervalId); 62 | }); 63 | 64 | $scope.scrollTo = function (id) { 65 | $location.hash(id); 66 | $anchorScroll(); 67 | }; 68 | 69 | $scope.toggleTimestamps = function () { 70 | getLogs(); 71 | }; 72 | 73 | $scope.toggleTail = function () { 74 | getLogs(); 75 | }; 76 | }]); 77 | -------------------------------------------------------------------------------- /app/components/containerLogs/containerlogs.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Logs for container: {{ container.Name }}

4 | 5 |
6 | 7 | 8 |
9 |
10 |
11 | Reload logs 12 | 14 | 15 |
16 |
17 | 19 |
20 |
21 |
22 | 23 |
24 |
25 |
26 |

STDOUT

27 |
28 |
29 |
{{stdout}}
30 |
31 |
32 |
33 |
34 |
35 |
36 |

STDERR

37 |
38 |
39 |
{{stderr}}
40 |
41 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /app/components/containerTop/containerTop.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Top for: {{ containerName }}

5 |
6 |
7 |
8 |
9 | 10 |
11 | 12 |
13 |
14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
{{title}}
{{processInfo}}
27 |
28 |
29 |
-------------------------------------------------------------------------------- /app/components/containerTop/containerTopController.js: -------------------------------------------------------------------------------- 1 | angular.module('containerTop', []) 2 | .controller('ContainerTopController', ['$scope', '$routeParams', 'ContainerTop', 'Container', 'ViewSpinner', function ($scope, $routeParams, ContainerTop, Container, ViewSpinner) { 3 | $scope.ps_args = ''; 4 | 5 | /** 6 | * Get container processes 7 | */ 8 | $scope.getTop = function () { 9 | ViewSpinner.spin(); 10 | ContainerTop.get($routeParams.id, { 11 | ps_args: $scope.ps_args 12 | }, function (data) { 13 | $scope.containerTop = data; 14 | ViewSpinner.stop(); 15 | }); 16 | }; 17 | 18 | Container.get({id: $routeParams.id}, function (d) { 19 | $scope.containerName = d.Name.substring(1); 20 | }, function (e) { 21 | Messages.error("Failure", e.data); 22 | }); 23 | 24 | $scope.getTop(); 25 | }]); -------------------------------------------------------------------------------- /app/components/containers/containers.html: -------------------------------------------------------------------------------- 1 | 2 |

Containers:

3 | 4 |
5 | 19 | 20 |
21 |   22 | 23 |
24 |
25 | 26 | 27 | 28 | 29 | 36 | 43 | 50 | 57 | 64 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 |
30 | 31 | Name 32 | 33 | 34 | 35 | 37 | 38 | Image 39 | 40 | 41 | 42 | 44 | 45 | Command 46 | 47 | 48 | 49 | 51 | 52 | Created 53 | 54 | 55 | 56 | 58 | 59 | Status 60 | 61 | 62 | 63 | 65 | Log 66 |
{{ container|containername}}{{ container.Image }}{{ container.Command|truncate:40 }}{{ container.Created * 1000 | date: 'yyyy-MM-dd' }}{{ container.Status }}stdout/stderr
81 | -------------------------------------------------------------------------------- /app/components/containers/containersController.js: -------------------------------------------------------------------------------- 1 | angular.module('containers', []) 2 | .controller('ContainersController', ['$scope', 'Container', 'Settings', 'Messages', 'ViewSpinner', 3 | function ($scope, Container, Settings, Messages, ViewSpinner) { 4 | $scope.sortType = 'Created'; 5 | $scope.sortReverse = true; 6 | $scope.toggle = false; 7 | $scope.displayAll = Settings.displayAll; 8 | 9 | $scope.order = function (sortType) { 10 | $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; 11 | $scope.sortType = sortType; 12 | }; 13 | 14 | var update = function (data) { 15 | ViewSpinner.spin(); 16 | Container.query(data, function (d) { 17 | $scope.containers = d.map(function (item) { 18 | return new ContainerViewModel(item); 19 | }); 20 | ViewSpinner.stop(); 21 | }); 22 | }; 23 | 24 | var batch = function (items, action, msg) { 25 | ViewSpinner.spin(); 26 | var counter = 0; 27 | var complete = function () { 28 | counter = counter - 1; 29 | if (counter === 0) { 30 | ViewSpinner.stop(); 31 | update({all: Settings.displayAll ? 1 : 0}); 32 | } 33 | }; 34 | angular.forEach(items, function (c) { 35 | if (c.Checked) { 36 | if (action === Container.start) { 37 | Container.get({id: c.Id}, function (d) { 38 | c = d; 39 | counter = counter + 1; 40 | action({id: c.Id}, {}, function (d) { 41 | Messages.send("Container " + msg, c.Id); 42 | var index = $scope.containers.indexOf(c); 43 | complete(); 44 | }, function (e) { 45 | Messages.error("Failure", e.data); 46 | complete(); 47 | }); 48 | }, function (e) { 49 | if (e.status === 404) { 50 | $('.detail').hide(); 51 | Messages.error("Not found", "Container not found."); 52 | } else { 53 | Messages.error("Failure", e.data); 54 | } 55 | complete(); 56 | }); 57 | } 58 | else { 59 | counter = counter + 1; 60 | action({id: c.Id}, function (d) { 61 | Messages.send("Container " + msg, c.Id); 62 | var index = $scope.containers.indexOf(c); 63 | complete(); 64 | }, function (e) { 65 | Messages.error("Failure", e.data); 66 | complete(); 67 | }); 68 | 69 | } 70 | 71 | } 72 | }); 73 | if (counter === 0) { 74 | ViewSpinner.stop(); 75 | } 76 | }; 77 | 78 | $scope.toggleSelectAll = function () { 79 | angular.forEach($scope.filteredContainers, function (i) { 80 | i.Checked = $scope.toggle; 81 | }); 82 | }; 83 | 84 | $scope.toggleGetAll = function () { 85 | Settings.displayAll = $scope.displayAll; 86 | update({all: Settings.displayAll ? 1 : 0}); 87 | }; 88 | 89 | $scope.startAction = function () { 90 | batch($scope.containers, Container.start, "Started"); 91 | }; 92 | 93 | $scope.stopAction = function () { 94 | batch($scope.containers, Container.stop, "Stopped"); 95 | }; 96 | 97 | $scope.restartAction = function () { 98 | batch($scope.containers, Container.restart, "Restarted"); 99 | }; 100 | 101 | $scope.killAction = function () { 102 | batch($scope.containers, Container.kill, "Killed"); 103 | }; 104 | 105 | $scope.pauseAction = function () { 106 | batch($scope.containers, Container.pause, "Paused"); 107 | }; 108 | 109 | $scope.unpauseAction = function () { 110 | batch($scope.containers, Container.unpause, "Unpaused"); 111 | }; 112 | 113 | $scope.removeAction = function () { 114 | batch($scope.containers, Container.remove, "Removed"); 115 | }; 116 | 117 | update({all: Settings.displayAll ? 1 : 0}); 118 | }]); 119 | -------------------------------------------------------------------------------- /app/components/containersNetwork/containersNetwork.html: -------------------------------------------------------------------------------- 1 |
2 |

Containers Network

3 | 4 |
5 |
6 | 8 | 9 |
10 |
11 |
12 |
13 | 14 | 15 | 16 | 17 |
18 | 20 |
21 |
22 | 24 |
25 |
26 | -------------------------------------------------------------------------------- /app/components/containersNetwork/containersNetworkController.js: -------------------------------------------------------------------------------- 1 | angular.module('containersNetwork', ['ngVis']) 2 | .controller('ContainersNetworkController', ['$scope', '$location', 'Container', 'Messages', 'VisDataSet', function ($scope, $location, Container, Messages, VisDataSet) { 3 | 4 | function ContainerNode(data) { 5 | this.Id = data.Id; 6 | // names have the following format: /Name 7 | this.Name = data.Name.substring(1); 8 | this.Image = data.Config.Image; 9 | this.Running = data.State.Running; 10 | var dataLinks = data.HostConfig.Links; 11 | if (dataLinks != null) { 12 | this.Links = {}; 13 | for (var i = 0; i < dataLinks.length; i++) { 14 | // links have the following format: /TargetContainerName:/SourceContainerName/LinkAlias 15 | var link = dataLinks[i].split(":"); 16 | var target = link[0].substring(1); 17 | var alias = link[1].substring(link[1].lastIndexOf("/") + 1); 18 | // only keep shortest alias 19 | if (this.Links[target] == null || alias.length < this.Links[target].length) { 20 | this.Links[target] = alias; 21 | } 22 | } 23 | } 24 | var dataVolumes = data.HostConfig.VolumesFrom; 25 | //converting array into properties for simpler and faster access 26 | if (dataVolumes != null) { 27 | this.VolumesFrom = {}; 28 | for (var j = 0; j < dataVolumes.length; j++) { 29 | this.VolumesFrom[dataVolumes[j]] = true; 30 | } 31 | } 32 | } 33 | 34 | function ContainersNetworkData() { 35 | this.nodes = new VisDataSet(); 36 | this.edges = new VisDataSet(); 37 | 38 | this.addContainerNode = function (container) { 39 | this.nodes.add({ 40 | id: container.Id, 41 | label: container.Name, 42 | title: "", 46 | color: (container.Running ? "#8888ff" : "#cccccc") 47 | }); 48 | }; 49 | 50 | this.hasEdge = function (from, to) { 51 | return this.edges.getIds({ 52 | filter: function (item) { 53 | return item.from === from.Id && item.to === to.Id; 54 | } 55 | }).length > 0; 56 | }; 57 | 58 | this.addLinkEdgeIfExists = function (from, to) { 59 | if (from.Links != null && from.Links[to.Name] != null && !this.hasEdge(from, to)) { 60 | this.edges.add({ 61 | from: from.Id, 62 | to: to.Id, 63 | label: from.Links[to.Name] 64 | }); 65 | } 66 | }; 67 | 68 | this.addVolumeEdgeIfExists = function (from, to) { 69 | if (from.VolumesFrom != null && (from.VolumesFrom[to.Id] != null || from.VolumesFrom[to.Name] != null) && !this.hasEdge(from, to)) { 70 | this.edges.add({ 71 | from: from.Id, 72 | to: to.Id, 73 | color: {color: '#A0A0A0', highlight: '#A0A0A0', hover: '#848484'} 74 | }); 75 | } 76 | }; 77 | 78 | this.removeContainersNodes = function (containersIds) { 79 | this.nodes.remove(containersIds); 80 | }; 81 | } 82 | 83 | function ContainersNetwork() { 84 | this.data = new ContainersNetworkData(); 85 | this.containers = {}; 86 | this.selectedContainersIds = []; 87 | this.shownContainersIds = []; 88 | this.events = { 89 | select: function (event) { 90 | $scope.network.selectedContainersIds = event.nodes; 91 | $scope.$apply(function () { 92 | $scope.query = ''; 93 | }); 94 | }, 95 | doubleClick: function (event) { 96 | $scope.$apply(function () { 97 | $location.path('/containers/' + event.nodes[0]); 98 | }); 99 | } 100 | }; 101 | this.options = { 102 | navigation: true, 103 | keyboard: true, 104 | height: '500px', width: '700px', 105 | nodes: { 106 | shape: 'box' 107 | }, 108 | edges: { 109 | style: 'arrow' 110 | }, 111 | physics: { 112 | barnesHut: { 113 | springLength: 200 114 | } 115 | } 116 | }; 117 | 118 | this.addContainer = function (data) { 119 | var container = new ContainerNode(data); 120 | this.containers[container.Id] = container; 121 | this.shownContainersIds.push(container.Id); 122 | this.data.addContainerNode(container); 123 | for (var otherContainerId in this.containers) { 124 | var otherContainer = this.containers[otherContainerId]; 125 | this.data.addLinkEdgeIfExists(container, otherContainer); 126 | this.data.addLinkEdgeIfExists(otherContainer, container); 127 | this.data.addVolumeEdgeIfExists(container, otherContainer); 128 | this.data.addVolumeEdgeIfExists(otherContainer, container); 129 | } 130 | }; 131 | 132 | this.selectContainers = function (query) { 133 | if (this.component != null) { 134 | this.selectedContainersIds = this.searchContainers(query); 135 | this.component.selectNodes(this.selectedContainersIds); 136 | } 137 | }; 138 | 139 | this.searchContainers = function (query) { 140 | if (query.trim() === "") { 141 | return []; 142 | } 143 | var selectedContainersIds = []; 144 | for (var i = 0; i < this.shownContainersIds.length; i++) { 145 | var container = this.containers[this.shownContainersIds[i]]; 146 | if (container.Name.indexOf(query) > -1 || 147 | container.Image.indexOf(query) > -1 || 148 | container.Id.indexOf(query) > -1) { 149 | selectedContainersIds.push(container.Id); 150 | } 151 | } 152 | return selectedContainersIds; 153 | }; 154 | 155 | this.hideSelected = function () { 156 | var i = 0; 157 | while (i < this.shownContainersIds.length) { 158 | if (this.selectedContainersIds.indexOf(this.shownContainersIds[i]) > -1) { 159 | this.shownContainersIds.splice(i, 1); 160 | } else { 161 | i++; 162 | } 163 | } 164 | this.data.removeContainersNodes(this.selectedContainersIds); 165 | $scope.query = ''; 166 | this.selectedContainersIds = []; 167 | }; 168 | 169 | this.searchDownstream = function (containerId, downstreamContainersIds) { 170 | if (downstreamContainersIds.indexOf(containerId) > -1) { 171 | return; 172 | } 173 | downstreamContainersIds.push(containerId); 174 | var container = this.containers[containerId]; 175 | if (container.Links == null && container.VolumesFrom == null) { 176 | return; 177 | } 178 | for (var otherContainerId in this.containers) { 179 | var otherContainer = this.containers[otherContainerId]; 180 | if (container.Links != null && container.Links[otherContainer.Name] != null) { 181 | this.searchDownstream(otherContainer.Id, downstreamContainersIds); 182 | } else if (container.VolumesFrom != null && 183 | container.VolumesFrom[otherContainer.Id] != null) { 184 | this.searchDownstream(otherContainer.Id, downstreamContainersIds); 185 | } 186 | } 187 | }; 188 | 189 | this.updateShownContainers = function (newShownContainersIds) { 190 | for (var containerId in this.containers) { 191 | if (newShownContainersIds.indexOf(containerId) > -1 && 192 | this.shownContainersIds.indexOf(containerId) === -1) { 193 | this.data.addContainerNode(this.containers[containerId]); 194 | } else if (newShownContainersIds.indexOf(containerId) === -1 && 195 | this.shownContainersIds.indexOf(containerId) > -1) { 196 | this.data.removeContainersNodes(containerId); 197 | } 198 | } 199 | this.shownContainersIds = newShownContainersIds; 200 | }; 201 | 202 | this.showSelectedDownstream = function () { 203 | var downstreamContainersIds = []; 204 | for (var i = 0; i < this.selectedContainersIds.length; i++) { 205 | this.searchDownstream(this.selectedContainersIds[i], downstreamContainersIds); 206 | } 207 | this.updateShownContainers(downstreamContainersIds); 208 | }; 209 | 210 | this.searchUpstream = function (containerId, upstreamContainersIds) { 211 | if (upstreamContainersIds.indexOf(containerId) > -1) { 212 | return; 213 | } 214 | upstreamContainersIds.push(containerId); 215 | var container = this.containers[containerId]; 216 | for (var otherContainerId in this.containers) { 217 | var otherContainer = this.containers[otherContainerId]; 218 | if (otherContainer.Links != null && otherContainer.Links[container.Name] != null) { 219 | this.searchUpstream(otherContainer.Id, upstreamContainersIds); 220 | } else if (otherContainer.VolumesFrom != null && 221 | otherContainer.VolumesFrom[container.Id] != null) { 222 | this.searchUpstream(otherContainer.Id, upstreamContainersIds); 223 | } 224 | } 225 | }; 226 | 227 | this.showSelectedUpstream = function () { 228 | var upstreamContainersIds = []; 229 | for (var i = 0; i < this.selectedContainersIds.length; i++) { 230 | this.searchUpstream(this.selectedContainersIds[i], upstreamContainersIds); 231 | } 232 | this.updateShownContainers(upstreamContainersIds); 233 | }; 234 | 235 | this.showAll = function () { 236 | for (var containerId in this.containers) { 237 | if (this.shownContainersIds.indexOf(containerId) === -1) { 238 | this.data.addContainerNode(this.containers[containerId]); 239 | this.shownContainersIds.push(containerId); 240 | } 241 | } 242 | }; 243 | 244 | } 245 | 246 | $scope.network = new ContainersNetwork(); 247 | 248 | var showFailure = function (event) { 249 | Messages.error('Failure', e.data); 250 | }; 251 | 252 | var addContainer = function (container) { 253 | $scope.network.addContainer(container); 254 | }; 255 | 256 | var update = function (data) { 257 | Container.query(data, function (d) { 258 | for (var i = 0; i < d.length; i++) { 259 | Container.get({id: d[i].Id}, addContainer, showFailure); 260 | } 261 | }); 262 | }; 263 | update({all: 0}); 264 | 265 | $scope.includeStopped = false; 266 | $scope.toggleIncludeStopped = function () { 267 | $scope.network.updateShownContainers([]); 268 | update({all: $scope.includeStopped ? 1 : 0}); 269 | }; 270 | 271 | }]); 272 | -------------------------------------------------------------------------------- /app/components/dashboard/dashboard.html: -------------------------------------------------------------------------------- 1 | 4 |
5 | 13 |
14 | 15 |
16 |
17 |
18 |

Running Containers

19 | 25 |
26 |
27 |

Status

28 | 29 |

You are using an outdated browser. Please upgrade your browser to improve your experience.

31 |
32 |
33 |
34 |
35 |
36 | 37 |
38 |
39 |

Containers created

40 | 41 |

You are using an outdated browser. Please upgrade your browser to improve your experience.

43 |
44 |

Images created

45 | 46 |

You are using an outdated browser. Please upgrade your browser to improve your experience.

48 |
49 |
50 |
-------------------------------------------------------------------------------- /app/components/dashboard/dashboardController.js: -------------------------------------------------------------------------------- 1 | angular.module('dashboard', []) 2 | .controller('DashboardController', ['$scope', 'Container', 'Image', 'Settings', 'LineChart', function ($scope, Container, Image, Settings, LineChart) { 3 | $scope.predicate = '-Created'; 4 | $scope.containers = []; 5 | 6 | var getStarted = function (data) { 7 | $scope.totalContainers = data.length; 8 | LineChart.build('#containers-started-chart', data, function (c) { 9 | return new Date(c.Created * 1000).toLocaleDateString(); 10 | }); 11 | var s = $scope; 12 | Image.query({}, function (d) { 13 | s.totalImages = d.length; 14 | LineChart.build('#images-created-chart', d, function (c) { 15 | return new Date(c.Created * 1000).toLocaleDateString(); 16 | }); 17 | }); 18 | }; 19 | 20 | var opts = {animation: false}; 21 | if (Settings.firstLoad) { 22 | opts.animation = true; 23 | Settings.firstLoad = false; 24 | localStorage.setItem('firstLoad', false); 25 | $('#masthead').show(); 26 | 27 | setTimeout(function () { 28 | $('#masthead').slideUp('slow'); 29 | }, 5000); 30 | } 31 | 32 | Container.query({all: 1}, function (d) { 33 | var running = 0; 34 | var ghost = 0; 35 | var stopped = 0; 36 | var created = 0; 37 | 38 | for (var i = 0; i < d.length; i++) { 39 | var item = d[i]; 40 | 41 | if (item.Status === "Ghost") { 42 | ghost += 1; 43 | } else if (item.Status === "Created") { 44 | created += 1; 45 | } else if (item.Status.indexOf('Exit') !== -1) { 46 | stopped += 1; 47 | } else { 48 | running += 1; 49 | $scope.containers.push(new ContainerViewModel(item)); 50 | } 51 | } 52 | 53 | getStarted(d); 54 | 55 | var c = new Chart($('#containers-chart').get(0).getContext("2d")); 56 | var data = [ 57 | { 58 | value: created, 59 | color: '#000000', 60 | title: 'Created' 61 | }, 62 | { 63 | value: running, 64 | color: '#5bb75b', 65 | title: 'Running' 66 | }, // running 67 | { 68 | value: stopped, 69 | color: '#C7604C', 70 | title: 'Stopped' 71 | }, // stopped 72 | { 73 | value: ghost, 74 | color: '#E2EAE9', 75 | title: 'Ghost' 76 | } // ghost 77 | ]; 78 | 79 | c.Doughnut(data, opts); 80 | var lgd = $('#chart-legend').get(0); 81 | legend(lgd, data); 82 | }); 83 | }]); 84 | -------------------------------------------------------------------------------- /app/components/events/events.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Events

4 | 5 |
6 |
7 | 8 | 9 |
10 |
11 | 12 | 13 |
14 | 15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 |
EventFromIDTime
27 | 28 | 29 | 30 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /app/components/events/eventsController.js: -------------------------------------------------------------------------------- 1 | angular.module('events', ['ngOboe']) 2 | .controller('EventsController', ['Settings', '$scope', 'Oboe', 'Messages', '$timeout', function (Settings, $scope, oboe, Messages, $timeout) { 3 | $scope.updateEvents = function () { 4 | $scope.dockerEvents = []; 5 | 6 | // TODO: Clean up URL building 7 | var url = Settings.url + '/events?'; 8 | 9 | if ($scope.model.since) { 10 | var sinceSecs = Math.floor($scope.model.since.getTime() / 1000); 11 | url += 'since=' + sinceSecs + '&'; 12 | } 13 | if ($scope.model.until) { 14 | var untilSecs = Math.floor($scope.model.until.getTime() / 1000); 15 | url += 'until=' + untilSecs; 16 | } 17 | 18 | oboe({ 19 | url: url, 20 | pattern: '{id status time}' 21 | }) 22 | .then(function (node) { 23 | // finished loading 24 | $timeout(function () { 25 | $scope.$apply(); 26 | }); 27 | }, function (error) { 28 | // handle errors 29 | Messages.error("Failure", error.data); 30 | }, function (node) { 31 | // node received 32 | $scope.dockerEvents.push(node); 33 | }); 34 | }; 35 | 36 | // Init 37 | $scope.model = {}; 38 | $scope.model.since = new Date(Date.now() - 86400000); // 24 hours in the past 39 | $scope.model.until = new Date(); 40 | $scope.updateEvents(); 41 | 42 | }]); -------------------------------------------------------------------------------- /app/components/footer/footerController.js: -------------------------------------------------------------------------------- 1 | angular.module('footer', []) 2 | .controller('FooterController', ['$scope', 'Settings', 'Version', function ($scope, Settings, Version) { 3 | $scope.template = 'app/components/footer/statusbar.html'; 4 | 5 | $scope.uiVersion = Settings.uiVersion; 6 | Version.get({}, function (d) { 7 | $scope.apiVersion = d.ApiVersion; 8 | }); 9 | }]); 10 | -------------------------------------------------------------------------------- /app/components/footer/statusbar.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 16 |
17 |
18 |
-------------------------------------------------------------------------------- /app/components/image/image.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 6 | 7 |
8 | 9 |

Image: {{ id }}

10 | 11 |
12 | 13 |
14 | 15 |
16 |

Containers created:

17 | 18 |

You are using an outdated browser. Please upgrade your browser to improve your experience.

20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
Tags: 28 |
    29 |
  • {{ tag }} 30 | 31 |
  • 32 |
33 |
Created:{{ image.Created | date: 'yyyy-MM-dd HH:mm:ss'}}
Parent:{{ image.Parent }}
Size (Virtual Size):{{ image.Size|humansize }} ({{ image.VirtualSize|humansize }})
Hostname:{{ image.ContainerConfig.Hostname }}
User:{{ image.ContainerConfig.User }}
Cmd:{{ image.ContainerConfig.Cmd }}
Volumes:{{ image.ContainerConfig.Volumes }}
Volumes from:{{ image.ContainerConfig.VolumesFrom }}
Built with:Docker {{ image.DockerVersion }} on {{ image.Os}}, {{ image.Architecture }}
75 | 76 |
77 |
78 | History: 79 |
80 |
81 | 82 |
83 |
84 | 85 |
86 |
    87 |
  • 88 | {{ change.Id }}: Created: {{ change.Created | date: 'yyyy-MM-dd' }} Created by: {{ change.CreatedBy 89 | }} 90 |
  • 91 |
92 |
93 | 94 |
95 | 96 |
97 |
98 |
99 | Tag image 100 |
101 | 102 | 103 | 104 |
105 |
106 | 109 |
110 | 111 |
112 |
113 |
114 | 115 |
116 | 117 |
118 | 119 |
120 |
121 | -------------------------------------------------------------------------------- /app/components/image/imageController.js: -------------------------------------------------------------------------------- 1 | angular.module('image', []) 2 | .controller('ImageController', ['$scope', '$q', '$routeParams', '$location', 'Image', 'Container', 'Messages', 'LineChart', 3 | function ($scope, $q, $routeParams, $location, Image, Container, Messages, LineChart) { 4 | $scope.history = []; 5 | $scope.tagInfo = {repo: '', version: '', force: false}; 6 | $scope.id = ''; 7 | $scope.repoTags = []; 8 | 9 | $scope.removeImage = function (id) { 10 | Image.remove({id: id}, function (d) { 11 | d.forEach(function(msg){ 12 | var key = Object.keys(msg)[0]; 13 | Messages.send(key, msg[key]); 14 | }); 15 | // If last message key is 'Deleted' then assume the image is gone and send to images page 16 | if (d[d.length-1].Deleted) { 17 | $location.path('/images'); 18 | } else { 19 | $location.path('/images/' + $scope.id); // Refresh the current page. 20 | } 21 | }, function (e) { 22 | $scope.error = e.data; 23 | $('#error-message').show(); 24 | }); 25 | }; 26 | 27 | $scope.getHistory = function () { 28 | Image.history({id: $routeParams.id}, function (d) { 29 | $scope.history = d; 30 | }); 31 | }; 32 | 33 | $scope.addTag = function () { 34 | var tag = $scope.tagInfo; 35 | Image.tag({ 36 | id: $routeParams.id, 37 | repo: tag.repo, 38 | tag: tag.version, 39 | force: tag.force ? 1 : 0 40 | }, function (d) { 41 | Messages.send("Tag Added", $routeParams.id); 42 | $location.path('/images/' + $scope.id); 43 | }, function (e) { 44 | $scope.error = e.data; 45 | $('#error-message').show(); 46 | }); 47 | }; 48 | 49 | function getContainersFromImage($q, Container, imageId) { 50 | var defer = $q.defer(); 51 | 52 | Container.query({all: 1, notruc: 1}, function (d) { 53 | var containers = []; 54 | for (var i = 0; i < d.length; i++) { 55 | var c = d[i]; 56 | if (c.ImageID === imageId) { 57 | containers.push(new ContainerViewModel(c)); 58 | } 59 | } 60 | defer.resolve(containers); 61 | }); 62 | 63 | return defer.promise; 64 | } 65 | 66 | /** 67 | * Get RepoTags from the /images/query endpoint instead of /image/json, 68 | * for backwards compatibility with Docker API versions older than 1.21 69 | * @param {string} imageId 70 | */ 71 | function getRepoTags(imageId) { 72 | Image.query({}, function (d) { 73 | d.forEach(function(image) { 74 | if (image.Id === imageId && image.RepoTags[0] !== ':') { 75 | $scope.RepoTags = image.RepoTags; 76 | } 77 | }); 78 | }); 79 | } 80 | 81 | Image.get({id: $routeParams.id}, function (d) { 82 | $scope.image = d; 83 | $scope.id = d.Id; 84 | if (d.RepoTags) { 85 | $scope.RepoTags = d.RepoTags; 86 | } else { 87 | getRepoTags($scope.id); 88 | } 89 | 90 | getContainersFromImage($q, Container, $scope.id).then(function (containers) { 91 | LineChart.build('#containers-started-chart', containers, function (c) { 92 | return new Date(c.Created * 1000).toLocaleDateString(); 93 | }); 94 | }); 95 | }, function (e) { 96 | if (e.status === 404) { 97 | $('.detail').hide(); 98 | $scope.error = "Image not found.
" + $routeParams.id; 99 | } else { 100 | $scope.error = e.data; 101 | } 102 | $('#error-message').show(); 103 | }); 104 | 105 | $scope.getHistory(); 106 | }]); 107 | -------------------------------------------------------------------------------- /app/components/images/images.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

Images:

5 | 6 |
7 | 16 | 17 |
18 | 19 |
20 |
21 | 22 | 23 | 24 | 25 | 32 | 39 | 46 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
26 | 27 | Id 28 | 29 | 30 | 31 | 33 | 34 | Repository 35 | 36 | 37 | 38 | 40 | 41 | VirtualSize 42 | 43 | 44 | 45 | 47 | 48 | Created 49 | 50 | 51 | 52 |
{{ image.Id|truncate:20}}{{ image|repotag }}{{ image.VirtualSize|humansize }}{{ image.Created * 1000 | date: 'yyyy-MM-dd' }}
65 | -------------------------------------------------------------------------------- /app/components/images/imagesController.js: -------------------------------------------------------------------------------- 1 | angular.module('images', []) 2 | .controller('ImagesController', ['$scope', 'Image', 'ViewSpinner', 'Messages', 3 | function ($scope, Image, ViewSpinner, Messages) { 4 | $scope.sortType = 'Created'; 5 | $scope.sortReverse = true; 6 | $scope.toggle = false; 7 | 8 | $scope.order = function(sortType) { 9 | $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; 10 | $scope.sortType = sortType; 11 | }; 12 | 13 | $scope.showBuilder = function () { 14 | $('#build-modal').modal('show'); 15 | }; 16 | 17 | $scope.removeAction = function () { 18 | ViewSpinner.spin(); 19 | var counter = 0; 20 | var complete = function () { 21 | counter = counter - 1; 22 | if (counter === 0) { 23 | ViewSpinner.stop(); 24 | } 25 | }; 26 | angular.forEach($scope.images, function (i) { 27 | if (i.Checked) { 28 | counter = counter + 1; 29 | Image.remove({id: i.Id}, function (d) { 30 | angular.forEach(d, function (resource) { 31 | Messages.send("Image deleted", resource.Deleted); 32 | }); 33 | var index = $scope.images.indexOf(i); 34 | $scope.images.splice(index, 1); 35 | complete(); 36 | }, function (e) { 37 | Messages.error("Failure", e.data); 38 | complete(); 39 | }); 40 | } 41 | }); 42 | }; 43 | 44 | $scope.toggleSelectAll = function () { 45 | angular.forEach($scope.filteredImages, function (i) { 46 | i.Checked = $scope.toggle; 47 | }); 48 | }; 49 | 50 | ViewSpinner.spin(); 51 | Image.query({}, function (d) { 52 | $scope.images = d.map(function (item) { 53 | return new ImageViewModel(item); 54 | }); 55 | ViewSpinner.stop(); 56 | }, function (e) { 57 | Messages.error("Failure", e.data); 58 | ViewSpinner.stop(); 59 | }); 60 | }]); 61 | -------------------------------------------------------------------------------- /app/components/info/info.html: -------------------------------------------------------------------------------- 1 |
2 |

Docker Information

3 | 4 |
5 |

6 | API Endpoint: {{ endpoint }}
7 | API Version: {{ docker.ApiVersion }}
8 | Docker version: {{ docker.Version }}
9 | Git Commit: {{ docker.GitCommit }}
10 | Go Version: {{ docker.GoVersion }}
11 |

12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 |
Containers:{{ info.Containers }}
Images:{{ info.Images }}
Debug:{{ info.Debug }}
CPUs:{{ info.NCPU }}
Total Memory:{{ info.MemTotal|humansize }}
Operating System:{{ info.OperatingSystem }}
Kernel Version:{{ info.KernelVersion }}
ID:{{ info.ID }}
Labels:{{ info.Labels }}
File Descriptors:{{ info.NFd }}
Goroutines:{{ info.NGoroutines }}
Storage Driver:{{ info.Driver }}
Storage Driver Status: 67 |

68 | {{ val[0] }}: {{ val[1] }} 69 |

70 |
Execution Driver:{{ info.ExecutionDriver }}
Events:Events
IPv4 Forwarding:{{ info.IPv4Forwarding }}
Index Server Address:{{ info.IndexServerAddress }}
Init Path:{{ info.InitPath }}
Docker Root Directory:{{ info.DockerRootDir }}
Init SHA1{{ info.InitSha1 }}
Memory Limit:{{ info.MemoryLimit }}
Swap Limit:{{ info.SwapLimit }}
110 |
111 | -------------------------------------------------------------------------------- /app/components/info/infoController.js: -------------------------------------------------------------------------------- 1 | angular.module('info', []) 2 | .controller('InfoController', ['$scope', 'Info', 'Version', 'Settings', 3 | function ($scope, Info, Version, Settings) { 4 | $scope.info = {}; 5 | $scope.docker = {}; 6 | $scope.endpoint = Settings.endpoint; 7 | 8 | Version.get({}, function (d) { 9 | $scope.docker = d; 10 | }); 11 | Info.get({}, function (d) { 12 | $scope.info = d; 13 | }); 14 | }]); 15 | -------------------------------------------------------------------------------- /app/components/masthead/masthead.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 31 |
32 |
-------------------------------------------------------------------------------- /app/components/masthead/mastheadController.js: -------------------------------------------------------------------------------- 1 | angular.module('masthead', []) 2 | .controller('MastheadController', ['$scope', 'Version', function ($scope, Version) { 3 | $scope.template = 'app/components/masthead/masthead.html'; 4 | $scope.showNetworksVolumes = false; 5 | 6 | Version.get(function(d) { 7 | if (d.ApiVersion >= 1.21) { 8 | $scope.showNetworksVolumes = true; 9 | } 10 | }); 11 | 12 | $scope.refresh = function() { 13 | location.reload(); 14 | }; 15 | }]); 16 | -------------------------------------------------------------------------------- /app/components/network/network.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Network: {{ network.Name }}

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 41 | 42 | 43 | 44 | 83 | 84 | 85 | 86 | 98 | 99 | 100 |
Name:{{ network.Name }}
Id:{{ network.Id }}
Scope:{{ network.Scope }}
Driver:{{ network.Driver }}
IPAM: 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
Driver:{{ network.IPAM.Driver }}
Subnet:{{ network.IPAM.Config[0].Subnet }}
Gateway:{{ network.IPAM.Config[0].Gateway }}
40 |
Containers: 45 | 46 | 47 | 48 | 49 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |
Id:{{ Id }} 50 | 53 |
EndpointID:{{ container.EndpointID}}
MacAddress:{{ container.MacAddress}}
IPv4Address:{{ container.IPv4Address}}
IPv6Address:{{ container.IPv6Address}}
72 |
73 |
74 | 77 |
78 | 81 |
82 |
Options: 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 |
KeyValue
{{ k }}{{ v }}
97 |
101 | 102 | 103 |
104 | 105 | 106 |
107 | 109 |
110 |
-------------------------------------------------------------------------------- /app/components/network/networkController.js: -------------------------------------------------------------------------------- 1 | angular.module('network', []).config(['$routeProvider', function ($routeProvider) { 2 | $routeProvider.when('/networks/:id/', { 3 | templateUrl: 'app/components/network/network.html', 4 | controller: 'NetworkController' 5 | }); 6 | }]).controller('NetworkController', ['$scope', 'Network', 'ViewSpinner', 'Messages', '$routeParams', '$location', 'errorMsgFilter', 7 | function ($scope, Network, ViewSpinner, Messages, $routeParams, $location, errorMsgFilter) { 8 | 9 | $scope.disconnect = function disconnect(networkId, containerId) { 10 | ViewSpinner.spin(); 11 | Network.disconnect({id: $routeParams.id}, {Container: containerId}, function (d) { 12 | ViewSpinner.stop(); 13 | Messages.send("Container disconnected", containerId); 14 | $location.path('/networks/' + $routeParams.id); // Refresh the current page. 15 | }, function (e) { 16 | ViewSpinner.stop(); 17 | Messages.error("Failure", e.data); 18 | }); 19 | }; 20 | $scope.connect = function connect(networkId, containerId) { 21 | ViewSpinner.spin(); 22 | Network.connect({id: $routeParams.id}, {Container: containerId}, function (d) { 23 | ViewSpinner.stop(); 24 | var errmsg = errorMsgFilter(d); 25 | if (errmsg) { 26 | Messages.error('Error', errmsg); 27 | } else { 28 | Messages.send("Container connected", d); 29 | } 30 | $location.path('/networks/' + $routeParams.id); // Refresh the current page. 31 | }, function (e) { 32 | ViewSpinner.stop(); 33 | Messages.error("Failure", e.data); 34 | }); 35 | }; 36 | $scope.remove = function remove(networkId) { 37 | ViewSpinner.spin(); 38 | Network.remove({id: $routeParams.id}, function (d) { 39 | ViewSpinner.stop(); 40 | Messages.send("Network removed", d); 41 | $location.path('/networks'); // Go to the networks page 42 | }, function (e) { 43 | ViewSpinner.stop(); 44 | Messages.error("Failure", e.data); 45 | }); 46 | }; 47 | 48 | ViewSpinner.spin(); 49 | Network.get({id: $routeParams.id}, function (d) { 50 | $scope.network = d; 51 | ViewSpinner.stop(); 52 | }, function (e) { 53 | Messages.error("Failure", e.data); 54 | ViewSpinner.stop(); 55 | }); 56 | }]); 57 | -------------------------------------------------------------------------------- /app/components/networks/networks.html: -------------------------------------------------------------------------------- 1 |

Networks:

2 | 3 |
4 | 13 | 14 |
15 | 17 |
18 |
19 | 20 | 21 | 22 | 23 | 30 | 37 | 44 | 51 | 58 | 65 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 |
24 | 25 | Name 26 | 27 | 28 | 29 | 31 | 32 | Id 33 | 34 | 35 | 36 | 38 | 39 | Scope 40 | 41 | 42 | 43 | 45 | 46 | Driver 47 | 48 | 49 | 50 | 52 | 53 | IPAM Driver 54 | 55 | 56 | 57 | 59 | 60 | IPAM Subnet 61 | 62 | 63 | 64 | 66 | 67 | IPAM Gateway 68 | 69 | 70 | 71 |
{{ network.Name|truncate:20}}{{ network.Id }}{{ network.Scope }}{{ network.Driver }}{{ network.IPAM.Driver }}{{ network.IPAM.Config[0].Subnet }}{{ network.IPAM.Config[0].Gateway }}
87 |
88 |
89 |
90 |
91 | 92 | 94 |
95 |
96 | 97 | 99 |
100 |
101 | 102 | 104 |
105 |
106 | 107 | 109 |
110 |
111 | 112 | 114 |
115 | 119 |
120 |
121 |
-------------------------------------------------------------------------------- /app/components/networks/networksController.js: -------------------------------------------------------------------------------- 1 | angular.module('networks', []).config(['$routeProvider', function ($routeProvider) { 2 | $routeProvider.when('/networks/', { 3 | templateUrl: 'app/components/networks/networks.html', 4 | controller: 'NetworksController' 5 | }); 6 | }]).controller('NetworksController', ['$scope', 'Network', 'ViewSpinner', 'Messages', '$route', 'errorMsgFilter', 7 | function ($scope, Network, ViewSpinner, Messages, $route, errorMsgFilter) { 8 | $scope.sortType = 'Name'; 9 | $scope.sortReverse = true; 10 | $scope.toggle = false; 11 | $scope.order = function(sortType) { 12 | $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; 13 | $scope.sortType = sortType; 14 | }; 15 | $scope.createNetworkConfig = { 16 | "Name": '', 17 | "Driver": '', 18 | "IPAM": { 19 | "Config": [{ 20 | "Subnet": '', 21 | "IPRange": '', 22 | "Gateway": '' 23 | }] 24 | } 25 | }; 26 | 27 | 28 | 29 | $scope.removeAction = function () { 30 | ViewSpinner.spin(); 31 | var counter = 0; 32 | var complete = function () { 33 | counter = counter - 1; 34 | if (counter === 0) { 35 | ViewSpinner.stop(); 36 | } 37 | }; 38 | angular.forEach($scope.networks, function (network) { 39 | if (network.Checked) { 40 | counter = counter + 1; 41 | Network.remove({id: network.Id}, function (d) { 42 | Messages.send("Network deleted", network.Id); 43 | var index = $scope.networks.indexOf(network); 44 | $scope.networks.splice(index, 1); 45 | complete(); 46 | }, function (e) { 47 | Messages.error("Failure", e.data); 48 | complete(); 49 | }); 50 | } 51 | }); 52 | }; 53 | 54 | $scope.toggleSelectAll = function () { 55 | angular.forEach($scope.filteredNetworks, function (i) { 56 | i.Checked = $scope.toggle; 57 | }); 58 | }; 59 | 60 | $scope.addNetwork = function addNetwork(createNetworkConfig) { 61 | ViewSpinner.spin(); 62 | Network.create(createNetworkConfig, function (d) { 63 | if (d.Id) { 64 | Messages.send("Network created", d.Id); 65 | } else { 66 | Messages.error('Failure', errorMsgFilter(d)); 67 | } 68 | ViewSpinner.stop(); 69 | fetchNetworks(); 70 | }, function (e) { 71 | Messages.error("Failure", e.data); 72 | ViewSpinner.stop(); 73 | }); 74 | }; 75 | 76 | function fetchNetworks() { 77 | ViewSpinner.spin(); 78 | Network.query({}, function (d) { 79 | $scope.networks = d; 80 | ViewSpinner.stop(); 81 | }, function (e) { 82 | Messages.error("Failure", e.data); 83 | ViewSpinner.stop(); 84 | }); 85 | } 86 | fetchNetworks(); 87 | }]); 88 | -------------------------------------------------------------------------------- /app/components/pullImage/pullImage.html: -------------------------------------------------------------------------------- 1 | 36 | -------------------------------------------------------------------------------- /app/components/pullImage/pullImageController.js: -------------------------------------------------------------------------------- 1 | angular.module('pullImage', []) 2 | .controller('PullImageController', ['$scope', '$log', 'Messages', 'Image', 'ViewSpinner', 3 | function ($scope, $log, Messages, Image, ViewSpinner) { 4 | $scope.template = 'app/components/pullImage/pullImage.html'; 5 | 6 | $scope.init = function () { 7 | $scope.config = { 8 | registry: '', 9 | fromImage: '', 10 | tag: 'latest' 11 | }; 12 | }; 13 | 14 | $scope.init(); 15 | 16 | function failedRequestHandler(e, Messages) { 17 | Messages.error('Error', errorMsgFilter(e)); 18 | } 19 | 20 | $scope.pull = function () { 21 | $('#error-message').hide(); 22 | var imageName = ($scope.config.registry ? $scope.config.registry + '/' : '' ) + 23 | ($scope.config.fromImage); 24 | var config = {}; 25 | config.fromImage = imageName; 26 | config.tag = $scope.config.tag; 27 | 28 | ViewSpinner.spin(); 29 | $('#pull-modal').modal('hide'); 30 | Image.create(config, function (data) { 31 | ViewSpinner.stop(); 32 | if (data.constructor === Array) { 33 | var f = data.length > 0 && data[data.length - 1].hasOwnProperty('error'); 34 | //check for error 35 | if (f) { 36 | var d = data[data.length - 1]; 37 | $scope.error = "Cannot pull image " + imageName + " Reason: " + d.error; 38 | $('#pull-modal').modal('show'); 39 | $('#error-message').show(); 40 | } else { 41 | Messages.send("Image Added", imageName); 42 | $scope.init(); 43 | } 44 | } else { 45 | Messages.send("Image Added", imageName); 46 | $scope.init(); 47 | } 48 | }, function (e) { 49 | ViewSpinner.stop(); 50 | $scope.error = "Cannot pull image " + imageName + " Reason: " + e.data; 51 | $('#pull-modal').modal('show'); 52 | $('#error-message').show(); 53 | }); 54 | }; 55 | }]); 56 | -------------------------------------------------------------------------------- /app/components/sidebar/sidebar.html: -------------------------------------------------------------------------------- 1 |
2 | Running containers: 3 |
4 | Endpoint: {{ endpoint }} 5 | 11 |
12 | -------------------------------------------------------------------------------- /app/components/sidebar/sidebarController.js: -------------------------------------------------------------------------------- 1 | angular.module('sidebar', []) 2 | .controller('SideBarController', ['$scope', 'Container', 'Settings', 3 | function ($scope, Container, Settings) { 4 | $scope.template = 'partials/sidebar.html'; 5 | $scope.containers = []; 6 | $scope.endpoint = Settings.endpoint; 7 | 8 | Container.query({all: 0}, function (d) { 9 | $scope.containers = d; 10 | }); 11 | }]); 12 | -------------------------------------------------------------------------------- /app/components/startContainer/startContainerController.js: -------------------------------------------------------------------------------- 1 | angular.module('startContainer', ['ui.bootstrap']) 2 | .controller('StartContainerController', ['$scope', '$routeParams', '$location', 'Container', 'Messages', 'containernameFilter', 'errorMsgFilter', 3 | function ($scope, $routeParams, $location, Container, Messages, containernameFilter, errorMsgFilter) { 4 | $scope.template = 'app/components/startContainer/startcontainer.html'; 5 | 6 | Container.query({all: 1}, function (d) { 7 | $scope.containerNames = d.map(function (container) { 8 | return containernameFilter(container); 9 | }); 10 | }); 11 | 12 | $scope.config = { 13 | Env: [], 14 | Labels: [], 15 | Volumes: [], 16 | SecurityOpts: [], 17 | HostConfig: { 18 | PortBindings: [], 19 | Binds: [], 20 | Links: [], 21 | Dns: [], 22 | DnsSearch: [], 23 | VolumesFrom: [], 24 | CapAdd: [], 25 | CapDrop: [], 26 | Devices: [], 27 | LxcConf: [], 28 | ExtraHosts: [] 29 | } 30 | }; 31 | 32 | $scope.menuStatus = { 33 | containerOpen: true, 34 | hostConfigOpen: false 35 | }; 36 | 37 | function failedRequestHandler(e, Messages) { 38 | Messages.error('Error', errorMsgFilter(e)); 39 | } 40 | 41 | function rmEmptyKeys(col) { 42 | for (var key in col) { 43 | if (col[key] === null || col[key] === undefined || col[key] === '' || ($.isPlainObject(col[key]) && $.isEmptyObject(col[key])) || col[key].length === 0) { 44 | delete col[key]; 45 | } 46 | } 47 | } 48 | 49 | function getNames(arr) { 50 | return arr.map(function (item) { 51 | return item.name; 52 | }); 53 | } 54 | 55 | $scope.create = function () { 56 | // Copy the config before transforming fields to the remote API format 57 | var config = angular.copy($scope.config); 58 | 59 | config.Image = $routeParams.id; 60 | 61 | if (config.Cmd && config.Cmd[0] === "[") { 62 | config.Cmd = angular.fromJson(config.Cmd); 63 | } else if (config.Cmd) { 64 | config.Cmd = config.Cmd.split(' '); 65 | } 66 | 67 | config.Env = config.Env.map(function (envar) { 68 | return envar.name + '=' + envar.value; 69 | }); 70 | var labels = {}; 71 | config.Labels = config.Labels.forEach(function(label) { 72 | labels[label.key] = label.value; 73 | }); 74 | config.Labels = labels; 75 | 76 | config.Volumes = getNames(config.Volumes); 77 | config.SecurityOpts = getNames(config.SecurityOpts); 78 | 79 | config.HostConfig.VolumesFrom = getNames(config.HostConfig.VolumesFrom); 80 | config.HostConfig.Binds = getNames(config.HostConfig.Binds); 81 | config.HostConfig.Links = getNames(config.HostConfig.Links); 82 | config.HostConfig.Dns = getNames(config.HostConfig.Dns); 83 | config.HostConfig.DnsSearch = getNames(config.HostConfig.DnsSearch); 84 | config.HostConfig.CapAdd = getNames(config.HostConfig.CapAdd); 85 | config.HostConfig.CapDrop = getNames(config.HostConfig.CapDrop); 86 | config.HostConfig.LxcConf = config.HostConfig.LxcConf.reduce(function (prev, cur, idx) { 87 | prev[cur.name] = cur.value; 88 | return prev; 89 | }, {}); 90 | config.HostConfig.ExtraHosts = config.HostConfig.ExtraHosts.map(function (entry) { 91 | return entry.host + ':' + entry.ip; 92 | }); 93 | 94 | var ExposedPorts = {}; 95 | var PortBindings = {}; 96 | config.HostConfig.PortBindings.forEach(function (portBinding) { 97 | var intPort = portBinding.intPort + "/tcp"; 98 | if (portBinding.protocol === "udp") { 99 | intPort = portBinding.intPort + "/udp"; 100 | } 101 | var binding = { 102 | HostIp: portBinding.ip, 103 | HostPort: portBinding.extPort 104 | }; 105 | if (portBinding.intPort) { 106 | ExposedPorts[intPort] = {}; 107 | if (intPort in PortBindings) { 108 | PortBindings[intPort].push(binding); 109 | } else { 110 | PortBindings[intPort] = [binding]; 111 | } 112 | } else { 113 | Messages.send('Warning', 'Internal port must be specified for PortBindings'); 114 | } 115 | }); 116 | config.ExposedPorts = ExposedPorts; 117 | config.HostConfig.PortBindings = PortBindings; 118 | 119 | // Remove empty fields from the request to avoid overriding defaults 120 | rmEmptyKeys(config.HostConfig); 121 | rmEmptyKeys(config); 122 | 123 | var ctor = Container; 124 | var loc = $location; 125 | var s = $scope; 126 | Container.create(config, function (d) { 127 | if (d.Id) { 128 | ctor.start({id: d.Id}, {}, function (cd) { 129 | Messages.send('Container Started', d.Id); 130 | $('#create-modal').modal('hide'); 131 | loc.path('/containers/' + d.Id + '/'); 132 | }, function (e) { 133 | failedRequestHandler(e, Messages); 134 | }); 135 | } else { 136 | failedRequestHandler(d, Messages); 137 | } 138 | }, function (e) { 139 | failedRequestHandler(e, Messages); 140 | }); 141 | }; 142 | 143 | $scope.addEntry = function (array, entry) { 144 | array.push(entry); 145 | }; 146 | $scope.rmEntry = function (array, entry) { 147 | var idx = array.indexOf(entry); 148 | array.splice(idx, 1); 149 | }; 150 | }]); 151 | -------------------------------------------------------------------------------- /app/components/stats/stats.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Stats for: {{ containerName }}

4 | 5 |

CPU

6 | 7 |
8 |
9 | 10 |
11 |
12 | 13 |

Memory

14 | 15 |
16 |
17 | 18 |
19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
Max usage{{ data.memory_stats.max_usage | humansize }}
Limit{{ data.memory_stats.limit | humansize }}
Fail count{{ data.memory_stats.failcnt }}
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
{{ key }}{{ value }}
42 |
43 |
44 |
45 |
46 | 47 |

Network {{ networkName}}

48 |
49 |
50 | 51 |
52 |
53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
{{ key }}{{ value }}
62 |
63 |
64 |
65 |
66 |
67 |
68 | -------------------------------------------------------------------------------- /app/components/stats/statsController.js: -------------------------------------------------------------------------------- 1 | angular.module('stats', []) 2 | .controller('StatsController', ['Settings', '$scope', 'Messages', '$timeout', 'Container', '$routeParams', 'humansizeFilter', '$sce', function (Settings, $scope, Messages, $timeout, Container, $routeParams, humansizeFilter, $sce) { 3 | // TODO: Force scale to 0-100 for cpu, fix charts on dashboard, 4 | // TODO: Force memory scale to 0 - max memory 5 | 6 | var cpuLabels = []; 7 | var cpuData = []; 8 | var memoryLabels = []; 9 | var memoryData = []; 10 | var networkLabels = []; 11 | var networkTxData = []; 12 | var networkRxData = []; 13 | for (var i = 0; i < 20; i++) { 14 | cpuLabels.push(''); 15 | cpuData.push(0); 16 | memoryLabels.push(''); 17 | memoryData.push(0); 18 | networkLabels.push(''); 19 | networkTxData.push(0); 20 | networkRxData.push(0); 21 | } 22 | var cpuDataset = { // CPU Usage 23 | fillColor: "rgba(151,187,205,0.5)", 24 | strokeColor: "rgba(151,187,205,1)", 25 | pointColor: "rgba(151,187,205,1)", 26 | pointStrokeColor: "#fff", 27 | data: cpuData 28 | }; 29 | var memoryDataset = { 30 | fillColor: "rgba(151,187,205,0.5)", 31 | strokeColor: "rgba(151,187,205,1)", 32 | pointColor: "rgba(151,187,205,1)", 33 | pointStrokeColor: "#fff", 34 | data: memoryData 35 | }; 36 | var networkRxDataset = { 37 | label: "Rx Bytes", 38 | fillColor: "rgba(151,187,205,0.5)", 39 | strokeColor: "rgba(151,187,205,1)", 40 | pointColor: "rgba(151,187,205,1)", 41 | pointStrokeColor: "#fff", 42 | data: networkRxData 43 | }; 44 | var networkTxDataset = { 45 | label: "Tx Bytes", 46 | fillColor: "rgba(255,180,174,0.5)", 47 | strokeColor: "rgba(255,180,174,1)", 48 | pointColor: "rgba(255,180,174,1)", 49 | pointStrokeColor: "#fff", 50 | data: networkTxData 51 | }; 52 | var networkLegendData = [ 53 | { 54 | //value: '', 55 | color: 'rgba(151,187,205,0.5)', 56 | title: 'Rx Data' 57 | }, 58 | { 59 | //value: '', 60 | color: 'rgba(255,180,174,0.5)', 61 | title: 'Rx Data' 62 | }]; 63 | legend($('#network-legend').get(0), networkLegendData); 64 | 65 | Chart.defaults.global.animationSteps = 30; // Lower from 60 to ease CPU load. 66 | var cpuChart = new Chart($('#cpu-stats-chart').get(0).getContext("2d")).Line({ 67 | labels: cpuLabels, 68 | datasets: [cpuDataset] 69 | }, { 70 | responsive: true 71 | }); 72 | 73 | var memoryChart = new Chart($('#memory-stats-chart').get(0).getContext('2d')).Line({ 74 | labels: memoryLabels, 75 | datasets: [memoryDataset] 76 | }, 77 | { 78 | scaleLabel: function (valueObj) { 79 | return humansizeFilter(parseInt(valueObj.value, 10)); 80 | }, 81 | responsive: true 82 | //scaleOverride: true, 83 | //scaleSteps: 10, 84 | //scaleStepWidth: Math.ceil(initialStats.memory_stats.limit / 10), 85 | //scaleStartValue: 0 86 | }); 87 | var networkChart = new Chart($('#network-stats-chart').get(0).getContext("2d")).Line({ 88 | labels: networkLabels, 89 | datasets: [networkRxDataset, networkTxDataset] 90 | }, { 91 | scaleLabel: function (valueObj) { 92 | return humansizeFilter(parseInt(valueObj.value, 10)); 93 | }, 94 | responsive: true 95 | }); 96 | $scope.networkLegend = $sce.trustAsHtml(networkChart.generateLegend()); 97 | 98 | function updateStats() { 99 | Container.stats({id: $routeParams.id}, function (d) { 100 | var arr = Object.keys(d).map(function (key) { 101 | return d[key]; 102 | }); 103 | if (arr.join('').indexOf('no such id') !== -1) { 104 | Messages.error('Unable to retrieve stats', 'Is this container running?'); 105 | return; 106 | } 107 | 108 | // Update graph with latest data 109 | $scope.data = d; 110 | updateCpuChart(d); 111 | updateMemoryChart(d); 112 | updateNetworkChart(d); 113 | timeout = $timeout(updateStats, 5000); 114 | }, function () { 115 | Messages.error('Unable to retrieve stats', 'Is this container running?'); 116 | timeout = $timeout(updateStats, 5000); 117 | }); 118 | } 119 | 120 | var timeout; 121 | $scope.$on('$destroy', function () { 122 | $timeout.cancel(timeout); 123 | }); 124 | 125 | updateStats(); 126 | 127 | function updateCpuChart(data) { 128 | cpuChart.addData([calculateCPUPercent(data)], new Date(data.read).toLocaleTimeString()); 129 | cpuChart.removeData(); 130 | } 131 | 132 | function updateMemoryChart(data) { 133 | memoryChart.addData([data.memory_stats.usage], new Date(data.read).toLocaleTimeString()); 134 | memoryChart.removeData(); 135 | } 136 | 137 | var lastRxBytes = 0, lastTxBytes = 0; 138 | 139 | function updateNetworkChart(data) { 140 | // 1.9+ contains an object of networks, for now we'll just show stats for the first network 141 | // TODO: Show graphs for all networks 142 | if (data.networks) { 143 | $scope.networkName = Object.keys(data.networks)[0]; 144 | data.network = data.networks[$scope.networkName]; 145 | } 146 | var rxBytes = 0, txBytes = 0; 147 | if (lastRxBytes !== 0 || lastTxBytes !== 0) { 148 | // These will be zero on first call, ignore to prevent large graph spike 149 | rxBytes = data.network.rx_bytes - lastRxBytes; 150 | txBytes = data.network.tx_bytes - lastTxBytes; 151 | } 152 | lastRxBytes = data.network.rx_bytes; 153 | lastTxBytes = data.network.tx_bytes; 154 | networkChart.addData([rxBytes, txBytes], new Date(data.read).toLocaleTimeString()); 155 | networkChart.removeData(); 156 | } 157 | 158 | function calculateCPUPercent(stats) { 159 | // Same algorithm the official client uses: https://github.com/docker/docker/blob/master/api/client/stats.go#L195-L208 160 | var prevCpu = stats.precpu_stats; 161 | var curCpu = stats.cpu_stats; 162 | 163 | var cpuPercent = 0.0; 164 | 165 | // calculate the change for the cpu usage of the container in between readings 166 | var cpuDelta = curCpu.cpu_usage.total_usage - prevCpu.cpu_usage.total_usage; 167 | // calculate the change for the entire system between readings 168 | var systemDelta = curCpu.system_cpu_usage - prevCpu.system_cpu_usage; 169 | 170 | if (systemDelta > 0.0 && cpuDelta > 0.0) { 171 | cpuPercent = (cpuDelta / systemDelta) * curCpu.cpu_usage.percpu_usage.length * 100.0; 172 | } 173 | return cpuPercent; 174 | } 175 | 176 | Container.get({id: $routeParams.id}, function (d) { 177 | $scope.containerName = d.Name.substring(1); 178 | }, function (e) { 179 | Messages.error("Failure", e.data); 180 | }); 181 | }]) 182 | ; -------------------------------------------------------------------------------- /app/components/volumes/volumes.html: -------------------------------------------------------------------------------- 1 |

Volumes:

2 | 3 |
4 | 13 | 14 |
15 | 17 |
18 |
19 | 20 | 21 | 22 | 23 | 30 | 37 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
24 | 25 | Name 26 | 27 | 28 | 29 | 31 | 32 | Driver 33 | 34 | 35 | 36 | 38 | 39 | Mountpoint 40 | 41 | 42 | 43 |
{{ volume.Name|truncate:20 }}{{ volume.Driver }}{{ volume.Mountpoint }}
55 |
56 |
57 |
58 |
59 | 60 | 62 |
63 |
64 | 65 | 67 |
68 | 72 |
73 |
74 |
-------------------------------------------------------------------------------- /app/components/volumes/volumesController.js: -------------------------------------------------------------------------------- 1 | angular.module('volumes', []).config(['$routeProvider', function ($routeProvider) { 2 | $routeProvider.when('/volumes/', { 3 | templateUrl: 'app/components/volumes/volumes.html', 4 | controller: 'VolumesController' 5 | }); 6 | }]).controller('VolumesController', ['$scope', 'Volume', 'ViewSpinner', 'Messages', '$route', 'errorMsgFilter', 7 | function ($scope, Volume, ViewSpinner, Messages, $route, errorMsgFilter) { 8 | $scope.sortType = 'Name'; 9 | $scope.sortReverse = true; 10 | $scope.toggle = false; 11 | $scope.order = function(sortType) { 12 | $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; 13 | $scope.sortType = sortType; 14 | }; 15 | $scope.createVolumeConfig = { 16 | "Name": "", 17 | "Driver": "" 18 | }; 19 | 20 | 21 | 22 | $scope.removeAction = function () { 23 | ViewSpinner.spin(); 24 | var counter = 0; 25 | var complete = function () { 26 | counter = counter - 1; 27 | if (counter === 0) { 28 | ViewSpinner.stop(); 29 | } 30 | }; 31 | angular.forEach($scope.volumes, function (volume) { 32 | if (volume.Checked) { 33 | counter = counter + 1; 34 | Volume.remove({name: volume.Name}, function (d) { 35 | Messages.send("Volume deleted", volume.Name); 36 | var index = $scope.volumes.indexOf(volume); 37 | $scope.volumes.splice(index, 1); 38 | complete(); 39 | }, function (e) { 40 | Messages.error("Failure", e.data); 41 | complete(); 42 | }); 43 | } 44 | }); 45 | }; 46 | 47 | $scope.toggleSelectAll = function () { 48 | angular.forEach($scope.filteredVolumes, function (i) { 49 | i.Checked = $scope.toggle; 50 | }); 51 | }; 52 | 53 | $scope.addVolume = function addVolume(createVolumeConfig) { 54 | ViewSpinner.spin(); 55 | Volume.create(createVolumeConfig, function (d) { 56 | if (d.Name) { 57 | Messages.send("Volume created", d.Name); 58 | } else { 59 | Messages.error('Failure', errorMsgFilter(d)); 60 | } 61 | ViewSpinner.stop(); 62 | fetchVolumes(); 63 | }, function (e) { 64 | Messages.error("Failure", e.data); 65 | ViewSpinner.stop(); 66 | }); 67 | }; 68 | 69 | function fetchVolumes() { 70 | ViewSpinner.spin(); 71 | Volume.query({}, function (d) { 72 | $scope.volumes = d.Volumes; 73 | ViewSpinner.stop(); 74 | }, function (e) { 75 | Messages.error("Failure", e.data); 76 | ViewSpinner.stop(); 77 | }); 78 | } 79 | fetchVolumes(); 80 | }]); 81 | -------------------------------------------------------------------------------- /app/shared/filters.js: -------------------------------------------------------------------------------- 1 | angular.module('uifordocker.filters', []) 2 | .filter('truncate', function () { 3 | 'use strict'; 4 | return function (text, length, end) { 5 | if (isNaN(length)) { 6 | length = 10; 7 | } 8 | 9 | if (end === undefined) { 10 | end = '...'; 11 | } 12 | 13 | if (text.length <= length || text.length - end.length <= length) { 14 | return text; 15 | } 16 | else { 17 | return String(text).substring(0, length - end.length) + end; 18 | } 19 | }; 20 | }) 21 | .filter('statusbadge', function () { 22 | 'use strict'; 23 | return function (text) { 24 | if (text === 'Ghost') { 25 | return 'important'; 26 | } else if (text.indexOf('Exit') !== -1 && text !== 'Exit 0') { 27 | return 'warning'; 28 | } 29 | return 'success'; 30 | }; 31 | }) 32 | .filter('getstatetext', function () { 33 | 'use strict'; 34 | return function (state) { 35 | if (state === undefined) { 36 | return ''; 37 | } 38 | if (state.Ghost && state.Running) { 39 | return 'Ghost'; 40 | } 41 | if (state.Running && state.Paused) { 42 | return 'Running (Paused)'; 43 | } 44 | if (state.Running) { 45 | return 'Running'; 46 | } 47 | return 'Stopped'; 48 | }; 49 | }) 50 | .filter('getstatelabel', function () { 51 | 'use strict'; 52 | return function (state) { 53 | if (state === undefined) { 54 | return 'label-default'; 55 | } 56 | 57 | if (state.Ghost && state.Running) { 58 | return 'label-important'; 59 | } 60 | if (state.Running) { 61 | return 'label-success'; 62 | } 63 | return 'label-default'; 64 | }; 65 | }) 66 | .filter('humansize', function () { 67 | 'use strict'; 68 | return function (bytes) { 69 | var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; 70 | if (bytes === 0) { 71 | return 'n/a'; 72 | } 73 | var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)), 10); 74 | var value = bytes / Math.pow(1024, i); 75 | var decimalPlaces = (i < 1) ? 0 : (i - 1); 76 | return value.toFixed(decimalPlaces) + ' ' + sizes[[i]]; 77 | }; 78 | }) 79 | .filter('containername', function () { 80 | 'use strict'; 81 | return function (container) { 82 | var name = container.Names[0]; 83 | return name.substring(1, name.length); 84 | }; 85 | }) 86 | .filter('repotag', function () { 87 | 'use strict'; 88 | return function (image) { 89 | if (image.RepoTags && image.RepoTags.length > 0) { 90 | var tag = image.RepoTags[0]; 91 | if (tag === ':') { 92 | tag = ''; 93 | } 94 | return tag; 95 | } 96 | return ''; 97 | }; 98 | }) 99 | .filter('errorMsg', function () { 100 | return function (object) { 101 | var idx = 0; 102 | var msg = ''; 103 | while (object[idx] && typeof(object[idx]) === 'string') { 104 | msg += object[idx]; 105 | idx++; 106 | } 107 | return msg; 108 | }; 109 | }); 110 | -------------------------------------------------------------------------------- /app/shared/services.js: -------------------------------------------------------------------------------- 1 | angular.module('uifordocker.services', ['ngResource', 'ngSanitize']) 2 | .factory('Container', ['$resource', 'Settings', function ContainerFactory($resource, Settings) { 3 | 'use strict'; 4 | // Resource for interacting with the docker containers 5 | // http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#2-1-containers 6 | return $resource(Settings.url + '/containers/:id/:action', { 7 | name: '@name' 8 | }, { 9 | query: {method: 'GET', params: {all: 0, action: 'json'}, isArray: true}, 10 | get: {method: 'GET', params: {action: 'json'}}, 11 | start: {method: 'POST', params: {id: '@id', action: 'start'}}, 12 | stop: {method: 'POST', params: {id: '@id', t: 5, action: 'stop'}}, 13 | restart: {method: 'POST', params: {id: '@id', t: 5, action: 'restart'}}, 14 | kill: {method: 'POST', params: {id: '@id', action: 'kill'}}, 15 | pause: {method: 'POST', params: {id: '@id', action: 'pause'}}, 16 | unpause: {method: 'POST', params: {id: '@id', action: 'unpause'}}, 17 | changes: {method: 'GET', params: {action: 'changes'}, isArray: true}, 18 | create: {method: 'POST', params: {action: 'create'}}, 19 | remove: {method: 'DELETE', params: {id: '@id', v: 0}}, 20 | rename: {method: 'POST', params: {id: '@id', action: 'rename'}, isArray: false}, 21 | stats: {method: 'GET', params: {id: '@id', stream: false, action: 'stats'}, timeout: 5000} 22 | }); 23 | }]) 24 | .factory('ContainerCommit', ['$resource', '$http', 'Settings', function ContainerCommitFactory($resource, $http, Settings) { 25 | 'use strict'; 26 | // http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#create-a-new-image-from-a-container-s-changes 27 | return { 28 | commit: function (params, callback) { 29 | $http({ 30 | method: 'POST', 31 | url: Settings.url + '/commit', 32 | params: { 33 | 'container': params.id, 34 | 'tag': params.tag || null, 35 | 'repo': params.repo || null 36 | }, 37 | data: params.config 38 | }).success(callback).error(function (data, status, headers, config) { 39 | console.log(error, data); 40 | }); 41 | } 42 | }; 43 | }]) 44 | .factory('ContainerLogs', ['$resource', '$http', 'Settings', function ContainerLogsFactory($resource, $http, Settings) { 45 | 'use strict'; 46 | // http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#get-container-logs 47 | return { 48 | get: function (id, params, callback) { 49 | $http({ 50 | method: 'GET', 51 | url: Settings.url + '/containers/' + id + '/logs', 52 | params: { 53 | 'stdout': params.stdout || 0, 54 | 'stderr': params.stderr || 0, 55 | 'timestamps': params.timestamps || 0, 56 | 'tail': params.tail || 'all' 57 | } 58 | }).success(callback).error(function (data, status, headers, config) { 59 | console.log(error, data); 60 | }); 61 | } 62 | }; 63 | }]) 64 | .factory('ContainerTop', ['$http', 'Settings', function ($http, Settings) { 65 | 'use strict'; 66 | // http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#list-processes-running-inside-a-container 67 | return { 68 | get: function (id, params, callback, errorCallback) { 69 | $http({ 70 | method: 'GET', 71 | url: Settings.url + '/containers/' + id + '/top', 72 | params: { 73 | ps_args: params.ps_args 74 | } 75 | }).success(callback); 76 | } 77 | }; 78 | }]) 79 | .factory('Image', ['$resource', 'Settings', function ImageFactory($resource, Settings) { 80 | 'use strict'; 81 | // http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#2-2-images 82 | return $resource(Settings.url + '/images/:id/:action', {}, { 83 | query: {method: 'GET', params: {all: 0, action: 'json'}, isArray: true}, 84 | get: {method: 'GET', params: {action: 'json'}}, 85 | search: {method: 'GET', params: {action: 'search'}}, 86 | history: {method: 'GET', params: {action: 'history'}, isArray: true}, 87 | create: { 88 | method: 'POST', isArray: true, transformResponse: [function f(data) { 89 | var str = data.replace(/\n/g, " ").replace(/\}\W*\{/g, "}, {"); 90 | return angular.fromJson("[" + str + "]"); 91 | }], 92 | params: {action: 'create', fromImage: '@fromImage', tag: '@tag'} 93 | }, 94 | insert: {method: 'POST', params: {id: '@id', action: 'insert'}}, 95 | push: {method: 'POST', params: {id: '@id', action: 'push'}}, 96 | tag: {method: 'POST', params: {id: '@id', action: 'tag', force: 0, repo: '@repo', tag: '@tag'}}, 97 | remove: {method: 'DELETE', params: {id: '@id'}, isArray: true}, 98 | inspect: {method: 'GET', params: {id: '@id', action: 'json'}} 99 | }); 100 | }]) 101 | .factory('Version', ['$resource', 'Settings', function VersionFactory($resource, Settings) { 102 | 'use strict'; 103 | // http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#show-the-docker-version-information 104 | return $resource(Settings.url + '/version', {}, { 105 | get: {method: 'GET'} 106 | }); 107 | }]) 108 | .factory('Auth', ['$resource', 'Settings', function AuthFactory($resource, Settings) { 109 | 'use strict'; 110 | // http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#check-auth-configuration 111 | return $resource(Settings.url + '/auth', {}, { 112 | get: {method: 'GET'}, 113 | update: {method: 'POST'} 114 | }); 115 | }]) 116 | .factory('Info', ['$resource', 'Settings', function InfoFactory($resource, Settings) { 117 | 'use strict'; 118 | // http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#display-system-wide-information 119 | return $resource(Settings.url + '/info', {}, { 120 | get: {method: 'GET'} 121 | }); 122 | }]) 123 | .factory('Network', ['$resource', 'Settings', function NetworkFactory($resource, Settings) { 124 | 'use strict'; 125 | // http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#2-5-networks 126 | return $resource(Settings.url + '/networks/:id/:action', {id: '@id'}, { 127 | query: {method: 'GET', isArray: true}, 128 | get: {method: 'GET'}, 129 | create: {method: 'POST', params: {action: 'create'}}, 130 | remove: {method: 'DELETE'}, 131 | connect: {method: 'POST', params: {action: 'connect'}}, 132 | disconnect: {method: 'POST', params: {action: 'disconnect'}} 133 | }); 134 | }]) 135 | .factory('Volume', ['$resource', 'Settings', function VolumeFactory($resource, Settings) { 136 | 'use strict'; 137 | // http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#2-5-networks 138 | return $resource(Settings.url + '/volumes/:name/:action', {name: '@name'}, { 139 | query: {method: 'GET'}, 140 | get: {method: 'GET'}, 141 | create: {method: 'POST', params: {action: 'create'}}, 142 | remove: {method: 'DELETE'} 143 | }); 144 | }]) 145 | .factory('Settings', ['DOCKER_ENDPOINT', 'DOCKER_PORT', 'UI_VERSION', function SettingsFactory(DOCKER_ENDPOINT, DOCKER_PORT, UI_VERSION) { 146 | 'use strict'; 147 | var url = DOCKER_ENDPOINT; 148 | if (DOCKER_PORT) { 149 | url = url + DOCKER_PORT + '\\' + DOCKER_PORT; 150 | } 151 | var firstLoad = (localStorage.getItem('firstLoad') || 'true') === 'true'; 152 | return { 153 | displayAll: false, 154 | endpoint: DOCKER_ENDPOINT, 155 | uiVersion: UI_VERSION, 156 | url: url, 157 | firstLoad: firstLoad 158 | }; 159 | }]) 160 | .factory('ViewSpinner', function ViewSpinnerFactory() { 161 | 'use strict'; 162 | var spinner = new Spinner(); 163 | var target = document.getElementById('view'); 164 | 165 | return { 166 | spin: function () { 167 | spinner.spin(target); 168 | }, 169 | stop: function () { 170 | spinner.stop(); 171 | } 172 | }; 173 | }) 174 | .factory('Messages', ['$rootScope', '$sanitize', function MessagesFactory($rootScope, $sanitize) { 175 | 'use strict'; 176 | return { 177 | send: function (title, text) { 178 | $.gritter.add({ 179 | title: $sanitize(title), 180 | text: $sanitize(text), 181 | time: 2000, 182 | before_open: function () { 183 | if ($('.gritter-item-wrapper').length === 3) { 184 | return false; 185 | } 186 | } 187 | }); 188 | }, 189 | error: function (title, text) { 190 | $.gritter.add({ 191 | title: $sanitize(title), 192 | text: $sanitize(text), 193 | time: 10000, 194 | before_open: function () { 195 | if ($('.gritter-item-wrapper').length === 4) { 196 | return false; 197 | } 198 | } 199 | }); 200 | } 201 | }; 202 | }]) 203 | .factory('LineChart', ['Settings', function LineChartFactory(Settings) { 204 | 'use strict'; 205 | return { 206 | build: function (id, data, getkey) { 207 | var chart = new Chart($(id).get(0).getContext("2d")); 208 | var map = {}; 209 | 210 | for (var i = 0; i < data.length; i++) { 211 | var c = data[i]; 212 | var key = getkey(c); 213 | 214 | var count = map[key]; 215 | if (count === undefined) { 216 | count = 0; 217 | } 218 | count += 1; 219 | map[key] = count; 220 | } 221 | 222 | var labels = []; 223 | data = []; 224 | var keys = Object.keys(map); 225 | var max = 1; 226 | 227 | for (i = keys.length - 1; i > -1; i--) { 228 | var k = keys[i]; 229 | labels.push(k); 230 | data.push(map[k]); 231 | if (map[k] > max) { 232 | max = map[k]; 233 | } 234 | } 235 | var steps = Math.min(max, 10); 236 | var dataset = { 237 | fillColor: "rgba(151,187,205,0.5)", 238 | strokeColor: "rgba(151,187,205,1)", 239 | pointColor: "rgba(151,187,205,1)", 240 | pointStrokeColor: "#fff", 241 | data: data 242 | }; 243 | chart.Line({ 244 | labels: labels, 245 | datasets: [dataset] 246 | }, 247 | { 248 | scaleStepWidth: Math.ceil(max / steps), 249 | pointDotRadius: 1, 250 | scaleIntegersOnly: true, 251 | scaleOverride: true, 252 | scaleSteps: steps 253 | }); 254 | } 255 | }; 256 | }]); 257 | -------------------------------------------------------------------------------- /app/shared/viewmodel.js: -------------------------------------------------------------------------------- 1 | function ImageViewModel(data) { 2 | this.Id = data.Id; 3 | this.Tag = data.Tag; 4 | this.Repository = data.Repository; 5 | this.Created = data.Created; 6 | this.Checked = false; 7 | this.RepoTags = data.RepoTags; 8 | this.VirtualSize = data.VirtualSize; 9 | } 10 | 11 | function ContainerViewModel(data) { 12 | this.Id = data.Id; 13 | this.Image = data.Image; 14 | this.Command = data.Command; 15 | this.Created = data.Created; 16 | this.SizeRw = data.SizeRw; 17 | this.Status = data.Status; 18 | this.Checked = false; 19 | this.Names = data.Names; 20 | } 21 | -------------------------------------------------------------------------------- /assets/css/app.css: -------------------------------------------------------------------------------- 1 | .container > hr { 2 | margin: 60px 0; 3 | } 4 | 5 | .jumbotron { 6 | margin: 80px 0; 7 | text-align: center; 8 | } 9 | 10 | .jumbotron h1 { 11 | font-size: 100px; 12 | line-height: 1; 13 | } 14 | 15 | .jumbotron .lead { 16 | font-size: 24px; 17 | line-height: 1.25; 18 | } 19 | 20 | .jumbotron .btn { 21 | padding: 14px 24px; 22 | font-size: 21px; 23 | } 24 | 25 | .marketing { 26 | margin: 60px 0; 27 | } 28 | 29 | .marketing p + h4 { 30 | margin-top: 28px; 31 | } 32 | 33 | .masthead .nav { 34 | margin: 0; 35 | margin: 0 0 2em 0; 36 | width: 100%; 37 | } 38 | 39 | .masthead .nav.well { 40 | padding: 0; 41 | } 42 | 43 | .masthead .nav li { 44 | display: table-cell; 45 | float: none; 46 | width: 1%; 47 | } 48 | 49 | .masthead .nav li a { 50 | font-weight: bold; 51 | text-align: center; 52 | border-right: 1px solid rgba(0,0,0,.1); 53 | border-left: 1px solid rgba(255,255,255,.75); 54 | } 55 | 56 | .masthead .nav li:first-child a { 57 | border-left: 0; 58 | border-radius: 3px 0 0 3px; 59 | } 60 | .masthead .nav li:last-child a { 61 | border-right: 0; 62 | border-radius: 0 3px 3px 0; 63 | } 64 | 65 | .btn-group button { 66 | margin: 3px; 67 | } 68 | 69 | .detail { 70 | width: 80%; 71 | margin: 0 auto; 72 | } 73 | 74 | .center { 75 | width: 100%; 76 | margin: 0 auto; 77 | } 78 | 79 | .btn-remove { 80 | max-width: 70%; 81 | margin: 0 auto; 82 | } 83 | 84 | .actions { 85 | margin: 0 auto; 86 | } 87 | 88 | .container-bottom { 89 | height: 50px; 90 | } 91 | 92 | .well { 93 | padding: 10px 15px 0 15px; 94 | } 95 | 96 | .messages { 97 | max-height: 50px; 98 | overflow-x: hidden; 99 | overflow-y: scroll; 100 | } 101 | 102 | .legend .title { 103 | padding: 0 0.3em; 104 | margin: 0.5em; 105 | border-style: solid; 106 | border-width: 0 0 0 1em; 107 | } 108 | 109 | .inline-four .form-control { 110 | max-width: 25%; 111 | } 112 | 113 | .dropdown { 114 | cursor: pointer; 115 | } 116 | 117 | .navbar-brand > img { 118 | max-height: 100%; 119 | float: left; 120 | margin-right: 6px; 121 | } -------------------------------------------------------------------------------- /assets/ico/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevana/ui-for-docker/f324a66f74bc6fff2379f59fa014875d206e948c/assets/ico/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /assets/ico/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevana/ui-for-docker/f324a66f74bc6fff2379f59fa014875d206e948c/assets/ico/favicon.ico -------------------------------------------------------------------------------- /assets/js/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "validthis": true, 3 | "laxcomma" : true, 4 | "laxbreak" : true, 5 | "browser" : true, 6 | "eqnull" : true, 7 | "debug" : true, 8 | "devel" : true, 9 | "boss" : true, 10 | "expr" : true, 11 | "asi" : true 12 | } -------------------------------------------------------------------------------- /assets/js/jquery.gritter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Gritter for jQuery 3 | * http://www.boedesign.com/ 4 | * 5 | * Copyright (c) 2012 Jordan Boesch 6 | * Dual licensed under the MIT and GPL licenses. 7 | * 8 | * Date: February 24, 2012 9 | * Version: 1.7.4 10 | */ 11 | 12 | (function($){ 13 | 14 | /** 15 | * Set it up as an object under the jQuery namespace 16 | */ 17 | $.gritter = {}; 18 | 19 | /** 20 | * Set up global options that the user can over-ride 21 | */ 22 | $.gritter.options = { 23 | position: '', 24 | class_name: '', // could be set to 'gritter-light' to use white notifications 25 | fade_in_speed: 'medium', // how fast notifications fade in 26 | fade_out_speed: 1000, // how fast the notices fade out 27 | time: 6000 // hang on the screen for... 28 | } 29 | 30 | /** 31 | * Add a gritter notification to the screen 32 | * @see Gritter#add(); 33 | */ 34 | $.gritter.add = function(params){ 35 | 36 | try { 37 | return Gritter.add(params || {}); 38 | } catch(e) { 39 | 40 | var err = 'Gritter Error: ' + e; 41 | (typeof(console) != 'undefined' && console.error) ? 42 | console.error(err, params) : 43 | alert(err); 44 | 45 | } 46 | 47 | } 48 | 49 | /** 50 | * Remove a gritter notification from the screen 51 | * @see Gritter#removeSpecific(); 52 | */ 53 | $.gritter.remove = function(id, params){ 54 | Gritter.removeSpecific(id, params || {}); 55 | } 56 | 57 | /** 58 | * Remove all notifications 59 | * @see Gritter#stop(); 60 | */ 61 | $.gritter.removeAll = function(params){ 62 | Gritter.stop(params || {}); 63 | } 64 | 65 | /** 66 | * Big fat Gritter object 67 | * @constructor (not really since its object literal) 68 | */ 69 | var Gritter = { 70 | 71 | // Public - options to over-ride with $.gritter.options in "add" 72 | position: '', 73 | fade_in_speed: '', 74 | fade_out_speed: '', 75 | time: '', 76 | 77 | // Private - no touchy the private parts 78 | _custom_timer: 0, 79 | _item_count: 0, 80 | _is_setup: 0, 81 | _tpl_close: 'Close Notification', 82 | _tpl_title: '[[title]]', 83 | _tpl_item: '', 84 | _tpl_wrap: '
', 85 | 86 | /** 87 | * Add a gritter notification to the screen 88 | * @param {Object} params The object that contains all the options for drawing the notification 89 | * @return {Integer} The specific numeric id to that gritter notification 90 | */ 91 | add: function(params){ 92 | // Handle straight text 93 | if(typeof(params) == 'string'){ 94 | params = {text:params}; 95 | } 96 | 97 | // We might have some issues if we don't have a title or text! 98 | if(params.text === null){ 99 | throw 'You must supply "text" parameter.'; 100 | } 101 | 102 | // Check the options and set them once 103 | if(!this._is_setup){ 104 | this._runSetup(); 105 | } 106 | 107 | // Basics 108 | var title = params.title, 109 | text = params.text, 110 | image = params.image || '', 111 | sticky = params.sticky || false, 112 | item_class = params.class_name || $.gritter.options.class_name, 113 | position = $.gritter.options.position, 114 | time_alive = params.time || ''; 115 | 116 | this._verifyWrapper(); 117 | 118 | this._item_count++; 119 | var number = this._item_count, 120 | tmp = this._tpl_item; 121 | 122 | // Assign callbacks 123 | $(['before_open', 'after_open', 'before_close', 'after_close']).each(function(i, val){ 124 | Gritter['_' + val + '_' + number] = ($.isFunction(params[val])) ? params[val] : function(){} 125 | }); 126 | 127 | // Reset 128 | this._custom_timer = 0; 129 | 130 | // A custom fade time set 131 | if(time_alive){ 132 | this._custom_timer = time_alive; 133 | } 134 | 135 | var image_str = (image != '') ? '' : '', 136 | class_name = (image != '') ? 'gritter-with-image' : 'gritter-without-image'; 137 | 138 | // String replacements on the template 139 | if(title){ 140 | title = this._str_replace('[[title]]',title,this._tpl_title); 141 | }else{ 142 | title = ''; 143 | } 144 | 145 | tmp = this._str_replace( 146 | ['[[title]]', '[[text]]', '[[close]]', '[[image]]', '[[number]]', '[[class_name]]', '[[item_class]]'], 147 | [title, text, this._tpl_close, image_str, this._item_count, class_name, item_class], tmp 148 | ); 149 | 150 | // If it's false, don't show another gritter message 151 | if(this['_before_open_' + number]() === false){ 152 | return false; 153 | } 154 | 155 | $('#gritter-notice-wrapper').addClass(position).append(tmp); 156 | 157 | var item = $('#gritter-item-' + this._item_count); 158 | 159 | item.fadeIn(this.fade_in_speed, function(){ 160 | Gritter['_after_open_' + number]($(this)); 161 | }); 162 | 163 | if(!sticky){ 164 | this._setFadeTimer(item, number); 165 | } 166 | 167 | // Bind the hover/unhover states 168 | $(item).bind('mouseenter mouseleave', function(event){ 169 | if(event.type == 'mouseenter'){ 170 | if(!sticky){ 171 | Gritter._restoreItemIfFading($(this), number); 172 | } 173 | } 174 | else { 175 | if(!sticky){ 176 | Gritter._setFadeTimer($(this), number); 177 | } 178 | } 179 | Gritter._hoverState($(this), event.type); 180 | }); 181 | 182 | // Clicking (X) makes the perdy thing close 183 | $(item).find('.gritter-close').click(function(){ 184 | Gritter.removeSpecific(number, {}, null, true); 185 | return false; 186 | }); 187 | 188 | return number; 189 | 190 | }, 191 | 192 | /** 193 | * If we don't have any more gritter notifications, get rid of the wrapper using this check 194 | * @private 195 | * @param {Integer} unique_id The ID of the element that was just deleted, use it for a callback 196 | * @param {Object} e The jQuery element that we're going to perform the remove() action on 197 | * @param {Boolean} manual_close Did we close the gritter dialog with the (X) button 198 | */ 199 | _countRemoveWrapper: function(unique_id, e, manual_close){ 200 | 201 | // Remove it then run the callback function 202 | e.remove(); 203 | this['_after_close_' + unique_id](e, manual_close); 204 | 205 | // Check if the wrapper is empty, if it is.. remove the wrapper 206 | if($('.gritter-item-wrapper').length == 0){ 207 | $('#gritter-notice-wrapper').remove(); 208 | } 209 | 210 | }, 211 | 212 | /** 213 | * Fade out an element after it's been on the screen for x amount of time 214 | * @private 215 | * @param {Object} e The jQuery element to get rid of 216 | * @param {Integer} unique_id The id of the element to remove 217 | * @param {Object} params An optional list of params to set fade speeds etc. 218 | * @param {Boolean} unbind_events Unbind the mouseenter/mouseleave events if they click (X) 219 | */ 220 | _fade: function(e, unique_id, params, unbind_events){ 221 | 222 | var params = params || {}, 223 | fade = (typeof(params.fade) != 'undefined') ? params.fade : true, 224 | fade_out_speed = params.speed || this.fade_out_speed, 225 | manual_close = unbind_events; 226 | 227 | this['_before_close_' + unique_id](e, manual_close); 228 | 229 | // If this is true, then we are coming from clicking the (X) 230 | if(unbind_events){ 231 | e.unbind('mouseenter mouseleave'); 232 | } 233 | 234 | // Fade it out or remove it 235 | if(fade){ 236 | 237 | e.animate({ 238 | opacity: 0 239 | }, fade_out_speed, function(){ 240 | e.animate({ height: 0 }, 300, function(){ 241 | Gritter._countRemoveWrapper(unique_id, e, manual_close); 242 | }) 243 | }) 244 | 245 | } 246 | else { 247 | 248 | this._countRemoveWrapper(unique_id, e); 249 | 250 | } 251 | 252 | }, 253 | 254 | /** 255 | * Perform actions based on the type of bind (mouseenter, mouseleave) 256 | * @private 257 | * @param {Object} e The jQuery element 258 | * @param {String} type The type of action we're performing: mouseenter or mouseleave 259 | */ 260 | _hoverState: function(e, type){ 261 | 262 | // Change the border styles and add the (X) close button when you hover 263 | if(type == 'mouseenter'){ 264 | 265 | e.addClass('hover'); 266 | 267 | // Show close button 268 | e.find('.gritter-close').show(); 269 | 270 | } 271 | // Remove the border styles and hide (X) close button when you mouse out 272 | else { 273 | 274 | e.removeClass('hover'); 275 | 276 | // Hide close button 277 | e.find('.gritter-close').hide(); 278 | 279 | } 280 | 281 | }, 282 | 283 | /** 284 | * Remove a specific notification based on an ID 285 | * @param {Integer} unique_id The ID used to delete a specific notification 286 | * @param {Object} params A set of options passed in to determine how to get rid of it 287 | * @param {Object} e The jQuery element that we're "fading" then removing 288 | * @param {Boolean} unbind_events If we clicked on the (X) we set this to true to unbind mouseenter/mouseleave 289 | */ 290 | removeSpecific: function(unique_id, params, e, unbind_events){ 291 | 292 | if(!e){ 293 | var e = $('#gritter-item-' + unique_id); 294 | } 295 | 296 | // We set the fourth param to let the _fade function know to 297 | // unbind the "mouseleave" event. Once you click (X) there's no going back! 298 | this._fade(e, unique_id, params || {}, unbind_events); 299 | 300 | }, 301 | 302 | /** 303 | * If the item is fading out and we hover over it, restore it! 304 | * @private 305 | * @param {Object} e The HTML element to remove 306 | * @param {Integer} unique_id The ID of the element 307 | */ 308 | _restoreItemIfFading: function(e, unique_id){ 309 | 310 | clearTimeout(this['_int_id_' + unique_id]); 311 | e.stop().css({ opacity: '', height: '' }); 312 | 313 | }, 314 | 315 | /** 316 | * Setup the global options - only once 317 | * @private 318 | */ 319 | _runSetup: function(){ 320 | 321 | for(var opt in $.gritter.options){ 322 | this[opt] = $.gritter.options[opt]; 323 | } 324 | this._is_setup = 1; 325 | 326 | }, 327 | 328 | /** 329 | * Set the notification to fade out after a certain amount of time 330 | * @private 331 | * @param {Object} item The HTML element we're dealing with 332 | * @param {Integer} unique_id The ID of the element 333 | */ 334 | _setFadeTimer: function(e, unique_id){ 335 | 336 | var timer_str = (this._custom_timer) ? this._custom_timer : this.time; 337 | this['_int_id_' + unique_id] = setTimeout(function(){ 338 | Gritter._fade(e, unique_id); 339 | }, timer_str); 340 | 341 | }, 342 | 343 | /** 344 | * Bring everything to a halt 345 | * @param {Object} params A list of callback functions to pass when all notifications are removed 346 | */ 347 | stop: function(params){ 348 | 349 | // callbacks (if passed) 350 | var before_close = ($.isFunction(params.before_close)) ? params.before_close : function(){}; 351 | var after_close = ($.isFunction(params.after_close)) ? params.after_close : function(){}; 352 | 353 | var wrap = $('#gritter-notice-wrapper'); 354 | before_close(wrap); 355 | wrap.fadeOut(function(){ 356 | $(this).remove(); 357 | after_close(); 358 | }); 359 | 360 | }, 361 | 362 | /** 363 | * An extremely handy PHP function ported to JS, works well for templating 364 | * @private 365 | * @param {String/Array} search A list of things to search for 366 | * @param {String/Array} replace A list of things to replace the searches with 367 | * @return {String} sa The output 368 | */ 369 | _str_replace: function(search, replace, subject, count){ 370 | 371 | var i = 0, j = 0, temp = '', repl = '', sl = 0, fl = 0, 372 | f = [].concat(search), 373 | r = [].concat(replace), 374 | s = subject, 375 | ra = r instanceof Array, sa = s instanceof Array; 376 | s = [].concat(s); 377 | 378 | if(count){ 379 | this.window[count] = 0; 380 | } 381 | 382 | for(i = 0, sl = s.length; i < sl; i++){ 383 | 384 | if(s[i] === ''){ 385 | continue; 386 | } 387 | 388 | for (j = 0, fl = f.length; j < fl; j++){ 389 | 390 | temp = s[i] + ''; 391 | repl = ra ? (r[j] !== undefined ? r[j] : '') : r[0]; 392 | s[i] = (temp).split(f[j]).join(repl); 393 | 394 | if(count && s[i] !== temp){ 395 | this.window[count] += (temp.length-s[i].length) / f[j].length; 396 | } 397 | 398 | } 399 | } 400 | 401 | return sa ? s : s[0]; 402 | 403 | }, 404 | 405 | /** 406 | * A check to make sure we have something to wrap our notices with 407 | * @private 408 | */ 409 | _verifyWrapper: function(){ 410 | 411 | if($('#gritter-notice-wrapper').length == 0){ 412 | $('body').append(this._tpl_wrap); 413 | } 414 | 415 | } 416 | 417 | } 418 | 419 | })(jQuery); 420 | -------------------------------------------------------------------------------- /assets/js/legend.js: -------------------------------------------------------------------------------- 1 | /* 2 | * legend.js v0.2.0 3 | * License: MIT 4 | */ 5 | function legend(parent, data) { 6 | parent.className = 'legend'; 7 | var datas = data.hasOwnProperty('datasets') ? data.datasets : data; 8 | 9 | datas.forEach(function(d) { 10 | var title = document.createElement('span'); 11 | title.className = 'title'; 12 | title.style.borderColor = d.hasOwnProperty('strokeColor') ? d.strokeColor : d.color; 13 | title.style.borderStyle = 'solid'; 14 | parent.appendChild(title); 15 | 16 | var text = document.createTextNode(d.title); 17 | title.appendChild(text); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uifordocker", 3 | "version": "0.11.0", 4 | "homepage": "https://github.com/kevana/ui-for-docker", 5 | "authors": [ 6 | "Michael Crosby ", 7 | "Kevan Ahlquist " 8 | ], 9 | "description": "A web interface for the Docker Remote API.", 10 | "keywords": [ 11 | "uifordocker", 12 | "dockerui", 13 | "docker", 14 | "api" 15 | ], 16 | "license": "MIT", 17 | "ignore": [ 18 | "**/.*", 19 | "node_modules", 20 | "bower_components", 21 | "test", 22 | "tests" 23 | ], 24 | "dependencies": { 25 | "Chart.js": "1.0.2", 26 | "angular": "1.3.15", 27 | "angular-sanitize": "1.3.15", 28 | "angular-bootstrap": "0.12.0", 29 | "angular-mocks": "1.3.15", 30 | "angular-oboe": "*", 31 | "angular-resource": "1.3.15", 32 | "angular-route": "1.3.15", 33 | "angular-visjs": "0.0.7", 34 | "bootstrap": "3.3.0", 35 | "jquery": "1.11.1", 36 | "jquery.gritter": "1.7.4", 37 | "spin.js": "1.3" 38 | }, 39 | "resolutions": { 40 | "angular": "1.3.15" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /container.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevana/ui-for-docker/f324a66f74bc6fff2379f59fa014875d206e948c/container.png -------------------------------------------------------------------------------- /containers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevana/ui-for-docker/f324a66f74bc6fff2379f59fa014875d206e948c/containers.png -------------------------------------------------------------------------------- /examples/nginx-basic-auth/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.9.9 2 | 3 | COPY default.conf /etc/nginx/conf.d/default.conf 4 | COPY users.htpasswd /etc/nginx/users.htpasswd 5 | -------------------------------------------------------------------------------- /examples/nginx-basic-auth/default.conf: -------------------------------------------------------------------------------- 1 | upstream dockerui { 2 | server dockerui:9000; 3 | } 4 | 5 | server { 6 | listen 80; 7 | server_name localhost; 8 | 9 | location / { 10 | auth_basic "Docker UI"; 11 | auth_basic_user_file /etc/nginx/users.htpasswd; 12 | 13 | proxy_http_version 1.1; 14 | proxy_set_header Connection ""; 15 | proxy_pass http://dockerui; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/nginx-basic-auth/docker-compose.yml: -------------------------------------------------------------------------------- 1 | dockerui: 2 | image: dockerui/dockerui 3 | privileged: true 4 | volumes: 5 | - /var/run/docker.sock:/var/run/docker.sock 6 | 7 | nginx: 8 | build: . 9 | links: 10 | - dockerui 11 | ports: 12 | - 80:80 13 | -------------------------------------------------------------------------------- /examples/nginx-basic-auth/users.htpasswd: -------------------------------------------------------------------------------- 1 | user:{PLAIN}password 2 | -------------------------------------------------------------------------------- /examples/swarm/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian 2 | 3 | RUN apt-get update && apt-get install -y socat 4 | -------------------------------------------------------------------------------- /examples/swarm/README.md: -------------------------------------------------------------------------------- 1 | # UI For Docker with Swarm 2 | 3 | This example works with swarm clusters created with docker-machine. 4 | 5 | ## Usage 6 | 7 | Make sure your client is pointed directly to the Docker daemon on the swarm-master's node (not through swarm). 8 | 9 | ``` 10 | docker-compose up -d 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/swarm/docker-compose.yml: -------------------------------------------------------------------------------- 1 | dockerui: 2 | image: uifd/ui-for-docker 3 | command: -e http://127.0.0.1:2375 4 | net: "host" 5 | 6 | socat: 7 | build: . 8 | net: "host" 9 | command: socat -d -d TCP-L:2375,fork,bind=localhost ssl:127.0.0.1:3376,cert=/var/lib/boot2docker/server.pem,cafile=/var/lib/boot2docker/ca.pem,key=/var/lib/boot2docker/server-key.pem 10 | volumes: 11 | - /var/lib/boot2docker:/var/lib/boot2docker 12 | -------------------------------------------------------------------------------- /gruntFile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 3 | grunt.loadNpmTasks('grunt-contrib-concat'); 4 | grunt.loadNpmTasks('grunt-contrib-jshint'); 5 | grunt.loadNpmTasks('grunt-contrib-uglify'); 6 | grunt.loadNpmTasks('grunt-contrib-clean'); 7 | grunt.loadNpmTasks('grunt-contrib-copy'); 8 | grunt.loadNpmTasks('grunt-contrib-watch'); 9 | grunt.loadNpmTasks('grunt-recess'); 10 | grunt.loadNpmTasks('grunt-karma'); 11 | grunt.loadNpmTasks('grunt-html2js'); 12 | grunt.loadNpmTasks('grunt-shell'); 13 | grunt.loadNpmTasks('grunt-if'); 14 | 15 | // Default task. 16 | grunt.registerTask('default', ['jshint', 'build', 'karma:unit']); 17 | grunt.registerTask('build', [ 18 | 'clean:app', 19 | 'if:binaryNotExist', 20 | 'html2js', 21 | 'concat', 22 | 'clean:tmpl', 23 | 'recess:build', 24 | 'copy' 25 | ]); 26 | grunt.registerTask('release', [ 27 | 'clean:app', 28 | 'if:binaryNotExist', 29 | 'html2js', 30 | 'uglify', 31 | 'clean:tmpl', 32 | //'jshint', 33 | //'karma:unit', 34 | 'concat:index', 35 | 'recess:min', 36 | 'copy' 37 | ]); 38 | grunt.registerTask('test-watch', ['karma:watch']); 39 | grunt.registerTask('run', ['if:binaryNotExist', 'build', 'shell:buildImage', 'shell:run']); 40 | grunt.registerTask('runSwarm', ['if:binaryNotExist', 'build', 'shell:buildImage', 'shell:runSwarm']); 41 | grunt.registerTask('run-dev', ['if:binaryNotExist', 'shell:buildImage', 'shell:run', 'watch:build']); 42 | 43 | // Print a timestamp (useful for when watching) 44 | grunt.registerTask('timestamp', function () { 45 | grunt.log.subhead(Date()); 46 | }); 47 | 48 | var karmaConfig = function (configFile, customOptions) { 49 | var options = {configFile: configFile, keepalive: true}; 50 | var travisOptions = process.env.TRAVIS && {browsers: ['Firefox'], reporters: 'dots'}; 51 | return grunt.util._.extend(options, customOptions, travisOptions); 52 | }; 53 | 54 | // Project configuration. 55 | grunt.initConfig({ 56 | distdir: 'dist', 57 | pkg: grunt.file.readJSON('package.json'), 58 | remoteApiVersion: 'v1.20', 59 | banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>\n' + 60 | '<%= pkg.homepage ? " * " + pkg.homepage + "\\n" : "" %>' + 61 | ' * Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author %>;\n' + 62 | ' * Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %>\n */\n', 63 | src: { 64 | js: ['app/**/*.js', '!app/**/*.spec.js'], 65 | jsTpl: ['<%= distdir %>/templates/**/*.js'], 66 | jsVendor: [ 67 | 'bower_components/jquery/dist/jquery.js', 68 | 'assets/js/jquery.gritter.js', // Using custom version to fix error in minified build due to "use strict" 69 | 'bower_components/bootstrap/dist/js/bootstrap.js', 70 | 'bower_components/spin.js/spin.js', 71 | 'bower_components/vis/dist/vis.js', 72 | 'bower_components/Chart.js/Chart.js', 73 | 'bower_components/oboe/dist/oboe-browser.js', 74 | 'assets/js/legend.js' // Not a bower package 75 | ], 76 | specs: ['test/**/*.spec.js'], 77 | scenarios: ['test/**/*.scenario.js'], 78 | html: ['index.html'], 79 | tpl: ['app/components/**/*.html'], 80 | css: ['assets/css/app.css'], 81 | cssVendor: [ 82 | 'bower_components/bootstrap/dist/css/bootstrap.css', 83 | 'bower_components/jquery.gritter/css/jquery.gritter.css', 84 | 'bower_components/vis/dist/vis.css' 85 | ] 86 | }, 87 | clean: { 88 | all: ['<%= distdir %>/*'], 89 | app: ['<%= distdir %>/*', '!<%= distdir %>/ui-for-docker'], 90 | tmpl: ['<%= distdir %>/templates'] 91 | }, 92 | copy: { 93 | assets: { 94 | files: [ 95 | {dest: '<%= distdir %>/fonts/', src: '**', expand: true, cwd: 'bower_components/bootstrap/fonts/'}, 96 | { 97 | dest: '<%= distdir %>/images/', 98 | src: ['**', '!trees.jpg'], 99 | expand: true, 100 | cwd: 'bower_components/jquery.gritter/images/' 101 | }, 102 | { 103 | dest: '<%= distdir %>/img', 104 | src: [ 105 | 'network/downArrow.png', 106 | 'network/leftArrow.png', 107 | 'network/upArrow.png', 108 | 'network/rightArrow.png', 109 | 'network/minus.png', 110 | 'network/plus.png', 111 | 'network/zoomExtends.png' 112 | ], 113 | expand: true, 114 | cwd: 'bower_components/vis/dist/img' 115 | }, 116 | {dest: '<%= distdir %>/ico', src: '**', expand: true, cwd: 'assets/ico'} 117 | ] 118 | } 119 | }, 120 | karma: { 121 | unit: {options: karmaConfig('test/unit/karma.conf.js')}, 122 | watch: {options: karmaConfig('test/unit/karma.conf.js', {singleRun: false, autoWatch: true})} 123 | }, 124 | html2js: { 125 | app: { 126 | options: { 127 | base: '.' 128 | }, 129 | src: ['<%= src.tpl %>'], 130 | dest: '<%= distdir %>/templates/app.js', 131 | module: '<%= pkg.name %>.templates' 132 | } 133 | }, 134 | concat: { 135 | dist: { 136 | options: { 137 | banner: "<%= banner %>", 138 | process: true 139 | }, 140 | src: ['<%= src.js %>', '<%= src.jsTpl %>'], 141 | dest: '<%= distdir %>/<%= pkg.name %>.js' 142 | }, 143 | vendor: { 144 | src: ['<%= src.jsVendor %>'], 145 | dest: '<%= distdir %>/vendor.js' 146 | }, 147 | index: { 148 | src: ['index.html'], 149 | dest: '<%= distdir %>/index.html', 150 | options: { 151 | process: true 152 | } 153 | }, 154 | angular: { 155 | src: ['bower_components/angular/angular.js', 156 | 'bower_components/angular-sanitize/angular-sanitize.js', 157 | 'bower_components/angular-route/angular-route.js', 158 | 'bower_components/angular-resource/angular-resource.js', 159 | 'bower_components/angular-bootstrap/ui-bootstrap-tpls.js', 160 | 'bower_components/angular-oboe/dist/angular-oboe.js', 161 | 'bower_components/angular-visjs/angular-vis.js'], 162 | dest: '<%= distdir %>/angular.js' 163 | } 164 | }, 165 | uglify: { 166 | dist: { 167 | options: { 168 | banner: "<%= banner %>" 169 | }, 170 | src: ['<%= src.js %>', '<%= src.jsTpl %>'], 171 | dest: '<%= distdir %>/<%= pkg.name %>.js' 172 | }, 173 | vendor: { 174 | options: { 175 | preserveComments: 'some' // Preserve license comments 176 | }, 177 | src: ['<%= src.jsVendor %>'], 178 | dest: '<%= distdir %>/vendor.js' 179 | }, 180 | angular: { 181 | options: { 182 | preserveComments: 'some' // Preserve license comments 183 | }, 184 | src: ['<%= concat.angular.src %>'], 185 | dest: '<%= distdir %>/angular.js' 186 | } 187 | }, 188 | recess: { // TODO: not maintained, unable to preserve license comments, switch out for something better. 189 | build: { 190 | files: { 191 | '<%= distdir %>/<%= pkg.name %>.css': ['<%= src.css %>'], 192 | '<%= distdir %>/vendor.css': ['<%= src.cssVendor %>'] 193 | }, 194 | options: { 195 | compile: true, 196 | noOverqualifying: false // TODO: Added because of .nav class, rename 197 | } 198 | }, 199 | min: { 200 | files: { 201 | '<%= distdir %>/<%= pkg.name %>.css': ['<%= src.css %>'], 202 | '<%= distdir %>/vendor.css': ['<%= src.cssVendor %>'] 203 | }, 204 | options: { 205 | compile: true, 206 | compress: true, 207 | noOverqualifying: false // TODO: Added because of .nav class, rename 208 | } 209 | } 210 | }, 211 | watch: { 212 | all: { 213 | files: ['<%= src.js %>', '<%= src.specs %>', '<%= src.css %>', '<%= src.tpl %>', '<%= src.html %>'], 214 | tasks: ['default', 'timestamp'] 215 | }, 216 | build: { 217 | files: ['<%= src.js %>', '<%= src.specs %>', '<%= src.css %>', '<%= src.tpl %>', '<%= src.html %>'], 218 | tasks: ['build', 'shell:buildImage', 'shell:run', 'shell:cleanImages'] 219 | /* 220 | * Why don't we just use a host volume 221 | * http.FileServer uses sendFile which virtualbox hates 222 | * Tried using a host volume with -v, copying files with `docker cp`, restating container, none worked 223 | * Rebuilding image on each change was only method that worked, takes ~4s per change to update 224 | */ 225 | } 226 | }, 227 | jshint: { 228 | files: ['gruntFile.js', '<%= src.js %>', '<%= src.specs %>', '<%= src.scenarios %>'], 229 | options: { 230 | curly: true, 231 | eqeqeq: true, 232 | immed: true, 233 | latedef: true, 234 | newcap: true, 235 | noarg: true, 236 | sub: true, 237 | boss: true, 238 | eqnull: true, 239 | globals: { 240 | angular: false, 241 | '$': false 242 | } 243 | } 244 | }, 245 | shell: { 246 | buildImage: { 247 | command: 'docker build --rm -t ui-for-docker .' 248 | }, 249 | buildBinary: { 250 | command: [ 251 | 'docker run --rm -v $(pwd)/api:/src centurylink/golang-builder', 252 | 'shasum api/ui-for-docker > ui-for-docker-checksum.txt', 253 | 'mkdir -p dist', 254 | 'mv api/ui-for-docker dist/' 255 | ].join(' && ') 256 | }, 257 | run: { 258 | command: [ 259 | 'docker stop ui-for-docker', 260 | 'docker rm ui-for-docker', 261 | 'docker run --privileged -d -p 9000:9000 -v /tmp/ui-for-docker:/data -v /var/run/docker.sock:/var/run/docker.sock --name ui-for-docker ui-for-docker -d /data' 262 | ].join(';') 263 | }, 264 | runSwarm: { 265 | command: [ 266 | 'docker stop ui-for-docker', 267 | 'docker rm ui-for-docker', 268 | 'docker run --net=host -d -v /tmp/ui-for-docker:/data --name ui-for-docker ui-for-docker -d /data -H tcp://127.0.0.1:2374' 269 | ].join(';') 270 | }, 271 | cleanImages: { 272 | command: 'docker rmi $(docker images -q -f dangling=true)' 273 | } 274 | }, 275 | 'if': { 276 | binaryNotExist: { 277 | options: { 278 | executable: 'dist/ui-for-docker' 279 | }, 280 | ifFalse: ['shell:buildBinary'] 281 | } 282 | } 283 | }); 284 | }; 285 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UI For Docker 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
32 | 33 |
34 | 35 |
36 |
37 |
38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Michael Crosby & Kevan Ahlquist", 3 | "name": "uifordocker", 4 | "homepage": "https://github.com/kevana/ui-for-docker", 5 | "version": "0.11.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:kevana/ui-for-docker.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/kevana/ui-for-docker/issues" 12 | }, 13 | "licenses": [ 14 | { 15 | "type": "MIT", 16 | "url": "https://raw.githubusercontent.com/kevana/ui-for-docker/master/LICENSE" 17 | } 18 | ], 19 | "engines": { 20 | "node": ">= 0.8.4" 21 | }, 22 | "dependencies": {}, 23 | "devDependencies": { 24 | "bower": "^1.5.2", 25 | "grunt": "~0.4.0", 26 | "grunt-contrib-clean": "~0.4.0", 27 | "grunt-contrib-concat": "~0.1.3", 28 | "grunt-contrib-copy": "~0.4.0", 29 | "grunt-contrib-jshint": "~0.2.0", 30 | "grunt-contrib-uglify": "^0.9.2", 31 | "grunt-contrib-watch": "~0.3.1", 32 | "grunt-html2js": "~0.1.0", 33 | "grunt-if": "^0.1.5", 34 | "grunt-karma": "~0.4.4", 35 | "grunt-recess": "~0.3", 36 | "grunt-shell": "^1.1.2" 37 | }, 38 | "scripts": { 39 | "postinstall": "bower install" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/unit/app/components/containerController.spec.js: -------------------------------------------------------------------------------- 1 | describe('ContainerController', function () { 2 | var $scope, $httpBackend, mockContainer, $routeParams; 3 | 4 | beforeEach(module('uifordocker')); 5 | 6 | 7 | beforeEach(inject(function ($rootScope, $controller, _$routeParams_) { 8 | 9 | $scope = $rootScope.$new(); 10 | $routeParams = _$routeParams_; 11 | $controller('ContainerController', { 12 | $scope: $scope 13 | }); 14 | 15 | angular.mock.inject(function (_$httpBackend_, _Container_) { 16 | mockContainer = _Container_; 17 | $httpBackend = _$httpBackend_; 18 | }); 19 | })); 20 | 21 | function expectGetContainer() { 22 | $httpBackend.expectGET('dockerapi/containers/json').respond({ 23 | Created: 1421817232, 24 | id: 'b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f', 25 | Image: 'ui-for-docker:latest', 26 | Name: '/ui-for-docker', 27 | Config: {}, 28 | HostConfig: { 29 | Binds: [] 30 | } 31 | }); 32 | } 33 | 34 | it("a correct rename request to the Docker remote API", function () { 35 | 36 | $routeParams.id = 'b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f'; 37 | $scope.container = { 38 | 'Created': 1421817232, 39 | 'id': 'b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f', 40 | 'Image': 'ui-for-docker:latest', 41 | 'Name': '/ui-for-docker' 42 | }; 43 | $scope.container.newContainerName = "newName"; 44 | 45 | var newContainerName = "newName"; 46 | expectGetContainer(); 47 | 48 | $httpBackend.expectGET('dockerapi/containers/changes').respond([{"Kind": 1, "Path": "/docker.sock"}]); 49 | 50 | $httpBackend.expectPOST('dockerapi/containers/' + $routeParams.id + '/rename?name=newName'). 51 | respond({ 52 | 'name': newContainerName 53 | }); 54 | 55 | $scope.renameContainer(); 56 | 57 | $httpBackend.flush(); 58 | expect($scope.container.Name).toBe(newContainerName); 59 | expect($scope.container.edit).toBeFalsy(); 60 | }); 61 | }); -------------------------------------------------------------------------------- /test/unit/app/components/containerTopController.spec.js: -------------------------------------------------------------------------------- 1 | describe("ContainerTopController", function () { 2 | var $scope, $httpBackend, $routeParams; 3 | 4 | beforeEach(angular.mock.module('uifordocker')); 5 | 6 | beforeEach(inject(function (_$rootScope_, _$httpBackend_, $controller, _$routeParams_) { 7 | $scope = _$rootScope_.$new(); 8 | $httpBackend = _$httpBackend_; 9 | $routeParams = _$routeParams_; 10 | $routeParams.id = 'b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f'; 11 | $controller('ContainerTopController', { 12 | '$scope': $scope, 13 | '$routeParams': $routeParams 14 | }); 15 | })); 16 | 17 | it("should test controller initialize", function () { 18 | $httpBackend.expectGET('dockerapi/containers/b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f/json').respond(200, {Name: '/foo'}); 19 | $httpBackend.expectGET('dockerapi/containers/b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f/top?ps_args=').respond(200); 20 | expect($scope.ps_args).toBeDefined(); 21 | $httpBackend.flush(); 22 | }); 23 | 24 | it("a correct top request to the Docker remote API", function () { 25 | $httpBackend.expectGET('dockerapi/containers/b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f/json').respond(200, {Name: '/foo'}); 26 | $httpBackend.expectGET('dockerapi/containers/' + $routeParams.id + '/top?ps_args=').respond(200); 27 | $routeParams.id = '123456789123456789123456789'; 28 | $scope.ps_args = 'aux'; 29 | $httpBackend.expectGET('dockerapi/containers/' + $routeParams.id + '/top?ps_args=' + $scope.ps_args).respond(200); 30 | $scope.getTop(); 31 | $httpBackend.flush(); 32 | }); 33 | }); -------------------------------------------------------------------------------- /test/unit/app/components/networkController.spec.js: -------------------------------------------------------------------------------- 1 | describe('NetworkController', function () { 2 | var $scope, $httpBackend, $routeParams; 3 | 4 | beforeEach(module('uifordocker')); 5 | beforeEach(inject(function (_$httpBackend_, $controller, _$routeParams_) { 6 | $scope = {}; 7 | $httpBackend = _$httpBackend_; 8 | $routeParams = _$routeParams_; 9 | $routeParams.id = 'f1e1ce1613ccd374a75caf5e2c3ab35520d1944f91498c1974ec86fb4019c79b'; 10 | $controller('NetworkController', { 11 | '$scope': $scope, 12 | '$routeParams': $routeParams 13 | }); 14 | })); 15 | 16 | it('initializes correctly', function () { 17 | expectGetNetwork(); 18 | $httpBackend.flush(); 19 | }); 20 | 21 | it('issues a correct connect call to the remote API', function () { 22 | expectGetNetwork(); 23 | $httpBackend.expectPOST('dockerapi/networks/f1e1ce1613ccd374a75caf5e2c3ab35520d1944f91498c1974ec86fb4019c79b/connect', {'Container': 'containerId'}).respond(200); 24 | $scope.connect($routeParams.id, 'containerId'); 25 | $httpBackend.flush(); 26 | }); 27 | it('issues a correct disconnect call to the remote API', function () { 28 | expectGetNetwork(); 29 | $httpBackend.expectPOST('dockerapi/networks/f1e1ce1613ccd374a75caf5e2c3ab35520d1944f91498c1974ec86fb4019c79b/disconnect', {'Container': 'containerId'}).respond(200); 30 | $scope.disconnect($routeParams.id, 'containerId'); 31 | $httpBackend.flush(); 32 | }); 33 | it('issues a correct remove call to the remote API', function () { 34 | expectGetNetwork(); 35 | $httpBackend.expectDELETE('dockerapi/networks/f1e1ce1613ccd374a75caf5e2c3ab35520d1944f91498c1974ec86fb4019c79b').respond(204); 36 | $scope.remove($routeParams.id); 37 | $httpBackend.flush(); 38 | }); 39 | 40 | function expectGetNetwork() { 41 | $httpBackend.expectGET('dockerapi/networks/f1e1ce1613ccd374a75caf5e2c3ab35520d1944f91498c1974ec86fb4019c79b').respond({ 42 | "Name": "bridge", 43 | "Id": "f1e1ce1613ccd374a75caf5e2c3ab35520d1944f91498c1974ec86fb4019c79b", 44 | "Scope": "local", 45 | "Driver": "bridge", 46 | "IPAM": { 47 | "Driver": "default", 48 | "Config": [{ 49 | "Subnet": "172.17.0.1/16", 50 | "Gateway": "172.17.0.1" 51 | }] 52 | }, 53 | "Containers": { 54 | "727fe76cd0bd65033baab3045508784a166fbc67d177e91c1874b6b29eae946a": { 55 | "EndpointID": "c17ec80e2cfc8eaedc7737b7bb6f954adff439767197ef89c4a5b4127d07b267", 56 | "MacAddress": "02:42:ac:11:00:03", 57 | "IPv4Address": "172.17.0.3/16", 58 | "IPv6Address": "" 59 | }, 60 | "8c32c2446c3dfe0defac2dc8b5fd927cd394f15e08051c677a681bf36877175b": { 61 | "EndpointID": "cf7e795c978ab194d1af4a3efdc177d84c075582ba30a7cff414c7d516236af1", 62 | "MacAddress": "02:42:ac:11:00:04", 63 | "IPv4Address": "172.17.0.4/16", 64 | "IPv6Address": "" 65 | }, 66 | "cfe81fc97b1f857fdb3061fe487a064b8b57d8f112910954ac16910400d2e058": { 67 | "EndpointID": "611929ffcff2ced1db8e88f77e009c4fb4a4736395251cd97553b242e2e23bf1", 68 | "MacAddress": "02:42:ac:11:00:02", 69 | "IPv4Address": "172.17.0.2/16", 70 | "IPv6Address": "" 71 | } 72 | }, 73 | "Options": { 74 | "com.docker.network.bridge.default_bridge": "true", 75 | "com.docker.network.bridge.enable_icc": "true", 76 | "com.docker.network.bridge.enable_ip_masquerade": "true", 77 | "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", 78 | "com.docker.network.bridge.name": "docker0", 79 | "com.docker.network.driver.mtu": "1500" 80 | } 81 | }); 82 | } 83 | }); -------------------------------------------------------------------------------- /test/unit/app/components/networksController.spec.js: -------------------------------------------------------------------------------- 1 | describe('NetworksController', function () { 2 | var $scope, $httpBackend, $routeParams; 3 | 4 | beforeEach(module('uifordocker')); 5 | beforeEach(inject(function (_$httpBackend_, $controller, _$routeParams_) { 6 | $scope = {}; 7 | $httpBackend = _$httpBackend_; 8 | $routeParams = _$routeParams_; 9 | $controller('NetworksController', { 10 | '$scope': $scope, 11 | '$routeParams': $routeParams 12 | }); 13 | })); 14 | 15 | it('initializes correctly', function () { 16 | expectGetNetwork(); 17 | $httpBackend.flush(); 18 | }); 19 | 20 | 21 | it('issues correct remove calls to the remote API', function () { 22 | expectGetNetwork(); 23 | $httpBackend.flush(); 24 | $scope.networks[0].Checked = true; 25 | $scope.networks[2].Checked = true; 26 | $httpBackend.expectDELETE('dockerapi/networks/f2de39df4171b0dc801e8002d1d999b77256983dfc63041c0f34030aa3977566').respond(204); 27 | $httpBackend.expectDELETE('dockerapi/networks/13e871235c677f196c4e1ecebb9dc733b9b2d2ab589e30c539efeda84a24215e').respond(204); 28 | $scope.removeAction(); 29 | $httpBackend.flush(); 30 | }); 31 | it('issues a correct network creation call to the remote API', function () { 32 | expectGetNetwork(); 33 | var createBody = { 34 | "Name":"isolated_nw", 35 | "Driver":"bridge", 36 | "IPAM":{ 37 | "Config":[{ 38 | "Subnet":"172.20.0.0/16", 39 | "IPRange":"172.20.10.0/24", 40 | "Gateway":"172.20.10.11" 41 | }] 42 | }}; 43 | $httpBackend.expectPOST('dockerapi/networks/create', createBody).respond(201); 44 | expectGetNetwork(); 45 | $scope.addNetwork(createBody); 46 | $httpBackend.flush(); 47 | }); 48 | 49 | function expectGetNetwork() { 50 | $httpBackend.expectGET('dockerapi/networks').respond([ 51 | { 52 | "Name": "bridge", 53 | "Id": "f2de39df4171b0dc801e8002d1d999b77256983dfc63041c0f34030aa3977566", 54 | "Scope": "local", 55 | "Driver": "bridge", 56 | "IPAM": { 57 | "Driver": "default", 58 | "Config": [ 59 | { 60 | "Subnet": "172.17.0.0/16" 61 | } 62 | ] 63 | }, 64 | "Containers": { 65 | "39b69226f9d79f5634485fb236a23b2fe4e96a0a94128390a7fbbcc167065867": { 66 | "EndpointID": "ed2419a97c1d9954d05b46e462e7002ea552f216e9b136b80a7db8d98b442eda", 67 | "MacAddress": "02:42:ac:11:00:02", 68 | "IPv4Address": "172.17.0.2/16", 69 | "IPv6Address": "" 70 | } 71 | }, 72 | "Options": { 73 | "com.docker.network.bridge.default_bridge": "true", 74 | "com.docker.network.bridge.enable_icc": "true", 75 | "com.docker.network.bridge.enable_ip_masquerade": "true", 76 | "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", 77 | "com.docker.network.bridge.name": "docker0", 78 | "com.docker.network.driver.mtu": "1500" 79 | } 80 | }, 81 | { 82 | "Name": "none", 83 | "Id": "e086a3893b05ab69242d3c44e49483a3bbbd3a26b46baa8f61ab797c1088d794", 84 | "Scope": "local", 85 | "Driver": "null", 86 | "IPAM": { 87 | "Driver": "default", 88 | "Config": [] 89 | }, 90 | "Containers": {}, 91 | "Options": {} 92 | }, 93 | { 94 | "Name": "host", 95 | "Id": "13e871235c677f196c4e1ecebb9dc733b9b2d2ab589e30c539efeda84a24215e", 96 | "Scope": "local", 97 | "Driver": "host", 98 | "IPAM": { 99 | "Driver": "default", 100 | "Config": [] 101 | }, 102 | "Containers": {}, 103 | "Options": {} 104 | } 105 | ]); 106 | } 107 | }); -------------------------------------------------------------------------------- /test/unit/app/components/startContainerController.spec.js: -------------------------------------------------------------------------------- 1 | describe('startContainerController', function () { 2 | var scope, $location, createController, mockContainer, $httpBackend; 3 | 4 | beforeEach(angular.mock.module('uifordocker')); 5 | 6 | beforeEach(inject(function ($rootScope, $controller, _$location_) { 7 | $location = _$location_; 8 | scope = $rootScope.$new(); 9 | 10 | createController = function () { 11 | return $controller('StartContainerController', { 12 | '$scope': scope 13 | }); 14 | }; 15 | 16 | angular.mock.inject(function (_Container_, _$httpBackend_) { 17 | mockContainer = _Container_; 18 | $httpBackend = _$httpBackend_; 19 | }); 20 | })); 21 | function expectGetContainers() { 22 | $httpBackend.expectGET('dockerapi/containers/json?all=1').respond([{ 23 | 'Command': './ui-for-docker -e /docker.sock', 24 | 'Created': 1421817232, 25 | 'Id': 'b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f', 26 | 'Image': 'ui-for-docker:latest', 27 | 'Names': ['/ui-for-docker'], 28 | 'Ports': [{ 29 | 'IP': '0.0.0.0', 30 | 'PrivatePort': 9000, 31 | 'PublicPort': 9000, 32 | 'Type': 'tcp' 33 | }], 34 | 'Status': 'Up 2 minutes' 35 | }]); 36 | } 37 | 38 | describe('Create and start a container with port bindings', function () { 39 | it('should issue a correct create request to the Docker remote API', function () { 40 | var controller = createController(); 41 | var id = '6abd8bfba81cf8a05a76a4bdefcb36c4b66cd02265f4bfcd0e236468696ebc6c'; 42 | var expectedBody = { 43 | 'name': 'container-name', 44 | 'ExposedPorts': { 45 | '9000/tcp': {} 46 | }, 47 | 'HostConfig': { 48 | 'PortBindings': { 49 | '9000/tcp': [{ 50 | 'HostPort': '9999', 51 | 'HostIp': '10.20.10.15' 52 | }] 53 | } 54 | } 55 | }; 56 | 57 | expectGetContainers(); 58 | 59 | $httpBackend.expectPOST('dockerapi/containers/create?name=container-name', expectedBody).respond({ 60 | 'Id': id, 61 | 'Warnings': null 62 | }); 63 | $httpBackend.expectPOST('dockerapi/containers/' + id + '/start').respond({ 64 | 'id': id, 65 | 'Warnings': null 66 | }); 67 | 68 | scope.config.name = 'container-name'; 69 | scope.config.HostConfig.PortBindings = [{ 70 | ip: '10.20.10.15', 71 | extPort: '9999', 72 | intPort: '9000' 73 | }]; 74 | 75 | scope.create(); 76 | $httpBackend.flush(); 77 | }); 78 | }); 79 | 80 | describe('Create and start a container with environment variables', function () { 81 | it('should issue a correct create request to the Docker remote API', function () { 82 | var controller = createController(); 83 | var id = '6abd8bfba81cf8a05a76a4bdefcb36c4b66cd02265f4bfcd0e236468696ebc6c'; 84 | var expectedBody = { 85 | 'name': 'container-name', 86 | 'Env': ['SHELL=/bin/bash', 'TERM=xterm-256color'] 87 | }; 88 | 89 | expectGetContainers(); 90 | 91 | $httpBackend.expectPOST('dockerapi/containers/create?name=container-name', expectedBody).respond({ 92 | 'Id': id, 93 | 'Warnings': null 94 | }); 95 | $httpBackend.expectPOST('dockerapi/containers/' + id + '/start').respond({ 96 | 'id': id, 97 | 'Warnings': null 98 | }); 99 | 100 | scope.config.name = 'container-name'; 101 | scope.config.Env = [{ 102 | name: 'SHELL', 103 | value: '/bin/bash' 104 | }, { 105 | name: 'TERM', 106 | value: 'xterm-256color' 107 | }]; 108 | 109 | scope.create(); 110 | $httpBackend.flush(); 111 | }); 112 | }); 113 | 114 | describe('Create and start a container with labels', function () { 115 | it('should issue a correct create request to the Docker remote API', function () { 116 | var controller = createController(); 117 | var id = '6abd8bfba81cf8a05a76a4bdefcb36c4b66cd02265f4bfcd0e236468696ebc6c'; 118 | var expectedBody = { 119 | 'name': 'container-name', 120 | 'Labels': { 121 | "org.foo.bar": "Baz", 122 | "com.biz.baz": "Boo" 123 | } 124 | }; 125 | 126 | expectGetContainers(); 127 | 128 | $httpBackend.expectPOST('dockerapi/containers/create?name=container-name', expectedBody).respond({ 129 | 'Id': id, 130 | 'Warnings': null 131 | }); 132 | $httpBackend.expectPOST('dockerapi/containers/' + id + '/start').respond({ 133 | 'id': id, 134 | 'Warnings': null 135 | }); 136 | 137 | scope.config.name = 'container-name'; 138 | scope.config.Labels = [{ 139 | key: 'org.foo.bar', 140 | value: 'Baz' 141 | }, { 142 | key: 'com.biz.baz', 143 | value: 'Boo' 144 | }]; 145 | 146 | scope.create(); 147 | $httpBackend.flush(); 148 | }); 149 | }); 150 | 151 | describe('Create and start a container with volumesFrom', function () { 152 | it('should issue a correct create request to the Docker remote API', function () { 153 | var controller = createController(); 154 | var id = '6abd8bfba81cf8a05a76a4bdefcb36c4b66cd02265f4bfcd0e236468696ebc6c'; 155 | var expectedBody = { 156 | HostConfig: { 157 | 'VolumesFrom': ['parent', 'other:ro'] 158 | }, 159 | 'name': 'container-name' 160 | }; 161 | 162 | expectGetContainers(); 163 | 164 | $httpBackend.expectPOST('dockerapi/containers/create?name=container-name', expectedBody).respond({ 165 | 'Id': id, 166 | 'Warnings': null 167 | }); 168 | $httpBackend.expectPOST('dockerapi/containers/' + id + '/start').respond({ 169 | 'id': id, 170 | 'Warnings': null 171 | }); 172 | 173 | scope.config.name = 'container-name'; 174 | scope.config.HostConfig.VolumesFrom = [{name: 'parent'}, {name: 'other:ro'}]; 175 | 176 | scope.create(); 177 | $httpBackend.flush(); 178 | }); 179 | }); 180 | 181 | describe('Create and start a container with multiple options', function () { 182 | it('should issue a correct create request to the Docker remote API', function () { 183 | var controller = createController(); 184 | var id = '6abd8bfba81cf8a05a76a4bdefcb36c4b66cd02265f4bfcd0e236468696ebc6c'; 185 | var expectedBody = { 186 | Volumes: ['/var/www'], 187 | SecurityOpts: ['label:type:svirt_apache'], 188 | HostConfig: { 189 | Binds: ['/app:/app'], 190 | Links: ['web:db'], 191 | Dns: ['8.8.8.8'], 192 | DnsSearch: ['example.com'], 193 | CapAdd: ['cap_sys_admin'], 194 | CapDrop: ['cap_foo_bar'], 195 | Devices: [{ 196 | 'PathOnHost': '/dev/deviceName', 197 | 'PathInContainer': '/dev/deviceName', 198 | 'CgroupPermissions': 'mrw' 199 | }], 200 | LxcConf: {'lxc.utsname': 'docker'}, 201 | ExtraHosts: ['hostname:127.0.0.1'], 202 | PublishAllPorts: true, 203 | Privileged: true, 204 | RestartPolicy: {name: 'always', MaximumRetryCount: 5} 205 | }, 206 | name: 'container-name', 207 | NetworkDisabled: true, 208 | Tty: true, 209 | OpenStdin: true, 210 | StdinOnce: true 211 | }; 212 | 213 | expectGetContainers(); 214 | 215 | $httpBackend.expectPOST('dockerapi/containers/create?name=container-name', expectedBody).respond({ 216 | 'Id': id, 217 | 'Warnings': null 218 | }); 219 | $httpBackend.expectPOST('dockerapi/containers/' + id + '/start').respond({ 220 | 'id': id, 221 | 'Warnings': null 222 | }); 223 | 224 | scope.config.name = 'container-name'; 225 | scope.config.Volumes = [{name: '/var/www'}]; 226 | scope.config.SecurityOpts = [{name: 'label:type:svirt_apache'}]; 227 | scope.config.NetworkDisabled = true; 228 | scope.config.Tty = true; 229 | scope.config.OpenStdin = true; 230 | scope.config.StdinOnce = true; 231 | 232 | scope.config.HostConfig.Binds = [{name: '/app:/app'}]; 233 | scope.config.HostConfig.Links = [{name: 'web:db'}]; 234 | scope.config.HostConfig.Dns = [{name: '8.8.8.8'}]; 235 | scope.config.HostConfig.DnsSearch = [{name: 'example.com'}]; 236 | scope.config.HostConfig.CapAdd = [{name: 'cap_sys_admin'}]; 237 | scope.config.HostConfig.CapDrop = [{name: 'cap_foo_bar'}]; 238 | scope.config.HostConfig.PublishAllPorts = true; 239 | scope.config.HostConfig.Privileged = true; 240 | scope.config.HostConfig.RestartPolicy = {name: 'always', MaximumRetryCount: 5}; 241 | scope.config.HostConfig.Devices = [{ 242 | 'PathOnHost': '/dev/deviceName', 243 | 'PathInContainer': '/dev/deviceName', 244 | 'CgroupPermissions': 'mrw' 245 | }]; 246 | scope.config.HostConfig.LxcConf = [{name: 'lxc.utsname', value: 'docker'}]; 247 | scope.config.HostConfig.ExtraHosts = [{host: 'hostname', ip: '127.0.0.1'}]; 248 | 249 | scope.create(); 250 | $httpBackend.flush(); 251 | }); 252 | }); 253 | }); -------------------------------------------------------------------------------- /test/unit/app/components/statsController.spec.js: -------------------------------------------------------------------------------- 1 | describe("StatsController", function () { 2 | var $scope, $httpBackend, $routeParams; 3 | 4 | beforeEach(angular.mock.module('uifordocker')); 5 | 6 | beforeEach(inject(function (_$rootScope_, _$httpBackend_, $controller, _$routeParams_) { 7 | $scope = _$rootScope_.$new(); 8 | $httpBackend = _$httpBackend_; 9 | $routeParams = _$routeParams_; 10 | $routeParams.id = 'b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f'; 11 | $controller('StatsController', { 12 | '$scope': $scope, 13 | '$routeParams': $routeParams 14 | }); 15 | })); 16 | 17 | //it("should test controller initialize", function () { 18 | // $httpBackend.expectGET('dockerapi/containers/b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f/stats?stream=false').respond(200); 19 | // //expect($scope.ps_args).toBeDefined(); 20 | // $httpBackend.flush(); 21 | //}); 22 | // 23 | //it("a correct top request to the Docker remote API", function () { 24 | // //$httpBackend.expectGET('dockerapi/containers/' + $routeParams.id + '/top?ps_args=').respond(200); 25 | // //$routeParams.id = '123456789123456789123456789'; 26 | // //$scope.ps_args = 'aux'; 27 | // //$httpBackend.expectGET('dockerapi/containers/' + $routeParams.id + '/top?ps_args=' + $scope.ps_args).respond(200); 28 | // //$scope.getTop(); 29 | // //$httpBackend.flush(); 30 | //}); 31 | }); -------------------------------------------------------------------------------- /test/unit/app/components/volumesController.spec.js: -------------------------------------------------------------------------------- 1 | describe('VolumesController', function () { 2 | var $scope, $httpBackend, $routeParams; 3 | 4 | beforeEach(module('uifordocker')); 5 | beforeEach(inject(function (_$httpBackend_, $controller, _$routeParams_) { 6 | $scope = {}; 7 | $httpBackend = _$httpBackend_; 8 | $routeParams = _$routeParams_; 9 | $controller('VolumesController', { 10 | '$scope': $scope, 11 | '$routeParams': $routeParams 12 | }); 13 | })); 14 | 15 | it('initializes correctly', function () { 16 | expectGetVolumes(); 17 | $httpBackend.flush(); 18 | }); 19 | 20 | 21 | it('issues correct remove calls to the remote API', function () { 22 | expectGetVolumes(); 23 | $httpBackend.flush(); 24 | $scope.volumes[0].Checked = true; 25 | $scope.volumes[2].Checked = true; 26 | $httpBackend.expectDELETE('dockerapi/volumes/tardis').respond(200); 27 | $httpBackend.expectDELETE('dockerapi/volumes/bar').respond(200); 28 | $scope.removeAction(); 29 | $httpBackend.flush(); 30 | }); 31 | it('issues a correct volume creation call to the remote API', function () { 32 | expectGetVolumes(); 33 | var createBody = { 34 | "Name": "tardis", 35 | "Driver": "local" 36 | }; 37 | $httpBackend.expectPOST('dockerapi/volumes/create', createBody).respond(201); 38 | expectGetVolumes(); 39 | $scope.addVolume(createBody); 40 | $httpBackend.flush(); 41 | }); 42 | 43 | function expectGetVolumes() { 44 | $httpBackend.expectGET('dockerapi/volumes').respond({ 45 | "Volumes": [ 46 | { 47 | "Name": "tardis", 48 | "Driver": "local", 49 | "Mountpoint": "/var/lib/docker/volumes/tardis" 50 | }, 51 | { 52 | "Name": "foo", 53 | "Driver": "local", 54 | "Mountpoint": "/var/lib/docker/volumes/foo" 55 | }, 56 | { 57 | "Name": "bar", 58 | "Driver": "local", 59 | "Mountpoint": "/var/lib/docker/volumes/bar" 60 | } 61 | ] 62 | }); 63 | } 64 | }); -------------------------------------------------------------------------------- /test/unit/app/shared/filters.spec.js: -------------------------------------------------------------------------------- 1 | describe('filters', function () { 2 | beforeEach(module('uifordocker.filters')); 3 | 4 | describe('truncate', function () { 5 | it('should truncate the string to 10 characters ending in "..." by default', inject(function (truncateFilter) { 6 | expect(truncateFilter('this is 20 chars long')).toBe('this is...'); 7 | })); 8 | 9 | it('should truncate the string to 7 characters ending in "..."', inject(function (truncateFilter) { 10 | expect(truncateFilter('this is 20 chars long', 7)).toBe('this...'); 11 | })); 12 | 13 | it('should truncate the string to 10 characters ending in "???"', inject(function (truncateFilter) { 14 | expect(truncateFilter('this is 20 chars long', 10, '???')).toBe('this is???'); 15 | })); 16 | }); 17 | 18 | describe('statusbadge', function () { 19 | it('should be "important" when input is "Ghost"', inject(function (statusbadgeFilter) { 20 | expect(statusbadgeFilter('Ghost')).toBe('important'); 21 | })); 22 | 23 | it('should be "success" when input is "Exit 0"', inject(function (statusbadgeFilter) { 24 | expect(statusbadgeFilter('Exit 0')).toBe('success'); 25 | })); 26 | 27 | it('should be "warning" when exit code is non-zero', inject(function (statusbadgeFilter) { 28 | expect(statusbadgeFilter('Exit 1')).toBe('warning'); 29 | })); 30 | }); 31 | 32 | describe('getstatetext', function () { 33 | 34 | it('should return an empty string when state is undefined', inject(function (getstatetextFilter) { 35 | expect(getstatetextFilter(undefined)).toBe(''); 36 | })); 37 | 38 | it('should detect a Ghost state', inject(function (getstatetextFilter) { 39 | var state = { 40 | Ghost: true, 41 | Running: true, 42 | Paused: false 43 | }; 44 | expect(getstatetextFilter(state)).toBe('Ghost'); 45 | })); 46 | 47 | it('should detect a Paused state', inject(function (getstatetextFilter) { 48 | var state = { 49 | Ghost: false, 50 | Running: true, 51 | Paused: true 52 | }; 53 | expect(getstatetextFilter(state)).toBe('Running (Paused)'); 54 | })); 55 | 56 | it('should detect a Running state', inject(function (getstatetextFilter) { 57 | var state = { 58 | Ghost: false, 59 | Running: true, 60 | Paused: false 61 | }; 62 | expect(getstatetextFilter(state)).toBe('Running'); 63 | })); 64 | 65 | it('should detect a Stopped state', inject(function (getstatetextFilter) { 66 | var state = { 67 | Ghost: false, 68 | Running: false, 69 | Paused: false 70 | }; 71 | expect(getstatetextFilter(state)).toBe('Stopped'); 72 | })); 73 | }); 74 | 75 | describe('getstatelabel', function () { 76 | it('should return default when state is undefined', inject(function (getstatelabelFilter) { 77 | expect(getstatelabelFilter(undefined)).toBe('label-default'); 78 | })); 79 | 80 | it('should return label-important when a ghost state is detected', inject(function (getstatelabelFilter) { 81 | var state = { 82 | Ghost: true, 83 | Running: true, 84 | Paused: false 85 | }; 86 | expect(getstatelabelFilter(state)).toBe('label-important'); 87 | })); 88 | 89 | it('should return label-success when a running state is detected', inject(function (getstatelabelFilter) { 90 | var state = { 91 | Ghost: false, 92 | Running: true, 93 | Paused: false 94 | }; 95 | expect(getstatelabelFilter(state)).toBe('label-success'); 96 | })); 97 | }); 98 | 99 | describe('humansize', function () { 100 | it('should return n/a when size is zero', inject(function (humansizeFilter) { 101 | expect(humansizeFilter(0)).toBe('n/a'); 102 | })); 103 | 104 | it('should handle Bytes values', inject(function (humansizeFilter) { 105 | expect(humansizeFilter(512)).toBe('512 Bytes'); 106 | })); 107 | 108 | it('should handle KB values', inject(function (humansizeFilter) { 109 | expect(humansizeFilter(5 * 1024)).toBe('5 KB'); 110 | })); 111 | 112 | it('should handle MB values', inject(function (humansizeFilter) { 113 | expect(humansizeFilter(5 * 1024 * 1024)).toBe('5.0 MB'); 114 | })); 115 | 116 | it('should handle GB values', inject(function (humansizeFilter) { 117 | expect(humansizeFilter(5 * 1024 * 1024 * 1024)).toBe('5.00 GB'); 118 | })); 119 | 120 | it('should handle TB values', inject(function (humansizeFilter) { 121 | expect(humansizeFilter(5 * 1024 * 1024 * 1024 * 1024)).toBe('5.000 TB'); 122 | })); 123 | }); 124 | 125 | describe('containername', function () { 126 | it('should strip the leading slash from container name', inject(function (containernameFilter) { 127 | var container = { 128 | Names: ['/elegant_ardinghelli'] 129 | }; 130 | 131 | expect(containernameFilter(container)).toBe('elegant_ardinghelli'); 132 | })); 133 | }); 134 | 135 | describe('repotag', function () { 136 | it('should not display empty repo tag', inject(function (repotagFilter) { 137 | var image = { 138 | RepoTags: [':'] 139 | }; 140 | expect(repotagFilter(image)).toBe(''); 141 | })); 142 | 143 | it('should display a normal repo tag', inject(function (repotagFilter) { 144 | var image = { 145 | RepoTags: ['ubuntu:latest'] 146 | }; 147 | expect(repotagFilter(image)).toBe('ubuntu:latest'); 148 | })); 149 | }); 150 | 151 | describe('errorMsgFilter', function () { 152 | it('should convert the $resource object to a string message', 153 | inject(function (errorMsgFilter) { 154 | var response = { 155 | '0': 'C', 156 | '1': 'o', 157 | '2': 'n', 158 | '3': 'f', 159 | '4': 'l', 160 | '5': 'i', 161 | '6': 'c', 162 | '7': 't', 163 | '8': ',', 164 | '9': ' ', 165 | '10': 'T', 166 | '11': 'h', 167 | '12': 'e', 168 | '13': ' ', 169 | '14': 'n', 170 | '15': 'a', 171 | '16': 'm', 172 | '17': 'e', 173 | '18': ' ', 174 | '19': 'u', 175 | '20': 'b', 176 | '21': 'u', 177 | '22': 'n', 178 | '23': 't', 179 | '24': 'u', 180 | '25': '-', 181 | '26': 's', 182 | '27': 'l', 183 | '28': 'e', 184 | '29': 'e', 185 | '30': 'p', 186 | '31': '-', 187 | '32': 'r', 188 | '33': 'u', 189 | '34': 'n', 190 | '35': 't', 191 | '36': 'i', 192 | '37': 'm', 193 | '38': 'e', 194 | '39': ' ', 195 | '40': 'i', 196 | '41': 's', 197 | '42': ' ', 198 | '43': 'a', 199 | '44': 'l', 200 | '45': 'r', 201 | '46': 'e', 202 | '47': 'a', 203 | '48': 'd', 204 | '49': 'y', 205 | '50': ' ', 206 | '51': 'a', 207 | '52': 's', 208 | '53': 's', 209 | '54': 'i', 210 | '55': 'g', 211 | '56': 'n', 212 | '57': 'e', 213 | '58': 'd', 214 | '59': ' ', 215 | '60': 't', 216 | '61': 'o', 217 | '62': ' ', 218 | '63': 'b', 219 | '64': '6', 220 | '65': '9', 221 | '66': 'e', 222 | '67': '5', 223 | '68': '3', 224 | '69': 'a', 225 | '70': '6', 226 | '71': '2', 227 | '72': '2', 228 | '73': 'c', 229 | '74': '8', 230 | '75': '.', 231 | '76': ' ', 232 | '77': 'Y', 233 | '78': 'o', 234 | '79': 'u', 235 | '80': ' ', 236 | '81': 'h', 237 | '82': 'a', 238 | '83': 'v', 239 | '84': 'e', 240 | '85': ' ', 241 | '86': 't', 242 | '87': 'o', 243 | '88': ' ', 244 | '89': 'd', 245 | '90': 'e', 246 | '91': 'l', 247 | '92': 'e', 248 | '93': 't', 249 | '94': 'e', 250 | '95': ' ', 251 | '96': '(', 252 | '97': 'o', 253 | '98': 'r', 254 | '99': ' ', 255 | '100': 'r', 256 | '101': 'e', 257 | '102': 'n', 258 | '103': 'a', 259 | '104': 'm', 260 | '105': 'e', 261 | '106': ')', 262 | '107': ' ', 263 | '108': 't', 264 | '109': 'h', 265 | '110': 'a', 266 | '111': 't', 267 | '112': ' ', 268 | '113': 'c', 269 | '114': 'o', 270 | '115': 'n', 271 | '116': 't', 272 | '117': 'a', 273 | '118': 'i', 274 | '119': 'n', 275 | '120': 'e', 276 | '121': 'r', 277 | '122': ' ', 278 | '123': 't', 279 | '124': 'o', 280 | '125': ' ', 281 | '126': 'b', 282 | '127': 'e', 283 | '128': ' ', 284 | '129': 'a', 285 | '130': 'b', 286 | '131': 'l', 287 | '132': 'e', 288 | '133': ' ', 289 | '134': 't', 290 | '135': 'o', 291 | '136': ' ', 292 | '137': 'a', 293 | '138': 's', 294 | '139': 's', 295 | '140': 'i', 296 | '141': 'g', 297 | '142': 'n', 298 | '143': ' ', 299 | '144': 'u', 300 | '145': 'b', 301 | '146': 'u', 302 | '147': 'n', 303 | '148': 't', 304 | '149': 'u', 305 | '150': '-', 306 | '151': 's', 307 | '152': 'l', 308 | '153': 'e', 309 | '154': 'e', 310 | '155': 'p', 311 | '156': '-', 312 | '157': 'r', 313 | '158': 'u', 314 | '159': 'n', 315 | '160': 't', 316 | '161': 'i', 317 | '162': 'm', 318 | '163': 'e', 319 | '164': ' ', 320 | '165': 't', 321 | '166': 'o', 322 | '167': ' ', 323 | '168': 'a', 324 | '169': ' ', 325 | '170': 'c', 326 | '171': 'o', 327 | '172': 'n', 328 | '173': 't', 329 | '174': 'a', 330 | '175': 'i', 331 | '176': 'n', 332 | '177': 'e', 333 | '178': 'r', 334 | '179': ' ', 335 | '180': 'a', 336 | '181': 'g', 337 | '182': 'a', 338 | '183': 'i', 339 | '184': 'n', 340 | '185': '.', 341 | '186': '\n', 342 | '$promise': {}, 343 | '$resolved': true 344 | }; 345 | 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'; 346 | expect(errorMsgFilter(response)).toBe(message); 347 | })); 348 | }); 349 | }); 350 | -------------------------------------------------------------------------------- /test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | // base path, that will be used to resolve files and exclude 2 | basePath = '../..'; 3 | 4 | // list of files / patterns to load in the browser 5 | files = [ 6 | JASMINE, 7 | JASMINE_ADAPTER, 8 | 'dist/angular.js', 9 | 'dist/vendor.js', 10 | 'dist/uifordocker.js', 11 | 'bower_components/angular-mocks/angular-mocks.js', 12 | 'test/unit/**/*.spec.js' 13 | ]; 14 | 15 | // use dots reporter, as travis terminal does not support escaping sequences 16 | // possible values: 'dots' || 'progress' 17 | reporters = 'progress'; 18 | 19 | // these are default values, just to show available options 20 | 21 | // web server port 22 | port = 8089; 23 | 24 | // cli runner port 25 | runnerPort = 9109; 26 | 27 | urlRoot = '/__test/'; 28 | 29 | // enable / disable colors in the output (reporters and logs) 30 | colors = true; 31 | 32 | // level of logging 33 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 34 | logLevel = LOG_INFO; 35 | 36 | // enable / disable watching file and executing tests whenever any file changes 37 | autoWatch = false; 38 | 39 | // polling interval in ms (ignored on OS that support inotify) 40 | autoWatchInterval = 0; 41 | 42 | // Start these browsers, currently available: 43 | // - Chrome 44 | // - ChromeCanary 45 | // - Firefox 46 | // - Opera 47 | // - Safari 48 | // - PhantomJS 49 | browsers = ['Chrome']; 50 | 51 | // Continuous Integration mode 52 | // if true, it capture browsers, run tests and exit 53 | singleRun = true; 54 | --------------------------------------------------------------------------------