diff --git a/api/bolt/endpoint_service.go b/api/bolt/endpoint_service.go index 9046bf30f..f8bbdd795 100644 --- a/api/bolt/endpoint_service.go +++ b/api/bolt/endpoint_service.go @@ -44,7 +44,7 @@ func (service *EndpointService) Endpoint(ID portainer.EndpointID) (*portainer.En // Endpoints return an array containing all the endpoints. func (service *EndpointService) Endpoints() ([]portainer.Endpoint, error) { - var endpoints []portainer.Endpoint + var endpoints = make([]portainer.Endpoint, 0) err := service.store.db.View(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(endpointBucketName)) @@ -160,3 +160,15 @@ func (service *EndpointService) SetActive(endpoint *portainer.Endpoint) error { return nil }) } + +// DeleteActive deletes the active endpoint. +func (service *EndpointService) DeleteActive() error { + return service.store.db.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(activeEndpointBucketName)) + err := bucket.Delete(internal.Itob(activeEndpointID)) + if err != nil { + return err + } + return nil + }) +} diff --git a/api/cli/defaults_windows.go b/api/cli/defaults_windows.go index 3a4106c74..9e4be3a80 100644 --- a/api/cli/defaults_windows.go +++ b/api/cli/defaults_windows.go @@ -2,11 +2,11 @@ package cli const ( defaultBindAddress = ":9000" - defaultDataDirectory = "C:\\data" + defaultDataDirectory = "C:\\ProgramData\\Portainer" defaultAssetsDirectory = "." defaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json" defaultTLSVerify = "false" - defaultTLSCACertPath = "C:\\certs\\ca.pem" - defaultTLSCertPath = "C:\\certs\\cert.pem" - defaultTLSKeyPath = "C:\\certs\\key.pem" + defaultTLSCACertPath = "C:\\ProgramData\\Portainer\\certs\\ca.pem" + defaultTLSCertPath = "C:\\ProgramData\\Portainer\\certs\\cert.pem" + defaultTLSKeyPath = "C:\\ProgramData\\Portainer\\certs\\key.pem" ) diff --git a/api/errors.go b/api/errors.go index 8fcd44758..48ea6ab75 100644 --- a/api/errors.go +++ b/api/errors.go @@ -7,7 +7,8 @@ const ( // User errors. const ( - ErrUserNotFound = Error("User not found") + ErrUserNotFound = Error("User not found") + ErrAdminAlreadyInitialized = Error("Admin user already initialized") ) // Endpoint errors. diff --git a/api/file/file.go b/api/file/file.go index c9ad71a4a..63837d525 100644 --- a/api/file/file.go +++ b/api/file/file.go @@ -34,12 +34,14 @@ func NewService(dataStorePath, fileStorePath string) (*Service, error) { fileStorePath: path.Join(dataStorePath, fileStorePath), } - err := createDirectoryIfNotExist(dataStorePath, 0755) - if err != nil { - return nil, err - } + // Checking if a mount directory exists is broken with Go on Windows. + // This will need to be reviewed after the issue has been fixed in Go. + // err := createDirectoryIfNotExist(dataStorePath, 0755) + // if err != nil { + // return nil, err + // } - err = service.createDirectoryInStoreIfNotExist(TLSStorePath) + err := service.createDirectoryInStoreIfNotExist(TLSStorePath) if err != nil { return nil, err } diff --git a/api/http/docker_handler.go b/api/http/docker_handler.go index 4894b797c..9cffa4b26 100644 --- a/api/http/docker_handler.go +++ b/api/http/docker_handler.go @@ -135,7 +135,7 @@ type unixSocketHandler struct { func (h *unixSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { conn, err := net.Dial("unix", h.path) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + Error(w, err, http.StatusInternalServerError, nil) return } c := httputil.NewClientConn(conn, nil) @@ -143,7 +143,7 @@ func (h *unixSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { res, err := c.Do(r) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + Error(w, err, http.StatusInternalServerError, nil) return } defer res.Body.Close() @@ -154,6 +154,6 @@ func (h *unixSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } if _, err := io.Copy(w, res.Body); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + Error(w, err, http.StatusInternalServerError, nil) } } diff --git a/api/http/endpoint_handler.go b/api/http/endpoint_handler.go index 42a77cbec..9f5ca31c0 100644 --- a/api/http/endpoint_handler.go +++ b/api/http/endpoint_handler.go @@ -260,6 +260,7 @@ type putEndpointsRequest struct { } // handleDeleteEndpoint handles DELETE requests on /endpoints/:id +// DELETE /endpoints/0 deletes the active endpoint func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id := vars["id"] @@ -270,7 +271,14 @@ func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *h return } - endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + var endpoint *portainer.Endpoint + if id == "0" { + endpoint, err = handler.EndpointService.GetActive() + endpointID = int(endpoint.ID) + } else { + endpoint, err = handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + } + if err == portainer.ErrEndpointNotFound { Error(w, err, http.StatusNotFound, handler.Logger) return @@ -284,6 +292,13 @@ func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *h Error(w, err, http.StatusInternalServerError, handler.Logger) return } + if id == "0" { + err = handler.EndpointService.DeleteActive() + if err != nil { + Error(w, err, http.StatusInternalServerError, handler.Logger) + return + } + } if endpoint.TLS { err = handler.FileService.DeleteTLSFiles(portainer.EndpointID(endpointID)) diff --git a/api/http/file_handler.go b/api/http/file_handler.go index 95ebc022c..09a849b7c 100644 --- a/api/http/file_handler.go +++ b/api/http/file_handler.go @@ -1,6 +1,9 @@ package http -import "net/http" +import ( + "net/http" + "strings" +) // FileHandler represents an HTTP API handler for managing static files. type FileHandler struct { @@ -14,7 +17,20 @@ func newFileHandler(assetPath string) *FileHandler { return h } +func isHTML(acceptContent []string) bool { + for _, accept := range acceptContent { + if strings.Contains(accept, "text/html") { + return true + } + } + return false +} + func (fileHandler *FileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Cache-Control", "max-age=31536000") + if !isHTML(r.Header["Accept"]) { + w.Header().Set("Cache-Control", "max-age=31536000") + } else { + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + } fileHandler.Handler.ServeHTTP(w, r) } diff --git a/api/http/handler.go b/api/http/handler.go index 5c88a2805..efce098b5 100644 --- a/api/http/handler.go +++ b/api/http/handler.go @@ -55,7 +55,9 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Error writes an API error message to the response and logger. func Error(w http.ResponseWriter, err error, code int, logger *log.Logger) { // Log error. - logger.Printf("http error: %s (code=%d)", err, code) + if logger != nil { + logger.Printf("http error: %s (code=%d)", err, code) + } // Write generic error response. w.WriteHeader(code) diff --git a/api/http/middleware.go b/api/http/middleware.go index 99775dec6..ff38a736a 100644 --- a/api/http/middleware.go +++ b/api/http/middleware.go @@ -47,13 +47,13 @@ func (service *middleWareService) middleWareAuthenticate(next http.Handler) http } if token == "" { - http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + Error(w, portainer.ErrUnauthorized, http.StatusUnauthorized, nil) return } err := service.jwtService.VerifyToken(token) if err != nil { - http.Error(w, err.Error(), http.StatusUnauthorized) + Error(w, err, http.StatusUnauthorized, nil) return } diff --git a/api/http/templates_handler.go b/api/http/templates_handler.go index 867891291..a72a6a4c1 100644 --- a/api/http/templates_handler.go +++ b/api/http/templates_handler.go @@ -1,7 +1,6 @@ package http import ( - "fmt" "io/ioutil" "log" "net/http" @@ -40,15 +39,13 @@ func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *ht resp, err := http.Get(handler.templatesURL) if err != nil { - log.Print(err) - http.Error(w, fmt.Sprintf("Error making request to %s: %s", handler.templatesURL, err.Error()), http.StatusInternalServerError) + Error(w, err, http.StatusInternalServerError, handler.Logger) return } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { - log.Print(err) - http.Error(w, "Error reading body from templates URL", http.StatusInternalServerError) + Error(w, err, http.StatusInternalServerError, handler.Logger) return } w.Header().Set("Content-Type", "application/json") diff --git a/api/http/upload_handler.go b/api/http/upload_handler.go index 24b992392..a0d53c36b 100644 --- a/api/http/upload_handler.go +++ b/api/http/upload_handler.go @@ -26,7 +26,7 @@ func NewUploadHandler(middleWareService *middleWareService) *UploadHandler { Logger: log.New(os.Stderr, "", log.LstdFlags), middleWareService: middleWareService, } - h.Handle("/upload/tls/{endpointID}/{certificate:(ca|cert|key)}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h.Handle("/upload/tls/{endpointID}/{certificate:(?:ca|cert|key)}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { h.handlePostUploadTLS(w, r) }))) return h diff --git a/api/http/user_handler.go b/api/http/user_handler.go index b47cda7ca..38243b0a6 100644 --- a/api/http/user_handler.go +++ b/api/http/user_handler.go @@ -227,18 +227,28 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R return } - user := &portainer.User{ - Username: "admin", - } - user.Password, err = handler.CryptoService.Hash(req.Password) - if err != nil { - Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger) + user, err := handler.UserService.User("admin") + if err == portainer.ErrUserNotFound { + user := &portainer.User{ + Username: "admin", + } + user.Password, err = handler.CryptoService.Hash(req.Password) + if err != nil { + Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger) + return + } + + err = handler.UserService.UpdateUser(user) + if err != nil { + Error(w, err, http.StatusInternalServerError, handler.Logger) + return + } + } else if err != nil { + Error(w, err, http.StatusInternalServerError, handler.Logger) return } - - err = handler.UserService.UpdateUser(user) - if err != nil { - Error(w, err, http.StatusInternalServerError, handler.Logger) + if user != nil { + Error(w, portainer.ErrAdminAlreadyInitialized, http.StatusForbidden, handler.Logger) return } } diff --git a/api/portainer.go b/api/portainer.go index c03679e84..0bd3fe6d8 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -94,6 +94,7 @@ type ( DeleteEndpoint(ID EndpointID) error GetActive() (*Endpoint, error) SetActive(endpoint *Endpoint) error + DeleteActive() error } // CryptoService represents a service for encrypting/hashing data. @@ -118,7 +119,7 @@ type ( const ( // APIVersion is the version number of portainer API. - APIVersion = "1.11.1" + APIVersion = "1.11.2" ) const ( diff --git a/app/app.js b/app/app.js index 2c2a58589..08d2de18a 100644 --- a/app/app.js +++ b/app/app.js @@ -36,6 +36,7 @@ angular.module('portainer', [ 'swarm', 'network', 'networks', + 'node', 'createNetwork', 'task', 'templates', @@ -49,8 +50,8 @@ angular.module('portainer', [ .setPrefix('portainer'); jwtOptionsProvider.config({ - tokenGetter: ['localStorageService', function(localStorageService) { - return localStorageService.get('JWT'); + tokenGetter: ['LocalStorage', function(LocalStorage) { + return LocalStorage.getJWT(); }], unauthenticatedRedirector: ['$state', function($state) { $state.go('auth', {error: 'Your session has expired'}); @@ -398,6 +399,22 @@ angular.module('portainer', [ requiresLogin: true } }) + .state('node', { + url: '^/nodes/:id/', + views: { + "content": { + templateUrl: 'app/components/node/node.html', + controller: 'NodeController' + }, + "sidebar": { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + }, + data: { + requiresLogin: true + } + }) .state('services', { url: '/services/', views: { @@ -528,21 +545,18 @@ angular.module('portainer', [ }; }); }]) - .run(['$rootScope', '$state', 'Authentication', 'authManager', 'EndpointMode', function ($rootScope, $state, Authentication, authManager, EndpointMode) { + .run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', function ($rootScope, $state, Authentication, authManager, StateManager) { authManager.checkAuthOnRefresh(); authManager.redirectWhenUnauthenticated(); + Authentication.init(); + StateManager.init(); + $rootScope.$state = $state; $rootScope.$on('tokenHasExpired', function($state) { $state.go('auth', {error: 'Your session has expired'}); }); - - $rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams) { - if (toState.name !== 'endpointInit' && (fromState.name === 'auth' || fromState.name === '' || fromState.name === 'endpointInit') && Authentication.isAuthenticated()) { - EndpointMode.determineEndpointMode(); - } - }); }]) // This is your docker url that the api will use to make requests // You need to set this to the api endpoint without the port i.e. http://192.168.1.9 @@ -554,4 +568,4 @@ angular.module('portainer', [ .constant('ENDPOINTS_ENDPOINT', 'api/endpoints') .constant('TEMPLATES_ENDPOINT', 'api/templates') .constant('PAGINATION_MAX_ITEMS', 10) - .constant('UI_VERSION', 'v1.11.1'); + .constant('UI_VERSION', 'v1.11.2'); diff --git a/app/components/auth/authController.js b/app/components/auth/authController.js index 89d99b6d3..2e30f2e5d 100644 --- a/app/components/auth/authController.js +++ b/app/components/auth/authController.js @@ -1,6 +1,6 @@ angular.module('auth', []) -.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Config', 'Authentication', 'Users', 'EndpointService', 'Messages', -function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Authentication, Users, EndpointService, Messages) { +.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Config', 'Authentication', 'Users', 'EndpointService', 'StateManager', 'Messages', +function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Authentication, Users, EndpointService, StateManager, Messages) { $scope.authData = { username: 'admin', @@ -60,7 +60,12 @@ function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Au var password = $sanitize($scope.authData.password); Authentication.login(username, password).then(function success() { EndpointService.getActive().then(function success(data) { - $state.go('dashboard'); + StateManager.updateEndpointState(true) + .then(function success() { + $state.go('dashboard'); + }, function error(err) { + Messages.error("Failure", err, 'Unable to connect to the Docker endpoint'); + }); }, function error(err) { if (err.status === 404) { $state.go('endpointInit'); diff --git a/app/components/container/container.html b/app/components/container/container.html index a204903d4..f618d7403 100644 --- a/app/components/container/container.html +++ b/app/components/container/container.html @@ -225,7 +225,18 @@
- + +
+ Items per page: + +
+
@@ -236,7 +247,7 @@ - + diff --git a/app/components/container/containerController.js b/app/components/container/containerController.js index ae3b793e1..1a1b3fd37 100644 --- a/app/components/container/containerController.js +++ b/app/components/container/containerController.js @@ -1,13 +1,18 @@ angular.module('container', []) -.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ImageHelper', 'Network', 'Messages', 'Settings', -function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ImageHelper, Network, Messages, Settings) { +.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ImageHelper', 'Network', 'Messages', 'Pagination', +function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ImageHelper, Network, Messages, Pagination) { $scope.activityTime = 0; $scope.portBindings = []; $scope.config = { Image: '', Registry: '' }; - $scope.pagination_count = Settings.pagination_count; + $scope.state = {}; + $scope.state.pagination_count = Pagination.getPaginationCount('container_networks'); + + $scope.changePaginationCount = function() { + Pagination.setPaginationCount('container_networks', $scope.state.pagination_count); + }; var update = function () { $('#loadingViewSpinner').show(); diff --git a/app/components/containers/containers.html b/app/components/containers/containers.html index 367763841..1da516551 100644 --- a/app/components/containers/containers.html +++ b/app/components/containers/containers.html @@ -3,6 +3,7 @@ + Containers @@ -11,7 +12,14 @@
- + Items per page: +
@@ -68,7 +76,7 @@ -
- + - - + + - + - + diff --git a/app/components/endpoints/endpointsController.js b/app/components/endpoints/endpointsController.js index 188258ba4..879ecbc1e 100644 --- a/app/components/endpoints/endpointsController.js +++ b/app/components/endpoints/endpointsController.js @@ -1,14 +1,14 @@ angular.module('endpoints', []) -.controller('EndpointsController', ['$scope', '$state', 'EndpointService', 'Settings', 'Messages', -function ($scope, $state, EndpointService, Settings, Messages) { +.controller('EndpointsController', ['$scope', '$state', 'EndpointService', 'Messages', 'Pagination', +function ($scope, $state, EndpointService, Messages, Pagination) { $scope.state = { error: '', uploadInProgress: false, - selectedItemCount: 0 + selectedItemCount: 0, + pagination_count: Pagination.getPaginationCount('endpoints') }; $scope.sortType = 'Name'; $scope.sortReverse = true; - $scope.pagination_count = Settings.pagination_count; $scope.formValues = { Name: '', @@ -24,6 +24,10 @@ function ($scope, $state, EndpointService, Settings, Messages) { $scope.sortType = sortType; }; + $scope.changePaginationCount = function() { + Pagination.setPaginationCount('endpoints', $scope.state.pagination_count); + }; + $scope.selectItem = function (item) { if (item.Checked) { $scope.state.selectedItemCount++; diff --git a/app/components/events/events.html b/app/components/events/events.html index d0de750ab..e59c9fc8d 100644 --- a/app/components/events/events.html +++ b/app/components/events/events.html @@ -3,6 +3,7 @@ + Events @@ -12,7 +13,14 @@
- + Items per page: +
@@ -49,7 +57,7 @@
- + diff --git a/app/components/events/eventsController.js b/app/components/events/eventsController.js index 330e388ec..b4f2ac9d8 100644 --- a/app/components/events/eventsController.js +++ b/app/components/events/eventsController.js @@ -1,16 +1,20 @@ angular.module('events', []) -.controller('EventsController', ['$scope', 'Settings', 'Messages', 'Events', -function ($scope, Settings, Messages, Events) { +.controller('EventsController', ['$scope', 'Messages', 'Events', 'Pagination', +function ($scope, Messages, Events, Pagination) { $scope.state = {}; + $scope.state.pagination_count = Pagination.getPaginationCount('events'); $scope.sortType = 'Time'; $scope.sortReverse = true; - $scope.pagination_count = Settings.pagination_count; $scope.order = function(sortType) { $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; $scope.sortType = sortType; }; + $scope.changePaginationCount = function() { + Pagination.setPaginationCount('events', $scope.state.pagination_count); + }; + var from = moment().subtract(24, 'hour').unix(); var to = moment().unix(); diff --git a/app/components/images/images.html b/app/components/images/images.html index 65b8577bb..972b0602b 100644 --- a/app/components/images/images.html +++ b/app/components/images/images.html @@ -3,6 +3,7 @@ + Images @@ -50,7 +51,14 @@
- + Items per page: +
@@ -100,7 +108,7 @@
- + - + diff --git a/app/components/networks/networksController.js b/app/components/networks/networksController.js index 0ec8f30ee..120ab0c86 100644 --- a/app/components/networks/networksController.js +++ b/app/components/networks/networksController.js @@ -1,19 +1,23 @@ angular.module('networks', []) -.controller('NetworksController', ['$scope', '$state', 'Network', 'Config', 'Messages', 'Settings', -function ($scope, $state, Network, Config, Messages, Settings) { +.controller('NetworksController', ['$scope', '$state', 'Network', 'Config', 'Messages', 'Pagination', +function ($scope, $state, Network, Config, Messages, Pagination) { $scope.state = {}; + $scope.state.pagination_count = Pagination.getPaginationCount('networks'); $scope.state.selectedItemCount = 0; $scope.state.advancedSettings = false; $scope.sortType = 'Name'; $scope.sortReverse = false; - $scope.pagination_count = Settings.pagination_count; $scope.config = { Name: '' }; + $scope.changePaginationCount = function() { + Pagination.setPaginationCount('networks', $scope.state.pagination_count); + }; + function prepareNetworkConfiguration() { var config = angular.copy($scope.config); - if ($scope.endpointMode.provider === 'DOCKER_SWARM' || $scope.endpointMode.provider === 'DOCKER_SWARM_MODE') { + if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') { config.Driver = 'overlay'; // Force IPAM Driver to 'default', should not be required. // See: https://github.com/docker/docker/issues/25735 diff --git a/app/components/node/node.html b/app/components/node/node.html new file mode 100644 index 000000000..996cf3c06 --- /dev/null +++ b/app/components/node/node.html @@ -0,0 +1,273 @@ + + + + + + + + Swarm nodes > {{ node.Hostname }} + + + +
+
+
+ Loading... +
+ + + + +

It looks like the node you wish to inspect does not exist.

+
+
+
+
+ +
+
+ + + +
Actions
{{ key }} {{ value.IPAddress || '-' }} {{ value.Gateway || '-' }} + Host IP @@ -77,7 +85,7 @@ - Exposed Ports + Published Ports @@ -85,14 +93,14 @@
{{ container.Status }}{{ container|swarmcontainername}}{{ container|containername}}{{ container|swarmcontainername}}{{ container|containername}} {{ container.Image }} {{ container.IP ? container.IP : '-' }}{{ container.hostIP }}{{ container.hostIP }} {{p.public}}:{{ p.private }} diff --git a/app/components/containers/containersController.js b/app/components/containers/containersController.js index 92fe95a9e..59f8caa6b 100644 --- a/app/components/containers/containersController.js +++ b/app/components/containers/containersController.js @@ -1,18 +1,22 @@ angular.module('containers', []) -.controller('ContainersController', ['$scope', '$filter', 'Container', 'ContainerHelper', 'Info', 'Settings', 'Messages', 'Config', -function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages, Config) { +.controller('ContainersController', ['$scope', '$filter', 'Container', 'ContainerHelper', 'Info', 'Settings', 'Messages', 'Config', 'Pagination', +function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages, Config, Pagination) { $scope.state = {}; + $scope.state.pagination_count = Pagination.getPaginationCount('containers'); $scope.state.displayAll = Settings.displayAll; $scope.state.displayIP = false; $scope.sortType = 'State'; $scope.sortReverse = false; $scope.state.selectedItemCount = 0; - $scope.pagination_count = Settings.pagination_count; $scope.order = function (sortType) { $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; $scope.sortType = sortType; }; + $scope.changePaginationCount = function() { + Pagination.setPaginationCount('containers', $scope.state.pagination_count); + }; + var update = function (data) { $('#loadContainersSpinner').show(); $scope.state.selectedItemCount = 0; @@ -28,7 +32,7 @@ function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages, if (model.IP) { $scope.state.displayIP = true; } - if ($scope.endpointMode.provider === 'DOCKER_SWARM') { + if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM') { model.hostIP = $scope.swarm_hosts[_.split(container.Names[0], '/')[1]]; } return model; @@ -77,6 +81,19 @@ function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages, complete(); }); } + else if (action === Container.pause) { + action({id: c.Id}, function (d) { + if (d.message) { + Messages.send("Container is already paused", c.Id); + } else { + Messages.send("Container " + msg, c.Id); + } + complete(); + }, function (e) { + Messages.error("Failure", e, 'Unable to pause container'); + complete(); + }); + } else { action({id: c.Id}, function (d) { Messages.send("Container " + msg, c.Id); @@ -161,7 +178,7 @@ function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages, Config.$promise.then(function (c) { $scope.containersToHideLabels = c.hiddenLabels; - if ($scope.endpointMode.provider === 'DOCKER_SWARM') { + if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM') { Info.get({}, function (d) { $scope.swarm_hosts = retrieveSwarmHostsInfo(d); update({all: Settings.displayAll ? 1 : 0}); diff --git a/app/components/createContainer/createContainerController.js b/app/components/createContainer/createContainerController.js index be70bdec7..606e84d04 100644 --- a/app/components/createContainer/createContainerController.js +++ b/app/components/createContainer/createContainerController.js @@ -72,7 +72,7 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai Network.query({}, function (d) { var networks = d; - if ($scope.endpointMode.provider === 'DOCKER_SWARM' || $scope.endpointMode.provider === 'DOCKER_SWARM_MODE') { + if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') { networks = d.filter(function (network) { if (network.Scope === 'global') { return network; @@ -220,7 +220,7 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai var containerName = container; if (container && typeof container === 'object') { containerName = $filter('trimcontainername')(container.Names[0]); - if ($scope.endpointMode.provider === 'DOCKER_SWARM') { + if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM') { containerName = $filter('swarmcontainername')(container); } } diff --git a/app/components/createContainer/createcontainer.html b/app/components/createContainer/createcontainer.html index d7697b0b8..9a27c741a 100644 --- a/app/components/createContainer/createcontainer.html +++ b/app/components/createContainer/createcontainer.html @@ -95,16 +95,6 @@ - -
- -
- - label - -
-
- @@ -269,7 +259,7 @@
-
+
@@ -289,10 +279,10 @@
- -
diff --git a/app/components/dashboard/dashboard.html b/app/components/dashboard/dashboard.html index f9beebe91..e2aed5b41 100644 --- a/app/components/dashboard/dashboard.html +++ b/app/components/dashboard/dashboard.html @@ -6,7 +6,7 @@
-
+
@@ -33,7 +33,7 @@
-
+
@@ -60,7 +60,7 @@
-
+
diff --git a/app/components/endpoint/endpointController.js b/app/components/endpoint/endpointController.js index 1d8d4cedc..0c254b3df 100644 --- a/app/components/endpoint/endpointController.js +++ b/app/components/endpoint/endpointController.js @@ -19,7 +19,8 @@ function ($scope, $state, $stateParams, $filter, EndpointService, Messages) { var TLSCACert = $scope.formValues.TLSCACert !== $scope.endpoint.TLSCACert ? $scope.formValues.TLSCACert : null; var TLSCert = $scope.formValues.TLSCert !== $scope.endpoint.TLSCert ? $scope.formValues.TLSCert : null; var TLSKey = $scope.formValues.TLSKey !== $scope.endpoint.TLSKey ? $scope.formValues.TLSKey : null; - EndpointService.updateEndpoint(ID, name, URL, TLS, TLSCACert, TLSCert, TLSKey).then(function success(data) { + var type = $scope.endpointType; + EndpointService.updateEndpoint(ID, name, URL, TLS, TLSCACert, TLSCert, TLSKey, type).then(function success(data) { Messages.send("Endpoint updated", $scope.endpoint.Name); $state.go('endpoints'); }, function error(err) { diff --git a/app/components/endpointInit/endpointInit.html b/app/components/endpointInit/endpointInit.html index 7d0f46eb5..5993ec833 100644 --- a/app/components/endpointInit/endpointInit.html +++ b/app/components/endpointInit/endpointInit.html @@ -21,10 +21,10 @@
- +
- +
@@ -41,7 +41,10 @@

{{ state.error }}

- + + + +
@@ -122,7 +125,10 @@

{{ state.error }}

- + + + +
diff --git a/app/components/endpointInit/endpointInitController.js b/app/components/endpointInit/endpointInitController.js index 2478bf8cf..b6aa99cdd 100644 --- a/app/components/endpointInit/endpointInitController.js +++ b/app/components/endpointInit/endpointInitController.js @@ -1,6 +1,6 @@ angular.module('endpointInit', []) -.controller('EndpointInitController', ['$scope', '$state', 'EndpointService', 'Messages', -function ($scope, $state, EndpointService, Messages) { +.controller('EndpointInitController', ['$scope', '$state', 'EndpointService', 'StateManager', 'Messages', +function ($scope, $state, EndpointService, StateManager, Messages) { $scope.state = { error: '', uploadInProgress: false @@ -15,27 +15,39 @@ function ($scope, $state, EndpointService, Messages) { TLSKey: null }; - EndpointService.getActive().then(function success(data) { + if (!_.isEmpty($scope.applicationState.endpoint)) { $state.go('dashboard'); - }, function error(err) { - if (err.status !== 404) { - Messages.error("Failure", err, 'Unable to verify Docker endpoint existence'); - } - }); + } + + $scope.cleanError = function() { + $scope.state.error = ''; + }; $scope.createLocalEndpoint = function() { + $('#initEndpointSpinner').show(); $scope.state.error = ''; var name = "local"; var URL = "unix:///var/run/docker.sock"; var TLS = false; EndpointService.createLocalEndpoint(name, URL, TLS, true).then(function success(data) { - $state.go('dashboard'); + StateManager.updateEndpointState(false) + .then(function success() { + $state.go('dashboard'); + }, function error(err) { + EndpointService.deleteEndpoint(0) + .then(function success() { + $('#initEndpointSpinner').hide(); + $scope.state.error = 'Unable to connect to the Docker endpoint'; + }); + }); }, function error(err) { + $('#initEndpointSpinner').hide(); $scope.state.error = 'Unable to create endpoint'; }); }; $scope.createRemoteEndpoint = function() { + $('#initEndpointSpinner').show(); $scope.state.error = ''; var name = $scope.formValues.Name; var URL = $scope.formValues.URL; @@ -43,9 +55,20 @@ function ($scope, $state, EndpointService, Messages) { var TLSCAFile = $scope.formValues.TLSCACert; var TLSCertFile = $scope.formValues.TLSCert; var TLSKeyFile = $scope.formValues.TLSKey; - EndpointService.createRemoteEndpoint(name, URL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile, TLS ? false : true).then(function success(data) { - $state.go('dashboard'); + EndpointService.createRemoteEndpoint(name, URL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile, TLS ? false : true) + .then(function success(data) { + StateManager.updateEndpointState(false) + .then(function success() { + $state.go('dashboard'); + }, function error(err) { + EndpointService.deleteEndpoint(0) + .then(function success() { + $('#initEndpointSpinner').hide(); + $scope.state.error = 'Unable to connect to the Docker endpoint'; + }); + }); }, function error(err) { + $('#initEndpointSpinner').hide(); $scope.state.uploadInProgress = false; $scope.state.error = err.msg; }, function update(evt) { diff --git a/app/components/endpoints/endpoints.html b/app/components/endpoints/endpoints.html index 6d05247b1..7c6acea3e 100644 --- a/app/components/endpoints/endpoints.html +++ b/app/components/endpoints/endpoints.html @@ -3,6 +3,7 @@ + Endpoint management @@ -101,7 +102,14 @@
- + Items per page: +
@@ -143,7 +151,7 @@
{{ endpoint.Name }} {{ endpoint.URL | stripprotocol }}
{{ event.Time|getisodatefromtimestamp }} {{ event.Type }} {{ event.Details }}
{{ image.Id|truncate:20}} diff --git a/app/components/images/imagesController.js b/app/components/images/imagesController.js index fe7c7cf08..7000cddaa 100644 --- a/app/components/images/imagesController.js +++ b/app/components/images/imagesController.js @@ -1,11 +1,11 @@ angular.module('images', []) -.controller('ImagesController', ['$scope', '$state', 'Config', 'Image', 'ImageHelper', 'Messages', 'Settings', -function ($scope, $state, Config, Image, ImageHelper, Messages, Settings) { +.controller('ImagesController', ['$scope', '$state', 'Config', 'Image', 'ImageHelper', 'Messages', 'Pagination', +function ($scope, $state, Config, Image, ImageHelper, Messages, Pagination) { $scope.state = {}; + $scope.state.pagination_count = Pagination.getPaginationCount('images'); $scope.sortType = 'RepoTags'; $scope.sortReverse = true; $scope.state.selectedItemCount = 0; - $scope.pagination_count = Settings.pagination_count; $scope.config = { Image: '', @@ -17,6 +17,10 @@ function ($scope, $state, Config, Image, ImageHelper, Messages, Settings) { $scope.sortType = sortType; }; + $scope.changePaginationCount = function() { + Pagination.setPaginationCount('images', $scope.state.pagination_count); + }; + $scope.selectItems = function (allSelected) { angular.forEach($scope.state.filteredImages, function (image) { if (image.Checked !== allSelected) { diff --git a/app/components/main/mainController.js b/app/components/main/mainController.js index 3a48c9bc4..3bfd98171 100644 --- a/app/components/main/mainController.js +++ b/app/components/main/mainController.js @@ -1,6 +1,6 @@ angular.module('main', []) -.controller('MainController', ['$scope', '$cookieStore', -function ($scope, $cookieStore) { +.controller('MainController', ['$scope', '$cookieStore', 'StateManager', +function ($scope, $cookieStore, StateManager) { /** * Sidebar Toggle & Cookie Control @@ -10,6 +10,8 @@ function ($scope, $cookieStore) { return window.innerWidth; }; + $scope.applicationState = StateManager.getState(); + $scope.$watch($scope.getWidth, function(newValue, oldValue) { if (newValue >= mobileView) { if (angular.isDefined($cookieStore.get('toggle'))) { diff --git a/app/components/network/networkController.js b/app/components/network/networkController.js index 656a6b062..77287618d 100644 --- a/app/components/network/networkController.js +++ b/app/components/network/networkController.js @@ -1,6 +1,6 @@ angular.module('network', []) -.controller('NetworkController', ['$scope', '$state', '$stateParams', 'Network', 'Container', 'ContainerHelper', 'Messages', -function ($scope, $state, $stateParams, Network, Container, ContainerHelper, Messages) { +.controller('NetworkController', ['$scope', '$state', '$stateParams', '$filter', 'Config', 'Network', 'Container', 'ContainerHelper', 'Messages', +function ($scope, $state, $stateParams, $filter, Config, Network, Container, ContainerHelper, Messages) { $scope.removeNetwork = function removeNetwork(networkId) { $('#loadingViewSpinner').show(); @@ -38,34 +38,63 @@ function ($scope, $state, $stateParams, Network, Container, ContainerHelper, Mes function getNetwork() { $('#loadingViewSpinner').show(); - Network.get({id: $stateParams.id}, function (d) { - $scope.network = d; - getContainersInNetwork(d); + Network.get({id: $stateParams.id}, function success(data) { + $scope.network = data; + getContainersInNetwork(data); + }, function error(err) { $('#loadingViewSpinner').hide(); - }, function (e) { - $('#loadingViewSpinner').hide(); - Messages.error("Failure", e, "Unable to retrieve network info"); + Messages.error("Failure", err, "Unable to retrieve network info"); }); } + function filterContainersInNetwork(network, containers) { + if ($scope.containersToHideLabels) { + containers = ContainerHelper.hideContainers(containers, $scope.containersToHideLabels); + } + var containersInNetwork = []; + containers.forEach(function(container) { + var containerInNetwork = network.Containers[container.Id]; + containerInNetwork.Id = container.Id; + // Name is not available in Docker 1.9 + if (!containerInNetwork.Name) { + containerInNetwork.Name = $filter('trimcontainername')(container.Names[0]); + } + containersInNetwork.push(containerInNetwork); + }); + $scope.containersInNetwork = containersInNetwork; + } + function getContainersInNetwork(network) { if (network.Containers) { - Container.query({ - filters: {network: [$stateParams.id]} - }, function (containersInNetworkResult) { - if ($scope.containersToHideLabels) { - containersInNetworkResult = ContainerHelper.hideContainers(containersInNetworkResult, $scope.containersToHideLabels); - } - var containersInNetwork = []; - containersInNetworkResult.forEach(function(container) { - var containerInNetwork = network.Containers[container.Id]; - containerInNetwork.Id = container.Id; - containersInNetwork.push(containerInNetwork); + if ($scope.applicationState.endpoint.apiVersion < 1.24) { + Container.query({}, function success(data) { + var containersInNetwork = data.filter(function filter(container) { + if (container.HostConfig.NetworkMode === network.Name) { + return container; + } + }); + filterContainersInNetwork(network, containersInNetwork); + $('#loadingViewSpinner').hide(); + }, function error(err) { + $('#loadingViewSpinner').hide(); + Messages.error("Failure", err, "Unable to retrieve containers in network"); }); - $scope.containersInNetwork = containersInNetwork; - }); + } else { + Container.query({ + filters: {network: [$stateParams.id]} + }, function success(data) { + filterContainersInNetwork(network, data); + $('#loadingViewSpinner').hide(); + }, function error(err) { + $('#loadingViewSpinner').hide(); + Messages.error("Failure", err, "Unable to retrieve containers in network"); + }); + } } } - getNetwork(); + Config.$promise.then(function (c) { + $scope.containersToHideLabels = c.hiddenLabels; + getNetwork(); + }); }]); diff --git a/app/components/networks/networks.html b/app/components/networks/networks.html index 3903365bf..15a5a5021 100644 --- a/app/components/networks/networks.html +++ b/app/components/networks/networks.html @@ -3,6 +3,7 @@ + Networks @@ -23,12 +24,12 @@ -
+
Note: The network will be created using the overlay driver and will allow containers to communicate across the hosts of your cluster.
-
+
Note: The network will be created using the bridge driver.
@@ -52,7 +53,14 @@
- + Items per page: +
@@ -123,7 +131,7 @@
{{ network.Name|truncate:40}} {{ network.Id }}
+ + + + + + + + + + + + + + + + + + + + + + +
Name + +
Host name{{ node.Hostname }}
Role{{ node.Role }}
Availability +
+ +
+
Status{{ node.Status }}
+
+ +

+ View the Docker Swarm mode Node documentation here. +

+ +
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + +
Leader + Yes + No +
Reachability{{ node.Reachability }}
Manager address{{ node.ManagerAddr }}
+
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + +
CPU{{ node.CPUs / 1000000000 }}
Memory{{ node.Memory|humansize: 2 }}
Platform{{ node.PlatformOS }} {{ node.PlatformArchitecture }}
Docker Engine version{{ node.EngineVersion }}
+
+
+
+
+ +
+
+ + + + + +

There are no labels for this node.

+
+ + + + + + + + + + + + + + +
LabelValue
+
+ name + +
+
+
+ value + + + + +
+
+
+ + + +
+
+
+ +
+
+ + +
+ Items per page: + +
+
+ + + + + + + + + + + + + + + + + + + + +
Id + + Status + + + + + + Slot + + + + + + Image + + + + + + Last update + + + +
{{ task.Id }}{{ task.Status }}{{ task.Slot }}{{ task.Image }}{{ task.Updated|getisodate }}
+
+ +
+
+
+
+
diff --git a/app/components/node/nodeController.js b/app/components/node/nodeController.js new file mode 100644 index 000000000..517424296 --- /dev/null +++ b/app/components/node/nodeController.js @@ -0,0 +1,112 @@ +angular.module('node', []) +.controller('NodeController', ['$scope', '$state', '$stateParams', 'LabelHelper', 'Node', 'NodeHelper', 'Task', 'Pagination', 'Messages', +function ($scope, $state, $stateParams, LabelHelper, Node, NodeHelper, Task, Pagination, Messages) { + + $scope.state = {}; + $scope.state.pagination_count = Pagination.getPaginationCount('node_tasks'); + $scope.loading = true; + $scope.tasks = []; + $scope.displayNode = false; + $scope.sortType = 'Status'; + $scope.sortReverse = false; + + var originalNode = {}; + var editedKeys = []; + + $scope.order = function(sortType) { + $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; + $scope.sortType = sortType; + }; + + $scope.changePaginationCount = function() { + Pagination.setPaginationCount('node_tasks', $scope.state.pagination_count); + }; + + $scope.updateNodeAttribute = function updateNodeAttribute(node, key) { + editedKeys.push(key); + }; + $scope.addLabel = function addLabel(node) { + node.Labels.push({ key: '', value: '', originalValue: '', originalKey: '' }); + $scope.updateNodeAttribute(node, 'Labels'); + }; + $scope.removeLabel = function removeLabel(node, index) { + var removedElement = node.Labels.splice(index, 1); + if (removedElement !== null) { + $scope.updateNodeAttribute(node, 'Labels'); + } + }; + $scope.updateLabel = function updateLabel(node, label) { + if (label.value !== label.originalValue || label.key !== label.originalKey) { + $scope.updateNodeAttribute(node, 'Labels'); + } + }; + + $scope.hasChanges = function(node, elements) { + if (!elements) { + elements = Object.keys(originalNode); + } + var hasChanges = false; + elements.forEach(function(key) { + hasChanges = hasChanges || ((editedKeys.indexOf(key) >= 0) && node[key] !== originalNode[key]); + }); + return hasChanges; + }; + + $scope.cancelChanges = function(node) { + editedKeys.forEach(function(key) { + node[key] = originalNode[key]; + }); + editedKeys = []; + }; + + $scope.updateNode = function updateNode(node) { + var config = NodeHelper.nodeToConfig(node.Model); + config.Name = node.Name; + config.Availability = node.Availability; + config.Role = node.Role; + config.Labels = LabelHelper.fromKeyValueToLabelHash(node.Labels); + + Node.update({ id: node.Id, version: node.Version }, config, function (data) { + $('#loadServicesSpinner').hide(); + Messages.send("Node successfully updated", "Node updated"); + $state.go('node', {id: node.Id}, {reload: true}); + }, function (e) { + $('#loadServicesSpinner').hide(); + Messages.error("Failure", e, "Failed to update node"); + }); + }; + + function loadNodeAndTasks() { + $scope.loading = true; + if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') { + Node.get({ id: $stateParams.id}, function(d) { + if (d.message) { + Messages.error("Failure", e, "Unable to inspect the node"); + } else { + var node = new NodeViewModel(d); + originalNode = angular.copy(node); + $scope.node = node; + getTasks(d); + } + $scope.loading = false; + }); + } else { + $scope.loading = false; + } + } + + function getTasks(node) { + if (node) { + Task.query({filters: {node: [node.ID]}}, function (tasks) { + $scope.tasks = tasks.map(function (task) { + return new TaskViewModel(task, [node]); + }); + }, function (e) { + Messages.error("Failure", e, "Unable to retrieve tasks associated to the node"); + }); + } + } + + loadNodeAndTasks(); + +}]); diff --git a/app/components/service/service.html b/app/components/service/service.html index ebfc0b639..ff6b66d2c 100644 --- a/app/components/service/service.html +++ b/app/components/service/service.html @@ -227,7 +227,18 @@
- + +
+ Items per page: + +
+
@@ -264,7 +275,7 @@ - + diff --git a/app/components/service/serviceController.js b/app/components/service/serviceController.js index 5181d0a47..35b086847 100644 --- a/app/components/service/serviceController.js +++ b/app/components/service/serviceController.js @@ -1,13 +1,14 @@ angular.module('service', []) -.controller('ServiceController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Task', 'Node', 'Messages', 'Settings', -function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Messages, Settings) { +.controller('ServiceController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Task', 'Node', 'Messages', 'Pagination', +function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Messages, Pagination) { + $scope.state = {}; + $scope.state.pagination_count = Pagination.getPaginationCount('service_tasks'); $scope.service = {}; $scope.tasks = []; $scope.displayNode = false; $scope.sortType = 'Status'; $scope.sortReverse = false; - $scope.pagination_count = Settings.pagination_count; var previousServiceValues = {}; @@ -16,6 +17,10 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Mess $scope.sortType = sortType; }; + $scope.changePaginationCount = function() { + Pagination.setPaginationCount('service_tasks', $scope.state.pagination_count); + }; + $scope.renameService = function renameService(service) { updateServiceAttribute(service, 'Name', service.newServiceName || service.name); service.EditName = false; diff --git a/app/components/services/services.html b/app/components/services/services.html index ced183077..0e958cecc 100644 --- a/app/components/services/services.html +++ b/app/components/services/services.html @@ -3,6 +3,7 @@ + Services @@ -12,7 +13,14 @@
- + Items per page: +
@@ -52,7 +60,7 @@
- + diff --git a/app/components/services/servicesController.js b/app/components/services/servicesController.js index b6587a04e..1fb5c1dbb 100644 --- a/app/components/services/servicesController.js +++ b/app/components/services/servicesController.js @@ -1,11 +1,28 @@ angular.module('services', []) -.controller('ServicesController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Messages', 'Settings', -function ($scope, $stateParams, $state, Service, ServiceHelper, Messages, Settings) { +.controller('ServicesController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Messages', 'Pagination', +function ($scope, $stateParams, $state, Service, ServiceHelper, Messages, Pagination) { $scope.state = {}; $scope.state.selectedItemCount = 0; + $scope.state.pagination_count = Pagination.getPaginationCount('services'); $scope.sortType = 'Name'; $scope.sortReverse = false; - $scope.pagination_count = Settings.pagination_count; + + $scope.changePaginationCount = function() { + Pagination.setPaginationCount('services', $scope.state.pagination_count); + }; + + $scope.order = function (sortType) { + $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; + $scope.sortType = sortType; + }; + + $scope.selectItem = function (item) { + if (item.Checked) { + $scope.state.selectedItemCount++; + } else { + $scope.state.selectedItemCount--; + } + }; $scope.scaleService = function scaleService(service) { $('#loadServicesSpinner').show(); @@ -23,19 +40,6 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Messages, Settin }); }; - $scope.order = function (sortType) { - $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; - $scope.sortType = sortType; - }; - - $scope.selectItem = function (item) { - if (item.Checked) { - $scope.state.selectedItemCount++; - } else { - $scope.state.selectedItemCount--; - } - }; - $scope.removeAction = function () { $('#loadServicesSpinner').show(); var counter = 0; diff --git a/app/components/sidebar/sidebar.html b/app/components/sidebar/sidebar.html index fda02fb20..d8e97c81b 100644 --- a/app/components/sidebar/sidebar.html +++ b/app/components/sidebar/sidebar.html @@ -22,7 +22,7 @@ - - - -
{{ task.Id }} {{ task.Status }} {{ task.Slot }}
{{ service.Name }} {{ service.Image }}
@@ -69,7 +80,7 @@ - + diff --git a/app/components/stats/statsController.js b/app/components/stats/statsController.js index 88e6e1f34..475af604c 100644 --- a/app/components/stats/statsController.js +++ b/app/components/stats/statsController.js @@ -1,17 +1,20 @@ angular.module('stats', []) -.controller('StatsController', ['Settings', '$scope', 'Messages', '$timeout', 'Container', 'ContainerTop', '$stateParams', 'humansizeFilter', '$sce', '$document', -function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateParams, humansizeFilter, $sce, $document) { +.controller('StatsController', ['Pagination', '$scope', 'Messages', '$timeout', 'Container', 'ContainerTop', '$stateParams', 'humansizeFilter', '$sce', '$document', +function (Pagination, $scope, Messages, $timeout, Container, ContainerTop, $stateParams, humansizeFilter, $sce, $document) { // TODO: Force scale to 0-100 for cpu, fix charts on dashboard, // TODO: Force memory scale to 0 - max memory $scope.ps_args = ''; $scope.state = {}; + $scope.state.pagination_count = Pagination.getPaginationCount('stats_processes'); $scope.sortType = 'CMD'; $scope.sortReverse = false; - $scope.pagination_count = Settings.pagination_count; $scope.order = function (sortType) { $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; $scope.sortType = sortType; }; + $scope.changePaginationCount = function() { + Pagination.setPaginationCount('stats_processes', $scope.state.pagination_count); + }; $scope.getTop = function () { ContainerTop.get($stateParams.id, { ps_args: $scope.ps_args @@ -114,6 +117,12 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP }); $scope.networkLegend = $sce.trustAsHtml(networkChart.generateLegend()); + function setUpdateStatsTimeout() { + if(!destroyed) { + timeout = $timeout(updateStats, 5000); + } + } + function updateStats() { Container.stats({id: $stateParams.id}, function (d) { var arr = Object.keys(d).map(function (key) { @@ -129,15 +138,17 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP updateCpuChart(d); updateMemoryChart(d); updateNetworkChart(d); - timeout = $timeout(updateStats, 5000); + setUpdateStatsTimeout(); }, function () { Messages.error('Unable to retrieve stats', {}, 'Is this container running?'); - timeout = $timeout(updateStats, 5000); + setUpdateStatsTimeout(); }); } + var destroyed = false; var timeout; $scope.$on('$destroy', function () { + destroyed = true; $timeout.cancel(timeout); }); @@ -162,16 +173,18 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP $scope.networkName = Object.keys(data.networks)[0]; data.network = data.networks[$scope.networkName]; } - var rxBytes = 0, txBytes = 0; - if (lastRxBytes !== 0 || lastTxBytes !== 0) { - // These will be zero on first call, ignore to prevent large graph spike - rxBytes = data.network.rx_bytes - lastRxBytes; - txBytes = data.network.tx_bytes - lastTxBytes; + if(data.network) { + var rxBytes = 0, txBytes = 0; + if (lastRxBytes !== 0 || lastTxBytes !== 0) { + // These will be zero on first call, ignore to prevent large graph spike + rxBytes = data.network.rx_bytes - lastRxBytes; + txBytes = data.network.tx_bytes - lastTxBytes; + } + lastRxBytes = data.network.rx_bytes; + lastTxBytes = data.network.tx_bytes; + networkChart.addData([rxBytes, txBytes], new Date(data.read).toLocaleTimeString()); + networkChart.removeData(); } - lastRxBytes = data.network.rx_bytes; - lastTxBytes = data.network.tx_bytes; - networkChart.addData([rxBytes, txBytes], new Date(data.read).toLocaleTimeString()); - networkChart.removeData(); } function calculateCPUPercent(stats) { diff --git a/app/components/swarm/swarm.html b/app/components/swarm/swarm.html index 71ed078ca..661dbd710 100644 --- a/app/components/swarm/swarm.html +++ b/app/components/swarm/swarm.html @@ -16,14 +16,14 @@ - - + + - + - + @@ -31,29 +31,29 @@ - + - - + + - - + + - + - + - + @@ -65,18 +65,29 @@
-
+
- + +
+ Items per page: + +
+
{{processInfo}}
Nodes{{ swarm.Nodes }}{{ info.Swarm.Nodes }}{{ swarm.Nodes }}{{ info.Swarm.Nodes }}
Images {{ info.Images }}
Swarm version {{ docker.Version|swarmversion }}
Docker API version {{ docker.ApiVersion }}
Strategy {{ swarm.Strategy }}
Total CPU{{ info.NCPU }}{{ totalCPU }}{{ info.NCPU }}{{ totalCPU }}
Total memory{{ info.MemTotal|humansize: 2 }}{{ totalMemory|humansize: 2 }}{{ info.MemTotal|humansize: 2 }}{{ totalMemory|humansize: 2 }}
Operating system {{ info.OperatingSystem }}
Kernel version {{ info.KernelVersion }}
Go version {{ docker.GoVersion }}
- + @@ -133,60 +144,71 @@ -
+
- + +
+ Items per page: + +
+
- + Name - - + + @@ -94,30 +105,30 @@ - + IP - - + + - + Engine - - + + - + Status - - + +
{{ node.name }} {{ node.cpu }} {{ node.memory }}
- - + + diff --git a/app/components/swarm/swarmController.js b/app/components/swarm/swarmController.js index 2f7659407..713db07ef 100644 --- a/app/components/swarm/swarmController.js +++ b/app/components/swarm/swarmController.js @@ -1,28 +1,32 @@ angular.module('swarm', []) -.controller('SwarmController', ['$scope', 'Info', 'Version', 'Node', 'Settings', -function ($scope, Info, Version, Node, Settings) { - - $scope.sortType = 'Name'; - $scope.sortReverse = true; +.controller('SwarmController', ['$scope', 'Info', 'Version', 'Node', 'Pagination', +function ($scope, Info, Version, Node, Pagination) { + $scope.state = {}; + $scope.state.pagination_count = Pagination.getPaginationCount('swarm_nodes'); + $scope.sortType = 'Spec.Role'; + $scope.sortReverse = false; $scope.info = {}; $scope.docker = {}; $scope.swarm = {}; $scope.totalCPU = 0; $scope.totalMemory = 0; - $scope.pagination_count = Settings.pagination_count; $scope.order = function(sortType) { $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; $scope.sortType = sortType; }; + $scope.changePaginationCount = function() { + Pagination.setPaginationCount('swarm_nodes', $scope.state.pagination_count); + }; + Version.get({}, function (d) { $scope.docker = d; }); Info.get({}, function (d) { $scope.info = d; - if ($scope.endpointMode.provider === 'DOCKER_SWARM_MODE') { + if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') { Node.query({}, function(d) { $scope.nodes = d; var CPU = 0, memory = 0; diff --git a/app/components/templates/templates.html b/app/components/templates/templates.html index f6b402aef..57ea3286f 100644 --- a/app/components/templates/templates.html +++ b/app/components/templates/templates.html @@ -3,6 +3,7 @@ + Templates @@ -13,12 +14,12 @@ -
+
When using Swarm, we recommend deploying containers in a shared network. Looks like you don't have any shared network, head over the networks view to create one.
-
+
App templates cannot be used with swarm-mode at the moment. You can still use them to quickly deploy containers to the Docker host. @@ -41,10 +42,10 @@
- - @@ -106,21 +107,24 @@
-
- -
-
- + Items per page: +
-
+
{{ tpl.title }}
{{ tpl.description }}
diff --git a/app/components/templates/templatesController.js b/app/components/templates/templatesController.js index 384516d75..4619979b9 100644 --- a/app/components/templates/templatesController.js +++ b/app/components/templates/templatesController.js @@ -1,19 +1,23 @@ angular.module('templates', []) -.controller('TemplatesController', ['$scope', '$q', '$state', '$filter', '$anchorScroll', 'Config', 'Info', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'Templates', 'TemplateHelper', 'Messages', 'Settings', -function ($scope, $q, $state, $filter, $anchorScroll, Config, Info, Container, ContainerHelper, Image, ImageHelper, Volume, Network, Templates, TemplateHelper, Messages, Settings) { +.controller('TemplatesController', ['$scope', '$q', '$state', '$filter', '$anchorScroll', 'Config', 'Info', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'Templates', 'TemplateHelper', 'Messages', 'Pagination', +function ($scope, $q, $state, $filter, $anchorScroll, Config, Info, Container, ContainerHelper, Image, ImageHelper, Volume, Network, Templates, TemplateHelper, Messages, Pagination) { $scope.state = { selectedTemplate: null, - showAdvancedOptions: false + showAdvancedOptions: false, + pagination_count: Pagination.getPaginationCount('templates') }; $scope.formValues = { network: "", name: "", ports: [] }; - $scope.pagination_count = Settings.pagination_count; var selectedItem = -1; + $scope.changePaginationCount = function() { + Pagination.setPaginationCount('templates', $scope.state.pagination_count); + }; + $scope.addPortBinding = function() { $scope.formValues.ports.push({ hostPort: '', containerPort: '', protocol: 'tcp' }); }; @@ -115,7 +119,7 @@ function ($scope, $q, $state, $filter, $anchorScroll, Config, Info, Container, C if (v.value || v.set) { var val; if (v.type && v.type === 'container') { - if ($scope.endpointMode.provider === 'DOCKER_SWARM' && $scope.formValues.network.Scope === 'global') { + if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' && $scope.formValues.network.Scope === 'global') { val = $filter('swarmcontainername')(v.value); } else { var container = v.value; @@ -206,7 +210,7 @@ function ($scope, $q, $state, $filter, $anchorScroll, Config, Info, Container, C var containersToHideLabels = c.hiddenLabels; Network.query({}, function (d) { var networks = d; - if ($scope.endpointMode.provider === 'DOCKER_SWARM' || $scope.endpointMode.provider === 'DOCKER_SWARM_MODE') { + if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') { networks = d.filter(function (network) { if (network.Scope === 'global') { return network; diff --git a/app/components/volumes/volumes.html b/app/components/volumes/volumes.html index ae741dcb6..530e24a7b 100644 --- a/app/components/volumes/volumes.html +++ b/app/components/volumes/volumes.html @@ -3,6 +3,7 @@ + Volumes @@ -11,7 +12,14 @@
- + Items per page: +
@@ -55,7 +63,7 @@
- + diff --git a/app/components/volumes/volumesController.js b/app/components/volumes/volumesController.js index 2910dec80..6bbc9c27c 100644 --- a/app/components/volumes/volumesController.js +++ b/app/components/volumes/volumesController.js @@ -1,14 +1,18 @@ angular.module('volumes', []) -.controller('VolumesController', ['$scope', '$state', 'Volume', 'Messages', 'Settings', -function ($scope, $state, Volume, Messages, Settings) { +.controller('VolumesController', ['$scope', '$state', 'Volume', 'Messages', 'Pagination', +function ($scope, $state, Volume, Messages, Pagination) { $scope.state = {}; + $scope.state.pagination_count = Pagination.getPaginationCount('volumes'); $scope.state.selectedItemCount = 0; $scope.sortType = 'Name'; $scope.sortReverse = true; $scope.config = { Name: '' }; - $scope.pagination_count = Settings.pagination_count; + + $scope.changePaginationCount = function() { + Pagination.setPaginationCount('volumes', $scope.state.pagination_count); + }; $scope.order = function(sortType) { $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; diff --git a/app/shared/filters.js b/app/shared/filters.js index 2efdf81b5..3e6881cbe 100644 --- a/app/shared/filters.js +++ b/app/shared/filters.js @@ -67,7 +67,7 @@ angular.module('portainer.filters', []) .filter('nodestatusbadge', function () { 'use strict'; return function (text) { - if (text === 'Unhealthy') { + if (text === 'down' || text === 'Unhealthy') { return 'danger'; } return 'success'; diff --git a/app/shared/helpers.js b/app/shared/helpers.js index 1bfc282c6..3cddb5c65 100644 --- a/app/shared/helpers.js +++ b/app/shared/helpers.js @@ -1,4 +1,57 @@ angular.module('portainer.helpers', []) +.factory('InfoHelper', [function InfoHelperFactory() { + 'use strict'; + return { + determineEndpointMode: function(info) { + var mode = { + provider: '', + role: '' + }; + if (_.startsWith(info.ServerVersion, 'swarm')) { + mode.provider = "DOCKER_SWARM"; + if (info.SystemStatus[0][1] === 'primary') { + mode.role = "PRIMARY"; + } else { + mode.role = "REPLICA"; + } + } else { + if (!info.Swarm || _.isEmpty(info.Swarm.NodeID)) { + mode.provider = "DOCKER_STANDALONE"; + } else { + mode.provider = "DOCKER_SWARM_MODE"; + if (info.Swarm.ControlAvailable) { + mode.role = "MANAGER"; + } else { + mode.role = "WORKER"; + } + } + } + return mode; + } + }; +}]) +.factory('LabelHelper', [function LabelHelperFactory() { + 'use strict'; + return { + fromLabelHashToKeyValue: function(labels) { + if (labels) { + return Object.keys(labels).map(function(key) { + return {key: key, value: labels[key], originalKey: key, originalValue: labels[key], added: true}; + }); + } + return []; + }, + fromKeyValueToLabelHash: function(labelKV) { + var labels = {}; + if (labelKV) { + labelKV.forEach(function(label) { + labels[label.key] = label.value; + }); + } + return labels; + } + }; +}]) .factory('ImageHelper', [function ImageHelperFactory() { 'use strict'; return { @@ -63,6 +116,19 @@ angular.module('portainer.helpers', []) } }; }]) +.factory('NodeHelper', [function NodeHelperFactory() { + 'use strict'; + return { + nodeToConfig: function(node) { + return { + Name: node.Spec.Name, + Role: node.Spec.Role, + Labels: node.Spec.Labels, + Availability: node.Spec.Availability + }; + } + }; +}]) .factory('TemplateHelper', [function TemplateHelperFactory() { 'use strict'; return { diff --git a/app/shared/responseHandlers.js b/app/shared/responseHandlers.js index 16bdc248a..8b60fea6f 100644 --- a/app/shared/responseHandlers.js +++ b/app/shared/responseHandlers.js @@ -56,7 +56,7 @@ function deleteImageHandler(data) { response.push({message: data}); } // A JSON object is returned on failure (Docker = 1.12) - else if (!isJSONArray) { + else if (!isJSONArray(data)) { var json = angular.fromJson(data); response.push(json); } diff --git a/app/shared/services.js b/app/shared/services.js index 1aba157aa..dbd982663 100644 --- a/app/shared/services.js +++ b/app/shared/services.js @@ -153,10 +153,11 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize']) .factory('Node', ['$resource', 'Settings', function NodeFactory($resource, Settings) { 'use strict'; // https://docs.docker.com/engine/reference/api/docker_remote_api_<%= remoteApiVersion %>/#/3-7-nodes - return $resource(Settings.url + '/nodes', {}, { - query: { - method: 'GET', isArray: true - } + return $resource(Settings.url + '/nodes/:id/:action', {}, { + query: {method: 'GET', isArray: true}, + get: {method: 'GET', params: {id: '@id'}}, + update: { method: 'POST', params: {id: '@id', action: 'update', version: '@version'} }, + remove: { method: 'DELETE', params: {id: '@id'} } }); }]) .factory('Swarm', ['$resource', 'Settings', function SwarmFactory($resource, Settings) { @@ -240,44 +241,11 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize']) initAdminUser: { method: 'POST', params: { username: 'admin', action: 'init' } } }); }]) - .factory('EndpointMode', ['$rootScope', 'Info', function EndpointMode($rootScope, Info) { - 'use strict'; - return { - determineEndpointMode: function() { - Info.get({}, function(d) { - var mode = { - provider: '', - role: '' - }; - if (_.startsWith(d.ServerVersion, 'swarm')) { - mode.provider = "DOCKER_SWARM"; - if (d.SystemStatus[0][1] === 'primary') { - mode.role = "PRIMARY"; - } else { - mode.role = "REPLICA"; - } - } else { - if (!d.Swarm || _.isEmpty(d.Swarm.NodeID)) { - mode.provider = "DOCKER_STANDALONE"; - } else { - mode.provider = "DOCKER_SWARM_MODE"; - if (d.Swarm.ControlAvailable) { - mode.role = "MANAGER"; - } else { - mode.role = "WORKER"; - } - } - } - $rootScope.endpointMode = mode; - }); - } - }; - }]) - .factory('Authentication', ['$q', '$rootScope', 'Auth', 'jwtHelper', 'localStorageService', function AuthenticationFactory($q, $rootScope, Auth, jwtHelper, localStorageService) { + .factory('Authentication', ['$q', '$rootScope', 'Auth', 'jwtHelper', 'LocalStorage', 'StateManager', function AuthenticationFactory($q, $rootScope, Auth, jwtHelper, LocalStorage, StateManager) { 'use strict'; return { init: function() { - var jwt = localStorageService.get('JWT'); + var jwt = LocalStorage.getJWT(); if (jwt) { var tokenPayload = jwtHelper.decodeToken(jwt); $rootScope.username = tokenPayload.username; @@ -287,7 +255,7 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize']) return $q(function (resolve, reject) { Auth.login({username: username, password: password}).$promise .then(function(data) { - localStorageService.set('JWT', data.jwt); + LocalStorage.storeJWT(data.jwt); $rootScope.username = username; resolve(); }, function() { @@ -296,10 +264,11 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize']) }); }, logout: function() { - localStorageService.remove('JWT'); + StateManager.clean(); + LocalStorage.clean(); }, isAuthenticated: function() { - var jwt = localStorageService.get('JWT'); + var jwt = LocalStorage.getJWT(); return jwt && !jwtHelper.isTokenExpired(jwt); } }; @@ -359,7 +328,97 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize']) setActiveEndpoint: { method: 'POST', params: { id: '@id', action: 'active' } } }); }]) - .factory('EndpointService', ['$q', '$timeout', 'Endpoints', 'FileUploadService', function EndpointServiceFactory($q, $timeout, Endpoints, FileUploadService) { + .factory('Pagination', ['LocalStorage', 'Settings', function PaginationFactory(LocalStorage, Settings) { + 'use strict'; + return { + getPaginationCount: function(key) { + var storedCount = LocalStorage.getPaginationCount(key); + var paginationCount = Settings.pagination_count; + if (storedCount !== null) { + paginationCount = storedCount; + } + return '' + paginationCount; + }, + setPaginationCount: function(key, count) { + LocalStorage.storePaginationCount(key, count); + } + }; + }]) + .factory('LocalStorage', ['localStorageService', function LocalStorageFactory(localStorageService) { + 'use strict'; + return { + storeEndpointState: function(state) { + localStorageService.set('ENDPOINT_STATE', state); + }, + getEndpointState: function() { + return localStorageService.get('ENDPOINT_STATE'); + }, + storeJWT: function(jwt) { + localStorageService.set('JWT', jwt); + }, + getJWT: function() { + return localStorageService.get('JWT'); + }, + deleteJWT: function() { + localStorageService.remove('JWT'); + }, + storePaginationCount: function(key, count) { + localStorageService.cookie.set('pagination_' + key, count); + }, + getPaginationCount: function(key) { + return localStorageService.cookie.get('pagination_' + key); + }, + clean: function() { + localStorageService.clearAll(); + } + }; + }]) + .factory('StateManager', ['$q', 'Info', 'InfoHelper', 'Version', 'LocalStorage', function StateManagerFactory($q, Info, InfoHelper, Version, LocalStorage) { + 'use strict'; + + var state = { + loading: true, + application: {}, + endpoint: {} + }; + + return { + init: function() { + var endpointState = LocalStorage.getEndpointState(); + if (endpointState) { + state.endpoint = endpointState; + } + state.loading = false; + }, + clean: function() { + state.endpoint = {}; + }, + updateEndpointState: function(loading) { + var deferred = $q.defer(); + if (loading) { + state.loading = true; + } + $q.all([Info.get({}).$promise, Version.get({}).$promise]) + .then(function success(data) { + var endpointMode = InfoHelper.determineEndpointMode(data[0]); + var endpointAPIVersion = parseFloat(data[1].ApiVersion); + state.endpoint.mode = endpointMode; + state.endpoint.apiVersion = endpointAPIVersion; + LocalStorage.storeEndpointState(state.endpoint); + state.loading = false; + deferred.resolve(); + }, function error(err) { + state.loading = false; + deferred.reject({msg: 'Unable to connect to the Docker endpoint', err: err}); + }); + return deferred.promise; + }, + getState: function() { + return state; + } + }; + }]) + .factory('EndpointService', ['$q', 'Endpoints', 'FileUploadService', function EndpointServiceFactory($q, Endpoints, FileUploadService) { 'use strict'; return { getActive: function() { @@ -374,11 +433,11 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize']) endpoints: function() { return Endpoints.query({}).$promise; }, - updateEndpoint: function(ID, name, URL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile) { + updateEndpoint: function(ID, name, URL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile, type) { var endpoint = { id: ID, Name: name, - URL: "tcp://" + URL, + URL: type === 'local' ? ("unix://" + URL) : ("tcp://" + URL), TLS: TLS }; var deferred = $q.defer(); diff --git a/app/shared/viewmodel.js b/app/shared/viewmodel.js index c7296a6e7..6d9e4e0b1 100644 --- a/app/shared/viewmodel.js +++ b/app/shared/viewmodel.js @@ -14,6 +14,7 @@ function TaskViewModel(data, node_data) { this.Updated = data.UpdatedAt; this.Slot = data.Slot; this.Status = data.Status.State; + this.Image = data.Spec.ContainerSpec ? data.Spec.ContainerSpec.Image : ''; if (node_data) { for (var i = 0; i < node_data.length; ++i) { if (data.NodeID === node_data[i].ID) { @@ -60,6 +61,42 @@ function ServiceViewModel(data) { this.EditName = false; } +function NodeViewModel(data) { + this.Model = data; + this.Id = data.ID; + this.Version = data.Version.Index; + this.Name = data.Spec.Name; + this.Role = data.Spec.Role; + this.CreatedAt = data.CreatedAt; + this.UpdatedAt = data.UpdatedAt; + this.Availability = data.Spec.Availability; + + var labels = data.Spec.Labels; + if (labels) { + this.Labels = Object.keys(labels).map(function(key) { + return { key: key, value: labels[key], originalKey: key, originalValue: labels[key], added: true }; + }); + } else { + this.Labels = []; + } + + this.Hostname = data.Description.Hostname; + this.PlatformArchitecture = data.Description.Platform.Architecture; + this.PlatformOS = data.Description.Platform.OS; + this.CPUs = data.Description.Resources.NanoCPUs; + this.Memory = data.Description.Resources.MemoryBytes; + this.EngineVersion = data.Description.Engine.EngineVersion; + this.EngineLabels = data.Description.Engine.Labels; + this.Plugins = data.Description.Engine.Plugins; + this.Status = data.Status.State; + + if (data.ManagerStatus) { + this.Leader = data.ManagerStatus.Leader; + this.Reachability = data.ManagerStatus.Reachability; + this.ManagerAddr = data.ManagerStatus.Addr; + } +} + function ContainerViewModel(data) { this.Id = data.Id; this.Status = data.Status; diff --git a/assets/css/app.css b/assets/css/app.css index 9a870e85f..cce7e440b 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -213,7 +213,6 @@ input[type="radio"] { } .page-wrapper { - margin-top: 25px; height: 100%; width: 100%; display: flex; diff --git a/bower.json b/bower.json index e33a4c23c..11fe570e4 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "portainer", - "version": "1.11.1", + "version": "1.11.2", "homepage": "https://github.com/portainer/portainer", "authors": [ "Anthony Lapenna " diff --git a/build/windows/microsoftservercore/Dockerfile b/build/windows/microsoftservercore/Dockerfile index dfcd8b15a..5d67a2899 100644 --- a/build/windows/microsoftservercore/Dockerfile +++ b/build/windows/microsoftservercore/Dockerfile @@ -2,7 +2,7 @@ FROM microsoft/windowsservercore COPY dist / -VOLUME C:\\data +VOLUME C:\\ProgramData\\Portainer WORKDIR / diff --git a/build/windows/nanoserver/Dockerfile b/build/windows/nanoserver/Dockerfile index ae8a25a39..7855cba8e 100644 --- a/build/windows/nanoserver/Dockerfile +++ b/build/windows/nanoserver/Dockerfile @@ -2,7 +2,7 @@ FROM microsoft/nanoserver COPY dist / -VOLUME C:\\data +VOLUME C:\\ProgramData\\Portainer WORKDIR / diff --git a/gruntfile.js b/gruntfile.js index 6e86f4404..825749762 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -398,14 +398,14 @@ module.exports = function (grunt) { command: [ 'docker stop portainer', 'docker rm portainer', - 'docker run --privileged -d -p 9000:9000 -v /tmp/portainer:/data -v /var/run/docker.sock:/var/run/docker.sock --name portainer portainer -d /data' + 'docker run --privileged -d -p 9000:9000 -v /tmp/portainer:/data -v /var/run/docker.sock:/var/run/docker.sock --name portainer portainer' ].join(';') }, runSwarm: { command: [ 'docker stop portainer', 'docker rm portainer', - 'docker run -d -p 9000:9000 -v /tmp/portainer:/data --name portainer portainer -H tcp://10.0.7.10:2375 -d /data' + 'docker run -d -p 9000:9000 --name portainer portainer -H tcp://10.0.7.10:2375' ].join(';') }, runSwarmLocal: { @@ -419,7 +419,7 @@ module.exports = function (grunt) { command: [ 'docker stop portainer', 'docker rm portainer', - 'docker run -d -p 9000:9000 -v /tmp/portainer:/data -v /tmp/docker-ssl:/certs --name portainer portainer -H tcp://10.0.7.10:2376 -d /data --tlsverify' + 'docker run -d -p 9000:9000 -v /tmp/portainer:/data -v /tmp/docker-ssl:/certs --name portainer portainer -H tcp://10.0.7.10:2376 --tlsverify' ].join(';') }, cleanImages: { diff --git a/index.html b/index.html index 0b0bd1a1d..06ca19d56 100644 --- a/index.html +++ b/index.html @@ -29,15 +29,36 @@ -
+
-
+
+
+ +
+
+ +
+ + +
+ + +
+ Connecting to the Docker enpoint... + +
+ +
+
+ +
+ -
+
diff --git a/package.json b/package.json index 1b8f6721a..5a08800ae 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Portainer.io", "name": "portainer", "homepage": "http://portainer.io", - "version": "1.11.1", + "version": "1.11.2", "repository": { "type": "git", "url": "git@github.com:portainer/portainer.git"
- + Name - - + + - + Role - - + + - + CPU - - + + - + Memory - - + + - + Engine - - + + - + Status - - + +
{{ node.Description.Hostname }}
{{ node.Description.Hostname }} {{ node.Spec.Role }} {{ node.Description.Resources.NanoCPUs / 1000000000 }} {{ node.Description.Resources.MemoryBytes|humansize }}
{{ volume.Name|truncate:50 }} {{ volume.Driver }}