diff --git a/api/crypto/ecdsa.go b/api/crypto/ecdsa.go index 1b453dd3e..003547531 100644 --- a/api/crypto/ecdsa.go +++ b/api/crypto/ecdsa.go @@ -3,7 +3,6 @@ package crypto import ( "crypto/ecdsa" "crypto/elliptic" - "crypto/md5" "crypto/rand" "crypto/x509" "encoding/base64" @@ -97,9 +96,7 @@ func (service *ECDSAService) GenerateKeyPair() ([]byte, []byte, error) { // that hash. // It then encodes the generated signature in base64. func (service *ECDSAService) Sign(message string) (string, error) { - digest := md5.New() - digest.Write([]byte(message)) - hash := digest.Sum(nil) + hash := HashFromBytes([]byte(message)) r := big.NewInt(0) s := big.NewInt(0) diff --git a/api/crypto/md5.go b/api/crypto/md5.go new file mode 100644 index 000000000..42ca24602 --- /dev/null +++ b/api/crypto/md5.go @@ -0,0 +1,10 @@ +package crypto + +import "crypto/md5" + +// HashFromBytes returns the hash of the specified data +func HashFromBytes(data []byte) []byte { + digest := md5.New() + digest.Write(data) + return digest.Sum(nil) +} diff --git a/api/http/client/client.go b/api/http/client/client.go index 29ebb7d88..541ec8257 100644 --- a/api/http/client/client.go +++ b/api/http/client/client.go @@ -13,6 +13,10 @@ import ( "github.com/portainer/portainer" ) +const ( + errInvalidResponseStatus = portainer.Error("Invalid response status (expecting 200)") +) + // HTTPClient represents a client to send HTTP requests. type HTTPClient struct { *http.Client @@ -75,6 +79,10 @@ func Get(url string) ([]byte, error) { } defer response.Body.Close() + if response.StatusCode != http.StatusOK { + return nil, errInvalidResponseStatus + } + body, err := ioutil.ReadAll(response.Body) if err != nil { return nil, err diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index dd15e1212..342230396 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -10,6 +10,7 @@ import ( "github.com/portainer/portainer/http/handler/endpointproxy" "github.com/portainer/portainer/http/handler/endpoints" "github.com/portainer/portainer/http/handler/file" + "github.com/portainer/portainer/http/handler/motd" "github.com/portainer/portainer/http/handler/registries" "github.com/portainer/portainer/http/handler/resourcecontrols" "github.com/portainer/portainer/http/handler/settings" @@ -33,6 +34,7 @@ type Handler struct { EndpointHandler *endpoints.Handler EndpointProxyHandler *endpointproxy.Handler FileHandler *file.Handler + MOTDHandler *motd.Handler RegistryHandler *registries.Handler ResourceControlHandler *resourcecontrols.Handler SettingsHandler *settings.Handler @@ -67,6 +69,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r) } + case strings.HasPrefix(r.URL.Path, "/api/motd"): + http.StripPrefix("/api", h.MOTDHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/registries"): http.StripPrefix("/api", h.RegistryHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/resource_controls"): diff --git a/api/http/handler/motd/handler.go b/api/http/handler/motd/handler.go new file mode 100644 index 000000000..429731aa4 --- /dev/null +++ b/api/http/handler/motd/handler.go @@ -0,0 +1,24 @@ +package motd + +import ( + "net/http" + + "github.com/gorilla/mux" + "github.com/portainer/portainer/http/security" +) + +// Handler is the HTTP handler used to handle MOTD operations. +type Handler struct { + *mux.Router +} + +// NewHandler returns a new Handler +func NewHandler(bouncer *security.RequestBouncer) *Handler { + h := &Handler{ + Router: mux.NewRouter(), + } + h.Handle("/motd", + bouncer.AuthenticatedAccess(http.HandlerFunc(h.motd))).Methods(http.MethodGet) + + return h +} diff --git a/api/http/handler/motd/motd.go b/api/http/handler/motd/motd.go new file mode 100644 index 000000000..b2599be2d --- /dev/null +++ b/api/http/handler/motd/motd.go @@ -0,0 +1,27 @@ +package motd + +import ( + "net/http" + + "github.com/portainer/portainer" + "github.com/portainer/portainer/crypto" + "github.com/portainer/portainer/http/client" + "github.com/portainer/portainer/http/response" +) + +type motdResponse struct { + Message string `json:"Message"` + Hash []byte `json:"Hash"` +} + +func (handler *Handler) motd(w http.ResponseWriter, r *http.Request) { + + motd, err := client.Get(portainer.MessageOfTheDayURL) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + hash := crypto.HashFromBytes(motd) + response.JSON(w, &motdResponse{Message: string(motd), Hash: hash}) +} diff --git a/api/http/server.go b/api/http/server.go index 6b3415280..116a6c1ec 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -11,6 +11,7 @@ import ( "github.com/portainer/portainer/http/handler/endpointproxy" "github.com/portainer/portainer/http/handler/endpoints" "github.com/portainer/portainer/http/handler/file" + "github.com/portainer/portainer/http/handler/motd" "github.com/portainer/portainer/http/handler/registries" "github.com/portainer/portainer/http/handler/resourcecontrols" "github.com/portainer/portainer/http/handler/settings" @@ -115,6 +116,8 @@ func (server *Server) Start() error { var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public")) + var motdHandler = motd.NewHandler(requestBouncer) + var registryHandler = registries.NewHandler(requestBouncer) registryHandler.RegistryService = server.RegistryService @@ -175,6 +178,7 @@ func (server *Server) Start() error { EndpointHandler: endpointHandler, EndpointProxyHandler: endpointProxyHandler, FileHandler: fileHandler, + MOTDHandler: motdHandler, RegistryHandler: registryHandler, ResourceControlHandler: resourceControlHandler, SettingsHandler: settingsHandler, diff --git a/api/portainer.go b/api/portainer.go index 4c65d1149..49f87c652 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -7,7 +7,7 @@ type ( Value string `json:"value"` } - // CLIFlags represents the available flags on the CLI. + // CLIFlags represents the available flags on the CLI CLIFlags struct { Addr *string AdminPassword *string @@ -35,7 +35,7 @@ type ( SnapshotInterval *string } - // Status represents the application status. + // Status represents the application status Status struct { Authentication bool `json:"Authentication"` EndpointManagement bool `json:"EndpointManagement"` @@ -44,7 +44,7 @@ type ( Version string `json:"Version"` } - // LDAPSettings represents the settings used to connect to a LDAP server. + // LDAPSettings represents the settings used to connect to a LDAP server LDAPSettings struct { ReaderDN string `json:"ReaderDN"` Password string `json:"Password"` @@ -56,7 +56,7 @@ type ( AutoCreateUsers bool `json:"AutoCreateUsers"` } - // TLSConfiguration represents a TLS configuration. + // TLSConfiguration represents a TLS configuration TLSConfiguration struct { TLS bool `json:"TLS"` TLSSkipVerify bool `json:"TLSSkipVerify"` @@ -65,21 +65,21 @@ type ( TLSKeyPath string `json:"TLSKey,omitempty"` } - // LDAPSearchSettings represents settings used to search for users in a LDAP server. + // LDAPSearchSettings represents settings used to search for users in a LDAP server LDAPSearchSettings struct { BaseDN string `json:"BaseDN"` Filter string `json:"Filter"` UserNameAttribute string `json:"UserNameAttribute"` } - // LDAPGroupSearchSettings represents settings used to search for groups in a LDAP server. + // LDAPGroupSearchSettings represents settings used to search for groups in a LDAP server LDAPGroupSearchSettings struct { GroupBaseDN string `json:"GroupBaseDN"` GroupFilter string `json:"GroupFilter"` GroupAttribute string `json:"GroupAttribute"` } - // Settings represents the application settings. + // Settings represents the application settings Settings struct { LogoURL string `json:"LogoURL"` BlackListedLabels []Pair `json:"BlackListedLabels"` @@ -95,7 +95,7 @@ type ( DisplayExternalContributors bool } - // User represents a user account. + // User represents a user account User struct { ID UserID `json:"Id"` Username string `json:"Username"` @@ -110,10 +110,10 @@ type ( // or a regular user UserRole int - // AuthenticationMethod represents the authentication method used to authenticate a user. + // AuthenticationMethod represents the authentication method used to authenticate a user AuthenticationMethod int - // Team represents a list of user accounts. + // Team represents a list of user accounts Team struct { ID TeamID `json:"Id"` Name string `json:"Name"` @@ -136,20 +136,20 @@ type ( // MembershipRole represents the role of a user within a team MembershipRole int - // TokenData represents the data embedded in a JWT token. + // TokenData represents the data embedded in a JWT token TokenData struct { ID UserID Username string Role UserRole } - // StackID represents a stack identifier (it must be composed of Name + "_" + SwarmID to create a unique identifier). + // StackID represents a stack identifier (it must be composed of Name + "_" + SwarmID to create a unique identifier) StackID int - // StackType represents the type of the stack (compose v2, stack deploy v3). + // StackType represents the type of the stack (compose v2, stack deploy v3) StackType int - // Stack represents a Docker stack created via docker stack deploy. + // Stack represents a Docker stack created via docker stack deploy Stack struct { ID StackID `json:"Id"` Name string `json:"Name"` @@ -161,11 +161,11 @@ type ( ProjectPath string } - // RegistryID represents a registry identifier. + // RegistryID represents a registry identifier RegistryID int // Registry represents a Docker registry with all the info required - // to connect to it. + // to connect to it Registry struct { ID RegistryID `json:"Id"` Name string `json:"Name"` @@ -178,24 +178,24 @@ type ( } // DockerHub represents all the required information to connect and use the - // Docker Hub. + // Docker Hub DockerHub struct { Authentication bool `json:"Authentication"` Username string `json:"Username"` Password string `json:"Password,omitempty"` } - // EndpointID represents an endpoint identifier. + // EndpointID represents an endpoint identifier EndpointID int - // EndpointType represents the type of an endpoint. + // EndpointType represents the type of an endpoint EndpointType int // EndpointStatus represents the status of an endpoint EndpointStatus int // Endpoint represents a Docker endpoint with all the info required - // to connect to it. + // to connect to it Endpoint struct { ID EndpointID `json:"Id"` Name string `json:"Name"` @@ -243,10 +243,10 @@ type ( StackCount int `json:"StackCount"` } - // EndpointGroupID represents an endpoint group identifier. + // EndpointGroupID represents an endpoint group identifier EndpointGroupID int - // EndpointGroup represents a group of endpoints. + // EndpointGroup represents a group of endpoints EndpointGroup struct { ID EndpointGroupID `json:"Id"` Name string `json:"Name"` @@ -259,17 +259,17 @@ type ( Labels []Pair `json:"Labels"` } - // EndpointExtension represents a extension associated to an endpoint. + // EndpointExtension represents a extension associated to an endpoint EndpointExtension struct { Type EndpointExtensionType `json:"Type"` URL string `json:"URL"` } // EndpointExtensionType represents the type of an endpoint extension. Only - // one extension of each type can be associated to an endpoint. + // one extension of each type can be associated to an endpoint EndpointExtensionType int - // ResourceControlID represents a resource control identifier. + // ResourceControlID represents a resource control identifier ResourceControlID int // ResourceControl represent a reference to a Docker resource with specific access controls @@ -291,37 +291,37 @@ type ( AdministratorsOnly bool `json:"AdministratorsOnly,omitempty"` } - // ResourceControlType represents the type of resource associated to the resource control (volume, container, service...). + // ResourceControlType represents the type of resource associated to the resource control (volume, container, service...) ResourceControlType int - // UserResourceAccess represents the level of control on a resource for a specific user. + // UserResourceAccess represents the level of control on a resource for a specific user UserResourceAccess struct { UserID UserID `json:"UserId"` AccessLevel ResourceAccessLevel `json:"AccessLevel"` } - // TeamResourceAccess represents the level of control on a resource for a specific team. + // TeamResourceAccess represents the level of control on a resource for a specific team TeamResourceAccess struct { TeamID TeamID `json:"TeamId"` AccessLevel ResourceAccessLevel `json:"AccessLevel"` } - // TagID represents a tag identifier. + // TagID represents a tag identifier TagID int - // Tag represents a tag that can be associated to a resource. + // Tag represents a tag that can be associated to a resource Tag struct { ID TagID Name string `json:"Name"` } - // TemplateID represents a template identifier. + // TemplateID represents a template identifier TemplateID int - // TemplateType represents the type of a template. + // TemplateType represents the type of a template TemplateType int - // Template represents an application template. + // Template represents an application template Template struct { // Mandatory container/stack fields ID TemplateID `json:"Id"` @@ -357,7 +357,7 @@ type ( Hostname string `json:"hostname,omitempty"` } - // TemplateEnv represents a template environment variable configuration. + // TemplateEnv represents a template environment variable configuration TemplateEnv struct { Name string `json:"name"` Label string `json:"label,omitempty"` @@ -367,41 +367,41 @@ type ( Select []TemplateEnvSelect `json:"select,omitempty"` } - // TemplateVolume represents a template volume configuration. + // TemplateVolume represents a template volume configuration TemplateVolume struct { Container string `json:"container"` Bind string `json:"bind,omitempty"` ReadOnly bool `json:"readonly,omitempty"` } - // TemplateRepository represents the git repository configuration for a template. + // TemplateRepository represents the git repository configuration for a template TemplateRepository struct { URL string `json:"url"` StackFile string `json:"stackfile"` } // TemplateEnvSelect represents text/value pair that will be displayed as a choice for the - // template user. + // template user TemplateEnvSelect struct { Text string `json:"text"` Value string `json:"value"` Default bool `json:"default"` } - // ResourceAccessLevel represents the level of control associated to a resource. + // ResourceAccessLevel represents the level of control associated to a resource ResourceAccessLevel int // TLSFileType represents a type of TLS file required to connect to a Docker endpoint. - // It can be either a TLS CA file, a TLS certificate file or a TLS key file. + // It can be either a TLS CA file, a TLS certificate file or a TLS key file TLSFileType int - // CLIService represents a service for managing CLI. + // CLIService represents a service for managing CLI CLIService interface { ParseFlags(version string) (*CLIFlags, error) ValidateFlags(flags *CLIFlags) error } - // DataStore defines the interface to manage the data. + // DataStore defines the interface to manage the data DataStore interface { Open() error Init() error @@ -409,12 +409,12 @@ type ( MigrateData() error } - // Server defines the interface to serve the API. + // Server defines the interface to serve the API Server interface { Start() error } - // UserService represents a service for managing user data. + // UserService represents a service for managing user data UserService interface { User(ID UserID) (*User, error) UserByUsername(username string) (*User, error) @@ -425,7 +425,7 @@ type ( DeleteUser(ID UserID) error } - // TeamService represents a service for managing user data. + // TeamService represents a service for managing user data TeamService interface { Team(ID TeamID) (*Team, error) TeamByName(name string) (*Team, error) @@ -435,7 +435,7 @@ type ( DeleteTeam(ID TeamID) error } - // TeamMembershipService represents a service for managing team membership data. + // TeamMembershipService represents a service for managing team membership data TeamMembershipService interface { TeamMembership(ID TeamMembershipID) (*TeamMembership, error) TeamMemberships() ([]TeamMembership, error) @@ -448,7 +448,7 @@ type ( DeleteTeamMembershipByTeamID(teamID TeamID) error } - // EndpointService represents a service for managing endpoint data. + // EndpointService represents a service for managing endpoint data EndpointService interface { Endpoint(ID EndpointID) (*Endpoint, error) Endpoints() ([]Endpoint, error) @@ -459,7 +459,7 @@ type ( GetNextIdentifier() int } - // EndpointGroupService represents a service for managing endpoint group data. + // EndpointGroupService represents a service for managing endpoint group data EndpointGroupService interface { EndpointGroup(ID EndpointGroupID) (*EndpointGroup, error) EndpointGroups() ([]EndpointGroup, error) @@ -468,7 +468,7 @@ type ( DeleteEndpointGroup(ID EndpointGroupID) error } - // RegistryService represents a service for managing registry data. + // RegistryService represents a service for managing registry data RegistryService interface { Registry(ID RegistryID) (*Registry, error) Registries() ([]Registry, error) @@ -477,7 +477,7 @@ type ( DeleteRegistry(ID RegistryID) error } - // StackService represents a service for managing stack data. + // StackService represents a service for managing stack data StackService interface { Stack(ID StackID) (*Stack, error) StackByName(name string) (*Stack, error) @@ -488,25 +488,25 @@ type ( GetNextIdentifier() int } - // DockerHubService represents a service for managing the DockerHub object. + // DockerHubService represents a service for managing the DockerHub object DockerHubService interface { DockerHub() (*DockerHub, error) UpdateDockerHub(registry *DockerHub) error } - // SettingsService represents a service for managing application settings. + // SettingsService represents a service for managing application settings SettingsService interface { Settings() (*Settings, error) UpdateSettings(settings *Settings) error } - // VersionService represents a service for managing version data. + // VersionService represents a service for managing version data VersionService interface { DBVersion() (int, error) StoreDBVersion(version int) error } - // ResourceControlService represents a service for managing resource control data. + // ResourceControlService represents a service for managing resource control data ResourceControlService interface { ResourceControl(ID ResourceControlID) (*ResourceControl, error) ResourceControlByResourceID(resourceID string) (*ResourceControl, error) @@ -516,14 +516,14 @@ type ( DeleteResourceControl(ID ResourceControlID) error } - // TagService represents a service for managing tag data. + // TagService represents a service for managing tag data TagService interface { Tags() ([]Tag, error) CreateTag(tag *Tag) error DeleteTag(ID TagID) error } - // TemplateService represents a service for managing template data. + // TemplateService represents a service for managing template data TemplateService interface { Templates() ([]Template, error) Template(ID TemplateID) (*Template, error) @@ -532,13 +532,13 @@ type ( DeleteTemplate(ID TemplateID) error } - // CryptoService represents a service for encrypting/hashing data. + // CryptoService represents a service for encrypting/hashing data CryptoService interface { Hash(data string) (string, error) CompareHashAndData(hash string, data string) error } - // DigitalSignatureService represents a service to manage digital signatures. + // DigitalSignatureService represents a service to manage digital signatures DigitalSignatureService interface { ParseKeyPair(private, public []byte) error GenerateKeyPair() ([]byte, []byte, error) @@ -547,13 +547,13 @@ type ( Sign(message string) (string, error) } - // JWTService represents a service for managing JWT tokens. + // JWTService represents a service for managing JWT tokens JWTService interface { GenerateToken(data *TokenData) (string, error) ParseAndVerifyToken(token string) (*TokenData, error) } - // FileService represents a service for managing files. + // FileService represents a service for managing files FileService interface { GetFileContent(filePath string) ([]byte, error) Rename(oldPath, newPath string) error @@ -571,13 +571,13 @@ type ( FileExists(path string) (bool, error) } - // GitService represents a service for managing Git. + // GitService represents a service for managing Git GitService interface { ClonePublicRepository(repositoryURL, referenceName string, destination string) error ClonePrivateRepositoryWithBasicAuth(repositoryURL, referenceName string, destination, username, password string) error } - // JobScheduler represents a service to run jobs on a periodic basis. + // JobScheduler represents a service to run jobs on a periodic basis JobScheduler interface { ScheduleEndpointSyncJob(endpointFilePath, interval string) error ScheduleSnapshotJob(interval string) error @@ -585,19 +585,19 @@ type ( Start() } - // Snapshotter represents a service used to create endpoint snapshots. + // Snapshotter represents a service used to create endpoint snapshots Snapshotter interface { CreateSnapshot(endpoint *Endpoint) (*Snapshot, error) } - // LDAPService represents a service used to authenticate users against a LDAP/AD. + // LDAPService represents a service used to authenticate users against a LDAP/AD LDAPService interface { AuthenticateUser(username, password string, settings *LDAPSettings) error TestConnectivity(settings *LDAPSettings) error GetUserGroups(username string, settings *LDAPSettings) ([]string, error) } - // SwarmStackManager represents a service to manage Swarm stacks. + // SwarmStackManager represents a service to manage Swarm stacks SwarmStackManager interface { Login(dockerhub *DockerHub, registries []Registry, endpoint *Endpoint) Logout(endpoint *Endpoint) error @@ -605,7 +605,7 @@ type ( Remove(stack *Stack, endpoint *Endpoint) error } - // ComposeStackManager represents a service to manage Compose stacks. + // ComposeStackManager represents a service to manage Compose stacks ComposeStackManager interface { Up(stack *Stack, endpoint *Endpoint) error Down(stack *Stack, endpoint *Endpoint) error @@ -613,13 +613,15 @@ type ( ) const ( - // APIVersion is the version number of the Portainer API. + // APIVersion is the version number of the Portainer API APIVersion = "1.19.2-dev" - // DBVersion is the version number of the Portainer database. + // DBVersion is the version number of the Portainer database DBVersion = 14 + // MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved + MessageOfTheDayURL = "https://raw.githubusercontent.com/portainer/motd/master/message.html" // PortainerAgentHeader represents the name of the header available in any agent response PortainerAgentHeader = "Portainer-Agent" - // PortainerAgentTargetHeader represent the name of the header containing the target node name. + // PortainerAgentTargetHeader represent the name of the header containing the target node name PortainerAgentTargetHeader = "X-PortainerAgent-Target" // PortainerAgentSignatureHeader represent the name of the header containing the digital signature PortainerAgentSignatureHeader = "X-PortainerAgent-Signature" @@ -628,16 +630,16 @@ const ( // PortainerAgentSignatureMessage represents the message used to create a digital signature // to be used when communicating with an agent PortainerAgentSignatureMessage = "Portainer-App" - // SupportedDockerAPIVersion is the minimum Docker API version supported by Portainer. + // SupportedDockerAPIVersion is the minimum Docker API version supported by Portainer SupportedDockerAPIVersion = "1.24" ) const ( - // TLSFileCA represents a TLS CA certificate file. + // TLSFileCA represents a TLS CA certificate file TLSFileCA TLSFileType = iota - // TLSFileCert represents a TLS certificate file. + // TLSFileCert represents a TLS certificate file TLSFileCert - // TLSFileKey represents a TLS key file. + // TLSFileKey represents a TLS key file TLSFileKey ) diff --git a/app/constants.js b/app/constants.js index 9b92ddf87..de9631ddd 100644 --- a/app/constants.js +++ b/app/constants.js @@ -3,6 +3,7 @@ angular.module('portainer') .constant('API_ENDPOINT_DOCKERHUB', 'api/dockerhub') .constant('API_ENDPOINT_ENDPOINTS', 'api/endpoints') .constant('API_ENDPOINT_ENDPOINT_GROUPS', 'api/endpoint_groups') +.constant('API_ENDPOINT_MOTD', 'api/motd') .constant('API_ENDPOINT_REGISTRIES', 'api/registries') .constant('API_ENDPOINT_RESOURCE_CONTROLS', 'api/resource_controls') .constant('API_ENDPOINT_SETTINGS', 'api/settings') diff --git a/app/docker/views/dashboard/dashboard.html b/app/docker/views/dashboard/dashboard.html index 3821dfead..5b1a2bac3 100644 --- a/app/docker/views/dashboard/dashboard.html +++ b/app/docker/views/dashboard/dashboard.html @@ -9,30 +9,22 @@ -
-
- - -
- Information -
-
- -

- - Portainer is connected to a node that is part of a Swarm cluster. Some resources located on other nodes in the cluster might not be available for management, have a look - at our agent setup for more details. -

-

- - Portainer is connected to a worker node. Swarm management features will not be available. -

-
-
-
-
-
-
+ + +

+ + Portainer is connected to a node that is part of a Swarm cluster. Some resources located on other nodes in the cluster might not be available for management, have a look + at our agent setup for more details. +

+

+ + Portainer is connected to a worker node. Swarm management features will not be available. +

+
+
diff --git a/app/docker/views/dashboard/dashboardController.js b/app/docker/views/dashboard/dashboardController.js index d9bf1b2c4..114c745d0 100644 --- a/app/docker/views/dashboard/dashboardController.js +++ b/app/docker/views/dashboard/dashboardController.js @@ -1,6 +1,10 @@ angular.module('portainer.docker') -.controller('DashboardController', ['$scope', '$q', 'ContainerService', 'ImageService', 'NetworkService', 'VolumeService', 'SystemService', 'ServiceService', 'StackService', 'EndpointService', 'Notifications', 'EndpointProvider', -function ($scope, $q, ContainerService, ImageService, NetworkService, VolumeService, SystemService, ServiceService, StackService, EndpointService, Notifications, EndpointProvider) { +.controller('DashboardController', ['$scope', '$q', 'ContainerService', 'ImageService', 'NetworkService', 'VolumeService', 'SystemService', 'ServiceService', 'StackService', 'EndpointService', 'Notifications', 'EndpointProvider', 'StateManager', +function ($scope, $q, ContainerService, ImageService, NetworkService, VolumeService, SystemService, ServiceService, StackService, EndpointService, Notifications, EndpointProvider, StateManager) { + + $scope.dismissInformationPanel = function(id) { + StateManager.dismissInformationPanel(id); + }; function initView() { var endpointMode = $scope.applicationState.endpoint.mode; diff --git a/app/portainer/components/information-panel/information-panel.js b/app/portainer/components/information-panel/information-panel.js index ea014f619..083b6d013 100644 --- a/app/portainer/components/information-panel/information-panel.js +++ b/app/portainer/components/information-panel/information-panel.js @@ -1,7 +1,8 @@ angular.module('portainer.app').component('informationPanel', { templateUrl: 'app/portainer/components/information-panel/informationPanel.html', bindings: { - titleText: '@' + titleText: '@', + dismissAction: '&' }, transclude: true }); diff --git a/app/portainer/components/information-panel/informationPanel.html b/app/portainer/components/information-panel/informationPanel.html index 7407ec823..21f434354 100644 --- a/app/portainer/components/information-panel/informationPanel.html +++ b/app/portainer/components/information-panel/informationPanel.html @@ -3,7 +3,12 @@
- {{ $ctrl.titleText }} + + {{ $ctrl.titleText }} + + + dismiss +
diff --git a/app/portainer/models/motd.js b/app/portainer/models/motd.js new file mode 100644 index 000000000..165317b31 --- /dev/null +++ b/app/portainer/models/motd.js @@ -0,0 +1,4 @@ +function MotdViewModel(data) { + this.Message = data.Message; + this.Hash = data.Hash; +} diff --git a/app/portainer/rest/motd.js b/app/portainer/rest/motd.js new file mode 100644 index 000000000..49e54f534 --- /dev/null +++ b/app/portainer/rest/motd.js @@ -0,0 +1,7 @@ +angular.module('portainer.app') +.factory('Motd', ['$resource', 'API_ENDPOINT_MOTD', function MotdFactory($resource, API_ENDPOINT_MOTD) { + 'use strict'; + return $resource(API_ENDPOINT_MOTD, {}, { + get: { method: 'GET' } + }); +}]); diff --git a/app/portainer/services/api/motdService.js b/app/portainer/services/api/motdService.js new file mode 100644 index 000000000..3ea5dcf73 --- /dev/null +++ b/app/portainer/services/api/motdService.js @@ -0,0 +1,22 @@ +angular.module('portainer.app') +.factory('MotdService', ['$q', 'Motd', function MotdServiceFactory($q, Motd) { + 'use strict'; + var service = {}; + + service.motd = function() { + var deferred = $q.defer(); + + Motd.get().$promise + .then(function success(data) { + var motd = new MotdViewModel(data); + deferred.resolve(motd); + }) + .catch(function error(err) { + deferred.reject({msg: 'Unable to retrieve information message', err: err}); + }); + + return deferred.promise; + }; + + return service; +}]); diff --git a/app/portainer/services/localStorage.js b/app/portainer/services/localStorage.js index cb1dadceb..b104b4076 100644 --- a/app/portainer/services/localStorage.js +++ b/app/portainer/services/localStorage.js @@ -26,6 +26,12 @@ angular.module('portainer.app') getApplicationState: function() { return localStorageService.get('APPLICATION_STATE'); }, + storeUIState: function(state) { + localStorageService.cookie.set('UI_STATE', state); + }, + getUIState: function() { + return localStorageService.cookie.get('UI_STATE'); + }, storeJWT: function(jwt) { localStorageService.set('JWT', jwt); }, diff --git a/app/portainer/services/stateManager.js b/app/portainer/services/stateManager.js index 557c0853c..2b80109fe 100644 --- a/app/portainer/services/stateManager.js +++ b/app/portainer/services/stateManager.js @@ -9,7 +9,20 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin loading: true, application: {}, endpoint: {}, - UI: {} + UI: { + dismissedInfoPanels: {}, + dismissedInfoHash: '' + } + }; + + manager.dismissInformationPanel = function(id) { + state.UI.dismissedInfoPanels[id] = true; + LocalStorage.storeUIState(state.UI); + }; + + manager.dismissImportantInformation = function(hash) { + state.UI.dismissedInfoHash = hash; + LocalStorage.storeUIState(state.UI); }; manager.getState = function() { @@ -68,6 +81,11 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin manager.initialize = function () { var deferred = $q.defer(); + var UIState = LocalStorage.getUIState(); + if (UIState) { + state.UI = UIState; + } + var endpointState = LocalStorage.getEndpointState(); if (endpointState) { state.endpoint = endpointState; diff --git a/app/portainer/views/home/home.html b/app/portainer/views/home/home.html index 1ffae5f9e..e9de46386 100644 --- a/app/portainer/views/home/home.html +++ b/app/portainer/views/home/home.html @@ -7,7 +7,19 @@ Endpoints - + + +

+
+
+ +

Welcome to Portainer ! Click on any endpoint in the list below to access management features. diff --git a/app/portainer/views/home/homeController.js b/app/portainer/views/home/homeController.js index 88bee800b..318e9e2c9 100644 --- a/app/portainer/views/home/homeController.js +++ b/app/portainer/views/home/homeController.js @@ -1,6 +1,6 @@ angular.module('portainer.app') -.controller('HomeController', ['$q', '$scope', '$state', 'Authentication', 'EndpointService', 'EndpointHelper', 'GroupService', 'Notifications', 'EndpointProvider', 'StateManager', 'ExtensionManager', 'ModalService', -function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, GroupService, Notifications, EndpointProvider, StateManager, ExtensionManager, ModalService) { +.controller('HomeController', ['$q', '$scope', '$state', 'Authentication', 'EndpointService', 'EndpointHelper', 'GroupService', 'Notifications', 'EndpointProvider', 'StateManager', 'ExtensionManager', 'ModalService', 'MotdService', +function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, GroupService, Notifications, EndpointProvider, StateManager, ExtensionManager, ModalService, MotdService) { $scope.goToDashboard = function(endpoint) { EndpointProvider.setEndpointID(endpoint.Id); @@ -12,6 +12,14 @@ function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, G } }; + $scope.dismissImportantInformation = function(hash) { + StateManager.dismissImportantInformation(hash); + }; + + $scope.dismissInformationPanel = function(id) { + StateManager.dismissInformationPanel(id); + }; + function triggerSnapshot() { EndpointService.snapshot() .then(function success(data) { @@ -57,6 +65,11 @@ function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, G function initView() { $scope.isAdmin = Authentication.getUserDetails().role === 1; + MotdService.motd() + .then(function success(data) { + $scope.motd = data; + }); + $q.all({ endpoints: EndpointService.endpoints(), groups: GroupService.groups()