From 5d0af27a3f38ea51a5cee10bf27b7c3348ffde2e Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 29 Jun 2016 18:08:50 +1200 Subject: [PATCH 1/8] fix(binary): persist CSRF auth file in a volume (#22) * fix(binary): persist CSRF auth file in a volume * docs(options): document the `-data` option --- Dockerfile | 2 ++ README.md | 1 + dockerui.go | 10 ++++++---- gruntFile.js | 4 ++-- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 696bb0539..889d328e0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,5 +2,7 @@ FROM scratch COPY dist / +VOLUME /data + EXPOSE 9000 ENTRYPOINT ["/ui-for-docker"] diff --git a/README.md b/README.md index db5f23915..421a9ea31 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ The following options are available for the `ui-for-docker` binary: * `-endpoint`, `-e`: Docker deamon endpoint (default: *"/var/run/docker.sock"*) * `-bind`, `-p`: Address and port to serve UI For Docker (default: *":9000"*) +* `-data`, `-d`: Path to the data folder (default: *"."*) * `-assets`, `-a`: Path to the assets (default: *"."*) * `-swarm`, `-s`: Swarm cluster support (default: *false*) * `-hide-label`, `-l`: Hide containers with a specific label in the UI diff --git a/dockerui.go b/dockerui.go index 7497436d1..ee235cdc7 100644 --- a/dockerui.go +++ b/dockerui.go @@ -21,6 +21,7 @@ var ( endpoint = kingpin.Flag("endpoint", "Dockerd endpoint").Default("/var/run/docker.sock").Short('e').String() addr = kingpin.Flag("bind", "Address and port to serve UI For Docker").Default(":9000").Short('p').String() assets = kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String() + data = kingpin.Flag("data", "Path to the data").Default(".").Short('d').String() swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool() labels = LabelParser(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')) authKey []byte @@ -117,7 +118,7 @@ func createUnixHandler(e string) http.Handler { return &UnixHandler{e} } -func createHandler(dir string, e string, c Config) http.Handler { +func createHandler(dir string, d string, e string, c Config) http.Handler { var ( mux = http.NewServeMux() fileHandler = http.FileServer(http.Dir(dir)) @@ -137,11 +138,12 @@ func createHandler(dir string, e string, c Config) http.Handler { } // Use existing csrf authKey if present or generate a new one. - dat, err := ioutil.ReadFile(authKeyFile) + var authKeyPath = d + "/" + authKeyFile + dat, err := ioutil.ReadFile(authKeyPath) if err != nil { fmt.Println(err) authKey = securecookie.GenerateRandomKey(32) - err := ioutil.WriteFile(authKeyFile, authKey, 0644) + err := ioutil.WriteFile(authKeyPath, authKey, 0644) if err != nil { fmt.Println("unable to persist auth key", err) } @@ -179,7 +181,7 @@ func main() { HiddenLabels: *labels, } - handler := createHandler(*assets, *endpoint, configuration) + handler := createHandler(*assets, *data, *endpoint, configuration) if err := http.ListenAndServe(*addr, handler); err != nil { log.Fatal(err) } diff --git a/gruntFile.js b/gruntFile.js index 7f54ff697..f48f1e6ba 100644 --- a/gruntFile.js +++ b/gruntFile.js @@ -260,14 +260,14 @@ module.exports = function (grunt) { command: [ 'docker stop ui-for-docker', 'docker rm ui-for-docker', - 'docker run --privileged -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock --name ui-for-docker ui-for-docker' + 'docker run --privileged -d -p 9000:9000 -v /tmp/docker-ui:/data -v /var/run/docker.sock:/var/run/docker.sock --name ui-for-docker ui-for-docker -d /data' ].join(';') }, runSwarm: { command: [ 'docker stop ui-for-docker', 'docker rm ui-for-docker', - 'docker run --privileged -d -p 9000:9000 --name ui-for-docker ui-for-docker -e http://10.0.7.10:4000 -swarm' + 'docker run --privileged -d -p 9000:9000 -v /tmp/docker-ui:/data --name ui-for-docker ui-for-docker -e http://10.0.7.10:4000 -swarm -d /data' ].join(';') }, cleanImages: { From 813c14d93c0660af4cd592d8a6e4e56efa5ea442 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 29 Jun 2016 18:09:50 +1200 Subject: [PATCH 2/8] feat(ui): automatically pull the image when creating a container (#24) feat(ui): automatically pull the image when creating a container --- .../startContainerController.js | 79 ++++++++++++------- 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/app/components/startContainer/startContainerController.js b/app/components/startContainer/startContainerController.js index fd2d3723e..6dc4027f8 100644 --- a/app/components/startContainer/startContainerController.js +++ b/app/components/startContainer/startContainerController.js @@ -1,6 +1,6 @@ angular.module('startContainer', ['ui.bootstrap']) -.controller('StartContainerController', ['$scope', '$state', 'Container', 'Messages', 'containernameFilter', 'errorMsgFilter', 'ViewSpinner', -function ($scope, $state, Container, Messages, containernameFilter, errorMsgFilter, ViewSpinner) { +.controller('StartContainerController', ['$scope', '$state', 'Container', 'Image', 'Messages', 'containernameFilter', 'errorMsgFilter', 'ViewSpinner', +function ($scope, $state, Container, Image, Messages, containernameFilter, errorMsgFilter, ViewSpinner) { $scope.template = 'app/components/startContainer/startcontainer.html'; Container.query({all: 1}, function (d) { @@ -52,8 +52,38 @@ function ($scope, $state, Container, Messages, containernameFilter, errorMsgFilt }); } + function createContainer(config) { + Container.create(config, function (d) { + if (d.Id) { + var reqBody = config.HostConfig || {}; + reqBody.id = d.Id; + Container.start(reqBody, function (cd) { + if (cd.id) { + ViewSpinner.stop(); + Messages.send('Container Started', d.Id); + $state.go('container', {id: d.Id}, {reload: true}); + } else { + ViewSpinner.stop(); + failedRequestHandler(cd, Messages); + Container.remove({id: d.Id}, function () { + Messages.send('Container Removed', d.Id); + }); + } + }, function (e) { + ViewSpinner.stop(); + failedRequestHandler(e, Messages); + }); + } else { + ViewSpinner.stop(); + failedRequestHandler(d, Messages); + } + }, function (e) { + ViewSpinner.stop(); + failedRequestHandler(e, Messages); + }); + } + $scope.create = function () { - // Copy the config before transforming fields to the remote API format $('#create-modal').modal('hide'); ViewSpinner.spin(); @@ -121,35 +151,26 @@ function ($scope, $state, Container, Messages, containernameFilter, errorMsgFilt rmEmptyKeys(config.HostConfig); rmEmptyKeys(config); - var ctor = Container; - var s = $scope; - Container.create(config, function (d) { - if (d.Id) { - var reqBody = config.HostConfig || {}; - reqBody.id = d.Id; - ctor.start(reqBody, function (cd) { - if (cd.id) { - ViewSpinner.stop(); - Messages.send('Container Started', d.Id); - $state.go('container', {id: d.Id}, {reload: true}); - } else { - ViewSpinner.stop(); - failedRequestHandler(cd, Messages); - ctor.remove({id: d.Id}, function () { - Messages.send('Container Removed', d.Id); - }); - } - }, function (e) { + var image = _.toLower(config.Image); + var imageNameAndTag = image.split(':'); + var imageConfig = { + fromImage: imageNameAndTag[0], + tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest', + }; + + Image.create(imageConfig, function (data) { + var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error'); + if (err) { + var detail = data[data.length - 1]; ViewSpinner.stop(); - failedRequestHandler(e, Messages); - }); - } else { - ViewSpinner.stop(); - failedRequestHandler(d, Messages); - } + Messages.error('Error', detail.error); + } else { + Messages.send("Image successfully pulled", image); + createContainer(config); + } }, function (e) { ViewSpinner.stop(); - failedRequestHandler(e, Messages); + Messages.error('Error', 'Unable to pull image ' + image); }); }; From 66ae15b4fbaaeb7318895d035b334c439343f9fb Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 29 Jun 2016 21:04:29 +1200 Subject: [PATCH 3/8] feat(ui): new containers view (#25) feat(ui): new containers view --- app/components/containers/containers.html | 49 ++-- .../containers/containersController.js | 18 +- app/components/swarm/swarm.html | 2 +- app/shared/filters.js | 269 ++++++++++-------- app/shared/viewmodel.js | 7 +- test/unit/app/shared/filters.spec.js | 16 +- 6 files changed, 182 insertions(+), 179 deletions(-) diff --git a/app/components/containers/containers.html b/app/components/containers/containers.html index eddb0771d..d18d5ae95 100644 --- a/app/components/containers/containers.html +++ b/app/components/containers/containers.html @@ -33,10 +33,17 @@
- +
- + + + + - - - + + + + + - - - +
+ + State + + + + Name @@ -44,6 +51,20 @@ + + IP Address + + + + + + Host + + + + Image @@ -58,30 +79,18 @@ - - Created - - - - - - Status - - - -
{{ container|containername}}{{ container.State }}{{ container|swarmcontainername}}{{ container|containername}}{{ container.IP ? container.IP : '-' }}{{ container|swarmhostname}} {{ container.Image }}{{ container.Command|truncate:40 }}{{ container.Created|getdate }}{{ container.Status }}{{ container.Command|truncate:60 }}
diff --git a/app/components/containers/containersController.js b/app/components/containers/containersController.js index c16e101f2..39f6a2b92 100644 --- a/app/components/containers/containersController.js +++ b/app/components/containers/containersController.js @@ -4,9 +4,8 @@ function ($scope, Container, Settings, Messages, ViewSpinner, Config) { $scope.state = {}; $scope.state.displayAll = Settings.displayAll; - $scope.sortType = 'Created'; + $scope.sortType = 'State'; $scope.sortReverse = true; - $scope.state.toggle = false; $scope.state.selectedItemCount = 0; $scope.order = function (sortType) { @@ -91,18 +90,6 @@ function ($scope, Container, Settings, Messages, ViewSpinner, Config) { } }; - $scope.toggleSelectAll = function () { - $scope.state.selectedItem = $scope.state.toggle; - angular.forEach($scope.state.filteredContainers, function (i) { - i.Checked = $scope.state.toggle; - }); - if ($scope.state.toggle) { - $scope.state.selectedItemCount = $scope.state.filteredContainers.length; - } else { - $scope.state.selectedItemCount = 0; - } - }; - $scope.toggleGetAll = function () { Settings.displayAll = $scope.state.displayAll; update({all: Settings.displayAll ? 1 : 0}); @@ -151,9 +138,10 @@ function ($scope, Container, Settings, Messages, ViewSpinner, Config) { }); }; - var hiddenLabels; + $scope.swarm = false; Config.$promise.then(function (c) { hiddenLabels = c.hiddenLabels; + $scope.swarm = c.swarm; update({all: Settings.displayAll ? 1 : 0}); }); }]); diff --git a/app/components/swarm/swarm.html b/app/components/swarm/swarm.html index 19c621a2b..cf4a186fe 100644 --- a/app/components/swarm/swarm.html +++ b/app/components/swarm/swarm.html @@ -137,7 +137,7 @@ {{ node.ip }} {{ node.containers }} {{ node.version }} - {{ node.status }} + {{ node.status }} diff --git a/app/shared/filters.js b/app/shared/filters.js index daf5bda64..ea08f0057 100644 --- a/app/shared/filters.js +++ b/app/shared/filters.js @@ -1,128 +1,149 @@ angular.module('dockerui.filters', []) - .filter('truncate', function () { - 'use strict'; - return function (text, length, end) { - if (isNaN(length)) { - length = 10; - } +.filter('truncate', function () { + 'use strict'; + return function (text, length, end) { + if (isNaN(length)) { + length = 10; + } - if (end === undefined) { - end = '...'; - } + if (end === undefined) { + end = '...'; + } - if (text.length <= length || text.length - end.length <= length) { - return text; - } - else { - return String(text).substring(0, length - end.length) + end; - } - }; - }) - .filter('statusbadge', function () { - 'use strict'; - return function (text) { - if (text === 'Ghost') { - return 'important'; - } else if (text === 'Unhealthy') { - return 'danger'; - } else if (text.indexOf('Exit') !== -1 && text !== 'Exit 0') { - return 'warning'; - } - return 'success'; - }; - }) - .filter('trimcontainername', function () { - 'use strict'; - return function (name) { - if (name) { - return (name.indexOf('/') === 0 ? name.replace('/','') : name); - } - return ''; - }; - }) - .filter('getstatetext', function () { - 'use strict'; - return function (state) { - if (state === undefined) { - return ''; - } - if (state.Ghost && state.Running) { - return 'Ghost'; - } - if (state.Running && state.Paused) { - return 'Running (Paused)'; - } - if (state.Running) { - return 'Running'; - } - return 'Stopped'; - }; - }) - .filter('getstatelabel', function () { - 'use strict'; - return function (state) { - if (state === undefined) { - return 'label-default'; - } + if (text.length <= length || text.length - end.length <= length) { + return text; + } + else { + return String(text).substring(0, length - end.length) + end; + } + }; +}) +.filter('containerstatusbadge', function () { + 'use strict'; + return function (text) { + if (text === 'paused') { + return 'warning'; + } else if (text === 'created') { + return 'info'; + } else if (text === 'exited') { + return 'danger'; + } + return 'success'; + }; +}) +.filter('nodestatusbadge', function () { + 'use strict'; + return function (text) { + if (text === 'Unhealthy') { + return 'danger'; + } + return 'success'; + }; +}) +.filter('trimcontainername', function () { + 'use strict'; + return function (name) { + if (name) { + return (name.indexOf('/') === 0 ? name.replace('/','') : name); + } + return ''; + }; +}) +.filter('getstatetext', function () { + 'use strict'; + return function (state) { + if (state === undefined) { + return ''; + } + if (state.Ghost && state.Running) { + return 'Ghost'; + } + if (state.Running && state.Paused) { + return 'Running (Paused)'; + } + if (state.Running) { + return 'Running'; + } + return 'Stopped'; + }; +}) +.filter('getstatelabel', function () { + 'use strict'; + return function (state) { + if (state === undefined) { + return 'label-default'; + } - if (state.Ghost && state.Running) { - return 'label-important'; - } - if (state.Running) { - return 'label-success'; - } - return 'label-default'; - }; - }) - .filter('humansize', function () { - 'use strict'; - return function (bytes) { - var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; - if (bytes === 0) { - return 'n/a'; - } - var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)), 10); - var value = bytes / Math.pow(1024, i); - var decimalPlaces = (i < 1) ? 0 : (i - 1); - return value.toFixed(decimalPlaces) + ' ' + sizes[[i]]; - }; - }) - .filter('containername', function () { - 'use strict'; - return function (container) { - var name = container.Names[0]; - return name.substring(1, name.length); - }; - }) - .filter('repotag', function () { - 'use strict'; - return function (image) { - if (image.RepoTags && image.RepoTags.length > 0) { - var tag = image.RepoTags[0]; - if (tag === ':') { - tag = ''; - } - return tag; - } - return ''; - }; - }) - .filter('getdate', function () { - 'use strict'; - return function (data) { - //Multiply by 1000 for the unix format - var date = new Date(data * 1000); - return date.toDateString(); - }; - }) - .filter('errorMsg', function () { - return function (object) { - var idx = 0; - var msg = ''; - while (object[idx] && typeof(object[idx]) === 'string') { - msg += object[idx]; - idx++; - } - return msg; - }; - }); + if (state.Ghost && state.Running) { + return 'label-important'; + } + if (state.Running) { + return 'label-success'; + } + return 'label-default'; + }; +}) +.filter('humansize', function () { + 'use strict'; + return function (bytes) { + var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + if (bytes === 0) { + return 'n/a'; + } + var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)), 10); + var value = bytes / Math.pow(1024, i); + var decimalPlaces = (i < 1) ? 0 : (i - 1); + return value.toFixed(decimalPlaces) + ' ' + sizes[[i]]; + }; +}) +.filter('containername', function () { + 'use strict'; + return function (container) { + var name = container.Names[0]; + return name.substring(1, name.length); + }; +}) +.filter('swarmcontainername', function () { + 'use strict'; + return function (container) { + return _.split(container.Names[0], '/')[2]; + }; +}) +.filter('swarmhostname', function () { + 'use strict'; + return function (container) { + return _.split(container.Names[0], '/')[1]; + }; +}) +.filter('repotag', function () { + 'use strict'; + return function (image) { + if (image.RepoTags && image.RepoTags.length > 0) { + var tag = image.RepoTags[0]; + if (tag === ':') { + tag = ''; + } + return tag; + } + return ''; + }; +}) +.filter('getdate', function () { + 'use strict'; + return function (data) { + //Multiply by 1000 for the unix format + var date = new Date(data * 1000); + return date.toDateString(); + }; +}) +.filter('errorMsg', function () { + return function (object) { + var idx = 0; + var msg = ''; + while (object[idx] && typeof(object[idx]) === 'string') { + msg += object[idx]; + idx++; + } + return msg; + }; +}); diff --git a/app/shared/viewmodel.js b/app/shared/viewmodel.js index 617d497d5..901a2fd86 100644 --- a/app/shared/viewmodel.js +++ b/app/shared/viewmodel.js @@ -10,11 +10,10 @@ function ImageViewModel(data) { function ContainerViewModel(data) { this.Id = data.Id; + this.State = data.State; + this.Names = data.Names; + this.IP = data.NetworkSettings.Networks[Object.keys(data.NetworkSettings.Networks)[0]].IPAddress; this.Image = data.Image; this.Command = data.Command; - this.Created = data.Created; - this.SizeRw = data.SizeRw; - this.Status = data.Status; this.Checked = false; - this.Names = data.Names; } diff --git a/test/unit/app/shared/filters.spec.js b/test/unit/app/shared/filters.spec.js index 8005572f7..db832cd8f 100644 --- a/test/unit/app/shared/filters.spec.js +++ b/test/unit/app/shared/filters.spec.js @@ -15,20 +15,6 @@ describe('filters', function () { })); }); - describe('statusbadge', function () { - it('should be "important" when input is "Ghost"', inject(function (statusbadgeFilter) { - expect(statusbadgeFilter('Ghost')).toBe('important'); - })); - - it('should be "success" when input is "Exit 0"', inject(function (statusbadgeFilter) { - expect(statusbadgeFilter('Exit 0')).toBe('success'); - })); - - it('should be "warning" when exit code is non-zero', inject(function (statusbadgeFilter) { - expect(statusbadgeFilter('Exit 1')).toBe('warning'); - })); - }); - describe('getstatetext', function () { it('should return an empty string when state is undefined', inject(function (getstatetextFilter) { @@ -352,4 +338,4 @@ describe('filters', function () { expect(errorMsgFilter(response)).toBe(message); })); }); -}); \ No newline at end of file +}); From 965a099495a933f021e1d09b910c106dab99688f Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 29 Jun 2016 21:08:36 +1200 Subject: [PATCH 4/8] fix(flags): fix grunt run-swarm command and update long flag format (#26) --- README.md | 16 ++++++++-------- gruntFile.js | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 421a9ea31..ce20e8267 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,11 @@ $ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -e http://127.0.0.1:23 **Supported Swarm version: 1.2.3** -You can access a specific view for you Swarm cluster by defining the `-swarm` flag: +You can access a specific view for you Swarm cluster by defining the `--swarm` flag: ``` # Connect to a tcp socket and enable Swarm: -$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -e http://: -swarm +$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -e http://: --swarm ``` *NOTE*: Due to Swarm not exposing information in a machine readable way, the app is bound to a specific version of Swarm at the moment. @@ -74,9 +74,9 @@ $ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docke The following options are available for the `ui-for-docker` binary: -* `-endpoint`, `-e`: Docker deamon endpoint (default: *"/var/run/docker.sock"*) -* `-bind`, `-p`: Address and port to serve UI For Docker (default: *":9000"*) -* `-data`, `-d`: Path to the data folder (default: *"."*) -* `-assets`, `-a`: Path to the assets (default: *"."*) -* `-swarm`, `-s`: Swarm cluster support (default: *false*) -* `-hide-label`, `-l`: Hide containers with a specific label in the UI +* `--endpoint`, `-e`: Docker deamon endpoint (default: *"/var/run/docker.sock"*) +* `--bind`, `-p`: Address and port to serve UI For Docker (default: *":9000"*) +* `--data`, `-d`: Path to the data folder (default: *"."*) +* `--assets`, `-a`: Path to the assets (default: *"."*) +* `--swarm`, `-s`: Swarm cluster support (default: *false*) +* `--hide-label`, `-l`: Hide containers with a specific label in the UI diff --git a/gruntFile.js b/gruntFile.js index f48f1e6ba..b9b0d9a5c 100644 --- a/gruntFile.js +++ b/gruntFile.js @@ -267,7 +267,7 @@ module.exports = function (grunt) { command: [ 'docker stop ui-for-docker', 'docker rm ui-for-docker', - 'docker run --privileged -d -p 9000:9000 -v /tmp/docker-ui:/data --name ui-for-docker ui-for-docker -e http://10.0.7.10:4000 -swarm -d /data' + 'docker run --privileged -d -p 9000:9000 -v /tmp/docker-ui:/data --name ui-for-docker ui-for-docker -e http://10.0.7.10:4000 --swarm -d /data' ].join(';') }, cleanImages: { From 87e835e8731f9bef717590ad6f4da8bb463e7f26 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 29 Jun 2016 22:11:22 +1200 Subject: [PATCH 5/8] feat(ui): display an error message when trying to remove a running container (#28) --- .../containers/containersController.js | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/app/components/containers/containersController.js b/app/components/containers/containersController.js index 39f6a2b92..c22d1e51f 100644 --- a/app/components/containers/containersController.js +++ b/app/components/containers/containersController.js @@ -1,6 +1,6 @@ angular.module('containers', []) -.controller('ContainersController', ['$scope', 'Container', 'Settings', 'Messages', 'ViewSpinner', 'Config', -function ($scope, Container, Settings, Messages, ViewSpinner, Config) { +.controller('ContainersController', ['$scope', 'Container', 'Settings', 'Messages', 'ViewSpinner', 'Config', 'errorMsgFilter', +function ($scope, Container, Settings, Messages, ViewSpinner, Config, errorMsgFilter) { $scope.state = {}; $scope.state.displayAll = Settings.displayAll; @@ -40,13 +40,12 @@ function ($scope, Container, Settings, Messages, ViewSpinner, Config) { }; angular.forEach(items, function (c) { if (c.Checked) { + counter = counter + 1; if (action === Container.start) { Container.get({id: c.Id}, function (d) { c = d; - counter = counter + 1; action({id: c.Id, HostConfig: c.HostConfig || {}}, function (d) { Messages.send("Container " + msg, c.Id); - var index = $scope.containers.indexOf(c); complete(); }, function (e) { Messages.error("Failure", e.data); @@ -62,11 +61,24 @@ function ($scope, Container, Settings, Messages, ViewSpinner, Config) { complete(); }); } + else if (action === Container.remove) { + action({id: c.Id}, function (d) { + var error = errorMsgFilter(d); + if (error) { + Messages.send("Error", "Unable to remove running container"); + } + else { + Messages.send("Container " + msg, c.Id); + } + complete(); + }, function (e) { + Messages.error("Failure", e.data); + complete(); + }); + } else { - counter = counter + 1; action({id: c.Id}, function (d) { Messages.send("Container " + msg, c.Id); - var index = $scope.containers.indexOf(c); complete(); }, function (e) { Messages.error("Failure", e.data); @@ -74,7 +86,6 @@ function ($scope, Container, Settings, Messages, ViewSpinner, Config) { }); } - } }); if (counter === 0) { From 227e5883e9f532f9690ab484d7cdedd79a00ddc8 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 6 Jul 2016 12:19:09 +1200 Subject: [PATCH 6/8] feat(ui): new container creation view (#29) --- app/app.js | 17 +- app/components/container/container.html | 1 - app/components/containers/containers.html | 4 +- .../createContainerController.js | 213 +++++++++ .../createContainer/createcontainer.html | 312 ++++++++++++ .../startContainerController.js | 184 -------- .../startContainer/startcontainer.html | 444 ------------------ app/shared/filters.js | 6 + app/shared/services.js | 6 +- assets/css/app.css | 19 + 10 files changed, 570 insertions(+), 636 deletions(-) create mode 100644 app/components/createContainer/createContainerController.js create mode 100644 app/components/createContainer/createcontainer.html delete mode 100644 app/components/startContainer/startContainerController.js delete mode 100644 app/components/startContainer/startcontainer.html diff --git a/app/app.js b/app/app.js index 0c3158372..ac8a51cf4 100644 --- a/app/app.js +++ b/app/app.js @@ -10,11 +10,11 @@ angular.module('uifordocker', [ 'dashboard', 'container', 'containers', + 'createContainer', 'docker', 'images', 'image', 'pullImage', - 'startContainer', 'containerLogs', 'stats', 'swarm', @@ -57,6 +57,21 @@ angular.module('uifordocker', [ templateUrl: 'app/components/containerLogs/containerlogs.html', controller: 'ContainerLogsController' }) + .state('actions', { + abstract: true, + url: "/actions", + template: '' + }) + .state('actions.create', { + abstract: true, + url: "/create", + template: '' + }) + .state('actions.create.container', { + url: "/container", + templateUrl: 'app/components/createContainer/createcontainer.html', + controller: 'CreateContainerController' + }) .state('docker', { url: '/docker/', templateUrl: 'app/components/docker/docker.html', diff --git a/app/components/container/container.html b/app/components/container/container.html index 0c146d78c..05803ca02 100644 --- a/app/components/container/container.html +++ b/app/components/container/container.html @@ -5,7 +5,6 @@ -
diff --git a/app/components/containers/containers.html b/app/components/containers/containers.html index d18d5ae95..d24cf5549 100644 --- a/app/components/containers/containers.html +++ b/app/components/containers/containers.html @@ -1,5 +1,3 @@ -
- @@ -23,8 +21,8 @@ -
+ Add container
diff --git a/app/components/createContainer/createContainerController.js b/app/components/createContainer/createContainerController.js new file mode 100644 index 000000000..2e7000286 --- /dev/null +++ b/app/components/createContainer/createContainerController.js @@ -0,0 +1,213 @@ +angular.module('createContainer', []) +.controller('CreateContainerController', ['$scope', '$state', 'Config', 'Container', 'Image', 'Volume', 'Network', 'Messages', 'ViewSpinner', 'errorMsgFilter', +function ($scope, $state, Config, Container, Image, Volume, Network, Messages, ViewSpinner, errorMsgFilter) { + + $scope.state = { + alwaysPull: true + }; + + $scope.formValues = { + Console: 'none', + Volumes: [], + }; + + $scope.config = { + Env: [], + HostConfig: { + RestartPolicy: { + Name: 'no' + }, + PortBindings: [], + Binds: [], + NetworkMode: 'bridge', + Privileged: false + } + }; + + $scope.resetVolumePath = function(index) { + $scope.formValues.Volumes[index].name = ''; + }; + + $scope.addVolume = function() { + $scope.formValues.Volumes.push({ name: '', containerPath: '', readOnly: false, isPath: false }); + }; + + $scope.removeVolume = function(index) { + $scope.formValues.Volumes.splice(index, 1); + }; + + $scope.addEnvironmentVariable = function() { + $scope.config.Env.push({ name: '', value: ''}); + }; + + $scope.removeEnvironmentVariable = function(index) { + $scope.config.Env.splice(index, 1); + }; + + $scope.addPortBinding = function() { + $scope.config.HostConfig.PortBindings.push({ hostPort: '', containerPort: '', protocol: 'tcp' }); + }; + + $scope.removePortBinding = function(index) { + $scope.config.HostConfig.PortBindings.splice(index, 1); + }; + + Config.$promise.then(function (c) { + var swarm = c.swarm; + + Volume.query({}, function (d) { + $scope.availableVolumes = d.Volumes; + }, function (e) { + Messages.error("Failure", e.data); + }); + + Network.query({}, function (d) { + var networks = d; + if (swarm) { + networks = d.filter(function (network) { + if (network.Scope === 'global') { + return network; + } + }); + networks.push({Name: "bridge"}); + networks.push({Name: "host"}); + networks.push({Name: "none"}); + } + $scope.availableNetworks = networks; + }, function (e) { + Messages.error("Failure", e.data); + }); + }); + + function createContainer(config) { + ViewSpinner.spin(); + Container.create(config, function (d) { + if (d.Id) { + var reqBody = config.HostConfig || {}; + reqBody.id = d.Id; + Container.start(reqBody, function (cd) { + ViewSpinner.stop(); + Messages.send('Container Started', d.Id); + $state.go('containers', {}, {reload: true}); + }, function (e) { + ViewSpinner.stop(); + Messages.error('Error', errorMsgFilter(e)); + }); + } else { + ViewSpinner.stop(); + Messages.error('Error', errorMsgFilter(d)); + } + }, function (e) { + ViewSpinner.stop(); + Messages.error('Error', errorMsgFilter(e)); + }); + } + + function createImageConfig(imageName) { + var imageNameAndTag = imageName.split(':'); + var imageConfig = { + fromImage: imageNameAndTag[0], + tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest' + }; + return imageConfig; + } + + function pullImageAndCreateContainer(config) { + ViewSpinner.spin(); + + var image = _.toLower(config.Image); + var imageConfig = createImageConfig(image); + + Image.create(imageConfig, function (data) { + var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error'); + if (err) { + var detail = data[data.length - 1]; + ViewSpinner.stop(); + Messages.error('Error', detail.error); + } else { + createContainer(config); + } + }, function (e) { + ViewSpinner.stop(); + Messages.error('Error', 'Unable to pull image ' + image); + }); + } + + function preparePortBindings(config) { + var bindings = {}; + config.HostConfig.PortBindings.forEach(function (portBinding) { + if (portBinding.hostPort && portBinding.containerPort) { + var key = portBinding.containerPort + "/" + portBinding.protocol; + bindings[key] = [{ HostPort: portBinding.hostPort }]; + } + }); + config.HostConfig.PortBindings = bindings; + } + + function prepareConsole(config) { + var value = $scope.formValues.Console; + var openStdin = true; + var tty = true; + if (value === 'tty') { + openStdin = false; + } else if (value === 'interactive') { + tty = false; + } else if (value === 'none') { + openStdin = false; + tty = false; + } + config.OpenStdin = openStdin; + config.Tty = tty; + } + + function prepareEnvironmentVariables(config) { + var env = []; + config.Env.forEach(function (v) { + if (v.name && v.value) { + env.push(v.name + "=" + v.value); + } + }); + config.Env = env; + } + + function prepareVolumes(config) { + var binds = []; + var volumes = {}; + + $scope.formValues.Volumes.forEach(function (volume) { + var name = volume.name; + var containerPath = volume.containerPath; + if (name && containerPath) { + var bind = name + ':' + containerPath; + volumes[containerPath] = {}; + if (volume.readOnly) { + bind += ':ro'; + } + binds.push(bind); + } + }); + config.HostConfig.Binds = binds; + config.Volumes = volumes; + } + + function prepareConfiguration() { + var config = angular.copy($scope.config); + preparePortBindings(config); + prepareConsole(config); + prepareEnvironmentVariables(config); + prepareVolumes(config); + return config; + } + + $scope.create = function () { + var config = prepareConfiguration(); + console.log(JSON.stringify(config, null, 4)); + + if ($scope.state.alwaysPull) { + pullImageAndCreateContainer(config); + } else { + createContainer(config); + } + }; + +}]); diff --git a/app/components/createContainer/createcontainer.html b/app/components/createContainer/createcontainer.html new file mode 100644 index 000000000..b1b1f5a3f --- /dev/null +++ b/app/components/createContainer/createcontainer.html @@ -0,0 +1,312 @@ + + + + Containers > Add container + + + +
+
+ + +
+ +
+ +
+ +
+
+ + +
+ +
+ +
+
+
+ +
+
+
+ + +
+ +
+ + + +
+
+ + +
+ +
+ + map port + +
+ +
+
+
+ host + +
+
+ container + +
+
+ + + + +
+
+
+ +
+ +
+
+
+
+
+ +
+
+ + + + + +
+ +
+
+ +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ +
+ +
+ +
+
+ + +
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+ + +
+ +
+ + environment variable + +
+ +
+
+
+ name + +
+
+ value + + + + +
+
+
+ +
+ +
+
+ + +
+
+ +
+ +
+ + volume + +
+ +
+
+
+
+ +
+
+
+ Path + + +
+
+ container + + + + +
+
+
+ +
+
+ +
+ + +
+
+ +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ +
+
+ +
+
+ + +
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+
+
+
+
+ +
+
+ + Cancel +
+
diff --git a/app/components/startContainer/startContainerController.js b/app/components/startContainer/startContainerController.js deleted file mode 100644 index 6dc4027f8..000000000 --- a/app/components/startContainer/startContainerController.js +++ /dev/null @@ -1,184 +0,0 @@ -angular.module('startContainer', ['ui.bootstrap']) -.controller('StartContainerController', ['$scope', '$state', 'Container', 'Image', 'Messages', 'containernameFilter', 'errorMsgFilter', 'ViewSpinner', -function ($scope, $state, Container, Image, Messages, containernameFilter, errorMsgFilter, ViewSpinner) { - $scope.template = 'app/components/startContainer/startcontainer.html'; - - Container.query({all: 1}, function (d) { - $scope.containerNames = d.map(function (container) { - return containernameFilter(container); - }); - }); - - $scope.config = { - Env: [], - Labels: [], - Volumes: [], - SecurityOpts: [], - HostConfig: { - PortBindings: [], - Binds: [], - Links: [], - Dns: [], - DnsSearch: [], - VolumesFrom: [], - CapAdd: [], - CapDrop: [], - Devices: [], - LxcConf: [], - ExtraHosts: [] - } - }; - - $scope.menuStatus = { - containerOpen: true, - hostConfigOpen: false - }; - - function failedRequestHandler(e, Messages) { - Messages.error('Error', errorMsgFilter(e)); - } - - function rmEmptyKeys(col) { - for (var key in col) { - if (col[key] === null || col[key] === undefined || col[key] === '' || ($.isPlainObject(col[key]) && $.isEmptyObject(col[key])) || col[key].length === 0) { - delete col[key]; - } - } - } - - function getNames(arr) { - return arr.map(function (item) { - return item.name; - }); - } - - function createContainer(config) { - Container.create(config, function (d) { - if (d.Id) { - var reqBody = config.HostConfig || {}; - reqBody.id = d.Id; - Container.start(reqBody, function (cd) { - if (cd.id) { - ViewSpinner.stop(); - Messages.send('Container Started', d.Id); - $state.go('container', {id: d.Id}, {reload: true}); - } else { - ViewSpinner.stop(); - failedRequestHandler(cd, Messages); - Container.remove({id: d.Id}, function () { - Messages.send('Container Removed', d.Id); - }); - } - }, function (e) { - ViewSpinner.stop(); - failedRequestHandler(e, Messages); - }); - } else { - ViewSpinner.stop(); - failedRequestHandler(d, Messages); - } - }, function (e) { - ViewSpinner.stop(); - failedRequestHandler(e, Messages); - }); - } - - $scope.create = function () { - $('#create-modal').modal('hide'); - ViewSpinner.spin(); - - var config = angular.copy($scope.config); - - if (config.Cmd && config.Cmd[0] === "[") { - config.Cmd = angular.fromJson(config.Cmd); - } else if (config.Cmd) { - config.Cmd = config.Cmd.split(' '); - } - - config.Env = config.Env.map(function (envar) { - return envar.name + '=' + envar.value; - }); - var labels = {}; - config.Labels = config.Labels.forEach(function(label) { - labels[label.key] = label.value; - }); - config.Labels = labels; - - config.Volumes = getNames(config.Volumes); - config.SecurityOpts = getNames(config.SecurityOpts); - - config.HostConfig.VolumesFrom = getNames(config.HostConfig.VolumesFrom); - config.HostConfig.Binds = getNames(config.HostConfig.Binds); - config.HostConfig.Links = getNames(config.HostConfig.Links); - config.HostConfig.Dns = getNames(config.HostConfig.Dns); - config.HostConfig.DnsSearch = getNames(config.HostConfig.DnsSearch); - config.HostConfig.CapAdd = getNames(config.HostConfig.CapAdd); - config.HostConfig.CapDrop = getNames(config.HostConfig.CapDrop); - config.HostConfig.LxcConf = config.HostConfig.LxcConf.reduce(function (prev, cur, idx) { - prev[cur.name] = cur.value; - return prev; - }, {}); - config.HostConfig.ExtraHosts = config.HostConfig.ExtraHosts.map(function (entry) { - return entry.host + ':' + entry.ip; - }); - - var ExposedPorts = {}; - var PortBindings = {}; - config.HostConfig.PortBindings.forEach(function (portBinding) { - var intPort = portBinding.intPort + "/tcp"; - if (portBinding.protocol === "udp") { - intPort = portBinding.intPort + "/udp"; - } - var binding = { - HostIp: portBinding.ip, - HostPort: portBinding.extPort - }; - if (portBinding.intPort) { - ExposedPorts[intPort] = {}; - if (intPort in PortBindings) { - PortBindings[intPort].push(binding); - } else { - PortBindings[intPort] = [binding]; - } - } else { - Messages.send('Warning', 'Internal port must be specified for PortBindings'); - } - }); - config.ExposedPorts = ExposedPorts; - config.HostConfig.PortBindings = PortBindings; - - // Remove empty fields from the request to avoid overriding defaults - rmEmptyKeys(config.HostConfig); - rmEmptyKeys(config); - - var image = _.toLower(config.Image); - var imageNameAndTag = image.split(':'); - var imageConfig = { - fromImage: imageNameAndTag[0], - tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest', - }; - - Image.create(imageConfig, function (data) { - var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error'); - if (err) { - var detail = data[data.length - 1]; - ViewSpinner.stop(); - Messages.error('Error', detail.error); - } else { - Messages.send("Image successfully pulled", image); - createContainer(config); - } - }, function (e) { - ViewSpinner.stop(); - Messages.error('Error', 'Unable to pull image ' + image); - }); - }; - - $scope.addEntry = function (array, entry) { - array.push(entry); - }; - $scope.rmEntry = function (array, entry) { - var idx = array.indexOf(entry); - array.splice(idx, 1); - }; -}]); diff --git a/app/components/startContainer/startcontainer.html b/app/components/startContainer/startcontainer.html deleted file mode 100644 index 9891c9e58..000000000 --- a/app/components/startContainer/startcontainer.html +++ /dev/null @@ -1,444 +0,0 @@ - diff --git a/app/shared/filters.js b/app/shared/filters.js index ea08f0057..4b5b93700 100644 --- a/app/shared/filters.js +++ b/app/shared/filters.js @@ -49,6 +49,12 @@ angular.module('dockerui.filters', []) return ''; }; }) +.filter('capitalize', function () { + 'use strict'; + return function (text) { + return _.capitalize(text); + }; +}) .filter('getstatetext', function () { 'use strict'; return function (state) { diff --git a/app/shared/services.js b/app/shared/services.js index 1085cdd58..102d011d4 100644 --- a/app/shared/services.js +++ b/app/shared/services.js @@ -86,10 +86,10 @@ angular.module('dockerui.services', ['ngResource', 'ngSanitize']) history: {method: 'GET', params: {action: 'history'}, isArray: true}, create: { method: 'POST', isArray: true, transformResponse: [function f(data) { - var str = data.replace(/\n/g, " ").replace(/\}\W*\{/g, "}, {"); - return angular.fromJson("[" + str + "]"); + var str = "[" + data.replace(/\n/g, " ").replace(/\}\s*\{/g, "}, {") + "]"; + return angular.fromJson(str); }], - params: {action: 'create', fromImage: '@fromImage', repo: '@repo', tag: '@tag', registry: '@registry'} + params: {action: 'create', fromImage: '@fromImage', tag: '@tag'} }, insert: {method: 'POST', params: {id: '@id', action: 'insert'}}, push: {method: 'POST', params: {id: '@id', action: 'push'}}, diff --git a/assets/css/app.css b/assets/css/app.css index b8d40de69..5d99a5c85 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -146,3 +146,22 @@ .header_title_content { margin-left: 5px; } + +.form-horizontal .control-label.text-left{ + text-align: left; + font-size: 0.9em; +} + +input[type="checkbox"] { + margin-top: 1px; + vertical-align: middle; +} + +input[type="radio"] { + margin-top: 1px; + vertical-align: middle; +} + +.clickable { + cursor: pointer; +} From f18aa8fe792467fc1610da5cba0b84c1884eac3f Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 6 Jul 2016 12:24:49 +1200 Subject: [PATCH 7/8] fix(ui): fix display of containers per node in Swarm view (#30) --- app/components/swarm/swarmController.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/components/swarm/swarmController.js b/app/components/swarm/swarmController.js index a3b096076..a12b238bf 100644 --- a/app/components/swarm/swarmController.js +++ b/app/components/swarm/swarmController.js @@ -52,11 +52,10 @@ angular.module('swarm', []) node.ip = info[offset][1]; node.id = info[offset + 1][1]; node.status = info[offset + 2][1]; - node.containers = info[offset + 2][1]; - node.cpu = info[offset + 3][1]; - node.memory = info[offset + 4][1]; - node.labels = info[offset + 5][1]; - node.error = info[offset + 6][1]; + node.containers = info[offset + 3][1]; + node.cpu = info[offset + 4][1]; + node.memory = info[offset + 5][1]; + node.labels = info[offset + 6][1]; node.version = info[offset + 8][1]; $scope.swarm.Status.push(node); } From e6e21e9f46e36ac906667a3f974a87f2bef5c3d7 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 6 Jul 2016 14:04:31 +1200 Subject: [PATCH 8/8] chore(version): bump version number --- app/app.js | 2 +- bower.json | 2 +- dockerui.go | 2 +- package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/app.js b/app/app.js index ac8a51cf4..6c882a01e 100644 --- a/app/app.js +++ b/app/app.js @@ -135,4 +135,4 @@ angular.module('uifordocker', [ .constant('DOCKER_ENDPOINT', 'dockerapi') .constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is requred. If you have a port, prefix it with a ':' i.e. :4243 .constant('CONFIG_ENDPOINT', '/config') - .constant('UI_VERSION', 'v1.0.4'); + .constant('UI_VERSION', 'v1.1.0'); diff --git a/bower.json b/bower.json index f4b19b158..af99b81db 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "uifordocker", - "version": "1.0.4", + "version": "1.1.0", "homepage": "https://github.com/kevana/ui-for-docker", "authors": [ "Michael Crosby ", diff --git a/dockerui.go b/dockerui.go index ee235cdc7..885fdc1dc 100644 --- a/dockerui.go +++ b/dockerui.go @@ -173,7 +173,7 @@ func csrfWrapper(h http.Handler) http.Handler { } func main() { - kingpin.Version("1.0.4") + kingpin.Version("1.1.0") kingpin.Parse() configuration := Config{ diff --git a/package.json b/package.json index 536b45b17..21b1815ed 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Michael Crosby & Kevan Ahlquist", "name": "uifordocker", "homepage": "https://github.com/kevana/ui-for-docker", - "version": "1.0.4", + "version": "1.1.0", "repository": { "type": "git", "url": "git@github.com:kevana/ui-for-docker.git"