mirror of https://github.com/portainer/portainer
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 librarypull/1978/head
parent
b349f16090
commit
5e73a49473
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 != "" {
|
||||
|
|
|
@ -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"):
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
119
api/swagger.yaml
119
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:
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
<div class="datatable">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle">
|
||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||
</div>
|
||||
<div class="settings">
|
||||
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
|
||||
<i class="fa fa-search" aria-hidden="true"></i> Search
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actionBar">
|
||||
<button type="button" class="btn btn-sm btn-danger"
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||
</button>
|
||||
</div>
|
||||
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<span class="md-checkbox">
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||
<label for="select_all"></label>
|
||||
</span>
|
||||
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||
Name
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||
<td>
|
||||
<span class="md-checkbox">
|
||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</span>
|
||||
{{ item.Name }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="1" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="1" class="text-center text-muted">No tag available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="footer" ng-if="$ctrl.dataset">
|
||||
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
|
||||
{{ $ctrl.state.selectedItemCount }} item(s) selected
|
||||
</div>
|
||||
<div class="paginationControls">
|
||||
<form class="form-inline">
|
||||
<span class="limitSelector">
|
||||
<span style="margin-right: 5px;">
|
||||
Items per page
|
||||
</span>
|
||||
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</span>
|
||||
<dir-pagination-controls max-size="5"></dir-pagination-controls>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
|
@ -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: '<'
|
||||
}
|
||||
});
|
|
@ -21,6 +21,7 @@ angular.module('portainer.app').component('groupForm', {
|
|||
bindings: {
|
||||
model: '=',
|
||||
availableEndpoints: '=',
|
||||
availableTags: '<',
|
||||
associatedEndpoints: '=',
|
||||
addLabelAction: '<',
|
||||
removeLabelAction: '<',
|
||||
|
|
|
@ -22,33 +22,17 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !description-input -->
|
||||
<!-- labels -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Labels</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addLabelAction()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add label
|
||||
</span>
|
||||
</div>
|
||||
<!-- labels-input-list -->
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="label in $ctrl.model.Labels" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input type="text" class="form-control" ng-model="label.name" placeholder="e.g. organization">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. acme">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removeLabelAction($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !labels-input-list -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Metadata
|
||||
</div>
|
||||
<!-- !labels -->
|
||||
<!-- tags -->
|
||||
<div class="form-group">
|
||||
<tag-selector
|
||||
tags="$ctrl.availableTags"
|
||||
model="$ctrl.model.Tags"
|
||||
></tag-selector>
|
||||
</div>
|
||||
<!-- !tags -->
|
||||
<!-- endpoints -->
|
||||
<div ng-if="$ctrl.model.Id !== 1">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
angular.module('portainer.app').component('tagSelector', {
|
||||
templateUrl: 'app/portainer/components/tag-selector/tagSelector.html',
|
||||
controller: 'TagSelectorController',
|
||||
bindings: {
|
||||
tags: '<',
|
||||
model: '='
|
||||
}
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
<div ng-show="$ctrl.model.length > 0" class="col-sm-12" style="padding: 0; margin-bottom: 15px;">
|
||||
<label class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Selected tags
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10" style="padding-top: 4px;">
|
||||
<span class="tag space-right interactive" ng-repeat="tag in $ctrl.model" ng-click="$ctrl.removeTag(tag)">
|
||||
{{ tag }}
|
||||
<a title="Remove tag" ng-click="$ctrl.removeTag(tag)" style="margin-left: 2px;">
|
||||
<span class="fa fa-trash-alt white-icon" aria-hidden="true"></span>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12" style="padding: 0">
|
||||
<label for="tags" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Tags
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10" ng-if="$ctrl.tags.length > 0">
|
||||
<input
|
||||
type="text" ng-model="$ctrl.state.selectedValue"
|
||||
id="tags" class="form-control"
|
||||
placeholder="Select tags..."
|
||||
uib-typeahead="tag for tag in $ctrl.tags | filter:$viewValue | limitTo:7"
|
||||
typeahead-on-select="$ctrl.selectTag($item, $model, $label)"
|
||||
typeahead-no-results="$ctrl.state.noResult"
|
||||
typeahead-show-hint="true" typeahead-min-length="0" />
|
||||
</div>
|
||||
<div class="col-sm-9 col-lg-10" ng-if="$ctrl.tags.length === 0">
|
||||
<span class="small text-muted">
|
||||
No tags available.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-offset-3 col-lg-offset-2 col-sm-12" ng-if="$ctrl.state.noResult" style="margin-top: 2px;">
|
||||
<span class="small text-muted">
|
||||
No tags matching your filter.
|
||||
</span>
|
||||
</div>
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
function TagViewModel(data) {
|
||||
this.Id = data.ID;
|
||||
this.Name = data.Name;
|
||||
}
|
|
@ -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'} }
|
||||
});
|
||||
}]);
|
|
@ -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);
|
||||
})
|
||||
|
|
|
@ -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;
|
||||
}]);
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -192,6 +192,12 @@
|
|||
<!-- !authenticationkey-input -->
|
||||
</div>
|
||||
<!-- !azure-details -->
|
||||
<!-- endpoint-security -->
|
||||
<por-endpoint-security ng-if="state.EnvironmentType === 'docker'" form-data="formValues.SecurityFormData"></por-endpoint-security>
|
||||
<!-- !endpoint-security -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Metadata
|
||||
</div>
|
||||
<!-- group -->
|
||||
<div class="form-group">
|
||||
<label for="endpoint_group" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
|
@ -202,9 +208,17 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !group -->
|
||||
<!-- endpoint-security -->
|
||||
<por-endpoint-security ng-if="state.EnvironmentType === 'docker'" form-data="formValues.SecurityFormData"></por-endpoint-security>
|
||||
<!-- !endpoint-security -->
|
||||
<!-- tags -->
|
||||
<div class="form-group">
|
||||
<tag-selector
|
||||
tags="availableTags"
|
||||
model="formValues.Tags"
|
||||
></tag-selector>
|
||||
</div>
|
||||
<!-- !tags -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
</div>
|
||||
<!-- actions -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
></azure-endpoint-config>
|
||||
<!-- !endpoint-public-url-input -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Grouping
|
||||
Metadata
|
||||
</div>
|
||||
<!-- group -->
|
||||
<div class="form-group">
|
||||
|
@ -61,6 +61,14 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !group -->
|
||||
<!-- tags -->
|
||||
<div class="form-group">
|
||||
<tag-selector
|
||||
tags="availableTags"
|
||||
model="endpoint.Tags"
|
||||
></tag-selector>
|
||||
</div>
|
||||
<!-- !tags -->
|
||||
<!-- endpoint-security -->
|
||||
<div ng-if="endpointType === 'remote' && endpoint.Type !== 3">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
<group-form
|
||||
model="model"
|
||||
available-endpoints="availableEndpoints"
|
||||
available-tags="availableTags"
|
||||
associated-endpoints="associatedEndpoints"
|
||||
add-label-action="addLabel"
|
||||
remove-label-action="removeLabel"
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
<group-form
|
||||
model="group"
|
||||
available-endpoints="availableEndpoints"
|
||||
available-tags="availableTags"
|
||||
associated-endpoints="associatedEndpoints"
|
||||
add-label-action="addLabel"
|
||||
remove-label-action="removeLabel"
|
||||
|
|
|
@ -1,19 +1,11 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('GroupController', ['$q', '$scope', '$state', '$transition$', 'GroupService', 'EndpointService', 'Notifications',
|
||||
function ($q, $scope, $state, $transition$, GroupService, EndpointService, Notifications) {
|
||||
.controller('GroupController', ['$q', '$scope', '$state', '$transition$', 'GroupService', 'EndpointService', 'TagService', 'Notifications',
|
||||
function ($q, $scope, $state, $transition$, GroupService, EndpointService, TagService, Notifications) {
|
||||
|
||||
$scope.state = {
|
||||
actionInProgress: false
|
||||
};
|
||||
|
||||
$scope.addLabel = function() {
|
||||
$scope.group.Labels.push({ name: '', value: '' });
|
||||
};
|
||||
|
||||
$scope.removeLabel = function(index) {
|
||||
$scope.group.Labels.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.update = function() {
|
||||
var model = $scope.group;
|
||||
|
||||
|
@ -42,7 +34,8 @@ function ($q, $scope, $state, $transition$, GroupService, EndpointService, Notif
|
|||
|
||||
$q.all({
|
||||
group: GroupService.group(groupId),
|
||||
endpoints: EndpointService.endpoints()
|
||||
endpoints: EndpointService.endpoints(),
|
||||
tags: TagService.tagNames()
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.group = data.group;
|
||||
|
@ -60,6 +53,7 @@ function ($q, $scope, $state, $transition$, GroupService, EndpointService, Notif
|
|||
|
||||
$scope.availableEndpoints = availableEndpoints;
|
||||
$scope.associatedEndpoints = associatedEndpoints;
|
||||
$scope.availableTags = data.tags;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to load view');
|
||||
|
|
|
@ -90,7 +90,7 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif
|
|||
var endpoint;
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey)
|
||||
EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, 1, [])
|
||||
.then(function success(data) {
|
||||
endpoint = data;
|
||||
EndpointProvider.setEndpointID(endpoint.Id);
|
||||
|
@ -110,7 +110,7 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif
|
|||
function createRemoteEndpoint(name, type, URL, PublicURL, TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
|
||||
var endpoint;
|
||||
$scope.state.actionInProgress = true;
|
||||
EndpointService.createRemoteEndpoint(name, type, URL, PublicURL, 1, TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)
|
||||
EndpointService.createRemoteEndpoint(name, type, URL, PublicURL, 1, [], TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)
|
||||
.then(function success(data) {
|
||||
endpoint = data;
|
||||
EndpointProvider.setEndpointID(endpoint.Id);
|
||||
|
|
|
@ -49,9 +49,12 @@
|
|||
</li>
|
||||
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
|
||||
<a ui-sref="portainer.endpoints" ui-sref-active="active">Endpoints <span class="menu-icon fa fa-plug fa-fw"></span></a>
|
||||
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'portainer.endpoints' || $state.current.name === 'portainer.endpoints.endpoint' || $state.current.name === 'portainer.endpoints.endpoint.access' || $state.current.name === 'portainer.groups' || $state.current.name === 'portainer.groups.group' || $state.current.name === 'portainer.groups.group.access' || $state.current.name === 'portainer.groups.new')">
|
||||
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'portainer.endpoints' || $state.current.name === 'portainer.endpoints.endpoint' || $state.current.name === 'portainer.endpoints.new' || $state.current.name === 'portainer.endpoints.endpoint.access' || $state.current.name === 'portainer.groups' || $state.current.name === 'portainer.groups.group' || $state.current.name === 'portainer.groups.group.access' || $state.current.name === 'portainer.groups.new' || $state.current.name === 'portainer.tags')">
|
||||
<a ui-sref="portainer.groups" ui-sref-active="active">Groups</a>
|
||||
</div>
|
||||
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'portainer.endpoints' || $state.current.name === 'portainer.endpoints.endpoint' || $state.current.name === 'portainer.endpoints.new' || $state.current.name === 'portainer.endpoints.endpoint.access' || $state.current.name === 'portainer.groups' || $state.current.name === 'portainer.groups.group' || $state.current.name === 'portainer.groups.group.access' || $state.current.name === 'portainer.groups.new' || $state.current.name === 'portainer.tags')">
|
||||
<a ui-sref="portainer.tags" ui-sref-active="active">Tags</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
|
||||
<a ui-sref="portainer.registries" ui-sref-active="active">Registries <span class="menu-icon fa fa-database fa-fw"></span></a>
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
<rd-header>
|
||||
<rd-header-title title-text="Tags">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="portainer.tags" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-sync" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Tag management</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-plus" title-text="Add a new tag">
|
||||
</rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="tagCreationForm" ng-submit="createTag()">
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="name" class="col-sm-2 control-label text-left">
|
||||
Name
|
||||
</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="text" class="form-control" name="name" ng-model="formValues.Name" placeholder="org/acme" required auto-focus />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="tagCreationForm.name.$invalid">
|
||||
<div class="col-sm-12 small text-danger">
|
||||
<div ng-messages="tagCreationForm.name.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress || !tagCreationForm.$valid" ng-click="createTag()" button-spinner="state.actionInProgress">
|
||||
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Create tag</span>
|
||||
<span ng-show="state.actionInProgress">Creating tag...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<tags-datatable
|
||||
title-text="Tags" title-icon="fa-tags"
|
||||
dataset="tags" table-key="tags"
|
||||
order-by="Name" show-text-filter="true"
|
||||
remove-action="removeAction"
|
||||
></tags-datatable>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,58 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('TagsController', ['$scope', '$state', 'TagService', 'Notifications',
|
||||
function ($scope, $state, TagService, Notifications) {
|
||||
|
||||
$scope.state = {
|
||||
actionInProgress: false
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
Name: ''
|
||||
};
|
||||
|
||||
$scope.removeAction = function (selectedItems) {
|
||||
var actionCount = selectedItems.length;
|
||||
angular.forEach(selectedItems, function (tag) {
|
||||
TagService.deleteTag(tag.Id)
|
||||
.then(function success() {
|
||||
Notifications.success('Tag successfully removed', tag.Name);
|
||||
var index = $scope.tags.indexOf(tag);
|
||||
$scope.tags.splice(index, 1);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to tag');
|
||||
})
|
||||
.finally(function final() {
|
||||
--actionCount;
|
||||
if (actionCount === 0) {
|
||||
$state.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.createTag = function() {
|
||||
var tagName = $scope.formValues.Name;
|
||||
TagService.createTag(tagName)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Tag successfully created', tagName);
|
||||
$state.reload();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to create tag');
|
||||
});
|
||||
};
|
||||
|
||||
function initView() {
|
||||
TagService.tags()
|
||||
.then(function success(data) {
|
||||
$scope.tags = data;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve tags');
|
||||
$scope.tags = [];
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
|
@ -729,6 +729,18 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
|
|||
background-color: #C5CAE9;
|
||||
}
|
||||
|
||||
.row.header .meta .page {
|
||||
padding-top: 7px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding: 2px 6px;
|
||||
color: white;
|
||||
background-color: #337ab7;
|
||||
border: 1px solid #2e6da4;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/*bootbox override*/
|
||||
.modal-open {
|
||||
padding-right: 0 !important;
|
||||
|
@ -787,12 +799,12 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* json-tree */
|
||||
|
||||
/* json-tree override */
|
||||
json-tree {
|
||||
font-size: 13px;
|
||||
color: #30426a;
|
||||
}
|
||||
|
||||
json-tree .key {
|
||||
color: #738bc0;
|
||||
padding-right: 5px;
|
||||
|
@ -803,7 +815,4 @@ json-tree .branch-preview {
|
|||
font-size: 11px;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.row.header .meta .page {
|
||||
padding-top: 7px;
|
||||
}
|
||||
/* !json-tree override */
|
||||
|
|
|
@ -55,7 +55,6 @@
|
|||
"rdash-ui": "1.0.*",
|
||||
"splitargs": "github:deviantony/splitargs#~0.2.0",
|
||||
"toastr": "github:CodeSeven/toastr#~2.1.3",
|
||||
"ui-select": "~0.19.6",
|
||||
"xterm": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -21,7 +21,6 @@ css:
|
|||
- 'bootstrap/dist/css/bootstrap.css'
|
||||
- 'rdash-ui/dist/css/rdash.css'
|
||||
- 'isteven-angular-multiselect/isteven-multi-select.css'
|
||||
- 'ui-select/dist/select.css'
|
||||
- '@fortawesome/fontawesome-free-webfonts/css/fa-brands.css'
|
||||
- '@fortawesome/fontawesome-free-webfonts/css/fa-solid.css'
|
||||
- '@fortawesome/fontawesome-free-webfonts/css/fontawesome.css'
|
||||
|
@ -42,7 +41,6 @@ angular:
|
|||
- 'angular-messages/angular-messages.js'
|
||||
- 'angular-resource/angular-resource.js'
|
||||
- 'angular-sanitize/angular-sanitize.js'
|
||||
- 'ui-select/dist/select.js'
|
||||
- '@uirouter/angularjs/release/angular-ui-router.js'
|
||||
- 'angular-utils-pagination/dirPagination.js'
|
||||
- 'ng-file-upload/dist/ng-file-upload.js'
|
||||
|
|
|
@ -4238,10 +4238,6 @@ uglify-to-browserify@~1.0.0:
|
|||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
|
||||
|
||||
ui-select@~0.19.6:
|
||||
version "0.19.8"
|
||||
resolved "https://registry.yarnpkg.com/ui-select/-/ui-select-0.19.8.tgz#74860848a7fd8bc494d9856d2f62776ea98637c1"
|
||||
|
||||
uid-safe@2.1.4:
|
||||
version "2.1.4"
|
||||
resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.4.tgz#3ad6f38368c6d4c8c75ec17623fb79aa1d071d81"
|
||||
|
|
Loading…
Reference in New Issue