From ca4428cff2cce5db9055673a9e933123b3ea52a9 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Mon, 13 Mar 2017 10:23:49 +0100 Subject: [PATCH 01/23] feat(build): update build script --- build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sh b/build.sh index 4e78cf178..f8720e24b 100755 --- a/build.sh +++ b/build.sh @@ -49,7 +49,7 @@ cd - grunt release-win rm -rf /tmp/portainer-builds/win && mkdir -pv /tmp/portainer-builds/win/portainer -mv dist/* /tmp/portainer-builds/win/portainer +cp -r dist/* /tmp/portainer-builds/win/portainer cd /tmp/portainer-builds/win tar cvpfz portainer-${VERSION}-windows-amd64.tar.gz portainer mv portainer-${VERSION}-windows-amd64.tar.gz /tmp/portainer-builds/ From 3861e964f4e6102ae6e30d3b508ae139179baa2b Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Tue, 14 Mar 2017 18:28:21 +0100 Subject: [PATCH 02/23] fix(dockerfile): fix an issue with the data directory in Windows images --- build/windows/microsoftservercore/Dockerfile | 2 +- build/windows/nanoserver/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/windows/microsoftservercore/Dockerfile b/build/windows/microsoftservercore/Dockerfile index dfcd8b15a..2a9f4040a 100644 --- a/build/windows/microsoftservercore/Dockerfile +++ b/build/windows/microsoftservercore/Dockerfile @@ -2,7 +2,7 @@ FROM microsoft/windowsservercore COPY dist / -VOLUME C:\\data +RUN mkdir C:\\data WORKDIR / diff --git a/build/windows/nanoserver/Dockerfile b/build/windows/nanoserver/Dockerfile index ae8a25a39..7a79da105 100644 --- a/build/windows/nanoserver/Dockerfile +++ b/build/windows/nanoserver/Dockerfile @@ -2,7 +2,7 @@ FROM microsoft/nanoserver COPY dist / -VOLUME C:\\data +RUN mkdir C:\\data WORKDIR / From b24825d45313f1a1325cacb0fb22ca784a4e1efc Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Thu, 16 Mar 2017 11:23:01 +0100 Subject: [PATCH 03/23] feat(backend): check for the full database path to verify its existence (#681) --- api/bolt/datastore.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/bolt/datastore.go b/api/bolt/datastore.go index 79e859a5f..0b41ee306 100644 --- a/api/bolt/datastore.go +++ b/api/bolt/datastore.go @@ -49,7 +49,7 @@ func NewStore(storePath string) (*Store, error) { store.ResourceControlService.store = store store.VersionService.store = store - _, err := os.Stat(storePath) + _, err := os.Stat(storePath + "/" + databaseFileName) if err != nil && os.IsNotExist(err) { store.checkForDataMigration = false } else if err != nil { From 9f12cbd43d7290aeb75b6f96d17a85e276e3ac3c Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Thu, 16 Mar 2017 11:24:47 +0100 Subject: [PATCH 04/23] fix(services): fix an issue with the sorting link for the ownership column (#682) --- app/components/services/services.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/services/services.html b/app/components/services/services.html index 6eb427ac6..02925a678 100644 --- a/app/components/services/services.html +++ b/app/components/services/services.html @@ -59,7 +59,7 @@ - + Ownership From 631b29eddc4cc7afaf79582a0f88bb223c54a115 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Thu, 16 Mar 2017 11:32:07 +0100 Subject: [PATCH 05/23] fix(jshint): fix lint issues --- app/components/containers/containersController.js | 2 +- app/components/services/servicesController.js | 2 +- app/components/volumes/volumesController.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/components/containers/containersController.js b/app/components/containers/containersController.js index eea0140e0..f88821698 100644 --- a/app/components/containers/containersController.js +++ b/app/components/containers/containersController.js @@ -47,7 +47,7 @@ angular.module('containers', []) angular.forEach($scope.containers, function (container) { if (container.Metadata) { var containerRC = container.Metadata.ResourceControl; - if (containerRC && containerRC.OwnerId != $scope.user.ID) { + if (containerRC && containerRC.OwnerId !== $scope.user.ID) { angular.forEach(users, function (user) { if (containerRC.OwnerId === user.Id) { container.Owner = user.Username; diff --git a/app/components/services/servicesController.js b/app/components/services/servicesController.js index 13f63fff6..e1147b17b 100644 --- a/app/components/services/servicesController.js +++ b/app/components/services/servicesController.js @@ -114,7 +114,7 @@ function ($q, $scope, $stateParams, $state, Service, ServiceHelper, Messages, Pa angular.forEach($scope.services, function (service) { if (service.Metadata) { var serviceRC = service.Metadata.ResourceControl; - if (serviceRC && serviceRC.OwnerId != $scope.user.ID) { + if (serviceRC && serviceRC.OwnerId !== $scope.user.ID) { angular.forEach(users, function (user) { if (serviceRC.OwnerId === user.Id) { service.Owner = user.Username; diff --git a/app/components/volumes/volumesController.js b/app/components/volumes/volumesController.js index e020f075b..1df40343a 100644 --- a/app/components/volumes/volumesController.js +++ b/app/components/volumes/volumesController.js @@ -99,7 +99,7 @@ function ($scope, $state, Volume, Messages, Pagination, ModalService, Authentica angular.forEach($scope.volumes, function (volume) { if (volume.Metadata) { var volumeRC = volume.Metadata.ResourceControl; - if (volumeRC && volumeRC.OwnerId != $scope.user.ID) { + if (volumeRC && volumeRC.OwnerId !== $scope.user.ID) { angular.forEach(users, function (user) { if (volumeRC.OwnerId === user.Id) { volume.Owner = user.Username; From dcce21167631e589de1a758f9530b2a3c79e2833 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Fri, 17 Mar 2017 11:52:17 +0100 Subject: [PATCH 06/23] fix(api): allow empty array when removing accesses to an endpoint (#692) --- api/http/endpoint_handler.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/http/endpoint_handler.go b/api/http/endpoint_handler.go index 5fac67276..2178ea475 100644 --- a/api/http/endpoint_handler.go +++ b/api/http/endpoint_handler.go @@ -20,7 +20,6 @@ type EndpointHandler struct { authorizeEndpointManagement bool EndpointService portainer.EndpointService FileService portainer.FileService - // server *Server } const ( @@ -214,7 +213,7 @@ func (handler *EndpointHandler) handlePutEndpointAccess(w http.ResponseWriter, r } type putEndpointAccessRequest struct { - AuthorizedUsers []int `valid:"required"` + AuthorizedUsers []int `valid:"-"` } // handlePutEndpoint handles PUT requests on /endpoints/:id From 497a8392f61521cfa96fa5603b272e38ac0e8853 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Sat, 18 Mar 2017 13:08:39 +0100 Subject: [PATCH 07/23] fix(sidebar): fix a display issue on low resolution (#697) --- assets/css/app.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/assets/css/app.css b/assets/css/app.css index 6e3b3db30..9ea1b028c 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -286,6 +286,10 @@ a[ng-click]{ margin: 0 auto; } +ul.sidebar { + bottom: 40px; +} + ul.sidebar .sidebar-list a.active { color: #fff; text-indent: 22px; From 097955e587176ea4e66465c166cd44a1760df2a7 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Sun, 19 Mar 2017 19:07:22 +0100 Subject: [PATCH 08/23] fix(templates): fix an issue where container links would fail (#701) --- app/components/templates/templatesController.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/components/templates/templatesController.js b/app/components/templates/templatesController.js index 5a5395977..8d501d30c 100644 --- a/app/components/templates/templatesController.js +++ b/app/components/templates/templatesController.js @@ -116,6 +116,7 @@ function ($scope, $q, $state, $anchorScroll, Config, ContainerService, Container } else if (network.Name !== "bridge") { containerMapping = 'BY_CONTAINER_NAME'; } + return containerMapping; } function filterNetworksBasedOnProvider(networks) { From b6627098c217759a037a6207608f34f3cc368cb1 Mon Sep 17 00:00:00 2001 From: AHumanPerson Date: Sun, 19 Mar 2017 16:24:09 -0400 Subject: [PATCH 09/23] docs(README): update demo username (#703) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 11bf86ad8..28afcfde8 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ -You can try out the public demo instance: http://demo.portainer.io/ (login with the username **demo** and the password **tryportainer**). +You can try out the public demo instance: http://demo.portainer.io/ (login with the username **admin** and the password **tryportainer**). Please note that the public demo cluster is **reset every 15min**. From c2e63070e6a797547c43851ca64794af1f4249d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Kov=C3=A1cs?= Date: Mon, 20 Mar 2017 11:45:04 +0100 Subject: [PATCH 10/23] feat(image-details): add the ability to pull/update a tag (#421) --- app/components/image/image.html | 48 +++++++++++++++---------- app/components/image/imageController.js | 26 ++++++++++++-- 2 files changed, 54 insertions(+), 20 deletions(-) diff --git a/app/components/image/image.html b/app/components/image/image.html index 59bc430d6..5cf83602a 100644 --- a/app/components/image/image.html +++ b/app/components/image/image.html @@ -11,24 +11,36 @@
- -
- - - - - {{ tag }} - - - - -
-
- - Note: you can click on the upload icon to push an image - and on the trash icon to delete a tag - -
+ +
+
+
+
+
+ {{ tag }} + + + + + + + + + + + +
+
+
+
+
+
+ + Note: you can click on the upload icon to push or on the download icon to pull an image and on the trash icon to delete a tag + +
+
+
diff --git a/app/components/image/imageController.js b/app/components/image/imageController.js index 7c474086a..fc663e23e 100644 --- a/app/components/image/imageController.js +++ b/app/components/image/imageController.js @@ -1,6 +1,11 @@ angular.module('image', []) -.controller('ImageController', ['$scope', '$stateParams', '$state', 'Image', 'ImageHelper', 'Messages', -function ($scope, $stateParams, $state, Image, ImageHelper, Messages) { +.filter('onlylabel', function(){ + return function(tag){ + return tag.substr(tag.indexOf(":")+1); + }; +}) +.controller('ImageController', ['$scope', '$stateParams', '$state', 'Image', 'ImageService', 'ImageHelper', 'Messages', +function ($scope, $stateParams, $state, Image, ImageService, ImageHelper, Messages) { $scope.RepoTags = []; $scope.config = { Image: '', @@ -49,6 +54,23 @@ function ($scope, $stateParams, $state, Image, ImageHelper, Messages) { }); }; + $scope.pullImage = function(tag) { + var items = tag.split(":"); + var image = items[0]; + tag = items[1]; + $('#loadingViewSpinner').show(); + ImageService.pullImage({fromImage: image, tag: tag}) + .then(function success(data) { + Messages.send('Image successfully pulled'); + }) + .catch(function error(error){ + Messages.error("Failure", error, "Unable to pull image"); + }) + .finally(function final() { + $('#loadingViewSpinner').hide(); + }); + }; + $scope.removeImage = function (id) { $('#loadingViewSpinner').show(); Image.remove({id: id}, function (d) { From 24b51a7e8760246b82828bcd68d6ff02ea824d0c Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Mon, 20 Mar 2017 12:01:35 +0100 Subject: [PATCH 11/23] refactor(image): refactor the code used in image and image details controller (#705) --- app/components/image/image.html | 28 +-- app/components/image/imageController.js | 202 +++++++++--------- app/components/images/imagesController.js | 69 +++--- .../templates/templatesController.js | 6 +- app/models/imageDetails.js | 19 ++ app/services/imageService.js | 75 ++++++- app/services/messages.js | 2 + app/services/templateService.js | 11 +- 8 files changed, 245 insertions(+), 167 deletions(-) create mode 100644 app/models/imageDetails.js diff --git a/app/components/image/image.html b/app/components/image/image.html index 5cf83602a..8dbc35b7a 100644 --- a/app/components/image/image.html +++ b/app/components/image/image.html @@ -7,7 +7,7 @@ -
+
@@ -15,7 +15,7 @@
-
+
{{ tag }} @@ -25,7 +25,7 @@ - + @@ -36,7 +36,9 @@
- Note: you can click on the upload icon to push or on the download icon to pull an image and on the trash icon to delete a tag + Note: you can click on the upload icon to push an image + or on the download icon to pull an image + or on the trash icon to delete a tag.
@@ -133,33 +135,33 @@ CMD - {{ image.ContainerConfig.Cmd|command }} + {{ image.Command|command }} - + ENTRYPOINT - {{ image.ContainerConfig.Entrypoint|command }} + {{ image.Entrypoint|command }} - + EXPOSE - + {{ port }} - + VOLUME - + {{ volume }} - + ENV - + diff --git a/app/components/image/imageController.js b/app/components/image/imageController.js index fc663e23e..8514ff1bb 100644 --- a/app/components/image/imageController.js +++ b/app/components/image/imageController.js @@ -1,111 +1,109 @@ angular.module('image', []) -.filter('onlylabel', function(){ - return function(tag){ - return tag.substr(tag.indexOf(":")+1); +.controller('ImageController', ['$scope', '$stateParams', '$state', 'ImageService', 'Messages', +function ($scope, $stateParams, $state, ImageService, Messages) { + $scope.config = { + Image: '', + Registry: '' }; -}) -.controller('ImageController', ['$scope', '$stateParams', '$state', 'Image', 'ImageService', 'ImageHelper', 'Messages', -function ($scope, $stateParams, $state, Image, ImageService, ImageHelper, Messages) { - $scope.RepoTags = []; - $scope.config = { - Image: '', - Registry: '' - }; - // Get RepoTags from the /images/query endpoint instead of /image/json, - // for backwards compatibility with Docker API versions older than 1.21 - function getRepoTags(imageId) { - Image.query({}, function (d) { - d.forEach(function(image) { - if (image.Id === imageId && image.RepoTags[0] !== ':') { - $scope.RepoTags = image.RepoTags; - } - }); - }); - } + $scope.tagImage = function() { + $('#loadingViewSpinner').show(); + var image = $scope.config.Image; + var registry = $scope.config.Registry; - $scope.tagImage = function() { - $('#loadingViewSpinner').show(); - var image = $scope.config.Image; - var registry = $scope.config.Registry; - var imageConfig = ImageHelper.createImageConfigForCommit(image, registry); - Image.tag({id: $stateParams.id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) { - Messages.send('Image successfully tagged'); - $('#loadingViewSpinner').hide(); - $state.go('image', {id: $stateParams.id}, {reload: true}); - }, function(e) { - $('#loadingViewSpinner').hide(); - Messages.error("Failure", e, "Unable to tag image"); - }); - }; + ImageService.tagImage($stateParams.id, image, registry) + .then(function success(data) { + Messages.send('Image successfully tagged'); + $state.go('image', {id: $stateParams.id}, {reload: true}); + }) + .catch(function error(err) { + Messages.error("Failure", err, "Unable to tag image"); + }) + .finally(function final() { + $('#loadingViewSpinner').hide(); + }); + }; - $scope.pushImage = function(tag) { - $('#loadingViewSpinner').show(); - Image.push({tag: tag}, function (d) { - if (d[d.length-1].error) { - Messages.error("Unable to push image", {}, d[d.length-1].error); - } else { - Messages.send('Image successfully pushed'); - } - $('#loadingViewSpinner').hide(); - }, function (e) { - $('#loadingViewSpinner').hide(); - Messages.error("Failure", e, "Unable to push image"); - }); - }; + $scope.pushImage = function(tag) { + $('#loadingViewSpinner').show(); + ImageService.pushImage(tag) + .then(function success() { + Messages.send('Image successfully pushed'); + }) + .catch(function error(err) { + Messages.error("Failure", err, "Unable to push image tag"); + }) + .finally(function final() { + $('#loadingViewSpinner').hide(); + }); + }; - $scope.pullImage = function(tag) { - var items = tag.split(":"); - var image = items[0]; - tag = items[1]; - $('#loadingViewSpinner').show(); - ImageService.pullImage({fromImage: image, tag: tag}) - .then(function success(data) { - Messages.send('Image successfully pulled'); - }) - .catch(function error(error){ - Messages.error("Failure", error, "Unable to pull image"); - }) - .finally(function final() { - $('#loadingViewSpinner').hide(); - }); - }; + $scope.pullImage = function(tag) { + $('#loadingViewSpinner').show(); + var image = $scope.config.Image; + var registry = $scope.config.Registry; - $scope.removeImage = function (id) { - $('#loadingViewSpinner').show(); - Image.remove({id: id}, function (d) { - if (d[0].message) { - $('#loadingViewSpinner').hide(); - Messages.error("Unable to remove image", {}, d[0].message); - } else { - // If last message key is 'Deleted' or if it's 'Untagged' and there is only one tag associated to the image - // then assume the image is gone and send to images page - if (d[d.length-1].Deleted || (d[d.length-1].Untagged && $scope.RepoTags.length === 1)) { - Messages.send('Image successfully deleted'); - $state.go('images', {}, {reload: true}); - } else { - Messages.send('Tag successfully deleted'); - $state.go('image', {id: $stateParams.id}, {reload: true}); - } - } - }, function (e) { - $('#loadingViewSpinner').hide(); - Messages.error("Failure", e, 'Unable to remove image'); - }); - }; + ImageService.pullImage(image, registry) + .then(function success(data) { + Messages.send('Image successfully pulled', image); + }) + .catch(function error(err){ + Messages.error("Failure", err, "Unable to pull image"); + }) + .finally(function final() { + $('#loadingViewSpinner').hide(); + }); + }; - $('#loadingViewSpinner').show(); - Image.get({id: $stateParams.id}, function (d) { - $scope.image = d; - if (d.RepoTags) { - $scope.RepoTags = d.RepoTags; - } else { - getRepoTags(d.Id); - } - $('#loadingViewSpinner').hide(); - $scope.exposedPorts = d.ContainerConfig.ExposedPorts ? Object.keys(d.ContainerConfig.ExposedPorts) : []; - $scope.volumes = d.ContainerConfig.Volumes ? Object.keys(d.ContainerConfig.Volumes) : []; - }, function (e) { - Messages.error("Failure", e, "Unable to retrieve image info"); - }); + $scope.removeTag = function(id) { + $('#loadingViewSpinner').show(); + ImageService.deleteImage(id, false) + .then(function success() { + if ($scope.image.RepoTags.length === 1) { + Messages.send('Image successfully deleted', id); + $state.go('images', {}, {reload: true}); + } else { + Messages.send('Tag successfully deleted', id); + $state.go('image', {id: $stateParams.id}, {reload: true}); + } + }) + .catch(function error(err) { + Messages.error("Failure", err, 'Unable to remove image'); + }) + .finally(function final() { + $('#loadingViewSpinner').hide(); + }); + }; + + $scope.removeImage = function (id) { + $('#loadingViewSpinner').show(); + ImageService.deleteImage(id, false) + .then(function success() { + Messages.send('Image successfully deleted', id); + $state.go('images', {}, {reload: true}); + }) + .catch(function error(err) { + Messages.error("Failure", err, 'Unable to remove image'); + }) + .finally(function final() { + $('#loadingViewSpinner').hide(); + }); + }; + + function retrieveImageDetails() { + $('#loadingViewSpinner').show(); + ImageService.image($stateParams.id) + .then(function success(data) { + $scope.image = data; + }) + .catch(function error(err) { + Messages.error("Failure", err, "Unable to retrieve image details"); + $state.go('images'); + }) + .finally(function final() { + $('#loadingViewSpinner').hide(); + }); + } + + retrieveImageDetails(); }]); diff --git a/app/components/images/imagesController.js b/app/components/images/imagesController.js index 44752330e..350b77c2a 100644 --- a/app/components/images/imagesController.js +++ b/app/components/images/imagesController.js @@ -1,6 +1,6 @@ angular.module('images', []) -.controller('ImagesController', ['$scope', '$state', 'Config', 'Image', 'ImageHelper', 'Messages', 'Pagination', 'ModalService', -function ($scope, $state, Config, Image, ImageHelper, Messages, Pagination, ModalService) { +.controller('ImagesController', ['$scope', '$state', 'Config', 'ImageService', 'Messages', 'Pagination', 'ModalService', +function ($scope, $state, Config, ImageService, Messages, Pagination, ModalService) { $scope.state = {}; $scope.state.pagination_count = Pagination.getPaginationCount('images'); $scope.sortType = 'RepoTags'; @@ -42,20 +42,15 @@ function ($scope, $state, Config, Image, ImageHelper, Messages, Pagination, Moda $('#pullImageSpinner').show(); var image = $scope.config.Image; var registry = $scope.config.Registry; - var imageConfig = ImageHelper.createImageConfigForContainer(image, registry); - Image.create(imageConfig, function (data) { - var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error'); - if (err) { - var detail = data[data.length - 1]; - $('#pullImageSpinner').hide(); - Messages.error('Error', {}, detail.error); - } else { - $('#pullImageSpinner').hide(); - $state.reload(); - } - }, function (e) { + ImageService.pullImage(image, registry) + .then(function success(data) { + $state.reload(); + }) + .catch(function error(err) { + Messages.error("Failure", err, "Unable to pull image"); + }) + .finally(function final() { $('#pullImageSpinner').hide(); - Messages.error("Failure", e, "Unable to pull image"); }); }; @@ -79,18 +74,16 @@ function ($scope, $state, Config, Image, ImageHelper, Messages, Pagination, Moda angular.forEach($scope.images, function (i) { if (i.Checked) { counter = counter + 1; - Image.remove({id: i.Id, force: force}, function (d) { - if (d[0].message) { - $('#loadImagesSpinner').hide(); - Messages.error("Unable to remove image", {}, d[0].message); - } else { - Messages.send("Image deleted", i.Id); - var index = $scope.images.indexOf(i); - $scope.images.splice(index, 1); - } - complete(); - }, function (e) { - Messages.error("Failure", e, 'Unable to remove image'); + ImageService.deleteImage(i.Id, force) + .then(function success(data) { + Messages.send("Image deleted", i.Id); + var index = $scope.images.indexOf(i); + $scope.images.splice(index, 1); + }) + .catch(function error(err) { + Messages.error("Failure", err, 'Unable to remove image'); + }) + .finally(function final() { complete(); }); } @@ -98,19 +91,19 @@ function ($scope, $state, Config, Image, ImageHelper, Messages, Pagination, Moda }; function fetchImages() { - Image.query({}, function (d) { - $scope.images = d.map(function (item) { - return new ImageViewModel(item); - }); - $('#loadImagesSpinner').hide(); - }, function (e) { - $('#loadImagesSpinner').hide(); - Messages.error("Failure", e, "Unable to retrieve images"); + $('#loadImagesSpinner').show(); + ImageService.images() + .then(function success(data) { + $scope.images = data; + }) + .catch(function error(err) { + Messages.error("Failure", err, "Unable to retrieve images"); $scope.images = []; + }) + .finally(function final() { + $('#loadImagesSpinner').hide(); }); } - Config.$promise.then(function (c) { - fetchImages(); - }); + fetchImages(); }]); diff --git a/app/components/templates/templatesController.js b/app/components/templates/templatesController.js index 8d501d30c..1db8927af 100644 --- a/app/components/templates/templatesController.js +++ b/app/components/templates/templatesController.js @@ -45,14 +45,14 @@ function ($scope, $q, $state, $anchorScroll, Config, ContainerService, Container volumeResourceControlQueries.push(ResourceControlService.setVolumeResourceControl(Authentication.getUserDetails().ID, volume.Name)); }); } - TemplateService.updateContainerConfigurationWithVolumes(templateConfiguration.container, template, data); + TemplateService.updateContainerConfigurationWithVolumes(templateConfiguration, template, data); return $q.all(volumeResourceControlQueries) .then(function success() { - return ImageService.pullImage(templateConfiguration.image); + return ImageService.pullImage(template.Image, template.Registry); }); }) .then(function success(data) { - return ContainerService.createAndStartContainer(templateConfiguration.container); + return ContainerService.createAndStartContainer(templateConfiguration); }) .then(function success(data) { Messages.send('Container Started', data.Id); diff --git a/app/models/imageDetails.js b/app/models/imageDetails.js new file mode 100644 index 000000000..df7da6183 --- /dev/null +++ b/app/models/imageDetails.js @@ -0,0 +1,19 @@ +function ImageDetailsViewModel(data) { + this.Id = data.Id; + this.Tag = data.Tag; + this.Parent = data.Parent; + this.Repository = data.Repository; + this.Created = data.Created; + this.Checked = false; + this.RepoTags = data.RepoTags; + this.VirtualSize = data.VirtualSize; + this.DockerVersion = data.DockerVersion; + this.Os = data.Os; + this.Architecture = data.Architecture; + this.Author = data.Author; + this.Command = data.ContainerConfig.Cmd; + this.Entrypoint = data.ContainerConfig.Entrypoint ? data.ContainerConfig.Entrypoint : ''; + this.ExposedPorts = data.ContainerConfig.ExposedPorts ? Object.keys(data.ContainerConfig.ExposedPorts) : []; + this.Volumes = data.ContainerConfig.Volumes ? Object.keys(data.ContainerConfig.Volumes) : []; + this.Env = data.ContainerConfig.Env ? data.ContainerConfig.Env : []; +} diff --git a/app/services/imageService.js b/app/services/imageService.js index bba649998..a8a29cac5 100644 --- a/app/services/imageService.js +++ b/app/services/imageService.js @@ -1,10 +1,43 @@ angular.module('portainer.services') -.factory('ImageService', ['$q', 'Image', function ImageServiceFactory($q, Image) { +.factory('ImageService', ['$q', 'Image', 'ImageHelper', function ImageServiceFactory($q, Image, ImageHelper) { 'use strict'; var service = {}; - service.pullImage = function(imageConfiguration) { + service.image = function(imageId) { var deferred = $q.defer(); + Image.get({id: imageId}).$promise + .then(function success(data) { + if (data.message) { + deferred.reject({ msg: data.message }); + } else { + var image = new ImageDetailsViewModel(data); + deferred.resolve(image); + } + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve image details', err: err }); + }); + return deferred.promise; + }; + + service.images = function() { + var deferred = $q.defer(); + Image.query({}).$promise + .then(function success(data) { + var images = data.map(function (item) { + return new ImageViewModel(item); + }); + deferred.resolve(images); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve images', err: err }); + }); + return deferred.promise; + }; + + service.pullImage = function(image, registry) { + var deferred = $q.defer(); + var imageConfiguration = ImageHelper.createImageConfigForContainer(image, registry); Image.create(imageConfiguration).$promise .then(function success(data) { var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error'); @@ -20,5 +53,43 @@ angular.module('portainer.services') }); return deferred.promise; }; + + service.tagImage = function(id, image, registry) { + var imageConfig = ImageHelper.createImageConfigForCommit(image, registry); + return Image.tag({id: id, tag: imageConfig.tag, repo: imageConfig.repo}).$promise; + }; + + service.deleteImage = function(id, forceRemoval) { + var deferred = $q.defer(); + Image.remove({id: id, force: forceRemoval}).$promise + .then(function success(data) { + if (data[0].message) { + deferred.reject({ msg: data[0].message }); + } else { + deferred.resolve(); + } + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to remove image', err: err }); + }); + return deferred.promise; + }; + + service.pushImage = function(tag) { + var deferred = $q.defer(); + Image.push({tag: tag}).$promise + .then(function success(data) { + if (data[data.length - 1].error) { + deferred.reject({ msg: data[data.length - 1].error }); + } else { + deferred.resolve(); + } + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to push image tag', err: err }); + }); + return deferred.promise; + }; + return service; }]); diff --git a/app/services/messages.js b/app/services/messages.js index 3caf63ba6..7ed10b87d 100644 --- a/app/services/messages.js +++ b/app/services/messages.js @@ -22,6 +22,8 @@ angular.module('portainer.services') msg = e.message; } else if (e.data && e.data.length > 0 && e.data[0].message) { msg = e.data[0].message; + } else if (e.msg) { + msg = e.msg; } $.gritter.add({ title: $sanitize(title), diff --git a/app/services/templateService.js b/app/services/templateService.js index 79958eb1c..fb1c86414 100644 --- a/app/services/templateService.js +++ b/app/services/templateService.js @@ -21,17 +21,10 @@ angular.module('portainer.services') }; service.createTemplateConfiguration = function(template, containerName, network, containerMapping) { - var imageConfiguration = service.createImageConfiguration(template); + var imageConfiguration = ImageHelper.createImageConfigForContainer(template.Image, template.Registry); var containerConfiguration = service.createContainerConfiguration(template, containerName, network, containerMapping); containerConfiguration.Image = imageConfiguration.fromImage + ':' + imageConfiguration.tag; - return { - container: containerConfiguration, - image: imageConfiguration - }; - }; - - service.createImageConfiguration = function(template) { - return ImageHelper.createImageConfigForContainer(template.Image, template.Registry); + return containerConfiguration; }; service.createContainerConfiguration = function(template, containerName, network, containerMapping) { From ab91ffe12ce1b9bac235eb6a9116037af71e7daa Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Mon, 20 Mar 2017 17:39:53 +0100 Subject: [PATCH 12/23] style(containers): use the same action sequence for container-details and containers (#707) --- app/components/container/container.html | 12 ++++++------ app/components/containers/containers.html | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/components/container/container.html b/app/components/container/container.html index f618d7403..e02a7f7e9 100644 --- a/app/components/container/container.html +++ b/app/components/container/container.html @@ -13,12 +13,12 @@
- - - - - - + + + + + +
diff --git a/app/components/containers/containers.html b/app/components/containers/containers.html index 39a754168..061218bf7 100644 --- a/app/components/containers/containers.html +++ b/app/components/containers/containers.html @@ -25,9 +25,9 @@
- - - + + + From a8f70d7f5912461cd71a5e51419234a33a6062f2 Mon Sep 17 00:00:00 2001 From: Glowbal Date: Mon, 20 Mar 2017 21:28:09 +0100 Subject: [PATCH 13/23] feat(service-details): add ability to edit service details (#453) --- .../service/includes/constraints.html | 66 ++++ .../service/includes/container-specs.html | 56 +++ .../service/includes/containerlabels.html | 59 ++++ .../includes/environmentvariables.html | 59 ++++ app/components/service/includes/mounts.html | 67 ++++ app/components/service/includes/networks.html | 26 ++ app/components/service/includes/ports.html | 71 ++++ .../service/includes/resources.html | 36 ++ app/components/service/includes/restart.html | 76 ++++ .../service/includes/servicelabels.html | 63 ++++ app/components/service/includes/tasks.html | 65 ++++ .../service/includes/updateconfig.html | 68 ++++ app/components/service/service.html | 327 +++++------------- app/components/service/serviceController.js | 252 +++++++++++--- app/components/services/services.html | 10 + app/models/service.js | 48 ++- assets/css/app.css | 4 + 17 files changed, 1057 insertions(+), 296 deletions(-) create mode 100644 app/components/service/includes/constraints.html create mode 100644 app/components/service/includes/container-specs.html create mode 100644 app/components/service/includes/containerlabels.html create mode 100644 app/components/service/includes/environmentvariables.html create mode 100644 app/components/service/includes/mounts.html create mode 100644 app/components/service/includes/networks.html create mode 100644 app/components/service/includes/ports.html create mode 100644 app/components/service/includes/resources.html create mode 100644 app/components/service/includes/restart.html create mode 100644 app/components/service/includes/servicelabels.html create mode 100644 app/components/service/includes/tasks.html create mode 100644 app/components/service/includes/updateconfig.html diff --git a/app/components/service/includes/constraints.html b/app/components/service/includes/constraints.html new file mode 100644 index 000000000..58be6fd88 --- /dev/null +++ b/app/components/service/includes/constraints.html @@ -0,0 +1,66 @@ +
+ + + + + +

There are no placement constraints for this service.

+
+ +
{{ var|key: '=' }} {{ var|value: '=' }}
+ + + + + + + + + + + + + + +
NameOperatorValue
+
+ +
+
+
+ +
+
+
+ + + + +
+
+ + + + + +
diff --git a/app/components/service/includes/container-specs.html b/app/components/service/includes/container-specs.html new file mode 100644 index 000000000..d8b9ad97f --- /dev/null +++ b/app/components/service/includes/container-specs.html @@ -0,0 +1,56 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CMD{{ service.Command|command }} +

+ Command to execute. +

+
Args{{ service.Arguments }} +

+ Arguments passed to command in container. +

+
User{{ service.User }} +

+ Username or UID. +

+
Working directory{{ service.Dir }} +

+ Working directory inside the container. +

+
Stop grace period{{ service.StopGracePeriod }} +

+ Time to wait before force killing a container (default none). +

+
+
+
+
diff --git a/app/components/service/includes/containerlabels.html b/app/components/service/includes/containerlabels.html new file mode 100644 index 000000000..40011fe2a --- /dev/null +++ b/app/components/service/includes/containerlabels.html @@ -0,0 +1,59 @@ +
+ + + + + +

There are no container labels for this service.

+
+ + + + + + + + + + + + + + +
LabelValue
+
+ name + +
+
+
+ value + + + + +
+
+
+ + + +
+
diff --git a/app/components/service/includes/environmentvariables.html b/app/components/service/includes/environmentvariables.html new file mode 100644 index 000000000..f915245b7 --- /dev/null +++ b/app/components/service/includes/environmentvariables.html @@ -0,0 +1,59 @@ +
+ + + + + +

There are no environment variables for this service.

+
+ + + + + + + + + + + + + + +
NameValue
+
+ name + +
+
+
+ value + + + + +
+
+
+ + + +
+
diff --git a/app/components/service/includes/mounts.html b/app/components/service/includes/mounts.html new file mode 100644 index 000000000..d273072c2 --- /dev/null +++ b/app/components/service/includes/mounts.html @@ -0,0 +1,67 @@ +
+ + + + + +

There are no mounts for this service.

+
+ + + + + + + + + + + + + + + + + + + + +
TypeSourceTargetRead onlyActions
+ + + + + + + + + + + +
+
+ + + +
+
diff --git a/app/components/service/includes/networks.html b/app/components/service/includes/networks.html new file mode 100644 index 000000000..f3c551ac4 --- /dev/null +++ b/app/components/service/includes/networks.html @@ -0,0 +1,26 @@ +
+ + + +

This service is not connected to any networks.

+
+ + + + + + + + + + + + + + +
IDIP address
+ {{ network.NetworkID }} + {{ network.Addr }}
+
+
+
diff --git a/app/components/service/includes/ports.html b/app/components/service/includes/ports.html new file mode 100644 index 000000000..2938489bf --- /dev/null +++ b/app/components/service/includes/ports.html @@ -0,0 +1,71 @@ +
+ + + + + +

This service has no ports published.

+
+ + + + + + + + + + + + + + + + + + +
Host portContainer portProtocolActions
+
+ host + +
+
+
+ container + +
+
+
+ +
+
+ + + +
+
+ + +
+ + +
diff --git a/app/components/service/includes/resources.html b/app/components/service/includes/resources.html new file mode 100644 index 000000000..f96b8dac4 --- /dev/null +++ b/app/components/service/includes/resources.html @@ -0,0 +1,36 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
CPU limits + {{ service.LimitNanoCPUs / 1000000000 }} + None
Memory limits{{service.LimitMemoryBytes|humansize}}None
CPU reservation + {{service.ReservationNanoCPUs / 1000000000}} + None
Memory reservation{{service.ReservationMemoryBytes|humansize}}None
+
+
+
diff --git a/app/components/service/includes/restart.html b/app/components/service/includes/restart.html new file mode 100644 index 000000000..22fee2774 --- /dev/null +++ b/app/components/service/includes/restart.html @@ -0,0 +1,76 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Restart condition +
+ +
+
+

+ Condition for restart. +

+
Restart delay + + +

+ Delay between restart attempts. Time in seconds. +

+
Restart max attempts + + +

+ Maximum attempts to restart a given container before giving up (default value is 0, which is ignored). +

+
Restart window + + +

+ The time window used to evaluate the restart policy (default value is 0, which is unbounded). +

+
+
+ + + +
+
diff --git a/app/components/service/includes/servicelabels.html b/app/components/service/includes/servicelabels.html new file mode 100644 index 000000000..cb0ff9992 --- /dev/null +++ b/app/components/service/includes/servicelabels.html @@ -0,0 +1,63 @@ +
+ + + + + +

There are no labels for this service.

+
+ + + + + + + + + + + + + + +
+ Label + + Value +
+
+ name + +
+
+
+ value + + + + +
+
+
+ + + +
+
diff --git a/app/components/service/includes/tasks.html b/app/components/service/includes/tasks.html new file mode 100644 index 000000000..48dc1b6f9 --- /dev/null +++ b/app/components/service/includes/tasks.html @@ -0,0 +1,65 @@ +
+ + +
+ Items per page: + +
+
+ + + + + + + + + + + + + + + + + + + + +
Id + + Status + + + + + + Slot + + + + + + Node + + + + + + Last update + + + +
{{ task.Id }}{{ task.Status }}{{ task.Slot }}{{ task.Node }}{{ task.Updated|getisodate }}
+
+ +
+
+
+
diff --git a/app/components/service/includes/updateconfig.html b/app/components/service/includes/updateconfig.html new file mode 100644 index 000000000..2fe2d89d8 --- /dev/null +++ b/app/components/service/includes/updateconfig.html @@ -0,0 +1,68 @@ +
+ + + + + + + + + + + + + + + + + + + + + + +
Update Parallelism + + +

+ Maximum number of tasks to be updated simultaneously (0 to update all at once). +

+
Update Delay + + +

+ Amount of time between updates. +

+
Update Failure Action +
+ + +
+
+

+ Action taken on failure to start after update. +

+
+
+ + + +
+
diff --git a/app/components/service/service.html b/app/components/service/service.html index ff6b66d2c..a00ff7ced 100644 --- a/app/components/service/service.html +++ b/app/components/service/service.html @@ -11,7 +11,16 @@
-
+
+ +
+
+ +
+
@@ -19,23 +28,32 @@ Name - - {{ service.Name }} - + + - - - - + + {{ service.Name }} ID {{ service.Id }} - + + + Created at + {{ service.CreatedAt|getisodate}} + + + Last updated at + {{ service.UpdatedAt|getisodate }} + + + Version + {{ service.Version }} + Scheduling mode {{ service.Mode }} @@ -43,251 +61,90 @@ Replicas - - {{ service.Replicas }} - Scale - - - - - + + Image - - {{ service.Image }} - - - - - - - - - - - Published ports -
- {{ mapping.TargetPort }} {{ mapping.PublishedPort }} -
- - - - Environment variables - -
-
- - environment variable - -
- -
-
-
- name - -
-
- value - - - - -
-
-
- -
- - - - Labels - -
-
- - label - -
- -
-
-
- name - -
-
- value - - - - -
-
-
- -
- - - - Container labels - -
-
- - container label - -
- -
-
-
- name - -
-
- value - - - - -
-
-
- -
- - - - Update Parallelism - - - {{ service.UpdateParallelism }} - Change - - - - - - - - - - Update Delay - - - {{ service.UpdateDelay }} - Change - - - - - - - - - - Update Failure Action - -
- - -
+
- -
- - + +

+ Do you need help? View the Docker Service documentation here. +

+
-
-
-
+
- -
- Items per page: - -
-
+ - - - - - - - - - - - - - - - - - - - -
Id - - Status - - - - - - Slot - - - - - - Node - - - - - - Last update - - - -
{{ task.Id }}{{ task.Status }}{{ task.Slot }}{{ task.Node }}{{ task.Updated|getisodate }}
-
- -
+
+ +
+
+
+

Container specification

+
+
+
+
+
+
+ +
+
+
+

Networks & ports

+
+
+
+
+ +
+
+
+

Service specification

+
+
+
+
+
+
+
+
diff --git a/app/components/service/serviceController.js b/app/components/service/serviceController.js index 35b086847..438038087 100644 --- a/app/components/service/serviceController.js +++ b/app/components/service/serviceController.js @@ -1,6 +1,6 @@ angular.module('service', []) -.controller('ServiceController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Task', 'Node', 'Messages', 'Pagination', -function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Messages, Pagination) { +.controller('ServiceController', ['$scope', '$stateParams', '$state', '$location', '$anchorScroll', 'Service', 'ServiceHelper', 'Task', 'Node', 'Messages', 'Pagination', +function ($scope, $stateParams, $state, $location, $anchorScroll, Service, ServiceHelper, Task, Node, Messages, Pagination) { $scope.state = {}; $scope.state.pagination_count = Pagination.getPaginationCount('service_tasks'); @@ -10,7 +10,10 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess $scope.sortType = 'Status'; $scope.sortReverse = false; - var previousServiceValues = {}; + $scope.lastVersion = 0; + + var originalService = {}; + var previousServiceValues = []; $scope.order = function (sortType) { $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; @@ -35,85 +38,175 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess service.EditReplicas = false; }; + $scope.goToItem = function(hash) { + $anchorScroll(hash); + }; + $scope.addEnvironmentVariable = function addEnvironmentVariable(service) { service.EnvironmentVariables.push({ key: '', value: '', originalValue: '' }); - service.hasChanges = true; + updateServiceArray(service, 'EnvironmentVariables', service.EnvironmentVariables); }; $scope.removeEnvironmentVariable = function removeEnvironmentVariable(service, index) { var removedElement = service.EnvironmentVariables.splice(index, 1); - service.hasChanges = service.hasChanges || removedElement !== null; + if (removedElement !== null) { + updateServiceArray(service, 'EnvironmentVariables', service.EnvironmentVariables); + } }; $scope.updateEnvironmentVariable = function updateEnvironmentVariable(service, variable) { - service.hasChanges = service.hasChanges || variable.value !== variable.originalValue; + if (variable.value !== variable.originalValue || variable.key !== variable.originalKey) { + updateServiceArray(service, 'EnvironmentVariables', service.EnvironmentVariables); + } }; $scope.addLabel = function addLabel(service) { - service.hasChanges = true; service.ServiceLabels.push({ key: '', value: '', originalValue: '' }); + updateServiceArray(service, 'ServiceLabels', service.ServiceLabels); }; $scope.removeLabel = function removeLabel(service, index) { var removedElement = service.ServiceLabels.splice(index, 1); - service.hasChanges = service.hasChanges || removedElement !== null; + if (removedElement !== null) { + updateServiceArray(service, 'ServiceLabels', service.ServiceLabels); + } }; $scope.updateLabel = function updateLabel(service, label) { - service.hasChanges = service.hasChanges || label.value !== label.originalValue; + if (label.value !== label.originalValue || label.key !== label.originalKey) { + updateServiceArray(service, 'ServiceLabels', service.ServiceLabels); + } }; $scope.addContainerLabel = function addContainerLabel(service) { - service.hasChanges = true; service.ServiceContainerLabels.push({ key: '', value: '', originalValue: '' }); + updateServiceArray(service, 'ServiceContainerLabels', service.ServiceContainerLabels); }; - $scope.removeContainerLabel = function removeContainerLabel(service, index) { + $scope.removeContainerLabel = function removeLabel(service, index) { var removedElement = service.ServiceContainerLabels.splice(index, 1); - service.hasChanges = service.hasChanges || removedElement !== null; + if (removedElement !== null) { + updateServiceArray(service, 'ServiceContainerLabels', service.ServiceContainerLabels); + } + }; + $scope.updateContainerLabel = function updateLabel(service, label) { + if (label.value !== label.originalValue || label.key !== label.originalKey) { + updateServiceArray(service, 'ServiceContainerLabels', service.ServiceContainerLabels); + } + }; + $scope.addMount = function addMount(service) { + service.ServiceMounts.push({Type: 'volume', Source: '', Target: '', ReadOnly: false }); + updateServiceArray(service, 'ServiceMounts', service.ServiceMounts); + }; + $scope.removeMount = function removeMount(service, index) { + var removedElement = service.ServiceMounts.splice(index, 1); + if (removedElement !== null) { + updateServiceArray(service, 'ServiceMounts', service.ServiceMounts); + } + }; + $scope.updateMount = function updateMount(service, mount) { + updateServiceArray(service, 'ServiceMounts', service.ServiceMounts); + }; + $scope.addPlacementConstraint = function addPlacementConstraint(service) { + service.ServiceConstraints.push({ key: '', operator: '==', value: '' }); + updateServiceArray(service, 'ServiceConstraints', service.ServiceConstraints); + }; + $scope.removePlacementConstraint = function removePlacementConstraint(service, index) { + var removedElement = service.ServiceConstraints.splice(index, 1); + if (removedElement !== null) { + updateServiceArray(service, 'ServiceConstraints', service.ServiceConstraints); + } + }; + $scope.updatePlacementConstraint = function updatePlacementConstraint(service, constraint) { + updateServiceArray(service, 'ServiceConstraints', service.ServiceConstraints); }; - $scope.changeParallelism = function changeParallelism(service) { - updateServiceAttribute(service, 'UpdateParallelism', service.newServiceUpdateParallelism); - service.EditParallelism = false; + $scope.addPublishedPort = function addPublishedPort(service) { + if (!service.Ports) { + service.Ports = []; + } + service.Ports.push({ PublishedPort: '', TargetPort: '', Protocol: 'tcp' }); }; - $scope.changeUpdateDelay = function changeUpdateDelay(service) { - updateServiceAttribute(service, 'UpdateDelay', service.newServiceUpdateDelay); - service.EditDelay = false; + $scope.updatePublishedPort = function updatePublishedPort(service, portMapping) { + updateServiceArray(service, 'Ports', service.Ports); }; - $scope.changeUpdateFailureAction = function changeUpdateFailureAction(service) { - updateServiceAttribute(service, 'UpdateFailureAction', service.newServiceUpdateFailureAction); + $scope.removePortPublishedBinding = function removePortPublishedBinding(service, index) { + var removedElement = service.Ports.splice(index, 1); + if (removedElement !== null) { + updateServiceArray(service, 'Ports', service.Ports); + } }; - $scope.cancelChanges = function changeServiceImage(service) { - Object.keys(previousServiceValues).forEach(function(attribute) { - service[attribute] = previousServiceValues[attribute]; // reset service values - service['newService' + attribute] = previousServiceValues[attribute]; // reset edit fields + $scope.cancelChanges = function cancelChanges(service, keys) { + if (keys) { // clean out the keys only from the list of modified keys + keys.forEach(function(key) { + var index = previousServiceValues.indexOf(key); + if (index >= 0) { + previousServiceValues.splice(index, 1); + } + }); + } else { // clean out all changes + keys = Object.keys(service); + previousServiceValues = []; + } + keys.forEach(function(attribute) { + service[attribute] = originalService[attribute]; // reset service values }); - previousServiceValues = {}; // clear out all changes - // clear out environment variable changes - service.EnvironmentVariables = translateEnvironmentVariables(service.Env); - service.ServiceLabels = translateLabelsToServiceLabels(service.Labels); - service.ServiceContainerLabels = translateLabelsToServiceLabels(service.ContainerLabels); - service.hasChanges = false; }; + $scope.hasChanges = function(service, elements) { + var hasChanges = false; + elements.forEach(function(key) { + hasChanges = hasChanges || (previousServiceValues.indexOf(key) >= 0); + }); + return hasChanges; + }; + $scope.updateService = function updateService(service) { $('#loadServicesSpinner').show(); var config = ServiceHelper.serviceToConfig(service.Model); - config.Name = service.newServiceName; + config.Name = service.Name; config.Labels = translateServiceLabelsToLabels(service.ServiceLabels); config.TaskTemplate.ContainerSpec.Env = translateEnvironmentVariablesToEnv(service.EnvironmentVariables); config.TaskTemplate.ContainerSpec.Labels = translateServiceLabelsToLabels(service.ServiceContainerLabels); - config.TaskTemplate.ContainerSpec.Image = service.newServiceImage; + config.TaskTemplate.ContainerSpec.Image = service.Image; + config.TaskTemplate.ContainerSpec.Secrets = service.ServiceSecrets; + if (service.Mode === 'replicated') { config.Mode.Replicated.Replicas = service.Replicas; } + config.TaskTemplate.ContainerSpec.Mounts = service.ServiceMounts; + if (typeof config.TaskTemplate.Placement === 'undefined') { + config.TaskTemplate.Placement = {}; + } + config.TaskTemplate.Placement.Constraints = translateKeyValueToConstraints(service.ServiceConstraints); + + config.TaskTemplate.Resources = { + Limits: { + NanoCPUs: service.LimitNanoCPUs, + MemoryBytes: service.LimitMemoryBytes + }, + Reservations: { + NanoCPUs: service.ReservationNanoCPUs, + MemoryBytes: service.ReservationMemoryBytes + } + }; config.UpdateConfig = { - Parallelism: service.newServiceUpdateParallelism, - Delay: service.newServiceUpdateDelay, - FailureAction: service.newServiceUpdateFailureAction + Parallelism: service.UpdateParallelism, + Delay: service.UpdateDelay, + FailureAction: service.UpdateFailureAction + }; + config.TaskTemplate.RestartPolicy = { + Condition: service.RestartCondition, + Delay: service.RestartDelay, + MaxAttempts: service.RestartMaxAttempts, + Window: service.RestartWindow + }; + config.EndpointSpec = { + Mode: config.EndpointSpec.Mode || 'vip', + Ports: service.Ports }; Service.update({ id: service.Id, version: service.Version }, config, function (data) { $('#loadServicesSpinner').hide(); Messages.send("Service successfully updated", "Service updated"); - $state.go('service', {id: service.Id}, {reload: true}); + $scope.cancelChanges({}); + fetchServiceDetails(); }, function (e) { $('#loadServicesSpinner').hide(); Messages.error("Failure", e, "Unable to update service"); @@ -138,22 +231,28 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess }); }; + function translateServiceArrays(service) { + service.ServiceSecrets = service.Secrets; + service.EnvironmentVariables = translateEnvironmentVariables(service.Env); + service.ServiceLabels = translateLabelsToServiceLabels(service.Labels); + service.ServiceContainerLabels = translateLabelsToServiceLabels(service.ContainerLabels); + service.ServiceMounts = angular.copy(service.Mounts); + service.ServiceConstraints = translateConstraintsToKeyValue(service.Constraints); + } + function fetchServiceDetails() { $('#loadingViewSpinner').show(); Service.get({id: $stateParams.id}, function (d) { var service = new ServiceViewModel(d); - service.newServiceName = service.Name; - service.newServiceImage = service.Image; - service.newServiceReplicas = service.Replicas; - service.newServiceUpdateParallelism = service.UpdateParallelism; - service.newServiceUpdateDelay = service.UpdateDelay; - service.newServiceUpdateFailureAction = service.UpdateFailureAction; - - service.EnvironmentVariables = translateEnvironmentVariables(service.Env); - service.ServiceLabels = translateLabelsToServiceLabels(service.Labels); - service.ServiceContainerLabels = translateLabelsToServiceLabels(service.ContainerLabels); + $scope.isUpdating = $scope.lastVersion >= service.Version; + if (!$scope.isUpdating) { + $scope.lastVersion = service.Version; + } + translateServiceArrays(service); $scope.service = service; + originalService = angular.copy(service); + Task.query({filters: {service: [service.Name]}}, function (tasks) { Node.query({}, function (nodes) { $scope.displayNode = true; @@ -178,13 +277,15 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess }); } - function updateServiceAttribute(service, name, newValue) { - // ensure we only capture the original previous value, in case we update the attribute multiple times - if (!previousServiceValues[name]) { - previousServiceValues[name] = service[name]; + $scope.updateServiceAttribute = function updateServiceAttribute(service, name) { + if (service[name] !== originalService[name] || !(name in originalService)) { + service.hasChanges = true; } - // update the attribute - service[name] = newValue; + previousServiceValues.push(name); + }; + + function updateServiceArray(service, name) { + previousServiceValues.push(name); service.hasChanges = true; } @@ -195,7 +296,7 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess var idx = variable.indexOf('='); var keyValue = [variable.slice(0,idx), variable.slice(idx+1)]; var originalValue = (keyValue.length > 1) ? keyValue[1] : ''; - variables.push({ key: keyValue[0], value: originalValue, originalValue: originalValue, added: true}); + variables.push({ key: keyValue[0], value: originalValue, originalKey: keyValue[0], originalValue: originalValue, added: true}); }); return variables; } @@ -218,7 +319,7 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess var labels = []; if (Labels) { Object.keys(Labels).forEach(function(key) { - labels.push({ key: key, value: Labels[key], originalValue: Labels[key], added: true}); + labels.push({ key: key, value: Labels[key], originalKey: key, originalValue: Labels[key], added: true}); }); } return labels; @@ -233,5 +334,48 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess return Labels; } + function translateConstraintsToKeyValue(constraints) { + function getOperator(constraint) { + var indexEquals = constraint.indexOf('=='); + if (indexEquals >= 0) { + return [indexEquals, '==']; + } + return [constraint.indexOf('!='), '!=']; + } + if (constraints) { + var keyValueConstraints = []; + constraints.forEach(function(constraint) { + var operatorIndices = getOperator(constraint); + + var key = constraint.slice(0, operatorIndices[0]); + var operator = operatorIndices[1]; + var value = constraint.slice(operatorIndices[0] + 2); + + keyValueConstraints.push({ + key: key, + value: value, + operator: operator, + originalKey: key, + originalValue: value + }); + }); + return keyValueConstraints; + } + return []; + } + + function translateKeyValueToConstraints(keyValueConstraints) { + if (keyValueConstraints) { + var constraints = []; + keyValueConstraints.forEach(function(keyValueConstraint) { + if (keyValueConstraint.key && keyValueConstraint.key !== '' && keyValueConstraint.value && keyValueConstraint.value !== '') { + constraints.push(keyValueConstraint.key + keyValueConstraint.operator + keyValueConstraint.value); + } + }); + return constraints; + } + return []; + } + fetchServiceDetails(); }]); diff --git a/app/components/services/services.html b/app/components/services/services.html index 02925a678..f68133fe0 100644 --- a/app/components/services/services.html +++ b/app/components/services/services.html @@ -58,6 +58,13 @@ + + + Updated at + + + + Ownership @@ -85,6 +92,9 @@ + + {{ service.UpdatedAt|getisodate }} + diff --git a/app/models/service.js b/app/models/service.js index 156faefef..c60c3faab 100644 --- a/app/models/service.js +++ b/app/models/service.js @@ -2,6 +2,8 @@ function ServiceViewModel(data, runningTasks, nodes) { this.Model = data; this.Id = data.ID; this.Name = data.Spec.Name; + this.CreatedAt = data.CreatedAt; + this.UpdatedAt = data.UpdatedAt; this.Image = data.Spec.TaskTemplate.ContainerSpec.Image; this.Version = data.Version.Index; if (data.Spec.Mode.Replicated) { @@ -16,20 +18,52 @@ function ServiceViewModel(data, runningTasks, nodes) { if (runningTasks) { this.Running = runningTasks.length; } + if (data.Spec.TaskTemplate.Resources) { + if (data.Spec.TaskTemplate.Resources.Limits) { + this.LimitNanoCPUs = data.Spec.TaskTemplate.Resources.Limits.NanoCPUs; + this.LimitMemoryBytes = data.Spec.TaskTemplate.Resources.Limits.MemoryBytes; + } + if (data.Spec.TaskTemplate.Resources.Reservations) { + this.ReservationNanoCPUs = data.Spec.TaskTemplate.Resources.Reservations.NanoCPUs; + this.ReservationMemoryBytes = data.Spec.TaskTemplate.Resources.Reservations.MemoryBytes; + } + } + + if (data.Spec.TaskTemplate.RestartPolicy) { + this.RestartCondition = data.Spec.TaskTemplate.RestartPolicy.Condition; + this.RestartDelay = data.Spec.TaskTemplate.RestartPolicy.Delay; + this.RestartMaxAttempts = data.Spec.TaskTemplate.RestartPolicy.MaxAttempts; + this.RestartWindow = data.Spec.TaskTemplate.RestartPolicy.Window; + } else { + this.RestartCondition = 'none'; + this.RestartDelay = 0; + this.RestartMaxAttempts = 0; + this.RestartWindow = 0; + } + this.Constraints = data.Spec.TaskTemplate.Placement ? data.Spec.TaskTemplate.Placement.Constraints || [] : []; this.Labels = data.Spec.Labels; - if (data.Spec.TaskTemplate.ContainerSpec) { - this.ContainerLabels = data.Spec.TaskTemplate.ContainerSpec.Labels; + + var containerSpec = data.Spec.TaskTemplate.ContainerSpec; + if (containerSpec) { + this.ContainerLabels = containerSpec.Labels; + this.Env = containerSpec.Env; + this.Mounts = containerSpec.Mounts || []; + this.User = containerSpec.User; + this.Dir = containerSpec.Dir; + this.Command = containerSpec.Command; + this.Secrets = containerSpec.Secrets; } - if (data.Spec.TaskTemplate.ContainerSpec.Env) { - this.Env = data.Spec.TaskTemplate.ContainerSpec.Env; + if (data.Spec.EndpointSpec) { + this.Ports = data.Spec.EndpointSpec.Ports; } + this.Mounts = []; if (data.Spec.TaskTemplate.ContainerSpec.Mounts) { this.Mounts = data.Spec.TaskTemplate.ContainerSpec.Mounts; } - if (data.Endpoint.Ports) { - this.Ports = data.Endpoint.Ports; - } + + this.VirtualIPs = data.Endpoint ? data.Endpoint.VirtualIPs : []; + if (data.Spec.UpdateConfig) { this.UpdateParallelism = (typeof data.Spec.UpdateConfig.Parallelism !== undefined) ? data.Spec.UpdateConfig.Parallelism || 0 : 1; this.UpdateDelay = data.Spec.UpdateConfig.Delay || 0; diff --git a/assets/css/app.css b/assets/css/app.css index 9ea1b028c..4371ab229 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -125,6 +125,10 @@ a[ng-click]{ padding: 0 !important; } +.padding-top { + padding-top: 15px !important; +} + .terminal-container { width: 100%; padding: 10px 5px; From 8e794be13fe1aaa5f1e99ea6e76d5741c0119b09 Mon Sep 17 00:00:00 2001 From: dantheman0207 Date: Wed, 22 Mar 2017 08:13:59 +0100 Subject: [PATCH 14/23] feat(containers): truncate long names & ids in the containers view (#699) --- app/components/containers/containers.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/containers/containers.html b/app/components/containers/containers.html index 061218bf7..bf2202334 100644 --- a/app/components/containers/containers.html +++ b/app/components/containers/containers.html @@ -103,8 +103,8 @@ {{ container.Status }} - {{ container|swarmcontainername}} - {{ container|containername}} + {{ container|swarmcontainername|truncate: 40}} + {{ container|containername|truncate: 40}} {{ container.Image | hideshasum }} {{ container.IP ? container.IP : '-' }} {{ container.hostIP }} From 967286f45d363cf4db728fe51f40e7d9ac9cd0a5 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Fri, 24 Mar 2017 12:22:58 +0100 Subject: [PATCH 15/23] docs(contributing): update contribution guidelines --- CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0113ed680..c3aef1e1f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,11 +22,13 @@ Some of the open issues are labeled with prefix `exp/`, this is used to mark the * **beginner**: a task that should be accessible with users not familiar with the codebase * **intermediate**: a task that require some understanding of the project codebase or some experience in either AngularJS or Golang +* **advanced**: a task that require a deep understanding of the project codebase You can have a use Github filters to list these issues: * beginner labeled issues: https://github.com/portainer/portainer/labels/exp%2Fbeginner * intermediate labeled issues: https://github.com/portainer/portainer/labels/exp%2Fintermediate +* advanced labeled issues: https://github.com/portainer/portainer/labels/exp%2Fadvanced ### Linting From c243a02e7a3a092e4c1ea79d128a0de48821a9ad Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Mon, 27 Mar 2017 14:44:39 +0200 Subject: [PATCH 16/23] feat(UX): UX/responsiveness enhancements --- app/app.js | 9 +- app/components/container/container.html | 11 +- app/components/containers/containers.html | 2 +- .../createContainerController.js | 2 +- .../createContainer/createcontainer.html | 386 ++++++++++-------- .../createNetwork/createnetwork.html | 103 ++--- .../createService/createServiceController.js | 2 +- .../createService/createservice.html | 330 ++++++++------- app/components/createVolume/createvolume.html | 53 ++- app/components/dashboard/dashboard.html | 8 +- app/components/endpoint/endpoint.html | 22 +- app/components/endpointInit/endpointInit.html | 28 +- app/components/endpoints/endpoints.html | 22 +- app/components/image/image.html | 13 +- app/components/images/images.html | 13 +- app/components/networks/networks.html | 4 +- app/components/node/node.html | 4 +- .../service/includes/constraints.html | 4 +- .../service/includes/containerlabels.html | 4 +- .../includes/environmentvariables.html | 4 +- app/components/service/includes/mounts.html | 4 +- app/components/service/includes/ports.html | 4 +- .../service/includes/servicelabels.html | 4 +- app/components/services/services.html | 2 +- app/components/templates/templates.html | 14 +- app/components/user/user.html | 20 +- app/components/user/userController.js | 12 +- app/components/users/users.html | 25 +- app/components/users/usersController.js | 4 +- app/components/volumes/volumes.html | 2 +- assets/css/app.css | 104 +++-- 31 files changed, 710 insertions(+), 509 deletions(-) diff --git a/app/app.js b/app/app.js index d0b0a810e..e854a4131 100644 --- a/app/app.js +++ b/app/app.js @@ -51,7 +51,7 @@ angular.module('portainer', [ 'user', 'users', 'volumes']) - .config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider) { + .config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', '$uibTooltipProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider, $uibTooltipProvider) { 'use strict'; localStorageServiceProvider @@ -73,6 +73,13 @@ angular.module('portainer', [ $urlRouterProvider.otherwise('/auth'); + $uibTooltipProvider.setTriggers({ + 'mouseenter': 'mouseleave', + 'click': 'click', + 'focus': 'blur', + 'outsideClick': 'outsideClick' + }); + $stateProvider .state('root', { abstract: true, diff --git a/app/components/container/container.html b/app/components/container/container.html index e02a7f7e9..af9c4a2e2 100644 --- a/app/components/container/container.html +++ b/app/components/container/container.html @@ -101,11 +101,14 @@
-
+
- -
+ +
@@ -119,7 +122,7 @@
- +
diff --git a/app/components/containers/containers.html b/app/components/containers/containers.html index bf2202334..41e322b5d 100644 --- a/app/components/containers/containers.html +++ b/app/components/containers/containers.html @@ -33,7 +33,7 @@
- Add container + Add container
diff --git a/app/components/createContainer/createContainerController.js b/app/components/createContainer/createContainerController.js index 7ca69caa0..4b12fef07 100644 --- a/app/components/createContainer/createContainerController.js +++ b/app/components/createContainer/createContainerController.js @@ -37,7 +37,7 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai }; $scope.addVolume = function() { - $scope.formValues.Volumes.push({ name: '', containerPath: '' }); + $scope.formValues.Volumes.push({ name: '', containerPath: '', readOnly: false, type: 'volume' }); }; $scope.removeVolume = function(index) { diff --git a/app/components/createContainer/createcontainer.html b/app/components/createContainer/createcontainer.html index 8302d29a5..3b5b13f30 100644 --- a/app/components/createContainer/createcontainer.html +++ b/app/components/createContainer/createcontainer.html @@ -18,88 +18,98 @@
+
+ Image configuration +
-
+
- -
- -
-
-
- -
+ +
+
- +
- -
-
- + +
+ Ports configuration +
+ +
+
+ + +
+
+
- -
-
- -
- - map port +
+ + + map additional port
-
+
-
+ +
host
-
+ + + + + +
container
-
- - - - + + +
+
+ + +
+
+
+
+ Access control +
@@ -108,11 +118,11 @@
-
+ +
+ Actions +
+
+
+ + Cancel + +
+
+ @@ -129,13 +151,16 @@
+ -