From 5e73a4947369535ff43bc304ea3fadd9714cd828 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Fri, 15 Jun 2018 09:18:25 +0200 Subject: [PATCH] feat(tags): add the ability to manage tags (#1971) * feat(tags): add the ability to manage tags * feat(tags): update tag selector UX * refactor(app): remove unused ui-select library --- api/bolt/datastore.go | 7 +- api/bolt/internal/internal.go | 10 ++ api/bolt/migrate_dbversion11.go | 37 ++++++ api/bolt/migrator.go | 14 +++ api/bolt/tag_service.go | 71 +++++++++++ api/cmd/portainer/main.go | 3 + api/errors.go | 5 + .../endpointgroups/endpointgroup_create.go | 7 +- .../endpointgroups/endpointgroup_update.go | 6 +- api/http/handler/endpoints/endpoint_create.go | 11 ++ api/http/handler/endpoints/endpoint_update.go | 5 + api/http/handler/handler.go | 8 +- api/http/handler/tags/handler.go | 31 +++++ api/http/handler/tags/tag_create.go | 53 ++++++++ api/http/handler/tags/tag_delete.go | 25 ++++ api/http/handler/tags/tag_list.go | 18 +++ api/http/server.go | 30 +++-- api/portainer.go | 24 +++- api/swagger.yaml | 119 ++++++++++++++++++ app/constants.js | 1 + app/portainer/__module.js | 12 ++ .../tags-datatable/tagsDatatable.html | 84 +++++++++++++ .../tags-datatable/tagsDatatable.js | 14 +++ .../components/forms/group-form/group-form.js | 1 + .../forms/group-form/groupForm.html | 36 ++---- .../components/tag-selector/tag-selector.js | 8 ++ .../components/tag-selector/tagSelector.html | 38 ++++++ .../tag-selector/tagSelectorController.js | 32 +++++ app/portainer/models/group.js | 8 +- app/portainer/models/tag.js | 4 + app/portainer/rest/tag.js | 9 ++ app/portainer/services/api/endpointService.js | 10 +- app/portainer/services/api/tagService.js | 49 ++++++++ app/portainer/services/fileUpload.js | 7 +- .../create/createEndpointController.js | 33 +++-- .../endpoints/create/createendpoint.html | 20 ++- .../views/endpoints/edit/endpoint.html | 10 +- .../endpoints/edit/endpointController.js | 9 +- .../groups/create/createGroupController.js | 20 ++- .../views/groups/create/creategroup.html | 1 + app/portainer/views/groups/edit/group.html | 1 + .../views/groups/edit/groupController.js | 16 +-- .../init/endpoint/initEndpointController.js | 4 +- app/portainer/views/sidebar/sidebar.html | 5 +- app/portainer/views/tags/tags.html | 58 +++++++++ app/portainer/views/tags/tagsController.js | 58 +++++++++ assets/css/app.css | 21 +++- package.json | 1 - vendor.yml | 2 - yarn.lock | 4 - 50 files changed, 942 insertions(+), 118 deletions(-) create mode 100644 api/bolt/migrate_dbversion11.go create mode 100644 api/bolt/tag_service.go create mode 100644 api/http/handler/tags/handler.go create mode 100644 api/http/handler/tags/tag_create.go create mode 100644 api/http/handler/tags/tag_delete.go create mode 100644 api/http/handler/tags/tag_list.go create mode 100644 app/portainer/components/datatables/tags-datatable/tagsDatatable.html create mode 100644 app/portainer/components/datatables/tags-datatable/tagsDatatable.js create mode 100644 app/portainer/components/tag-selector/tag-selector.js create mode 100644 app/portainer/components/tag-selector/tagSelector.html create mode 100644 app/portainer/components/tag-selector/tagSelectorController.js create mode 100644 app/portainer/models/tag.js create mode 100644 app/portainer/rest/tag.js create mode 100644 app/portainer/services/api/tagService.js create mode 100644 app/portainer/views/tags/tags.html create mode 100644 app/portainer/views/tags/tagsController.js diff --git a/api/bolt/datastore.go b/api/bolt/datastore.go index 8017d8add..37ad96ce7 100644 --- a/api/bolt/datastore.go +++ b/api/bolt/datastore.go @@ -27,6 +27,7 @@ type Store struct { RegistryService *RegistryService DockerHubService *DockerHubService StackService *StackService + TagService *TagService db *bolt.DB checkForDataMigration bool @@ -45,6 +46,7 @@ const ( registryBucketName = "registries" dockerhubBucketName = "dockerhub" stackBucketName = "stacks" + tagBucketName = "tags" ) // NewStore initializes a new Store and the associated services @@ -62,6 +64,7 @@ func NewStore(storePath string) (*Store, error) { RegistryService: &RegistryService{}, DockerHubService: &DockerHubService{}, StackService: &StackService{}, + TagService: &TagService{}, } store.UserService.store = store store.TeamService.store = store @@ -74,6 +77,7 @@ func NewStore(storePath string) (*Store, error) { store.RegistryService.store = store store.DockerHubService.store = store store.StackService.store = store + store.TagService.store = store _, err := os.Stat(storePath + "/" + databaseFileName) if err != nil && os.IsNotExist(err) { @@ -99,7 +103,7 @@ func (store *Store) Open() error { bucketsToCreate := []string{versionBucketName, userBucketName, teamBucketName, endpointBucketName, endpointGroupBucketName, resourceControlBucketName, teamMembershipBucketName, settingsBucketName, - registryBucketName, dockerhubBucketName, stackBucketName} + registryBucketName, dockerhubBucketName, stackBucketName, tagBucketName} return db.Update(func(tx *bolt.Tx) error { @@ -128,6 +132,7 @@ func (store *Store) Init() error { Labels: []portainer.Pair{}, AuthorizedUsers: []portainer.UserID{}, AuthorizedTeams: []portainer.TeamID{}, + Tags: []string{}, } return store.EndpointGroupService.CreateEndpointGroup(unassignedGroup) diff --git a/api/bolt/internal/internal.go b/api/bolt/internal/internal.go index b247268ee..7ed0d72be 100644 --- a/api/bolt/internal/internal.go +++ b/api/bolt/internal/internal.go @@ -107,6 +107,16 @@ func UnmarshalDockerHub(data []byte, settings *portainer.DockerHub) error { return json.Unmarshal(data, settings) } +// MarshalTag encodes a Tag object to binary format. +func MarshalTag(tag *portainer.Tag) ([]byte, error) { + return json.Marshal(tag) +} + +// UnmarshalTag decodes a Tag object from a binary data. +func UnmarshalTag(data []byte, tag *portainer.Tag) error { + return json.Unmarshal(data, tag) +} + // Itob returns an 8-byte big endian representation of v. // This function is typically used for encoding integer IDs to byte slices // so that they can be used as BoltDB keys. diff --git a/api/bolt/migrate_dbversion11.go b/api/bolt/migrate_dbversion11.go new file mode 100644 index 000000000..a16cc093b --- /dev/null +++ b/api/bolt/migrate_dbversion11.go @@ -0,0 +1,37 @@ +package bolt + +func (m *Migrator) updateEndpointsToVersion12() error { + legacyEndpoints, err := m.EndpointService.Endpoints() + if err != nil { + return err + } + + for _, endpoint := range legacyEndpoints { + endpoint.Tags = []string{} + + err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint) + if err != nil { + return err + } + } + + return nil +} + +func (m *Migrator) updateEndpointGroupsToVersion12() error { + legacyEndpointGroups, err := m.EndpointGroupService.EndpointGroups() + if err != nil { + return err + } + + for _, group := range legacyEndpointGroups { + group.Tags = []string{} + + err = m.EndpointGroupService.UpdateEndpointGroup(group.ID, &group) + if err != nil { + return err + } + } + + return nil +} diff --git a/api/bolt/migrator.go b/api/bolt/migrator.go index 48242adaf..8f914a7b4 100644 --- a/api/bolt/migrator.go +++ b/api/bolt/migrator.go @@ -6,6 +6,7 @@ import "github.com/portainer/portainer" type Migrator struct { UserService *UserService EndpointService *EndpointService + EndpointGroupService *EndpointGroupService ResourceControlService *ResourceControlService SettingsService *SettingsService VersionService *VersionService @@ -18,6 +19,7 @@ func NewMigrator(store *Store, version int) *Migrator { return &Migrator{ UserService: store.UserService, EndpointService: store.EndpointService, + EndpointGroupService: store.EndpointGroupService, ResourceControlService: store.ResourceControlService, SettingsService: store.SettingsService, VersionService: store.VersionService, @@ -120,6 +122,18 @@ func (m *Migrator) Migrate() error { } } + if m.CurrentDBVersion < 12 { + err := m.updateEndpointsToVersion12() + if err != nil { + return err + } + + err = m.updateEndpointGroupsToVersion12() + if err != nil { + return err + } + } + err := m.VersionService.StoreDBVersion(portainer.DBVersion) if err != nil { return err diff --git a/api/bolt/tag_service.go b/api/bolt/tag_service.go new file mode 100644 index 000000000..0c57ace8f --- /dev/null +++ b/api/bolt/tag_service.go @@ -0,0 +1,71 @@ +package bolt + +import ( + "github.com/portainer/portainer" + "github.com/portainer/portainer/bolt/internal" + + "github.com/boltdb/bolt" +) + +// TagService represents a service for managing tags. +type TagService struct { + store *Store +} + +// Tags return an array containing all the tags. +func (service *TagService) Tags() ([]portainer.Tag, error) { + var tags = make([]portainer.Tag, 0) + err := service.store.db.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(tagBucketName)) + + cursor := bucket.Cursor() + for k, v := cursor.First(); k != nil; k, v = cursor.Next() { + var tag portainer.Tag + err := internal.UnmarshalTag(v, &tag) + if err != nil { + return err + } + tags = append(tags, tag) + } + + return nil + }) + if err != nil { + return nil, err + } + + return tags, nil +} + +// CreateTag creates a new tag. +func (service *TagService) CreateTag(tag *portainer.Tag) error { + return service.store.db.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(tagBucketName)) + + id, _ := bucket.NextSequence() + tag.ID = portainer.TagID(id) + + data, err := internal.MarshalTag(tag) + if err != nil { + return err + } + + err = bucket.Put(internal.Itob(int(tag.ID)), data) + if err != nil { + return err + } + return nil + }) +} + +// DeleteTag deletes a tag. +func (service *TagService) DeleteTag(ID portainer.TagID) error { + return service.store.db.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(tagBucketName)) + err := bucket.Delete(internal.Itob(int(ID))) + if err != nil { + return err + } + return nil + }) +} diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 0c13e118f..4f37e7846 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -236,6 +236,7 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, endpointService portain AuthorizedUsers: []portainer.UserID{}, AuthorizedTeams: []portainer.TeamID{}, Extensions: []portainer.EndpointExtension{}, + Tags: []string{}, } if strings.HasPrefix(endpoint.URL, "tcp://") { @@ -274,6 +275,7 @@ func createUnsecuredEndpoint(endpointURL string, endpointService portainer.Endpo AuthorizedUsers: []portainer.UserID{}, AuthorizedTeams: []portainer.TeamID{}, Extensions: []portainer.EndpointExtension{}, + Tags: []string{}, } return endpointService.CreateEndpoint(endpoint) @@ -401,6 +403,7 @@ func main() { RegistryService: store.RegistryService, DockerHubService: store.DockerHubService, StackService: store.StackService, + TagService: store.TagService, SwarmStackManager: swarmStackManager, ComposeStackManager: composeStackManager, CryptoService: cryptoService, diff --git a/api/errors.go b/api/errors.go index c8f534f2c..4c8823891 100644 --- a/api/errors.go +++ b/api/errors.go @@ -68,6 +68,11 @@ const ( ErrStackNotExternal = Error("Not an external stack") ) +// Tag errors +const ( + ErrTagAlreadyExists = Error("A tag already exists with this name") +) + // Endpoint extensions error const ( ErrEndpointExtensionNotSupported = Error("This extension is not supported") diff --git a/api/http/handler/endpointgroups/endpointgroup_create.go b/api/http/handler/endpointgroups/endpointgroup_create.go index 834122f08..e2d7bd0c0 100644 --- a/api/http/handler/endpointgroups/endpointgroup_create.go +++ b/api/http/handler/endpointgroups/endpointgroup_create.go @@ -13,14 +13,17 @@ import ( type endpointGroupCreatePayload struct { Name string Description string - Labels []portainer.Pair AssociatedEndpoints []portainer.EndpointID + Tags []string } func (payload *endpointGroupCreatePayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.Name) { return portainer.Error("Invalid endpoint group name") } + if payload.Tags == nil { + payload.Tags = []string{} + } return nil } @@ -35,9 +38,9 @@ func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Reque endpointGroup := &portainer.EndpointGroup{ Name: payload.Name, Description: payload.Description, - Labels: payload.Labels, AuthorizedUsers: []portainer.UserID{}, AuthorizedTeams: []portainer.TeamID{}, + Tags: payload.Tags, } err = handler.EndpointGroupService.CreateEndpointGroup(endpointGroup) diff --git a/api/http/handler/endpointgroups/endpointgroup_update.go b/api/http/handler/endpointgroups/endpointgroup_update.go index 2f23c0f66..237e5d9a6 100644 --- a/api/http/handler/endpointgroups/endpointgroup_update.go +++ b/api/http/handler/endpointgroups/endpointgroup_update.go @@ -12,8 +12,8 @@ import ( type endpointGroupUpdatePayload struct { Name string Description string - Labels []portainer.Pair AssociatedEndpoints []portainer.EndpointID + Tags []string } func (payload *endpointGroupUpdatePayload) Validate(r *http.Request) error { @@ -48,7 +48,9 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque endpointGroup.Description = payload.Description } - endpointGroup.Labels = payload.Labels + if payload.Tags != nil { + endpointGroup.Tags = payload.Tags + } err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup) if err != nil { diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go index 38d2cfd47..7827068cb 100644 --- a/api/http/handler/endpoints/endpoint_create.go +++ b/api/http/handler/endpoints/endpoint_create.go @@ -28,6 +28,7 @@ type endpointCreatePayload struct { AzureApplicationID string AzureTenantID string AzureAuthenticationKey string + Tags []string } func (payload *endpointCreatePayload) Validate(r *http.Request) error { @@ -49,6 +50,13 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { } payload.GroupID = groupID + var tags []string + err = request.RetrieveMultiPartFormJSONValue(r, "Tags", &tags, true) + if err != nil { + return portainer.Error("Invalid Tags parameter") + } + payload.Tags = tags + useTLS, _ := request.RetrieveBooleanMultiPartFormValue(r, "TLS", true) payload.TLS = useTLS @@ -168,6 +176,7 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po AuthorizedTeams: []portainer.TeamID{}, Extensions: []portainer.EndpointExtension{}, AzureCredentials: credentials, + Tags: payload.Tags, } err = handler.EndpointService.CreateEndpoint(endpoint) @@ -203,6 +212,7 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload) AuthorizedUsers: []portainer.UserID{}, AuthorizedTeams: []portainer.TeamID{}, Extensions: []portainer.EndpointExtension{}, + Tags: payload.Tags, } err := handler.EndpointService.CreateEndpoint(endpoint) @@ -242,6 +252,7 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload) AuthorizedUsers: []portainer.UserID{}, AuthorizedTeams: []portainer.TeamID{}, Extensions: []portainer.EndpointExtension{}, + Tags: payload.Tags, } err = handler.EndpointService.CreateEndpoint(endpoint) diff --git a/api/http/handler/endpoints/endpoint_update.go b/api/http/handler/endpoints/endpoint_update.go index a65e9872b..7532c0450 100644 --- a/api/http/handler/endpoints/endpoint_update.go +++ b/api/http/handler/endpoints/endpoint_update.go @@ -22,6 +22,7 @@ type endpointUpdatePayload struct { AzureApplicationID string AzureTenantID string AzureAuthenticationKey string + Tags []string } func (payload *endpointUpdatePayload) Validate(r *http.Request) error { @@ -68,6 +69,10 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * endpoint.GroupID = portainer.EndpointGroupID(payload.GroupID) } + if payload.Tags != nil { + endpoint.Tags = payload.Tags + } + if endpoint.Type == portainer.AzureEnvironment { credentials := endpoint.AzureCredentials if payload.AzureApplicationID != "" { diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index 84d01ddc5..dd15e1212 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -15,6 +15,7 @@ import ( "github.com/portainer/portainer/http/handler/settings" "github.com/portainer/portainer/http/handler/stacks" "github.com/portainer/portainer/http/handler/status" + "github.com/portainer/portainer/http/handler/tags" "github.com/portainer/portainer/http/handler/teammemberships" "github.com/portainer/portainer/http/handler/teams" "github.com/portainer/portainer/http/handler/templates" @@ -37,16 +38,13 @@ type Handler struct { SettingsHandler *settings.Handler StackHandler *stacks.Handler StatusHandler *status.Handler + TagHandler *tags.Handler TeamMembershipHandler *teammemberships.Handler TeamHandler *teams.Handler TemplatesHandler *templates.Handler UploadHandler *upload.Handler UserHandler *users.Handler WebSocketHandler *websocket.Handler - - // StoridgeHandler *extensions.StoridgeHandler - // AzureHandler *azure.Handler - // DockerHandler *docker.Handler } // ServeHTTP delegates a request to the appropriate subhandler. @@ -79,6 +77,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.StripPrefix("/api", h.StackHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/status"): http.StripPrefix("/api", h.StatusHandler).ServeHTTP(w, r) + case strings.HasPrefix(r.URL.Path, "/api/tags"): + http.StripPrefix("/api", h.TagHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/templates"): http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/upload"): diff --git a/api/http/handler/tags/handler.go b/api/http/handler/tags/handler.go new file mode 100644 index 000000000..a700f7c3e --- /dev/null +++ b/api/http/handler/tags/handler.go @@ -0,0 +1,31 @@ +package tags + +import ( + "net/http" + + "github.com/gorilla/mux" + "github.com/portainer/portainer" + httperror "github.com/portainer/portainer/http/error" + "github.com/portainer/portainer/http/security" +) + +// Handler is the HTTP handler used to handle tag operations. +type Handler struct { + *mux.Router + TagService portainer.TagService +} + +// NewHandler creates a handler to manage tag operations. +func NewHandler(bouncer *security.RequestBouncer) *Handler { + h := &Handler{ + Router: mux.NewRouter(), + } + h.Handle("/tags", + bouncer.AdministratorAccess(httperror.LoggerHandler(h.tagCreate))).Methods(http.MethodPost) + h.Handle("/tags", + bouncer.AdministratorAccess(httperror.LoggerHandler(h.tagList))).Methods(http.MethodGet) + h.Handle("/tags/{id}", + bouncer.AdministratorAccess(httperror.LoggerHandler(h.tagDelete))).Methods(http.MethodDelete) + + return h +} diff --git a/api/http/handler/tags/tag_create.go b/api/http/handler/tags/tag_create.go new file mode 100644 index 000000000..f75c050b9 --- /dev/null +++ b/api/http/handler/tags/tag_create.go @@ -0,0 +1,53 @@ +package tags + +import ( + "net/http" + + "github.com/asaskevich/govalidator" + "github.com/portainer/portainer" + httperror "github.com/portainer/portainer/http/error" + "github.com/portainer/portainer/http/request" + "github.com/portainer/portainer/http/response" +) + +type tagCreatePayload struct { + Name string +} + +func (payload *tagCreatePayload) Validate(r *http.Request) error { + if govalidator.IsNull(payload.Name) { + return portainer.Error("Invalid tag name") + } + return nil +} + +// POST request on /api/tags +func (handler *Handler) tagCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + var payload tagCreatePayload + err := request.DecodeAndValidateJSONPayload(r, &payload) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} + } + + tags, err := handler.TagService.Tags() + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve tags from the database", err} + } + + for _, tag := range tags { + if tag.Name == payload.Name { + return &httperror.HandlerError{http.StatusConflict, "This name is already associated to a tag", portainer.ErrTagAlreadyExists} + } + } + + tag := &portainer.Tag{ + Name: payload.Name, + } + + err = handler.TagService.CreateTag(tag) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the tag inside the database", err} + } + + return response.JSON(w, tag) +} diff --git a/api/http/handler/tags/tag_delete.go b/api/http/handler/tags/tag_delete.go new file mode 100644 index 000000000..b1b4fe867 --- /dev/null +++ b/api/http/handler/tags/tag_delete.go @@ -0,0 +1,25 @@ +package tags + +import ( + "net/http" + + "github.com/portainer/portainer" + httperror "github.com/portainer/portainer/http/error" + "github.com/portainer/portainer/http/request" + "github.com/portainer/portainer/http/response" +) + +// DELETE request on /api/tags/:name +func (handler *Handler) tagDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + id, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid tag identifier route variable", err} + } + + err = handler.TagService.DeleteTag(portainer.TagID(id)) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the tag from the database", err} + } + + return response.Empty(w) +} diff --git a/api/http/handler/tags/tag_list.go b/api/http/handler/tags/tag_list.go new file mode 100644 index 000000000..b572add68 --- /dev/null +++ b/api/http/handler/tags/tag_list.go @@ -0,0 +1,18 @@ +package tags + +import ( + "net/http" + + httperror "github.com/portainer/portainer/http/error" + "github.com/portainer/portainer/http/response" +) + +// GET request on /api/tags +func (handler *Handler) tagList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + tags, err := handler.TagService.Tags() + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve tags from the database", err} + } + + return response.JSON(w, tags) +} diff --git a/api/http/server.go b/api/http/server.go index f0b1cdbda..9c476c74d 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -16,6 +16,7 @@ import ( "github.com/portainer/portainer/http/handler/settings" "github.com/portainer/portainer/http/handler/stacks" "github.com/portainer/portainer/http/handler/status" + "github.com/portainer/portainer/http/handler/tags" "github.com/portainer/portainer/http/handler/teammemberships" "github.com/portainer/portainer/http/handler/teams" "github.com/portainer/portainer/http/handler/templates" @@ -36,24 +37,25 @@ type Server struct { AuthDisabled bool EndpointManagement bool Status *portainer.Status - UserService portainer.UserService - TeamService portainer.TeamService - TeamMembershipService portainer.TeamMembershipService + ComposeStackManager portainer.ComposeStackManager + CryptoService portainer.CryptoService + SignatureService portainer.DigitalSignatureService + DockerHubService portainer.DockerHubService EndpointService portainer.EndpointService EndpointGroupService portainer.EndpointGroupService + FileService portainer.FileService + GitService portainer.GitService + JWTService portainer.JWTService + LDAPService portainer.LDAPService + RegistryService portainer.RegistryService ResourceControlService portainer.ResourceControlService SettingsService portainer.SettingsService - CryptoService portainer.CryptoService - JWTService portainer.JWTService - FileService portainer.FileService - RegistryService portainer.RegistryService - DockerHubService portainer.DockerHubService StackService portainer.StackService SwarmStackManager portainer.SwarmStackManager - ComposeStackManager portainer.ComposeStackManager - LDAPService portainer.LDAPService - GitService portainer.GitService - SignatureService portainer.DigitalSignatureService + TagService portainer.TagService + TeamService portainer.TeamService + TeamMembershipService portainer.TeamMembershipService + UserService portainer.UserService Handler *handler.Handler SSL bool SSLCert string @@ -126,6 +128,9 @@ func (server *Server) Start() error { stackHandler.RegistryService = server.RegistryService stackHandler.DockerHubService = server.DockerHubService + var tagHandler = tags.NewHandler(requestBouncer) + tagHandler.TagService = server.TagService + var teamHandler = teams.NewHandler(requestBouncer) teamHandler.TeamService = server.TeamService teamHandler.TeamMembershipService = server.TeamMembershipService @@ -164,6 +169,7 @@ func (server *Server) Start() error { SettingsHandler: settingsHandler, StatusHandler: statusHandler, StackHandler: stackHandler, + TagHandler: tagHandler, TeamHandler: teamHandler, TeamMembershipHandler: teamMembershipHandler, TemplatesHandler: templatesHandler, diff --git a/api/portainer.go b/api/portainer.go index 5053cfccd..bfe84a213 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -190,6 +190,7 @@ type ( AuthorizedTeams []TeamID `json:"AuthorizedTeams"` Extensions []EndpointExtension `json:"Extensions"` AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"` + Tags []string `json:"Tags"` // Deprecated fields // Deprecated in DBVersion == 4 @@ -217,7 +218,10 @@ type ( Description string `json:"Description"` AuthorizedUsers []UserID `json:"AuthorizedUsers"` AuthorizedTeams []TeamID `json:"AuthorizedTeams"` - Labels []Pair `json:"Labels"` + Tags []string `json:"Tags"` + + // Deprecated fields + Labels []Pair `json:"Labels"` } // EndpointExtension represents a extension associated to an endpoint. @@ -264,6 +268,15 @@ type ( AccessLevel ResourceAccessLevel `json:"AccessLevel"` } + // TagID represents a tag identifier. + TagID int + + // Tag represents a tag that can be associated to a resource. + Tag struct { + ID TagID + Name string `json:"Name"` + } + // ResourceAccessLevel represents the level of control associated to a resource. ResourceAccessLevel int @@ -390,6 +403,13 @@ type ( DeleteResourceControl(ID ResourceControlID) error } + // TagService represents a service for managing tag data. + TagService interface { + Tags() ([]Tag, error) + CreateTag(tag *Tag) error + DeleteTag(ID TagID) error + } + // CryptoService represents a service for encrypting/hashing data. CryptoService interface { Hash(data string) (string, error) @@ -463,7 +483,7 @@ const ( // APIVersion is the version number of the Portainer API. APIVersion = "1.17.1-dev" // DBVersion is the version number of the Portainer database. - DBVersion = 11 + DBVersion = 12 // DefaultTemplatesURL represents the default URL for the templates definitions. DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json" // PortainerAgentHeader represents the name of the header available in any agent response diff --git a/api/swagger.yaml b/api/swagger.yaml index 13277649e..224b9ecf1 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -81,6 +81,8 @@ tags: description: "Manage Docker stacks" - name: "users" description: "Manage users" +- name: "tags" + description: "Manage tags" - name: "teams" description: "Manage teams" - name: "team_memberships" @@ -1958,6 +1960,99 @@ paths: schema: $ref: "#/definitions/GenericError" + /tags: + get: + tags: + - "tags" + summary: "List tags" + description: | + List tags. + **Access policy**: administrator + operationId: "TagList" + produces: + - "application/json" + parameters: [] + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/TagListResponse" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + post: + tags: + - "tags" + summary: "Create a new tag" + description: | + Create a new tag. + **Access policy**: administrator + operationId: "TagCreate" + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - in: "body" + name: "body" + description: "Tag details" + required: true + schema: + $ref: "#/definitions/TagCreateRequest" + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/Tag" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request data format" + 409: + description: "Conflict" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "A tag with the specified name already exists" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + /tags/{id}: + delete: + tags: + - "tags" + summary: "Remove a tag" + description: | + Remove a tag. + **Access policy**: administrator + operationId: "TagDelete" + parameters: + - name: "id" + in: "path" + description: "Tag identifier" + required: true + type: "integer" + responses: + 204: + description: "Success" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + /teams: get: tags: @@ -2411,6 +2506,17 @@ securityDefinitions: name: "Authorization" in: "header" definitions: + Tag: + type: "object" + properties: + Id: + type: "integer" + example: 1 + description: "Tag identifier" + Name: + type: "string" + example: "org/acme" + description: "Tag name" Team: type: "object" properties: @@ -3334,6 +3440,19 @@ definitions: type: "boolean" example: true description: "Is the password valid" + TagListResponse: + type: "array" + items: + $ref: "#/definitions/Tag" + TagCreateRequest: + type: "object" + required: + - "Name" + properties: + Name: + type: "string" + example: "org/acme" + description: "Name" TeamCreateRequest: type: "object" required: diff --git a/app/constants.js b/app/constants.js index a51ce323e..c4f911034 100644 --- a/app/constants.js +++ b/app/constants.js @@ -9,6 +9,7 @@ angular.module('portainer') .constant('API_ENDPOINT_STACKS', 'api/stacks') .constant('API_ENDPOINT_STATUS', 'api/status') .constant('API_ENDPOINT_USERS', 'api/users') +.constant('API_ENDPOINT_TAGS', 'api/tags') .constant('API_ENDPOINT_TEAMS', 'api/teams') .constant('API_ENDPOINT_TEAM_MEMBERSHIPS', 'api/team_memberships') .constant('API_ENDPOINT_TEMPLATES', 'api/templates') diff --git a/app/portainer/__module.js b/app/portainer/__module.js index 701a2bb8e..6109cc422 100644 --- a/app/portainer/__module.js +++ b/app/portainer/__module.js @@ -296,6 +296,17 @@ angular.module('portainer.app', []) } }; + var tags = { + name: 'portainer.tags', + url: '/tags', + views: { + 'content@': { + templateUrl: 'app/portainer/views/tags/tags.html', + controller: 'TagsController' + } + } + }; + var users = { name: 'portainer.users', url: '/users', @@ -366,6 +377,7 @@ angular.module('portainer.app', []) $stateRegistryProvider.register(stack); $stateRegistryProvider.register(stackCreation); $stateRegistryProvider.register(support); + $stateRegistryProvider.register(tags); $stateRegistryProvider.register(users); $stateRegistryProvider.register(user); $stateRegistryProvider.register(teams); diff --git a/app/portainer/components/datatables/tags-datatable/tagsDatatable.html b/app/portainer/components/datatables/tags-datatable/tagsDatatable.html new file mode 100644 index 000000000..29613f496 --- /dev/null +++ b/app/portainer/components/datatables/tags-datatable/tagsDatatable.html @@ -0,0 +1,84 @@ +
+ + +
+
+ {{ $ctrl.titleText }} +
+
+ + Search + +
+
+
+ +
+ +
+ + + + + + + + + + + + + + + + + +
+ + + + + + Name + + + +
+ + + + + {{ item.Name }} +
Loading...
No tag available.
+
+ +
+
+
diff --git a/app/portainer/components/datatables/tags-datatable/tagsDatatable.js b/app/portainer/components/datatables/tags-datatable/tagsDatatable.js new file mode 100644 index 000000000..cc33e7b7a --- /dev/null +++ b/app/portainer/components/datatables/tags-datatable/tagsDatatable.js @@ -0,0 +1,14 @@ +angular.module('portainer.app').component('tagsDatatable', { + templateUrl: 'app/portainer/components/datatables/tags-datatable/tagsDatatable.html', + controller: 'GenericDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + showTextFilter: '<', + removeAction: '<' + } +}); diff --git a/app/portainer/components/forms/group-form/group-form.js b/app/portainer/components/forms/group-form/group-form.js index e72054a7b..a4bb71231 100644 --- a/app/portainer/components/forms/group-form/group-form.js +++ b/app/portainer/components/forms/group-form/group-form.js @@ -21,6 +21,7 @@ angular.module('portainer.app').component('groupForm', { bindings: { model: '=', availableEndpoints: '=', + availableTags: '<', associatedEndpoints: '=', addLabelAction: '<', removeLabelAction: '<', diff --git a/app/portainer/components/forms/group-form/groupForm.html b/app/portainer/components/forms/group-form/groupForm.html index 0555c40e7..4a2d64331 100644 --- a/app/portainer/components/forms/group-form/groupForm.html +++ b/app/portainer/components/forms/group-form/groupForm.html @@ -22,33 +22,17 @@ - -
-
- - - add label - -
- -
-
-
- name - -
-
- value - -
- -
-
- +
+ Metadata
- + +
+ +
+
diff --git a/app/portainer/components/tag-selector/tag-selector.js b/app/portainer/components/tag-selector/tag-selector.js new file mode 100644 index 000000000..46b397d41 --- /dev/null +++ b/app/portainer/components/tag-selector/tag-selector.js @@ -0,0 +1,8 @@ +angular.module('portainer.app').component('tagSelector', { + templateUrl: 'app/portainer/components/tag-selector/tagSelector.html', + controller: 'TagSelectorController', + bindings: { + tags: '<', + model: '=' + } +}); diff --git a/app/portainer/components/tag-selector/tagSelector.html b/app/portainer/components/tag-selector/tagSelector.html new file mode 100644 index 000000000..be92e1aa4 --- /dev/null +++ b/app/portainer/components/tag-selector/tagSelector.html @@ -0,0 +1,38 @@ +
+ +
+ + {{ tag }} + + + + +
+
+
+ +
+ +
+
+ + No tags available. + +
+
+
+ + No tags matching your filter. + +
diff --git a/app/portainer/components/tag-selector/tagSelectorController.js b/app/portainer/components/tag-selector/tagSelectorController.js new file mode 100644 index 000000000..b7c18c2ca --- /dev/null +++ b/app/portainer/components/tag-selector/tagSelectorController.js @@ -0,0 +1,32 @@ +angular.module('portainer.app') +.controller('TagSelectorController', function () { + + var ctrl = this; + + this.$onChanges = function(changes) { + if(angular.isDefined(changes.tags.currentValue)) { + this.tags = _.difference(changes.tags.currentValue, this.model); + } + }; + + this.state = { + selectedValue: '', + noResult: false + }; + + this.selectTag = function($item, $model, $label) { + this.state.selectedValue = ''; + this.model.push($item); + this.tags = _.remove(this.tags, function(item) { + return item !== $item; + }); + }; + + this.removeTag = function(tag) { + var idx = this.model.indexOf(tag); + if (idx > -1) { + this.model.splice(idx, 1); + this.tags.push(tag); + } + }; +}); diff --git a/app/portainer/models/group.js b/app/portainer/models/group.js index bb8cf0b78..b16d825f7 100644 --- a/app/portainer/models/group.js +++ b/app/portainer/models/group.js @@ -1,14 +1,14 @@ function EndpointGroupDefaultModel() { this.Name = ''; this.Description = ''; - this.Labels = []; + this.Tags = []; } function EndpointGroupModel(data) { this.Id = data.Id; this.Name = data.Name; this.Description = data.Description; - this.Labels = data.Labels; + this.Tags = data.Tags; this.AuthorizedUsers = data.AuthorizedUsers; this.AuthorizedTeams = data.AuthorizedTeams; } @@ -16,7 +16,7 @@ function EndpointGroupModel(data) { function EndpointGroupCreateRequest(model, endpoints) { this.Name = model.Name; this.Description = model.Description; - this.Labels = model.Labels; + this.Tags = model.Tags; this.AssociatedEndpoints = endpoints; } @@ -24,6 +24,6 @@ function EndpointGroupUpdateRequest(model, endpoints) { this.id = model.Id; this.Name = model.Name; this.Description = model.Description; - this.Labels = model.Labels; + this.Tags = model.Tags; this.AssociatedEndpoints = endpoints; } diff --git a/app/portainer/models/tag.js b/app/portainer/models/tag.js new file mode 100644 index 000000000..dfefaff61 --- /dev/null +++ b/app/portainer/models/tag.js @@ -0,0 +1,4 @@ +function TagViewModel(data) { + this.Id = data.ID; + this.Name = data.Name; +} diff --git a/app/portainer/rest/tag.js b/app/portainer/rest/tag.js new file mode 100644 index 000000000..e3e656036 --- /dev/null +++ b/app/portainer/rest/tag.js @@ -0,0 +1,9 @@ +angular.module('portainer.app') +.factory('Tags', ['$resource', 'API_ENDPOINT_TAGS', function TagsFactory($resource, API_ENDPOINT_TAGS) { + 'use strict'; + return $resource(API_ENDPOINT_TAGS + '/:id', {}, { + create: { method: 'POST' }, + query: { method: 'GET', isArray: true }, + remove: { method: 'DELETE', params: { id: '@id'} } + }); +}]); diff --git a/app/portainer/services/api/endpointService.js b/app/portainer/services/api/endpointService.js index 8f1eb5f4d..e7dd9bb97 100644 --- a/app/portainer/services/api/endpointService.js +++ b/app/portainer/services/api/endpointService.js @@ -57,7 +57,7 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) { service.createLocalEndpoint = function() { var deferred = $q.defer(); - FileUploadService.createEndpoint('local', 1, 'unix:///var/run/docker.sock', '', 1, false) + FileUploadService.createEndpoint('local', 1, 'unix:///var/run/docker.sock', '', 1, [], false) .then(function success(response) { deferred.resolve(response.data); }) @@ -68,10 +68,10 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) { return deferred.promise; }; - service.createRemoteEndpoint = function(name, type, URL, PublicURL, groupID, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) { + service.createRemoteEndpoint = function(name, type, URL, PublicURL, groupID, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) { var deferred = $q.defer(); - FileUploadService.createEndpoint(name, type, 'tcp://' + URL, PublicURL, groupID, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) + FileUploadService.createEndpoint(name, type, 'tcp://' + URL, PublicURL, groupID, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) .then(function success(response) { deferred.resolve(response.data); }) @@ -82,10 +82,10 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) { return deferred.promise; }; - service.createAzureEndpoint = function(name, applicationId, tenantId, authenticationKey) { + service.createAzureEndpoint = function(name, applicationId, tenantId, authenticationKey, groupId, tags) { var deferred = $q.defer(); - FileUploadService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey) + FileUploadService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tags) .then(function success(response) { deferred.resolve(response.data); }) diff --git a/app/portainer/services/api/tagService.js b/app/portainer/services/api/tagService.js new file mode 100644 index 000000000..a73b9f932 --- /dev/null +++ b/app/portainer/services/api/tagService.js @@ -0,0 +1,49 @@ +angular.module('portainer.app') +.factory('TagService', ['$q', 'Tags', function TagServiceFactory($q, Tags) { + 'use strict'; + var service = {}; + + service.tags = function() { + var deferred = $q.defer(); + Tags.query().$promise + .then(function success(data) { + var tags = data.map(function (item) { + return new TagViewModel(item); + }); + deferred.resolve(tags); + }) + .catch(function error(err) { + deferred.reject({msg: 'Unable to retrieve tags', err: err}); + }); + return deferred.promise; + }; + + service.tagNames = function() { + var deferred = $q.defer(); + Tags.query().$promise + .then(function success(data) { + var tags = data.map(function (item) { + return item.Name; + }); + deferred.resolve(tags); + }) + .catch(function error(err) { + deferred.reject({msg: 'Unable to retrieve tags', err: err}); + }); + return deferred.promise; + }; + + service.createTag = function(name) { + var payload = { + Name: name + }; + + return Tags.create({}, payload).$promise; + }; + + service.deleteTag = function(id) { + return Tags.remove({id: id}).$promise; + }; + + return service; +}]); diff --git a/app/portainer/services/fileUpload.js b/app/portainer/services/fileUpload.js index d71f7f244..aef41e47a 100644 --- a/app/portainer/services/fileUpload.js +++ b/app/portainer/services/fileUpload.js @@ -52,7 +52,7 @@ angular.module('portainer.app') }); }; - service.createEndpoint = function(name, type, URL, PublicURL, groupID, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) { + service.createEndpoint = function(name, type, URL, PublicURL, groupID, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) { return Upload.upload({ url: 'api/endpoints', data: { @@ -61,6 +61,7 @@ angular.module('portainer.app') URL: URL, PublicURL: PublicURL, GroupID: groupID, + Tags: Upload.json(tags), TLS: TLS, TLSSkipVerify: TLSSkipVerify, TLSSkipClientVerify: TLSSkipClientVerify, @@ -72,12 +73,14 @@ angular.module('portainer.app') }); }; - service.createAzureEndpoint = function(name, applicationId, tenantId, authenticationKey) { + service.createAzureEndpoint = function(name, applicationId, tenantId, authenticationKey, groupId, tags) { return Upload.upload({ url: 'api/endpoints', data: { Name: name, EndpointType: 3, + GroupID: groupID, + Tags: Upload.json(tags), AzureApplicationID: applicationId, AzureTenantID: tenantId, AzureAuthenticationKey: authenticationKey diff --git a/app/portainer/views/endpoints/create/createEndpointController.js b/app/portainer/views/endpoints/create/createEndpointController.js index 3418f251a..6453902e9 100644 --- a/app/portainer/views/endpoints/create/createEndpointController.js +++ b/app/portainer/views/endpoints/create/createEndpointController.js @@ -1,6 +1,6 @@ angular.module('portainer.app') -.controller('CreateEndpointController', ['$scope', '$state', '$filter', 'EndpointService', 'GroupService', 'Notifications', -function ($scope, $state, $filter, EndpointService, GroupService, Notifications) { +.controller('CreateEndpointController', ['$q', '$scope', '$state', '$filter', 'EndpointService', 'GroupService', 'TagService', 'Notifications', +function ($q, $scope, $state, $filter, EndpointService, GroupService, TagService, Notifications) { $scope.state = { EnvironmentType: 'docker', @@ -15,7 +15,8 @@ function ($scope, $state, $filter, EndpointService, GroupService, Notifications) SecurityFormData: new EndpointSecurityFormData(), AzureApplicationId: '', AzureTenantId: '', - AzureAuthenticationKey: '' + AzureAuthenticationKey: '', + Tags: [] }; $scope.addDockerEndpoint = function() { @@ -23,6 +24,7 @@ function ($scope, $state, $filter, EndpointService, GroupService, Notifications) var URL = $filter('stripprotocol')($scope.formValues.URL); var publicURL = $scope.formValues.PublicURL === '' ? URL.split(':')[0] : $scope.formValues.PublicURL; var groupId = $scope.formValues.GroupId; + var tags = $scope.formValues.Tags; var securityData = $scope.formValues.SecurityFormData; var TLS = securityData.TLS; @@ -33,7 +35,7 @@ function ($scope, $state, $filter, EndpointService, GroupService, Notifications) var TLSCertFile = TLSSkipClientVerify ? null : securityData.TLSCert; var TLSKeyFile = TLSSkipClientVerify ? null : securityData.TLSKey; - addEndpoint(name, 1, URL, publicURL, groupId, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile); + addEndpoint(name, 1, URL, publicURL, groupId, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile); }; $scope.addAgentEndpoint = function() { @@ -41,8 +43,9 @@ function ($scope, $state, $filter, EndpointService, GroupService, Notifications) var URL = $filter('stripprotocol')($scope.formValues.URL); var publicURL = $scope.formValues.PublicURL === '' ? URL.split(':')[0] : $scope.formValues.PublicURL; var groupId = $scope.formValues.GroupId; + var tags = $scope.formValues.Tags; - addEndpoint(name, 2, URL, publicURL, groupId, true, true, true, null, null, null); + addEndpoint(name, 2, URL, publicURL, groupId, tags, true, true, true, null, null, null); }; $scope.addAzureEndpoint = function() { @@ -50,15 +53,17 @@ function ($scope, $state, $filter, EndpointService, GroupService, Notifications) var applicationId = $scope.formValues.AzureApplicationId; var tenantId = $scope.formValues.AzureTenantId; var authenticationKey = $scope.formValues.AzureAuthenticationKey; + var groupId = $scope.formValues.GroupId; + var tags = $scope.formValues.Tags; - createAzureEndpoint(name, applicationId, tenantId, authenticationKey); + createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tags); }; - function createAzureEndpoint(name, applicationId, tenantId, authenticationKey) { + function createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tags) { var endpoint; $scope.state.actionInProgress = true; - EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey) + EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tags) .then(function success() { Notifications.success('Endpoint created', name); $state.go('portainer.endpoints', {}, {reload: true}); @@ -71,9 +76,9 @@ function ($scope, $state, $filter, EndpointService, GroupService, Notifications) }); } - function addEndpoint(name, type, URL, PublicURL, groupId, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) { + function addEndpoint(name, type, URL, PublicURL, groupId, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) { $scope.state.actionInProgress = true; - EndpointService.createRemoteEndpoint(name, type, URL, PublicURL, groupId, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) + EndpointService.createRemoteEndpoint(name, type, URL, PublicURL, groupId, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) .then(function success() { Notifications.success('Endpoint created', name); $state.go('portainer.endpoints', {}, {reload: true}); @@ -87,9 +92,13 @@ function ($scope, $state, $filter, EndpointService, GroupService, Notifications) } function initView() { - GroupService.groups() + $q.all({ + groups: GroupService.groups(), + tags: TagService.tagNames() + }) .then(function success(data) { - $scope.groups = data; + $scope.groups = data.groups; + $scope.availableTags = data.tags; }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to load groups'); diff --git a/app/portainer/views/endpoints/create/createendpoint.html b/app/portainer/views/endpoints/create/createendpoint.html index a4053d8f1..cce0f67c7 100644 --- a/app/portainer/views/endpoints/create/createendpoint.html +++ b/app/portainer/views/endpoints/create/createendpoint.html @@ -192,6 +192,12 @@
+ + + +
+ Metadata +
- - - + +
+ +
+ +
+ Actions +
diff --git a/app/portainer/views/endpoints/edit/endpoint.html b/app/portainer/views/endpoints/edit/endpoint.html index c225d34fa..6a1349d35 100644 --- a/app/portainer/views/endpoints/edit/endpoint.html +++ b/app/portainer/views/endpoints/edit/endpoint.html @@ -49,7 +49,7 @@ >
- Grouping + Metadata
@@ -61,6 +61,14 @@
+ +
+ +
+
diff --git a/app/portainer/views/endpoints/edit/endpointController.js b/app/portainer/views/endpoints/edit/endpointController.js index 1d7cd8c3d..378924ccf 100644 --- a/app/portainer/views/endpoints/edit/endpointController.js +++ b/app/portainer/views/endpoints/edit/endpointController.js @@ -1,6 +1,6 @@ angular.module('portainer.app') -.controller('EndpointController', ['$q', '$scope', '$state', '$transition$', '$filter', 'EndpointService', 'GroupService', 'EndpointProvider', 'Notifications', -function ($q, $scope, $state, $transition$, $filter, EndpointService, GroupService, EndpointProvider, Notifications) { +.controller('EndpointController', ['$q', '$scope', '$state', '$transition$', '$filter', 'EndpointService', 'GroupService', 'TagService', 'EndpointProvider', 'Notifications', +function ($q, $scope, $state, $transition$, $filter, EndpointService, GroupService, TagService, EndpointProvider, Notifications) { if (!$scope.applicationState.application.endpointManagement) { $state.go('portainer.endpoints'); @@ -27,6 +27,7 @@ function ($q, $scope, $state, $transition$, $filter, EndpointService, GroupServi Name: endpoint.Name, PublicURL: endpoint.PublicURL, GroupID: endpoint.GroupId, + Tags: endpoint.Tags, TLS: TLS, TLSSkipVerify: TLSSkipVerify, TLSSkipClientVerify: TLSSkipClientVerify, @@ -61,7 +62,8 @@ function ($q, $scope, $state, $transition$, $filter, EndpointService, GroupServi function initView() { $q.all({ endpoint: EndpointService.endpoint($transition$.params().id), - groups: GroupService.groups() + groups: GroupService.groups(), + tags: TagService.tagNames() }) .then(function success(data) { var endpoint = data.endpoint; @@ -73,6 +75,7 @@ function ($q, $scope, $state, $transition$, $filter, EndpointService, GroupServi endpoint.URL = $filter('stripprotocol')(endpoint.URL); $scope.endpoint = endpoint; $scope.groups = data.groups; + $scope.availableTags = data.tags; }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve endpoint details'); diff --git a/app/portainer/views/groups/create/createGroupController.js b/app/portainer/views/groups/create/createGroupController.js index 1a14510fa..9fcf47c84 100644 --- a/app/portainer/views/groups/create/createGroupController.js +++ b/app/portainer/views/groups/create/createGroupController.js @@ -1,19 +1,11 @@ angular.module('portainer.app') -.controller('CreateGroupController', ['$scope', '$state', 'GroupService', 'EndpointService', 'Notifications', -function ($scope, $state, GroupService, EndpointService, Notifications) { +.controller('CreateGroupController', ['$q', '$scope', '$state', 'GroupService', 'EndpointService', 'TagService', 'Notifications', +function ($q, $scope, $state, GroupService, EndpointService, TagService, Notifications) { $scope.state = { actionInProgress: false }; - $scope.addLabel = function() { - $scope.model.Labels.push({ name: '', value: '' }); - }; - - $scope.removeLabel = function(index) { - $scope.model.Labels.splice(index, 1); - }; - $scope.create = function() { var model = $scope.model; @@ -40,10 +32,14 @@ function ($scope, $state, GroupService, EndpointService, Notifications) { function initView() { $scope.model = new EndpointGroupDefaultModel(); - EndpointService.endpointsByGroup(1) + $q.all({ + endpoints: EndpointService.endpointsByGroup(1), + tags: TagService.tagNames() + }) .then(function success(data) { - $scope.availableEndpoints = data; + $scope.availableEndpoints = data.endpoints; $scope.associatedEndpoints = []; + $scope.availableTags = data.tags; }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve endpoints'); diff --git a/app/portainer/views/groups/create/creategroup.html b/app/portainer/views/groups/create/creategroup.html index 8d307d40a..36334979c 100644 --- a/app/portainer/views/groups/create/creategroup.html +++ b/app/portainer/views/groups/create/creategroup.html @@ -12,6 +12,7 @@