

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 @@
Actions | -||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
{{ key }} | {{ value.IPAddress || '-' }} | {{ value.Gateway || '-' }} | 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 @@ ++ | 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 @@
-
-
-
-
-
@@ -269,7 +259,7 @@
-
- label
-
-
- | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{{ endpoint.Name }} | {{ endpoint.URL | stripprotocol }} | 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 @@ +|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{{ event.Time|getisodatefromtimestamp }} | {{ event.Type }} | {{ event.Details }} | 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 @@ +||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{{ 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 @@
+
+
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:
+
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{{ network.Name|truncate:40}} | {{ network.Id }} | 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 @@ +
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.
+Label | +Value | +
---|---|
+
+ name
+
+
+ |
+
+
+ value
+
+
+
+
+
+ |
+
Id | ++ + Status + + + + | ++ + Slot + + + + | ++ + Image + + + + | ++ + Last update + + + + | +
---|---|---|---|---|
{{ task.Id }} | +{{ task.Status }} | +{{ task.Slot }} | +{{ task.Image }} | +{{ task.Updated|getisodate }} | +
{{ task.Id }} | {{ task.Status }} | {{ task.Slot }} | 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 @@ +|||||||||||||||||||||||||||||||||||||||||||||||||||||||
{{ service.Name }} | {{ service.Image }} | 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 @@
{{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 }} |
- + Name - - + + | @@ -94,30 +105,30 @@ | - + IP - - + + | - + Engine - - + + | - + Status - - + + | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
{{ node.name }} | {{ node.cpu }} | {{ node.memory }} | @@ -133,60 +144,71 @@ -
- + 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 }} | 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 @@ +||
{{ volume.Name|truncate:50 }} | {{ volume.Driver }} | 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