diff --git a/README.md b/README.md index a3483f6f9..14a404772 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ [![Gitter](https://badges.gitter.im/portainer/Lobby.svg)](https://gitter.im/portainer/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6) -**_Portainer_** is a lightweight management UI which allows you to **easily** manage your Docker host or Swarm cluster. +**_Portainer_** is a lightweight management UI which allows you to **easily** manage your different Docker environments (Docker hosts or Swarm clusters). -**_Portainer_** is meant to be as **simple** to deploy as it is to use. It consists of a single container that can run on any Docker engine (Docker for Linux and Docker for Windows are supported). +**_Portainer_** is meant to be as **simple** to deploy as it is to use. It consists of a single container that can run on any Docker engine (can be deployed as Linux container or a Windows native container). -**_Portainer_** allows you to manage your Docker containers, images, volumes, networks and more ! It is compatible with the *standalone Docker* engine and with *Docker Swarm*. +**_Portainer_** allows you to manage your Docker containers, images, volumes, networks and more ! It is compatible with the *standalone Docker* engine and with *Docker Swarm mode*. ## Demo @@ -34,8 +34,8 @@ Please note that the public demo cluster is **reset every 15min**. * Issues: https://github.com/portainer/portainer/issues * FAQ: https://portainer.readthedocs.io/en/latest/faq.html +* Slack (chat): https://portainer.io/slack/ * Gitter (chat): https://gitter.im/portainer/Lobby -* Slack: https://portainer.io/slack/ ## Reporting bugs and contributing diff --git a/api/cron/endpoint_sync.go b/api/cron/endpoint_sync.go index 3401c6893..9fbb634dc 100644 --- a/api/cron/endpoint_sync.go +++ b/api/cron/endpoint_sync.go @@ -117,7 +117,7 @@ func (job endpointSyncJob) prepareSyncData(storedEndpoints, fileEndpoints []port } for idx, endpoint := range fileEndpoints { - if endpoint.Name == "" || endpoint.URL == "" { + if !isValidEndpoint(&endpoint) { job.logger.Printf("Invalid file endpoint definition, skipping. [name: %v] [url: %v]", endpoint.Name, endpoint.URL) continue } diff --git a/api/http/handler/auth.go b/api/http/handler/auth.go index 3b464a165..4c8218282 100644 --- a/api/http/handler/auth.go +++ b/api/http/handler/auth.go @@ -75,7 +75,7 @@ func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Reques u, err := handler.UserService.UserByUsername(username) if err == portainer.ErrUserNotFound { - httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger) + httperror.WriteErrorResponse(w, ErrInvalidCredentials, http.StatusBadRequest, handler.Logger) return } else if err != nil { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) diff --git a/api/http/proxy/response.go b/api/http/proxy/response.go index ece319672..4208f438c 100644 --- a/api/http/proxy/response.go +++ b/api/http/proxy/response.go @@ -85,6 +85,11 @@ func rewriteResponse(response *http.Response, newResponseData interface{}, statu response.StatusCode = statusCode response.Body = body response.ContentLength = int64(len(jsonData)) + + if response.Header == nil { + response.Header = make(http.Header) + } response.Header.Set("Content-Length", strconv.Itoa(len(jsonData))) + return nil } diff --git a/api/http/proxy/transport.go b/api/http/proxy/transport.go index 29c1d6c72..76c4f43f7 100644 --- a/api/http/proxy/transport.go +++ b/api/http/proxy/transport.go @@ -59,6 +59,8 @@ func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Respon return p.proxyServiceRequest(request) } else if strings.HasPrefix(path, "/volumes") { return p.proxyVolumeRequest(request) + } else if strings.HasPrefix(path, "/swarm") { + return p.proxySwarmRequest(request) } return p.executeDockerRequest(request) @@ -143,6 +145,10 @@ func (p *proxyTransport) proxyVolumeRequest(request *http.Request) (*http.Respon } } +func (p *proxyTransport) proxySwarmRequest(request *http.Request) (*http.Response, error) { + return p.administratorOperation(request) +} + // restrictedOperation ensures that the current user has the required authorizations // before executing the original request. func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID string) (*http.Response, error) { diff --git a/api/http/security/authorization.go b/api/http/security/authorization.go index b19dad1f6..30d6dfb72 100644 --- a/api/http/security/authorization.go +++ b/api/http/security/authorization.go @@ -22,7 +22,7 @@ func AuthorizedResourceControlDeletion(resourceControl *portainer.ResourceContro if teamAccessesCount > 0 { for _, access := range resourceControl.TeamAccesses { for _, membership := range context.UserMemberships { - if membership.TeamID == access.TeamID && membership.Role == portainer.TeamLeader { + if membership.TeamID == access.TeamID { return true } } diff --git a/api/portainer.go b/api/portainer.go index fad8e5168..756653787 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -305,7 +305,7 @@ type ( const ( // APIVersion is the version number of the Portainer API. - APIVersion = "1.13.4" + APIVersion = "1.13.5" // DBVersion is the version number of the Portainer database. DBVersion = 2 // DefaultTemplatesURL represents the default URL for the templates definitions. diff --git a/app/app.js b/app/app.js index bba4ad835..ab07489e5 100644 --- a/app/app.js +++ b/app/app.js @@ -20,11 +20,10 @@ angular.module('portainer', [ 'portainer.services', 'auth', 'dashboard', - 'common.accesscontrol.panel', - 'common.accesscontrol.form', 'container', 'containerConsole', 'containerLogs', + 'serviceLogs', 'containers', 'createContainer', 'createNetwork', @@ -166,7 +165,7 @@ angular.module('portainer', [ } } }) - .state('logs', { + .state('containerlogs', { url: '^/containers/:id/logs', views: { 'content@': { @@ -179,6 +178,19 @@ angular.module('portainer', [ } } }) + .state('servicelogs', { + url: '^/services/:id/logs', + views: { + 'content@': { + templateUrl: 'app/components/serviceLogs/servicelogs.html', + controller: 'ServiceLogsController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) .state('console', { url: '^/containers/:id/console', views: { diff --git a/app/components/common/accessControlForm/accessControlForm.html b/app/components/common/accessControlForm/accessControlForm.html deleted file mode 100644 index 21e4c3869..000000000 --- a/app/components/common/accessControlForm/accessControlForm.html +++ /dev/null @@ -1,126 +0,0 @@ -
-
- Access control -
- -
-
- - -
-
- - -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
- - - You have not yet created any team. Head over the teams view to manage user teams. - - -
-
- - -
-
- - - You have not yet created any user. Head over the users view to manage users. - - -
-
- -
diff --git a/app/components/common/accessControlForm/accessControlFormController.js b/app/components/common/accessControlForm/accessControlFormController.js deleted file mode 100644 index 2656b900e..000000000 --- a/app/components/common/accessControlForm/accessControlFormController.js +++ /dev/null @@ -1,55 +0,0 @@ -angular.module('common.accesscontrol.form', []) -.controller('AccessControlFormController', ['$q', '$scope', '$state', 'UserService', 'ResourceControlService', 'Notifications', 'Authentication', 'ModalService', 'ControllerDataPipeline', -function ($q, $scope, $state, UserService, ResourceControlService, Notifications, Authentication, ModalService, ControllerDataPipeline) { - - $scope.availableTeams = []; - $scope.availableUsers = []; - - $scope.formValues = { - enableAccessControl: true, - Ownership_Teams: [], - Ownership_Users: [], - Ownership: 'private' - }; - - $scope.synchronizeFormData = function() { - ControllerDataPipeline.setAccessControlFormData($scope.formValues.enableAccessControl, - $scope.formValues.Ownership, $scope.formValues.Ownership_Users, $scope.formValues.Ownership_Teams); - }; - - function initAccessControlForm() { - $('#loadingViewSpinner').show(); - - var userDetails = Authentication.getUserDetails(); - var isAdmin = userDetails.role === 1 ? true: false; - $scope.isAdmin = isAdmin; - - if (isAdmin) { - $scope.formValues.Ownership = 'administrators'; - } - - $q.all({ - availableTeams: UserService.userTeams(userDetails.ID), - availableUsers: isAdmin ? UserService.users(false) : [] - }) - .then(function success(data) { - $scope.availableUsers = data.availableUsers; - - var availableTeams = data.availableTeams; - $scope.availableTeams = availableTeams; - if (!isAdmin && availableTeams.length === 1) { - $scope.formValues.Ownership_Teams = availableTeams; - } - - $scope.synchronizeFormData(); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve access control information'); - }) - .finally(function final() { - $('#loadingViewSpinner').hide(); - }); - } - - initAccessControlForm(); -}]); diff --git a/app/components/common/accessControlPanel/accessControlPanelController.js b/app/components/common/accessControlPanel/accessControlPanelController.js deleted file mode 100644 index 8283b6d97..000000000 --- a/app/components/common/accessControlPanel/accessControlPanelController.js +++ /dev/null @@ -1,158 +0,0 @@ -angular.module('common.accesscontrol.panel', []) -.controller('AccessControlPanelController', ['$q', '$scope', '$state', 'UserService', 'ResourceControlService', 'Notifications', 'Authentication', 'ModalService', 'ControllerDataPipeline', 'FormValidator', -function ($q, $scope, $state, UserService, ResourceControlService, Notifications, Authentication, ModalService, ControllerDataPipeline, FormValidator) { - - $scope.state = { - displayAccessControlPanel: false, - canEditOwnership: false, - editOwnership: false, - formValidationError: '' - }; - - $scope.formValues = { - Ownership: 'public', - Ownership_Users: [], - Ownership_Teams: [] - }; - - $scope.authorizedUsers = []; - $scope.availableUsers = []; - $scope.authorizedTeams = []; - $scope.availableTeams = []; - - $scope.confirmUpdateOwnership = function (force) { - if (!validateForm()) { - return; - } - ModalService.confirmAccessControlUpdate(function (confirmed) { - if(!confirmed) { return; } - updateOwnership(); - }); - }; - - function processOwnershipFormValues() { - var userIds = []; - angular.forEach($scope.formValues.Ownership_Users, function(user) { - userIds.push(user.Id); - }); - var teamIds = []; - angular.forEach($scope.formValues.Ownership_Teams, function(team) { - teamIds.push(team.Id); - }); - var administratorsOnly = $scope.formValues.Ownership === 'administrators' ? true : false; - - return { - ownership: $scope.formValues.Ownership, - authorizedUserIds: administratorsOnly ? [] : userIds, - authorizedTeamIds: administratorsOnly ? [] : teamIds, - administratorsOnly: administratorsOnly - }; - } - - function validateForm() { - $scope.state.formValidationError = ''; - var error = ''; - - var accessControlData = { - ownership: $scope.formValues.Ownership, - authorizedUsers: $scope.formValues.Ownership_Users, - authorizedTeams: $scope.formValues.Ownership_Teams - }; - var isAdmin = $scope.isAdmin; - error = FormValidator.validateAccessControl(accessControlData, isAdmin); - if (error) { - $scope.state.formValidationError = error; - return false; - } - return true; - } - - function updateOwnership() { - $('#loadingViewSpinner').show(); - - var accessControlData = ControllerDataPipeline.getAccessControlData(); - var resourceId = accessControlData.resourceId; - var ownershipParameters = processOwnershipFormValues(); - - ResourceControlService.applyResourceControlChange(accessControlData.resourceType, resourceId, - $scope.resourceControl, ownershipParameters) - .then(function success(data) { - Notifications.success('Access control successfully updated'); - $state.reload(); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to update access control'); - }) - .finally(function final() { - $('#loadingViewSpinner').hide(); - }); - } - - function initAccessControlPanel() { - $('#loadingViewSpinner').show(); - - var userDetails = Authentication.getUserDetails(); - var isAdmin = userDetails.role === 1 ? true: false; - var userId = userDetails.ID; - $scope.isAdmin = isAdmin; - - var accessControlData = ControllerDataPipeline.getAccessControlData(); - var resourceControl = accessControlData.resourceControl; - $scope.resourceType = accessControlData.resourceType; - $scope.resourceControl = resourceControl; - - if (isAdmin) { - if (resourceControl) { - $scope.formValues.Ownership = resourceControl.Ownership === 'private' ? 'restricted' : resourceControl.Ownership; - } else { - $scope.formValues.Ownership = 'public'; - } - } else { - $scope.formValues.Ownership = 'public'; - } - - ResourceControlService.retrieveOwnershipDetails(resourceControl) - .then(function success(data) { - $scope.authorizedUsers = data.authorizedUsers; - $scope.authorizedTeams = data.authorizedTeams; - return ResourceControlService.retrieveUserPermissionsOnResource(userId, isAdmin, resourceControl); - }) - .then(function success(data) { - $scope.state.canEditOwnership = data.isPartOfRestrictedUsers || data.isLeaderOfAnyRestrictedTeams; - $scope.state.canChangeOwnershipToTeam = data.isPartOfRestrictedUsers; - - return $q.all({ - availableUsers: isAdmin ? UserService.users(false) : [], - availableTeams: isAdmin || data.isPartOfRestrictedUsers ? UserService.userTeams(userId) : [] - }); - }) - .then(function success(data) { - $scope.availableUsers = data.availableUsers; - angular.forEach($scope.availableUsers, function(user) { - var found = _.find($scope.authorizedUsers, { Id: user.Id }); - if (found) { - user.selected = true; - } - }); - $scope.availableTeams = data.availableTeams; - angular.forEach(data.availableTeams, function(team) { - var found = _.find($scope.authorizedTeams, { Id: team.Id }); - if (found) { - team.selected = true; - } - }); - if (data.availableTeams.length === 1) { - $scope.formValues.Ownership_Teams.push(data.availableTeams[0]); - } - $scope.state.displayAccessControlPanel = true; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve access control information'); - }) - .finally(function final() { - $('#loadingViewSpinner').hide(); - }); - } - - initAccessControlPanel(); -}]); diff --git a/app/components/container/container.html b/app/components/container/container.html index 574e67aec..11b831635 100644 --- a/app/components/container/container.html +++ b/app/components/container/container.html @@ -3,7 +3,7 @@ - Containers > {{ container.Name|trimcontainername }} + Containers > {{ container.Name|trimcontainername }} @@ -33,6 +33,10 @@ + + + + @@ -87,7 +91,13 @@ -
+ + + +
@@ -114,7 +124,7 @@
ID{{ container.Id }}
Name @@ -75,7 +79,7 @@
- + diff --git a/app/components/container/containerController.js b/app/components/container/containerController.js index e9c6b3303..22acf056c 100644 --- a/app/components/container/containerController.js +++ b/app/components/container/containerController.js @@ -1,6 +1,6 @@ angular.module('container', []) -.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ContainerService', 'ImageHelper', 'Network', 'Notifications', 'Pagination', 'ModalService', 'ControllerDataPipeline', -function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ContainerService, ImageHelper, Network, Notifications, Pagination, ModalService, ControllerDataPipeline) { +.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ContainerService', 'ImageHelper', 'Network', 'Notifications', 'Pagination', 'ModalService', +function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ContainerService, ImageHelper, Network, Notifications, Pagination, ModalService) { $scope.activityTime = 0; $scope.portBindings = []; $scope.config = { @@ -19,7 +19,6 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Con Container.get({id: $stateParams.id}, function (d) { var container = new ContainerDetailsViewModel(d); $scope.container = container; - ControllerDataPipeline.setAccessControlData('container', $stateParams.id, container.ResourceControl); $scope.container.edit = false; $scope.container.newContainerName = $filter('trimcontainername')(container.Name); @@ -86,7 +85,7 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Con $('#createImageSpinner').show(); var image = $scope.config.Image; var registry = $scope.config.Registry; - var imageConfig = ImageHelper.createImageConfigForCommit(image, registry); + var imageConfig = ImageHelper.createImageConfigForCommit(image, registry.URL); ContainerCommit.commit({id: $stateParams.id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) { $('#createImageSpinner').hide(); update(); diff --git a/app/components/containerConsole/containerConsole.html b/app/components/containerConsole/containerConsole.html index e2f1f4d30..716b5ffb8 100644 --- a/app/components/containerConsole/containerConsole.html +++ b/app/components/containerConsole/containerConsole.html @@ -3,7 +3,7 @@ - Containers > {{ container.Name|trimcontainername }} > Console + Containers > {{ container.Name|trimcontainername }} > Console @@ -16,29 +16,53 @@ -
-
+ +
-
-
- - - - - +
+ +
+
+ + + + + +
+ +
+
+ +
+ + +
+
+ +
+
- -
- - +
+
+ +
+
+ + +
diff --git a/app/components/containerConsole/containerConsoleController.js b/app/components/containerConsole/containerConsoleController.js index b9da6c20f..2ff1214ad 100644 --- a/app/components/containerConsole/containerConsoleController.js +++ b/app/components/containerConsole/containerConsoleController.js @@ -1,9 +1,10 @@ angular.module('containerConsole', []) -.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Container', 'Image', 'Exec', '$timeout', 'EndpointProvider', 'Notifications', -function ($scope, $stateParams, Container, Image, Exec, $timeout, EndpointProvider, Notifications) { +.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Container', 'Image', 'EndpointProvider', 'Notifications', 'ContainerHelper', 'ContainerService', 'ExecService', +function ($scope, $stateParams, Container, Image, EndpointProvider, Notifications, ContainerHelper, ContainerService, ExecService) { $scope.state = {}; $scope.state.loaded = false; $scope.state.connected = false; + $scope.formValues = {}; var socket, term; @@ -22,7 +23,7 @@ function ($scope, $stateParams, Container, Image, Exec, $timeout, EndpointProvid } else { Image.get({id: d.Image}, function(imgData) { $scope.imageOS = imgData.Os; - $scope.state.command = imgData.Os === 'windows' ? 'powershell' : 'bash'; + $scope.formValues.command = imgData.Os === 'windows' ? 'powershell' : 'bash'; $scope.state.loaded = true; $('#loadingViewSpinner').hide(); }, function (e) { @@ -39,33 +40,36 @@ function ($scope, $stateParams, Container, Image, Exec, $timeout, EndpointProvid $('#loadConsoleSpinner').show(); var termWidth = Math.round($('#terminal-container').width() / 8.2); var termHeight = 30; + var command = $scope.formValues.isCustomCommand ? + $scope.formValues.customCommand : $scope.formValues.command; var execConfig = { id: $stateParams.id, AttachStdin: true, AttachStdout: true, AttachStderr: true, Tty: true, - Cmd: $scope.state.command.replace(' ', ',').split(',') + User: $scope.formValues.user, + Cmd: ContainerHelper.commandStringToArray(command) }; - Container.exec(execConfig, function(d) { - if (d.message) { - $('#loadConsoleSpinner').hide(); - Notifications.error('Error', {}, d.message); + var execId; + ContainerService.createExec(execConfig) + .then(function success(data) { + execId = data.Id; + var url = window.location.href.split('#')[0] + 'api/websocket/exec?id=' + execId + '&endpointId=' + EndpointProvider.endpointID(); + if (url.indexOf('https') > -1) { + url = url.replace('https://', 'wss://'); } else { - var execId = d.Id; - resizeTTY(execId, termHeight, termWidth); - var url = window.location.href.split('#')[0] + 'api/websocket/exec?id=' + execId + '&endpointId=' + EndpointProvider.endpointID(); - if (url.indexOf('https') > -1) { - url = url.replace('https://', 'wss://'); - } else { - url = url.replace('http://', 'ws://'); - } - initTerm(url, termHeight, termWidth); + url = url.replace('http://', 'ws://'); } - }, function (e) { + initTerm(url, termHeight, termWidth); + return ExecService.resizeTTY(execId, termHeight, termWidth, 2000); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to exec into container'); + }) + .finally(function final() { $('#loadConsoleSpinner').hide(); - Notifications.error('Failure', e, 'Unable to start an exec instance'); }); }; @@ -79,19 +83,6 @@ function ($scope, $stateParams, Container, Image, Exec, $timeout, EndpointProvid } }; - function resizeTTY(execId, height, width) { - $timeout(function() { - Exec.resize({id: execId, height: height, width: width}, function (d) { - if (d.message) { - Notifications.error('Error', {}, 'Unable to resize TTY'); - } - }, function (e) { - Notifications.error('Failure', {}, 'Unable to resize TTY'); - }); - }, 2000); - - } - function initTerm(url, height, width) { socket = new WebSocket(url); @@ -103,7 +94,7 @@ function ($scope, $stateParams, Container, Image, Exec, $timeout, EndpointProvid term.on('data', function (data) { socket.send(data); }); - term.open(document.getElementById('terminal-container')); + term.open(document.getElementById('terminal-container'), true); term.resize(width, height); term.setOption('cursorBlink', true); diff --git a/app/components/containerLogs/containerlogs.html b/app/components/containerLogs/containerlogs.html index b20af8bf8..fe12b3732 100644 --- a/app/components/containerLogs/containerlogs.html +++ b/app/components/containerLogs/containerlogs.html @@ -3,7 +3,7 @@ - Containers > {{ container.Name|trimcontainername }} > Logs + Containers > {{ container.Name|trimcontainername }} > Logs diff --git a/app/components/containers/containers.html b/app/components/containers/containers.html index 9d5a86722..3cd1a3aee 100644 --- a/app/components/containers/containers.html +++ b/app/components/containers/containers.html @@ -25,12 +25,12 @@
- - + + - - + +
Add container @@ -137,5 +137,5 @@
- +
diff --git a/app/components/containers/containersController.js b/app/components/containers/containersController.js index da858ade5..b08d41705 100644 --- a/app/components/containers/containersController.js +++ b/app/components/containers/containersController.js @@ -41,6 +41,7 @@ angular.module('containers', []) } return model; }); + updateSelectionFlags(); $('#loadContainersSpinner').hide(); }, function (e) { $('#loadContainersSpinner').hide(); @@ -117,17 +118,15 @@ angular.module('containers', []) angular.forEach($scope.state.filteredContainers, function (container) { if (container.Checked !== allSelected) { container.Checked = allSelected; - $scope.selectItem(container); + toggleItemSelection(container); } }); + updateSelectionFlags(); }; $scope.selectItem = function (item) { - if (item.Checked) { - $scope.state.selectedItemCount++; - } else { - $scope.state.selectedItemCount--; - } + toggleItemSelection(item); + updateSelectionFlags(); }; $scope.toggleGetAll = function () { @@ -187,6 +186,33 @@ angular.module('containers', []) ); }; + function toggleItemSelection(item) { + if (item.Checked) { + $scope.state.selectedItemCount++; + } else { + $scope.state.selectedItemCount--; + } + } + + function updateSelectionFlags() { + $scope.state.noStoppedItemsSelected = true; + $scope.state.noRunningItemsSelected = true; + $scope.state.noPausedItemsSelected = true; + $scope.containers.forEach(function(container) { + if(!container.Checked) { + return; + } + + if(container.Status === 'paused') { + $scope.state.noPausedItemsSelected = false; + } else if(container.Status === 'stopped') { + $scope.state.noStoppedItemsSelected = false; + } else if(container.Status === 'running') { + $scope.state.noRunningItemsSelected = false; + } + } ); + } + function retrieveSwarmHostsInfo(data) { var swarm_hosts = {}; var systemStatus = data.SystemStatus; @@ -207,7 +233,7 @@ angular.module('containers', []) $q.when(provider !== 'DOCKER_SWARM' || SystemService.info()) .then(function success(data) { if (provider === 'DOCKER_SWARM') { - $scope.swarm_hosts = retrieveSwarmHostsInfo(d); + $scope.swarm_hosts = retrieveSwarmHostsInfo(data); } update({all: $scope.state.displayAll ? 1 : 0}); }) diff --git a/app/components/createContainer/createContainerController.js b/app/components/createContainer/createContainerController.js index b578a8bc0..048e3f2ce 100644 --- a/app/components/createContainer/createContainerController.js +++ b/app/components/createContainer/createContainerController.js @@ -1,8 +1,8 @@ // @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services. // See app/components/templates/templatesController.js as a reference. angular.module('createContainer', []) -.controller('CreateContainerController', ['$q', '$scope', '$state', '$stateParams', '$filter', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'ControllerDataPipeline', 'FormValidator', -function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, Image, ImageHelper, Volume, Network, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, ControllerDataPipeline, FormValidator) { +.controller('CreateContainerController', ['$q', '$scope', '$state', '$stateParams', '$filter', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'FormValidator', +function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, Image, ImageHelper, Volume, Network, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, FormValidator) { $scope.formValues = { alwaysPull: true, @@ -13,7 +13,8 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, Labels: [], ExtraHosts: [], IPv4: '', - IPv6: '' + IPv6: '', + AccessControlData: new AccessControlFormData() }; $scope.state = { @@ -285,7 +286,7 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, $scope.create = function () { $('#createContainerSpinner').show(); - var accessControlData = ControllerDataPipeline.getAccessControlFormData(); + var accessControlData = $scope.formValues.AccessControlData; var userDetails = Authentication.getUserDetails(); var isAdmin = userDetails.role === 1 ? true : false; diff --git a/app/components/createContainer/createcontainer.html b/app/components/createContainer/createcontainer.html index 1798c0230..356f244f5 100644 --- a/app/components/createContainer/createcontainer.html +++ b/app/components/createContainer/createcontainer.html @@ -1,7 +1,7 @@ - Containers > Add container + Containers > Add container @@ -98,7 +98,7 @@
-
+
diff --git a/app/components/createNetwork/createNetworkController.js b/app/components/createNetwork/createNetworkController.js index 10aaf9049..01b0b3c7a 100644 --- a/app/components/createNetwork/createNetworkController.js +++ b/app/components/createNetwork/createNetworkController.js @@ -1,6 +1,6 @@ angular.module('createNetwork', []) -.controller('CreateNetworkController', ['$scope', '$state', 'Notifications', 'Network', -function ($scope, $state, Notifications, Network) { +.controller('CreateNetworkController', ['$scope', '$state', 'Notifications', 'Network', 'LabelHelper', +function ($scope, $state, Notifications, Network, LabelHelper) { $scope.formValues = { DriverOptions: [], Subnet: '', @@ -30,7 +30,7 @@ function ($scope, $state, Notifications, Network) { }; $scope.addLabel = function() { - $scope.formValues.Labels.push({ name: '', value: ''}); + $scope.formValues.Labels.push({ key: '', value: ''}); }; $scope.removeLabel = function(index) { @@ -74,13 +74,7 @@ function ($scope, $state, Notifications, Network) { } function prepareLabelsConfig(config) { - var labels = {}; - $scope.formValues.Labels.forEach(function (label) { - if (label.name && label.value) { - labels[label.name] = label.value; - } - }); - config.Labels = labels; + config.Labels = LabelHelper.fromKeyValueToLabelHash($scope.formValues.Labels); } function prepareConfiguration() { diff --git a/app/components/createNetwork/createnetwork.html b/app/components/createNetwork/createnetwork.html index 98992d438..a9f1ef7d4 100644 --- a/app/components/createNetwork/createnetwork.html +++ b/app/components/createNetwork/createnetwork.html @@ -1,7 +1,7 @@ - Networks > Add network + Networks > Add network @@ -90,7 +90,7 @@
name - +
value diff --git a/app/components/createRegistry/createregistry.html b/app/components/createRegistry/createregistry.html index def77d867..f1204ba6f 100644 --- a/app/components/createRegistry/createregistry.html +++ b/app/components/createRegistry/createregistry.html @@ -3,7 +3,7 @@ - Registries > Add registry + Registries > Add registry diff --git a/app/components/createSecret/createSecretController.js b/app/components/createSecret/createSecretController.js index ca2fcdc79..3f2533270 100644 --- a/app/components/createSecret/createSecretController.js +++ b/app/components/createSecret/createSecretController.js @@ -1,6 +1,6 @@ angular.module('createSecret', []) -.controller('CreateSecretController', ['$scope', '$state', 'Notifications', 'SecretService', -function ($scope, $state, Notifications, SecretService) { +.controller('CreateSecretController', ['$scope', '$state', 'Notifications', 'SecretService', 'LabelHelper', +function ($scope, $state, Notifications, SecretService, LabelHelper) { $scope.formValues = { Name: '', Data: '', @@ -9,7 +9,7 @@ function ($scope, $state, Notifications, SecretService) { }; $scope.addLabel = function() { - $scope.formValues.Labels.push({ name: '', value: ''}); + $scope.formValues.Labels.push({ key: '', value: ''}); }; $scope.removeLabel = function(index) { @@ -17,13 +17,7 @@ function ($scope, $state, Notifications, SecretService) { }; function prepareLabelsConfig(config) { - var labels = {}; - $scope.formValues.Labels.forEach(function (label) { - if (label.name && label.value) { - labels[label.name] = label.value; - } - }); - config.Labels = labels; + config.Labels = LabelHelper.fromKeyValueToLabelHash($scope.formValues.Labels); } function prepareSecretData(config) { diff --git a/app/components/createSecret/createsecret.html b/app/components/createSecret/createsecret.html index 385bb3067..c918e8cd5 100644 --- a/app/components/createSecret/createsecret.html +++ b/app/components/createSecret/createsecret.html @@ -1,7 +1,7 @@ - Secrets > Add secret + Secrets > Add secret @@ -52,7 +52,7 @@
name - +
value diff --git a/app/components/createService/createServiceController.js b/app/components/createService/createServiceController.js index 9a29f315d..3ede157b1 100644 --- a/app/components/createService/createServiceController.js +++ b/app/components/createService/createServiceController.js @@ -1,8 +1,8 @@ // @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services. // See app/components/templates/templatesController.js as a reference. angular.module('createService', []) -.controller('CreateServiceController', ['$q', '$scope', '$state', 'Service', 'ServiceHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'ControllerDataPipeline', 'FormValidator', 'RegistryService', 'HttpRequestHelper', -function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, Authentication, ResourceControlService, Notifications, ControllerDataPipeline, FormValidator, RegistryService, HttpRequestHelper) { +.controller('CreateServiceController', ['$q', '$scope', '$state', 'Service', 'ServiceHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'LabelHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'FormValidator', 'RegistryService', 'HttpRequestHelper', +function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, LabelHelper, Authentication, ResourceControlService, Notifications, FormValidator, RegistryService, HttpRequestHelper) { $scope.formValues = { Name: '', @@ -23,9 +23,11 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic Ports: [], Parallelism: 1, PlacementConstraints: [], + PlacementPreferences: [], UpdateDelay: 0, FailureAction: 'pause', - Secrets: [] + Secrets: [], + AccessControlData: new AccessControlFormData() }; $scope.state = { @@ -81,7 +83,7 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic }; $scope.addPlacementPreference = function() { - $scope.formValues.PlacementPreferences.push({ key: '', operator: '==', value: '' }); + $scope.formValues.PlacementPreferences.push({ strategy: 'spread', value: '' }); }; $scope.removePlacementPreference = function(index) { @@ -89,7 +91,7 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic }; $scope.addLabel = function() { - $scope.formValues.Labels.push({ name: '', value: ''}); + $scope.formValues.Labels.push({ key: '', value: ''}); }; $scope.removeLabel = function(index) { @@ -97,7 +99,7 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic }; $scope.addContainerLabel = function() { - $scope.formValues.ContainerLabels.push({ name: '', value: ''}); + $scope.formValues.ContainerLabels.push({ key: '', value: ''}); }; $scope.removeContainerLabel = function(index) { @@ -170,21 +172,8 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic } function prepareLabelsConfig(config, input) { - var labels = {}; - input.Labels.forEach(function (label) { - if (label.name && label.value) { - labels[label.name] = label.value; - } - }); - config.Labels = labels; - - var containerLabels = {}; - input.ContainerLabels.forEach(function (label) { - if (label.name && label.value) { - containerLabels[label.name] = label.value; - } - }); - config.TaskTemplate.ContainerSpec.Labels = containerLabels; + config.Labels = LabelHelper.fromKeyValueToLabelHash(input.Labels); + config.TaskTemplate.ContainerSpec.Labels = LabelHelper.fromKeyValueToLabelHash(input.ContainerLabels); } function prepareVolumes(config, input) { @@ -213,8 +202,10 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic FailureAction: input.FailureAction }; } + function preparePlacementConfig(config, input) { config.TaskTemplate.Placement.Constraints = ServiceHelper.translateKeyValueToPlacementConstraints(input.PlacementConstraints); + config.TaskTemplate.Placement.Preferences = ServiceHelper.translateKeyValueToPlacementPreferences(input.PlacementPreferences); } function prepareSecretConfig(config, input) { @@ -296,7 +287,7 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic $scope.create = function createService() { $('#createServiceSpinner').show(); - var accessControlData = ControllerDataPipeline.getAccessControlFormData(); + var accessControlData = $scope.formValues.AccessControlData; var userDetails = Authentication.getUserDetails(); var isAdmin = userDetails.role === 1 ? true : false; diff --git a/app/components/createService/createservice.html b/app/components/createService/createservice.html index 3f2f03016..49e796069 100644 --- a/app/components/createService/createservice.html +++ b/app/components/createService/createservice.html @@ -3,7 +3,7 @@ - Services > Add service + Services > Add service @@ -101,7 +101,7 @@
-
+
@@ -328,7 +328,7 @@
name - +
value @@ -355,7 +355,7 @@
name - +
value diff --git a/app/components/createService/includes/placement.html b/app/components/createService/includes/placement.html index f33c5ab88..c12547fac 100644 --- a/app/components/createService/includes/placement.html +++ b/app/components/createService/includes/placement.html @@ -29,3 +29,29 @@
+ +
+
+
+ + + placement preference + +
+
+
+
+ strategy + +
+
+ value + +
+ +
+
+
+
diff --git a/app/components/createVolume/createVolumeController.js b/app/components/createVolume/createVolumeController.js index a7b5f3d85..579b948d1 100644 --- a/app/components/createVolume/createVolumeController.js +++ b/app/components/createVolume/createVolumeController.js @@ -1,10 +1,11 @@ angular.module('createVolume', []) -.controller('CreateVolumeController', ['$scope', '$state', 'VolumeService', 'SystemService', 'ResourceControlService', 'Authentication', 'Notifications', 'ControllerDataPipeline', 'FormValidator', -function ($scope, $state, VolumeService, SystemService, ResourceControlService, Authentication, Notifications, ControllerDataPipeline, FormValidator) { +.controller('CreateVolumeController', ['$scope', '$state', 'VolumeService', 'SystemService', 'ResourceControlService', 'Authentication', 'Notifications', 'FormValidator', +function ($scope, $state, VolumeService, SystemService, ResourceControlService, Authentication, Notifications, FormValidator) { $scope.formValues = { Driver: 'local', - DriverOptions: [] + DriverOptions: [], + AccessControlData: new AccessControlFormData() }; $scope.state = { @@ -40,8 +41,8 @@ function ($scope, $state, VolumeService, SystemService, ResourceControlService, var driver = $scope.formValues.Driver; var driverOptions = $scope.formValues.DriverOptions; var volumeConfiguration = VolumeService.createVolumeConfiguration(name, driver, driverOptions); + var accessControlData = $scope.formValues.AccessControlData; var userDetails = Authentication.getUserDetails(); - var accessControlData = ControllerDataPipeline.getAccessControlFormData(); var isAdmin = userDetails.role === 1 ? true : false; if (!validateForm(accessControlData, isAdmin)) { @@ -69,16 +70,18 @@ function ($scope, $state, VolumeService, SystemService, ResourceControlService, function initView() { $('#loadingViewSpinner').show(); - SystemService.getVolumePlugins() - .then(function success(data) { - $scope.availableVolumeDrivers = data; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve volume drivers'); - }) - .finally(function final() { - $('#loadingViewSpinner').hide(); - }); + if ($scope.applicationState.endpoint.mode.provider !== 'DOCKER_SWARM') { + SystemService.getVolumePlugins() + .then(function success(data) { + $scope.availableVolumeDrivers = data; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve volume drivers'); + }) + .finally(function final() { + $('#loadingViewSpinner').hide(); + }); + } } initView(); diff --git a/app/components/createVolume/createvolume.html b/app/components/createVolume/createvolume.html index 178194fe7..2ca08ec84 100644 --- a/app/components/createVolume/createvolume.html +++ b/app/components/createVolume/createvolume.html @@ -3,7 +3,7 @@ - Volumes > Add volume + Volumes > Add volume @@ -65,7 +65,7 @@
-
+
diff --git a/app/components/endpoint/endpoint.html b/app/components/endpoint/endpoint.html index 543bc7062..80be7c92c 100644 --- a/app/components/endpoint/endpoint.html +++ b/app/components/endpoint/endpoint.html @@ -3,7 +3,7 @@ - Endpoints > {{ endpoint.Name }} + Endpoints > {{ endpoint.Name }} diff --git a/app/components/endpointAccess/endpointAccess.html b/app/components/endpointAccess/endpointAccess.html index e4b14312c..695dc6955 100644 --- a/app/components/endpointAccess/endpointAccess.html +++ b/app/components/endpointAccess/endpointAccess.html @@ -3,7 +3,7 @@ - Endpoints > {{ endpoint.Name }} > Access management + Endpoints > {{ endpoint.Name }} > Access management diff --git a/app/components/endpoints/endpoints.html b/app/components/endpoints/endpoints.html index fd6660b66..6528ae711 100644 --- a/app/components/endpoints/endpoints.html +++ b/app/components/endpoints/endpoints.html @@ -14,8 +14,11 @@ - Portainer has been started using the --external-endpoints flag. Endpoint management via the UI is disabled. You can still manage endpoint access. - + Portainer has been started using the --external-endpoints flag. + Endpoint management via the UI is disabled. + You can still manage endpoint access. + +
@@ -203,6 +206,6 @@
- +
diff --git a/app/components/events/events.html b/app/components/events/events.html index e59c9fc8d..829bc38e3 100644 --- a/app/components/events/events.html +++ b/app/components/events/events.html @@ -69,6 +69,6 @@
- + diff --git a/app/components/image/image.html b/app/components/image/image.html index 799dd2386..7e4341611 100644 --- a/app/components/image/image.html +++ b/app/components/image/image.html @@ -3,7 +3,7 @@ - Images > {{ image.Id }} + Images > {{ image.Id }} @@ -167,3 +167,58 @@ + +
+
+ + + + + + + + + + + + + + +
+ + Size + + + + + + Layer + + + +
+ {{ layer.Size | humansize }} + +
+ + + {{ layer.CreatedBy | imagelayercommand | truncate:130 }} + + + + + + +
+
+ + {{ layer.CreatedBy | imagelayercommand }} + +
+
+
+
+
+
diff --git a/app/components/image/imageController.js b/app/components/image/imageController.js index fa2925b07..f5c0c6142 100644 --- a/app/components/image/imageController.js +++ b/app/components/image/imageController.js @@ -6,6 +6,20 @@ function ($scope, $stateParams, $state, $timeout, ImageService, RegistryService, Registry: '' }; + $scope.sortType = 'Size'; + $scope.sortReverse = true; + + $scope.order = function(sortType) { + $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; + $scope.sortType = sortType; + }; + + $scope.toggleLayerCommand = function(layerId) { + $('#layer-command-expander'+layerId+' span').toggleClass('glyphicon-plus-sign glyphicon-minus-sign'); + $('#layer-command-'+layerId+'-short').toggle(); + $('#layer-command-'+layerId+'-full').toggle(); + }; + $scope.tagImage = function() { $('#loadingViewSpinner').show(); var image = $scope.formValues.Image; @@ -108,6 +122,18 @@ function ($scope, $stateParams, $state, $timeout, ImageService, RegistryService, .finally(function final() { $('#loadingViewSpinner').hide(); }); + + $('#loadingViewSpinner').show(); + ImageService.history($stateParams.id) + .then(function success(data) { + $scope.history = data; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve image history'); + }) + .finally(function final() { + $('#loadingViewSpinner').hide(); + }); } retrieveImageDetails(); diff --git a/app/components/images/images.html b/app/components/images/images.html index cfecf029a..19dd4669b 100644 --- a/app/components/images/images.html +++ b/app/components/images/images.html @@ -70,6 +70,17 @@
+ + + + +
@@ -110,9 +121,11 @@ - + - {{ image.Id|truncate:20}} + + {{ image.Id|truncate:20}} + Unused {{ tag }} @@ -132,6 +145,6 @@
- - - + + + diff --git a/app/components/images/imagesController.js b/app/components/images/imagesController.js index feb0f89ee..cbb723274 100644 --- a/app/components/images/imagesController.js +++ b/app/components/images/imagesController.js @@ -93,7 +93,8 @@ function ($scope, $state, ImageService, Notifications, Pagination, ModalService) function fetchImages() { $('#loadImagesSpinner').show(); - ImageService.images() + var endpointProvider = $scope.applicationState.endpoint.mode.provider; + ImageService.images(endpointProvider !== 'DOCKER_SWARM') .then(function success(data) { $scope.images = data; }) diff --git a/app/components/network/network.html b/app/components/network/network.html index ae0cc846d..b2c1c9b6e 100644 --- a/app/components/network/network.html +++ b/app/components/network/network.html @@ -3,7 +3,7 @@ - Networks > {{ network.Name }} + Networks > {{ network.Name }} diff --git a/app/components/networks/networks.html b/app/components/networks/networks.html index e25170c97..5d8c110be 100644 --- a/app/components/networks/networks.html +++ b/app/components/networks/networks.html @@ -134,7 +134,7 @@ {{ network.Name|truncate:40}} - {{ network.Id }} + {{ network.Id|truncate:20 }} {{ network.Scope }} {{ network.Driver }} {{ network.IPAM.Driver }} @@ -154,6 +154,6 @@ - + diff --git a/app/components/node/node.html b/app/components/node/node.html index 480009e57..4b9f87910 100644 --- a/app/components/node/node.html +++ b/app/components/node/node.html @@ -5,7 +5,7 @@ - Swarm nodes > {{ node.Hostname }} + Swarm nodes > {{ node.Hostname }} diff --git a/app/components/registries/registries.html b/app/components/registries/registries.html index cf5772dcc..fac2ee8d5 100644 --- a/app/components/registries/registries.html +++ b/app/components/registries/registries.html @@ -133,10 +133,10 @@ - Loading... + Loading... - No registries available. + No registries available. @@ -145,6 +145,6 @@ - + diff --git a/app/components/registry/registry.html b/app/components/registry/registry.html index 2bbb7c2e6..971d99848 100644 --- a/app/components/registry/registry.html +++ b/app/components/registry/registry.html @@ -3,7 +3,7 @@ - Registries > {{ registry.Name }} + Registries > {{ registry.Name }} diff --git a/app/components/registryAccess/registryAccess.html b/app/components/registryAccess/registryAccess.html index 84c3d2395..44ff3d130 100644 --- a/app/components/registryAccess/registryAccess.html +++ b/app/components/registryAccess/registryAccess.html @@ -3,7 +3,7 @@ - Registries > {{ registry.Name }} > Access management + Registries > {{ registry.Name }} > Access management diff --git a/app/components/secret/secret.html b/app/components/secret/secret.html index 8c9d10e18..fb349ebbb 100644 --- a/app/components/secret/secret.html +++ b/app/components/secret/secret.html @@ -6,7 +6,7 @@ - Secrets > {{ secret.Name }} + Secrets > {{ secret.Name }} diff --git a/app/components/secrets/secrets.html b/app/components/secrets/secrets.html index 215ac1796..abd9ba6eb 100644 --- a/app/components/secrets/secrets.html +++ b/app/components/secrets/secrets.html @@ -63,6 +63,6 @@ - + diff --git a/app/components/service/includes/placementPreferences.html b/app/components/service/includes/placementPreferences.html new file mode 100644 index 000000000..210556e72 --- /dev/null +++ b/app/components/service/includes/placementPreferences.html @@ -0,0 +1,57 @@ +
+ + + + + +

There are no placement preferences for this service.

+
+ + + + + + + + + + + + + + +
StrategyValue
+
+ +
+
+
+ + + + +
+
+
+ + + +
+
diff --git a/app/components/service/includes/ports.html b/app/components/service/includes/ports.html index 7367bfbb7..95e4b232d 100644 --- a/app/components/service/includes/ports.html +++ b/app/components/service/includes/ports.html @@ -63,7 +63,7 @@ - -
+ + + +

@@ -145,6 +159,7 @@

Service specification

+
diff --git a/app/components/service/serviceController.js b/app/components/service/serviceController.js index a69c2fb78..b61dbb256 100644 --- a/app/components/service/serviceController.js +++ b/app/components/service/serviceController.js @@ -1,6 +1,6 @@ angular.module('service', []) -.controller('ServiceController', ['$q', '$scope', '$stateParams', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'Secret', 'SecretHelper', 'Service', 'ServiceHelper', 'TaskService', 'NodeService', 'Notifications', 'Pagination', 'ModalService', 'ControllerDataPipeline', -function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll, ServiceService, Secret, SecretHelper, Service, ServiceHelper, TaskService, NodeService, Notifications, Pagination, ModalService, ControllerDataPipeline) { +.controller('ServiceController', ['$q', '$scope', '$stateParams', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'Secret', 'SecretHelper', 'Service', 'ServiceHelper', 'LabelHelper', 'TaskService', 'NodeService', 'Notifications', 'Pagination', 'ModalService', +function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll, ServiceService, Secret, SecretHelper, Service, ServiceHelper, LabelHelper, TaskService, NodeService, Notifications, Pagination, ModalService) { $scope.state = {}; $scope.state.pagination_count = Pagination.getPaginationCount('service_tasks'); @@ -124,10 +124,24 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll, updateServiceArray(service, 'ServiceConstraints', service.ServiceConstraints); } }; - $scope.updatePlacementConstraint = function updatePlacementConstraint(service, constraint) { + $scope.updatePlacementConstraint = function(service, constraint) { updateServiceArray(service, 'ServiceConstraints', service.ServiceConstraints); }; + $scope.addPlacementPreference = function(service) { + service.ServicePreferences.push({ strategy: 'spread', value: '' }); + updateServiceArray(service, 'ServicePreferences', service.ServicePreferences); + }; + $scope.removePlacementPreference = function(service, index) { + var removedElement = service.ServicePreferences.splice(index, 1); + if (removedElement !== null) { + updateServiceArray(service, 'ServicePreferences', service.ServicePreferences); + } + }; + $scope.updatePlacementPreference = function(service, constraint) { + updateServiceArray(service, 'ServicePreferences', service.ServicePreferences); + }; + $scope.addPublishedPort = function addPublishedPort(service) { if (!service.Ports) { service.Ports = []; @@ -174,9 +188,9 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll, $('#loadingViewSpinner').show(); var config = ServiceHelper.serviceToConfig(service.Model); config.Name = service.Name; - config.Labels = translateServiceLabelsToLabels(service.ServiceLabels); - config.TaskTemplate.ContainerSpec.Env = translateEnvironmentVariablesToEnv(service.EnvironmentVariables); - config.TaskTemplate.ContainerSpec.Labels = translateServiceLabelsToLabels(service.ServiceContainerLabels); + config.Labels = LabelHelper.fromKeyValueToLabelHash(service.ServiceLabels); + config.TaskTemplate.ContainerSpec.Env = ServiceHelper.translateEnvironmentVariablesToEnv(service.EnvironmentVariables); + config.TaskTemplate.ContainerSpec.Labels = LabelHelper.fromKeyValueToLabelHash(service.ServiceContainerLabels); config.TaskTemplate.ContainerSpec.Image = service.Image; config.TaskTemplate.ContainerSpec.Secrets = service.ServiceSecrets ? service.ServiceSecrets.map(SecretHelper.secretConfig) : []; @@ -188,6 +202,7 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll, config.TaskTemplate.Placement = {}; } config.TaskTemplate.Placement.Constraints = ServiceHelper.translateKeyValueToPlacementConstraints(service.ServiceConstraints); + config.TaskTemplate.Placement.Preferences = ServiceHelper.translateKeyValueToPlacementPreferences(service.ServicePreferences); config.TaskTemplate.Resources = { Limits: { @@ -263,11 +278,12 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll, function translateServiceArrays(service) { service.ServiceSecrets = service.Secrets ? service.Secrets.map(SecretHelper.flattenSecret) : []; - service.EnvironmentVariables = translateEnvironmentVariables(service.Env); - service.ServiceLabels = translateLabelsToServiceLabels(service.Labels); - service.ServiceContainerLabels = translateLabelsToServiceLabels(service.ContainerLabels); + service.EnvironmentVariables = ServiceHelper.translateEnvironmentVariables(service.Env); + service.ServiceLabels = LabelHelper.fromLabelHashToKeyValue(service.Labels); + service.ServiceContainerLabels = LabelHelper.fromLabelHashToKeyValue(service.ContainerLabels); service.ServiceMounts = angular.copy(service.Mounts); - service.ServiceConstraints = translateConstraintsToKeyValue(service.Constraints); + service.ServiceConstraints = ServiceHelper.translateConstraintsToKeyValue(service.Constraints); + service.ServicePreferences = ServiceHelper.translatePreferencesToKeyValue(service.Preferences); } function initView() { @@ -283,7 +299,6 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll, translateServiceArrays(service); $scope.service = service; - ControllerDataPipeline.setAccessControlData('service', $stateParams.id, service.ResourceControl); originalService = angular.copy(service); return $q.all({ @@ -310,7 +325,6 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll, Notifications.error('Failure', err, 'Unable to retrieve service details'); }) .finally(function final() { - $('#loadingViewSpinner').hide(); }); } @@ -341,80 +355,5 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll, service.hasChanges = true; } - function translateEnvironmentVariables(env) { - if (env) { - var variables = []; - env.forEach(function(variable) { - var idx = variable.indexOf('='); - var keyValue = [variable.slice(0,idx), variable.slice(idx+1)]; - var originalValue = (keyValue.length > 1) ? keyValue[1] : ''; - variables.push({ key: keyValue[0], value: originalValue, originalKey: keyValue[0], originalValue: originalValue, added: true}); - }); - return variables; - } - return []; - } - function translateEnvironmentVariablesToEnv(env) { - if (env) { - var variables = []; - env.forEach(function(variable) { - if (variable.key && variable.key !== '') { - variables.push(variable.key + '=' + variable.value); - } - }); - return variables; - } - return []; - } - - function translateLabelsToServiceLabels(Labels) { - var labels = []; - if (Labels) { - Object.keys(Labels).forEach(function(key) { - labels.push({ key: key, value: Labels[key], originalKey: key, originalValue: Labels[key], added: true}); - }); - } - return labels; - } - function translateServiceLabelsToLabels(labels) { - var Labels = {}; - if (labels) { - labels.forEach(function(label) { - Labels[label.key] = label.value; - }); - } - return Labels; - } - - 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 []; - } - initView(); }]); diff --git a/app/components/serviceLogs/serviceLogsController.js b/app/components/serviceLogs/serviceLogsController.js new file mode 100644 index 000000000..9895bbb6e --- /dev/null +++ b/app/components/serviceLogs/serviceLogsController.js @@ -0,0 +1,83 @@ +angular.module('serviceLogs', []) +.controller('ServiceLogsController', ['$scope', '$stateParams', '$anchorScroll', 'ServiceLogs', 'Service', +function ($scope, $stateParams, $anchorScroll, ServiceLogs, Service) { + $scope.state = {}; + $scope.state.displayTimestampsOut = false; + $scope.state.displayTimestampsErr = false; + $scope.stdout = ''; + $scope.stderr = ''; + $scope.tailLines = 2000; + + function getLogs() { + $('#loadingViewSpinner').show(); + getLogsStdout(); + getLogsStderr(); + $('#loadingViewSpinner').hide(); + } + + function getLogsStderr() { + ServiceLogs.get($stateParams.id, { + stdout: 0, + stderr: 1, + timestamps: $scope.state.displayTimestampsErr, + tail: $scope.tailLines + }, function (data, status, headers, config) { + // Replace carriage returns with newlines to clean up output + data = data.replace(/[\r]/g, '\n'); + // Strip 8 byte header from each line of output + data = data.substring(8); + data = data.replace(/\n(.{8})/g, '\n'); + $scope.stderr = data; + }); + } + + function getLogsStdout() { + ServiceLogs.get($stateParams.id, { + stdout: 1, + stderr: 0, + timestamps: $scope.state.displayTimestampsOut, + tail: $scope.tailLines + }, function (data, status, headers, config) { + // Replace carriage returns with newlines to clean up output + data = data.replace(/[\r]/g, '\n'); + // Strip 8 byte header from each line of output + data = data.substring(8); + data = data.replace(/\n(.{8})/g, '\n'); + $scope.stdout = data; + }); + } + + function getService() { + $('#loadingViewSpinner').show(); + Service.get({id: $stateParams.id}, function (d) { + $scope.service = d; + $('#loadingViewSpinner').hide(); + }, function (e) { + Notifications.error('Failure', e, 'Unable to retrieve service info'); + $('#loadingViewSpinner').hide(); + }); + } + + function initView() { + getService(); + getLogs(); + + var logIntervalId = window.setInterval(getLogs, 5000); + + $scope.$on('$destroy', function () { + // clearing interval when view changes + clearInterval(logIntervalId); + }); + + $scope.toggleTimestampsOut = function () { + getLogsStdout(); + }; + + $scope.toggleTimestampsErr = function () { + getLogsStderr(); + }; + } + + initView(); + +}]); diff --git a/app/components/serviceLogs/servicelogs.html b/app/components/serviceLogs/servicelogs.html new file mode 100644 index 000000000..3bbe090e2 --- /dev/null +++ b/app/components/serviceLogs/servicelogs.html @@ -0,0 +1,56 @@ + + + + + + Services > {{ service.Spec.Name }} > Logs + + + +
+
+ + +
+ +
+
{{ service.Spec.Name }}
+
Name
+
+
+
+
+ +
+
+ + + + + + + +
+
{{stdout}}
+
+
+
+
+
+ +
+
+ + + + + + + +
+
{{stderr}}
+
+
+
+
+
diff --git a/app/components/services/services.html b/app/components/services/services.html index 945c6db40..3b373937a 100644 --- a/app/components/services/services.html +++ b/app/components/services/services.html @@ -128,6 +128,6 @@
- + diff --git a/app/components/sidebar/sidebar.html b/app/components/sidebar/sidebar.html index b4a590722..3d8d6a2c3 100644 --- a/app/components/sidebar/sidebar.html +++ b/app/components/sidebar/sidebar.html @@ -58,7 +58,7 @@