mirror of
https://github.com/portainer/portainer.git
synced 2025-07-22 14:59:41 +02:00
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
This commit is contained in:
parent
b349f16090
commit
5e73a49473
50 changed files with 942 additions and 118 deletions
|
@ -27,6 +27,7 @@ type Store struct {
|
||||||
RegistryService *RegistryService
|
RegistryService *RegistryService
|
||||||
DockerHubService *DockerHubService
|
DockerHubService *DockerHubService
|
||||||
StackService *StackService
|
StackService *StackService
|
||||||
|
TagService *TagService
|
||||||
|
|
||||||
db *bolt.DB
|
db *bolt.DB
|
||||||
checkForDataMigration bool
|
checkForDataMigration bool
|
||||||
|
@ -45,6 +46,7 @@ const (
|
||||||
registryBucketName = "registries"
|
registryBucketName = "registries"
|
||||||
dockerhubBucketName = "dockerhub"
|
dockerhubBucketName = "dockerhub"
|
||||||
stackBucketName = "stacks"
|
stackBucketName = "stacks"
|
||||||
|
tagBucketName = "tags"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewStore initializes a new Store and the associated services
|
// NewStore initializes a new Store and the associated services
|
||||||
|
@ -62,6 +64,7 @@ func NewStore(storePath string) (*Store, error) {
|
||||||
RegistryService: &RegistryService{},
|
RegistryService: &RegistryService{},
|
||||||
DockerHubService: &DockerHubService{},
|
DockerHubService: &DockerHubService{},
|
||||||
StackService: &StackService{},
|
StackService: &StackService{},
|
||||||
|
TagService: &TagService{},
|
||||||
}
|
}
|
||||||
store.UserService.store = store
|
store.UserService.store = store
|
||||||
store.TeamService.store = store
|
store.TeamService.store = store
|
||||||
|
@ -74,6 +77,7 @@ func NewStore(storePath string) (*Store, error) {
|
||||||
store.RegistryService.store = store
|
store.RegistryService.store = store
|
||||||
store.DockerHubService.store = store
|
store.DockerHubService.store = store
|
||||||
store.StackService.store = store
|
store.StackService.store = store
|
||||||
|
store.TagService.store = store
|
||||||
|
|
||||||
_, err := os.Stat(storePath + "/" + databaseFileName)
|
_, err := os.Stat(storePath + "/" + databaseFileName)
|
||||||
if err != nil && os.IsNotExist(err) {
|
if err != nil && os.IsNotExist(err) {
|
||||||
|
@ -99,7 +103,7 @@ func (store *Store) Open() error {
|
||||||
|
|
||||||
bucketsToCreate := []string{versionBucketName, userBucketName, teamBucketName, endpointBucketName,
|
bucketsToCreate := []string{versionBucketName, userBucketName, teamBucketName, endpointBucketName,
|
||||||
endpointGroupBucketName, resourceControlBucketName, teamMembershipBucketName, settingsBucketName,
|
endpointGroupBucketName, resourceControlBucketName, teamMembershipBucketName, settingsBucketName,
|
||||||
registryBucketName, dockerhubBucketName, stackBucketName}
|
registryBucketName, dockerhubBucketName, stackBucketName, tagBucketName}
|
||||||
|
|
||||||
return db.Update(func(tx *bolt.Tx) error {
|
return db.Update(func(tx *bolt.Tx) error {
|
||||||
|
|
||||||
|
@ -128,6 +132,7 @@ func (store *Store) Init() error {
|
||||||
Labels: []portainer.Pair{},
|
Labels: []portainer.Pair{},
|
||||||
AuthorizedUsers: []portainer.UserID{},
|
AuthorizedUsers: []portainer.UserID{},
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
AuthorizedTeams: []portainer.TeamID{},
|
||||||
|
Tags: []string{},
|
||||||
}
|
}
|
||||||
|
|
||||||
return store.EndpointGroupService.CreateEndpointGroup(unassignedGroup)
|
return store.EndpointGroupService.CreateEndpointGroup(unassignedGroup)
|
||||||
|
|
|
@ -107,6 +107,16 @@ func UnmarshalDockerHub(data []byte, settings *portainer.DockerHub) error {
|
||||||
return json.Unmarshal(data, settings)
|
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.
|
// Itob returns an 8-byte big endian representation of v.
|
||||||
// This function is typically used for encoding integer IDs to byte slices
|
// This function is typically used for encoding integer IDs to byte slices
|
||||||
// so that they can be used as BoltDB keys.
|
// so that they can be used as BoltDB keys.
|
||||||
|
|
37
api/bolt/migrate_dbversion11.go
Normal file
37
api/bolt/migrate_dbversion11.go
Normal file
|
@ -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 {
|
type Migrator struct {
|
||||||
UserService *UserService
|
UserService *UserService
|
||||||
EndpointService *EndpointService
|
EndpointService *EndpointService
|
||||||
|
EndpointGroupService *EndpointGroupService
|
||||||
ResourceControlService *ResourceControlService
|
ResourceControlService *ResourceControlService
|
||||||
SettingsService *SettingsService
|
SettingsService *SettingsService
|
||||||
VersionService *VersionService
|
VersionService *VersionService
|
||||||
|
@ -18,6 +19,7 @@ func NewMigrator(store *Store, version int) *Migrator {
|
||||||
return &Migrator{
|
return &Migrator{
|
||||||
UserService: store.UserService,
|
UserService: store.UserService,
|
||||||
EndpointService: store.EndpointService,
|
EndpointService: store.EndpointService,
|
||||||
|
EndpointGroupService: store.EndpointGroupService,
|
||||||
ResourceControlService: store.ResourceControlService,
|
ResourceControlService: store.ResourceControlService,
|
||||||
SettingsService: store.SettingsService,
|
SettingsService: store.SettingsService,
|
||||||
VersionService: store.VersionService,
|
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)
|
err := m.VersionService.StoreDBVersion(portainer.DBVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
71
api/bolt/tag_service.go
Normal file
71
api/bolt/tag_service.go
Normal file
|
@ -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{},
|
AuthorizedUsers: []portainer.UserID{},
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
AuthorizedTeams: []portainer.TeamID{},
|
||||||
Extensions: []portainer.EndpointExtension{},
|
Extensions: []portainer.EndpointExtension{},
|
||||||
|
Tags: []string{},
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(endpoint.URL, "tcp://") {
|
if strings.HasPrefix(endpoint.URL, "tcp://") {
|
||||||
|
@ -274,6 +275,7 @@ func createUnsecuredEndpoint(endpointURL string, endpointService portainer.Endpo
|
||||||
AuthorizedUsers: []portainer.UserID{},
|
AuthorizedUsers: []portainer.UserID{},
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
AuthorizedTeams: []portainer.TeamID{},
|
||||||
Extensions: []portainer.EndpointExtension{},
|
Extensions: []portainer.EndpointExtension{},
|
||||||
|
Tags: []string{},
|
||||||
}
|
}
|
||||||
|
|
||||||
return endpointService.CreateEndpoint(endpoint)
|
return endpointService.CreateEndpoint(endpoint)
|
||||||
|
@ -401,6 +403,7 @@ func main() {
|
||||||
RegistryService: store.RegistryService,
|
RegistryService: store.RegistryService,
|
||||||
DockerHubService: store.DockerHubService,
|
DockerHubService: store.DockerHubService,
|
||||||
StackService: store.StackService,
|
StackService: store.StackService,
|
||||||
|
TagService: store.TagService,
|
||||||
SwarmStackManager: swarmStackManager,
|
SwarmStackManager: swarmStackManager,
|
||||||
ComposeStackManager: composeStackManager,
|
ComposeStackManager: composeStackManager,
|
||||||
CryptoService: cryptoService,
|
CryptoService: cryptoService,
|
||||||
|
|
|
@ -68,6 +68,11 @@ const (
|
||||||
ErrStackNotExternal = Error("Not an external stack")
|
ErrStackNotExternal = Error("Not an external stack")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Tag errors
|
||||||
|
const (
|
||||||
|
ErrTagAlreadyExists = Error("A tag already exists with this name")
|
||||||
|
)
|
||||||
|
|
||||||
// Endpoint extensions error
|
// Endpoint extensions error
|
||||||
const (
|
const (
|
||||||
ErrEndpointExtensionNotSupported = Error("This extension is not supported")
|
ErrEndpointExtensionNotSupported = Error("This extension is not supported")
|
||||||
|
|
|
@ -13,14 +13,17 @@ import (
|
||||||
type endpointGroupCreatePayload struct {
|
type endpointGroupCreatePayload struct {
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
Labels []portainer.Pair
|
|
||||||
AssociatedEndpoints []portainer.EndpointID
|
AssociatedEndpoints []portainer.EndpointID
|
||||||
|
Tags []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *endpointGroupCreatePayload) Validate(r *http.Request) error {
|
func (payload *endpointGroupCreatePayload) Validate(r *http.Request) error {
|
||||||
if govalidator.IsNull(payload.Name) {
|
if govalidator.IsNull(payload.Name) {
|
||||||
return portainer.Error("Invalid endpoint group name")
|
return portainer.Error("Invalid endpoint group name")
|
||||||
}
|
}
|
||||||
|
if payload.Tags == nil {
|
||||||
|
payload.Tags = []string{}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,9 +38,9 @@ func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Reque
|
||||||
endpointGroup := &portainer.EndpointGroup{
|
endpointGroup := &portainer.EndpointGroup{
|
||||||
Name: payload.Name,
|
Name: payload.Name,
|
||||||
Description: payload.Description,
|
Description: payload.Description,
|
||||||
Labels: payload.Labels,
|
|
||||||
AuthorizedUsers: []portainer.UserID{},
|
AuthorizedUsers: []portainer.UserID{},
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
AuthorizedTeams: []portainer.TeamID{},
|
||||||
|
Tags: payload.Tags,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.EndpointGroupService.CreateEndpointGroup(endpointGroup)
|
err = handler.EndpointGroupService.CreateEndpointGroup(endpointGroup)
|
||||||
|
|
|
@ -12,8 +12,8 @@ import (
|
||||||
type endpointGroupUpdatePayload struct {
|
type endpointGroupUpdatePayload struct {
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
Labels []portainer.Pair
|
|
||||||
AssociatedEndpoints []portainer.EndpointID
|
AssociatedEndpoints []portainer.EndpointID
|
||||||
|
Tags []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *endpointGroupUpdatePayload) Validate(r *http.Request) error {
|
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.Description = payload.Description
|
||||||
}
|
}
|
||||||
|
|
||||||
endpointGroup.Labels = payload.Labels
|
if payload.Tags != nil {
|
||||||
|
endpointGroup.Tags = payload.Tags
|
||||||
|
}
|
||||||
|
|
||||||
err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
|
err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -28,6 +28,7 @@ type endpointCreatePayload struct {
|
||||||
AzureApplicationID string
|
AzureApplicationID string
|
||||||
AzureTenantID string
|
AzureTenantID string
|
||||||
AzureAuthenticationKey string
|
AzureAuthenticationKey string
|
||||||
|
Tags []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
||||||
|
@ -49,6 +50,13 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
||||||
}
|
}
|
||||||
payload.GroupID = groupID
|
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)
|
useTLS, _ := request.RetrieveBooleanMultiPartFormValue(r, "TLS", true)
|
||||||
payload.TLS = useTLS
|
payload.TLS = useTLS
|
||||||
|
|
||||||
|
@ -168,6 +176,7 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
AuthorizedTeams: []portainer.TeamID{},
|
||||||
Extensions: []portainer.EndpointExtension{},
|
Extensions: []portainer.EndpointExtension{},
|
||||||
AzureCredentials: credentials,
|
AzureCredentials: credentials,
|
||||||
|
Tags: payload.Tags,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.EndpointService.CreateEndpoint(endpoint)
|
err = handler.EndpointService.CreateEndpoint(endpoint)
|
||||||
|
@ -203,6 +212,7 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload)
|
||||||
AuthorizedUsers: []portainer.UserID{},
|
AuthorizedUsers: []portainer.UserID{},
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
AuthorizedTeams: []portainer.TeamID{},
|
||||||
Extensions: []portainer.EndpointExtension{},
|
Extensions: []portainer.EndpointExtension{},
|
||||||
|
Tags: payload.Tags,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := handler.EndpointService.CreateEndpoint(endpoint)
|
err := handler.EndpointService.CreateEndpoint(endpoint)
|
||||||
|
@ -242,6 +252,7 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload)
|
||||||
AuthorizedUsers: []portainer.UserID{},
|
AuthorizedUsers: []portainer.UserID{},
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
AuthorizedTeams: []portainer.TeamID{},
|
||||||
Extensions: []portainer.EndpointExtension{},
|
Extensions: []portainer.EndpointExtension{},
|
||||||
|
Tags: payload.Tags,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.EndpointService.CreateEndpoint(endpoint)
|
err = handler.EndpointService.CreateEndpoint(endpoint)
|
||||||
|
|
|
@ -22,6 +22,7 @@ type endpointUpdatePayload struct {
|
||||||
AzureApplicationID string
|
AzureApplicationID string
|
||||||
AzureTenantID string
|
AzureTenantID string
|
||||||
AzureAuthenticationKey string
|
AzureAuthenticationKey string
|
||||||
|
Tags []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *endpointUpdatePayload) Validate(r *http.Request) error {
|
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)
|
endpoint.GroupID = portainer.EndpointGroupID(payload.GroupID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if payload.Tags != nil {
|
||||||
|
endpoint.Tags = payload.Tags
|
||||||
|
}
|
||||||
|
|
||||||
if endpoint.Type == portainer.AzureEnvironment {
|
if endpoint.Type == portainer.AzureEnvironment {
|
||||||
credentials := endpoint.AzureCredentials
|
credentials := endpoint.AzureCredentials
|
||||||
if payload.AzureApplicationID != "" {
|
if payload.AzureApplicationID != "" {
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"github.com/portainer/portainer/http/handler/settings"
|
"github.com/portainer/portainer/http/handler/settings"
|
||||||
"github.com/portainer/portainer/http/handler/stacks"
|
"github.com/portainer/portainer/http/handler/stacks"
|
||||||
"github.com/portainer/portainer/http/handler/status"
|
"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/teammemberships"
|
||||||
"github.com/portainer/portainer/http/handler/teams"
|
"github.com/portainer/portainer/http/handler/teams"
|
||||||
"github.com/portainer/portainer/http/handler/templates"
|
"github.com/portainer/portainer/http/handler/templates"
|
||||||
|
@ -37,16 +38,13 @@ type Handler struct {
|
||||||
SettingsHandler *settings.Handler
|
SettingsHandler *settings.Handler
|
||||||
StackHandler *stacks.Handler
|
StackHandler *stacks.Handler
|
||||||
StatusHandler *status.Handler
|
StatusHandler *status.Handler
|
||||||
|
TagHandler *tags.Handler
|
||||||
TeamMembershipHandler *teammemberships.Handler
|
TeamMembershipHandler *teammemberships.Handler
|
||||||
TeamHandler *teams.Handler
|
TeamHandler *teams.Handler
|
||||||
TemplatesHandler *templates.Handler
|
TemplatesHandler *templates.Handler
|
||||||
UploadHandler *upload.Handler
|
UploadHandler *upload.Handler
|
||||||
UserHandler *users.Handler
|
UserHandler *users.Handler
|
||||||
WebSocketHandler *websocket.Handler
|
WebSocketHandler *websocket.Handler
|
||||||
|
|
||||||
// StoridgeHandler *extensions.StoridgeHandler
|
|
||||||
// AzureHandler *azure.Handler
|
|
||||||
// DockerHandler *docker.Handler
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP delegates a request to the appropriate subhandler.
|
// 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)
|
http.StripPrefix("/api", h.StackHandler).ServeHTTP(w, r)
|
||||||
case strings.HasPrefix(r.URL.Path, "/api/status"):
|
case strings.HasPrefix(r.URL.Path, "/api/status"):
|
||||||
http.StripPrefix("/api", h.StatusHandler).ServeHTTP(w, r)
|
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"):
|
case strings.HasPrefix(r.URL.Path, "/api/templates"):
|
||||||
http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r)
|
||||||
case strings.HasPrefix(r.URL.Path, "/api/upload"):
|
case strings.HasPrefix(r.URL.Path, "/api/upload"):
|
||||||
|
|
31
api/http/handler/tags/handler.go
Normal file
31
api/http/handler/tags/handler.go
Normal file
|
@ -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
|
||||||
|
}
|
53
api/http/handler/tags/tag_create.go
Normal file
53
api/http/handler/tags/tag_create.go
Normal file
|
@ -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)
|
||||||
|
}
|
25
api/http/handler/tags/tag_delete.go
Normal file
25
api/http/handler/tags/tag_delete.go
Normal file
|
@ -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)
|
||||||
|
}
|
18
api/http/handler/tags/tag_list.go
Normal file
18
api/http/handler/tags/tag_list.go
Normal file
|
@ -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/settings"
|
||||||
"github.com/portainer/portainer/http/handler/stacks"
|
"github.com/portainer/portainer/http/handler/stacks"
|
||||||
"github.com/portainer/portainer/http/handler/status"
|
"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/teammemberships"
|
||||||
"github.com/portainer/portainer/http/handler/teams"
|
"github.com/portainer/portainer/http/handler/teams"
|
||||||
"github.com/portainer/portainer/http/handler/templates"
|
"github.com/portainer/portainer/http/handler/templates"
|
||||||
|
@ -36,24 +37,25 @@ type Server struct {
|
||||||
AuthDisabled bool
|
AuthDisabled bool
|
||||||
EndpointManagement bool
|
EndpointManagement bool
|
||||||
Status *portainer.Status
|
Status *portainer.Status
|
||||||
UserService portainer.UserService
|
ComposeStackManager portainer.ComposeStackManager
|
||||||
TeamService portainer.TeamService
|
CryptoService portainer.CryptoService
|
||||||
TeamMembershipService portainer.TeamMembershipService
|
SignatureService portainer.DigitalSignatureService
|
||||||
|
DockerHubService portainer.DockerHubService
|
||||||
EndpointService portainer.EndpointService
|
EndpointService portainer.EndpointService
|
||||||
EndpointGroupService portainer.EndpointGroupService
|
EndpointGroupService portainer.EndpointGroupService
|
||||||
|
FileService portainer.FileService
|
||||||
|
GitService portainer.GitService
|
||||||
|
JWTService portainer.JWTService
|
||||||
|
LDAPService portainer.LDAPService
|
||||||
|
RegistryService portainer.RegistryService
|
||||||
ResourceControlService portainer.ResourceControlService
|
ResourceControlService portainer.ResourceControlService
|
||||||
SettingsService portainer.SettingsService
|
SettingsService portainer.SettingsService
|
||||||
CryptoService portainer.CryptoService
|
|
||||||
JWTService portainer.JWTService
|
|
||||||
FileService portainer.FileService
|
|
||||||
RegistryService portainer.RegistryService
|
|
||||||
DockerHubService portainer.DockerHubService
|
|
||||||
StackService portainer.StackService
|
StackService portainer.StackService
|
||||||
SwarmStackManager portainer.SwarmStackManager
|
SwarmStackManager portainer.SwarmStackManager
|
||||||
ComposeStackManager portainer.ComposeStackManager
|
TagService portainer.TagService
|
||||||
LDAPService portainer.LDAPService
|
TeamService portainer.TeamService
|
||||||
GitService portainer.GitService
|
TeamMembershipService portainer.TeamMembershipService
|
||||||
SignatureService portainer.DigitalSignatureService
|
UserService portainer.UserService
|
||||||
Handler *handler.Handler
|
Handler *handler.Handler
|
||||||
SSL bool
|
SSL bool
|
||||||
SSLCert string
|
SSLCert string
|
||||||
|
@ -126,6 +128,9 @@ func (server *Server) Start() error {
|
||||||
stackHandler.RegistryService = server.RegistryService
|
stackHandler.RegistryService = server.RegistryService
|
||||||
stackHandler.DockerHubService = server.DockerHubService
|
stackHandler.DockerHubService = server.DockerHubService
|
||||||
|
|
||||||
|
var tagHandler = tags.NewHandler(requestBouncer)
|
||||||
|
tagHandler.TagService = server.TagService
|
||||||
|
|
||||||
var teamHandler = teams.NewHandler(requestBouncer)
|
var teamHandler = teams.NewHandler(requestBouncer)
|
||||||
teamHandler.TeamService = server.TeamService
|
teamHandler.TeamService = server.TeamService
|
||||||
teamHandler.TeamMembershipService = server.TeamMembershipService
|
teamHandler.TeamMembershipService = server.TeamMembershipService
|
||||||
|
@ -164,6 +169,7 @@ func (server *Server) Start() error {
|
||||||
SettingsHandler: settingsHandler,
|
SettingsHandler: settingsHandler,
|
||||||
StatusHandler: statusHandler,
|
StatusHandler: statusHandler,
|
||||||
StackHandler: stackHandler,
|
StackHandler: stackHandler,
|
||||||
|
TagHandler: tagHandler,
|
||||||
TeamHandler: teamHandler,
|
TeamHandler: teamHandler,
|
||||||
TeamMembershipHandler: teamMembershipHandler,
|
TeamMembershipHandler: teamMembershipHandler,
|
||||||
TemplatesHandler: templatesHandler,
|
TemplatesHandler: templatesHandler,
|
||||||
|
|
|
@ -190,6 +190,7 @@ type (
|
||||||
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
||||||
Extensions []EndpointExtension `json:"Extensions"`
|
Extensions []EndpointExtension `json:"Extensions"`
|
||||||
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"`
|
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"`
|
||||||
|
Tags []string `json:"Tags"`
|
||||||
|
|
||||||
// Deprecated fields
|
// Deprecated fields
|
||||||
// Deprecated in DBVersion == 4
|
// Deprecated in DBVersion == 4
|
||||||
|
@ -217,7 +218,10 @@ type (
|
||||||
Description string `json:"Description"`
|
Description string `json:"Description"`
|
||||||
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
||||||
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
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.
|
// EndpointExtension represents a extension associated to an endpoint.
|
||||||
|
@ -264,6 +268,15 @@ type (
|
||||||
AccessLevel ResourceAccessLevel `json:"AccessLevel"`
|
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 represents the level of control associated to a resource.
|
||||||
ResourceAccessLevel int
|
ResourceAccessLevel int
|
||||||
|
|
||||||
|
@ -390,6 +403,13 @@ type (
|
||||||
DeleteResourceControl(ID ResourceControlID) error
|
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 represents a service for encrypting/hashing data.
|
||||||
CryptoService interface {
|
CryptoService interface {
|
||||||
Hash(data string) (string, error)
|
Hash(data string) (string, error)
|
||||||
|
@ -463,7 +483,7 @@ const (
|
||||||
// APIVersion is the version number of the Portainer API.
|
// APIVersion is the version number of the Portainer API.
|
||||||
APIVersion = "1.17.1-dev"
|
APIVersion = "1.17.1-dev"
|
||||||
// DBVersion is the version number of the Portainer database.
|
// DBVersion is the version number of the Portainer database.
|
||||||
DBVersion = 11
|
DBVersion = 12
|
||||||
// DefaultTemplatesURL represents the default URL for the templates definitions.
|
// DefaultTemplatesURL represents the default URL for the templates definitions.
|
||||||
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
|
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
|
||||||
// PortainerAgentHeader represents the name of the header available in any agent response
|
// 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"
|
description: "Manage Docker stacks"
|
||||||
- name: "users"
|
- name: "users"
|
||||||
description: "Manage users"
|
description: "Manage users"
|
||||||
|
- name: "tags"
|
||||||
|
description: "Manage tags"
|
||||||
- name: "teams"
|
- name: "teams"
|
||||||
description: "Manage teams"
|
description: "Manage teams"
|
||||||
- name: "team_memberships"
|
- name: "team_memberships"
|
||||||
|
@ -1958,6 +1960,99 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/definitions/GenericError"
|
$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:
|
/teams:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
@ -2411,6 +2506,17 @@ securityDefinitions:
|
||||||
name: "Authorization"
|
name: "Authorization"
|
||||||
in: "header"
|
in: "header"
|
||||||
definitions:
|
definitions:
|
||||||
|
Tag:
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
Id:
|
||||||
|
type: "integer"
|
||||||
|
example: 1
|
||||||
|
description: "Tag identifier"
|
||||||
|
Name:
|
||||||
|
type: "string"
|
||||||
|
example: "org/acme"
|
||||||
|
description: "Tag name"
|
||||||
Team:
|
Team:
|
||||||
type: "object"
|
type: "object"
|
||||||
properties:
|
properties:
|
||||||
|
@ -3334,6 +3440,19 @@ definitions:
|
||||||
type: "boolean"
|
type: "boolean"
|
||||||
example: true
|
example: true
|
||||||
description: "Is the password valid"
|
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:
|
TeamCreateRequest:
|
||||||
type: "object"
|
type: "object"
|
||||||
required:
|
required:
|
||||||
|
|
|
@ -9,6 +9,7 @@ angular.module('portainer')
|
||||||
.constant('API_ENDPOINT_STACKS', 'api/stacks')
|
.constant('API_ENDPOINT_STACKS', 'api/stacks')
|
||||||
.constant('API_ENDPOINT_STATUS', 'api/status')
|
.constant('API_ENDPOINT_STATUS', 'api/status')
|
||||||
.constant('API_ENDPOINT_USERS', 'api/users')
|
.constant('API_ENDPOINT_USERS', 'api/users')
|
||||||
|
.constant('API_ENDPOINT_TAGS', 'api/tags')
|
||||||
.constant('API_ENDPOINT_TEAMS', 'api/teams')
|
.constant('API_ENDPOINT_TEAMS', 'api/teams')
|
||||||
.constant('API_ENDPOINT_TEAM_MEMBERSHIPS', 'api/team_memberships')
|
.constant('API_ENDPOINT_TEAM_MEMBERSHIPS', 'api/team_memberships')
|
||||||
.constant('API_ENDPOINT_TEMPLATES', 'api/templates')
|
.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 = {
|
var users = {
|
||||||
name: 'portainer.users',
|
name: 'portainer.users',
|
||||||
url: '/users',
|
url: '/users',
|
||||||
|
@ -366,6 +377,7 @@ angular.module('portainer.app', [])
|
||||||
$stateRegistryProvider.register(stack);
|
$stateRegistryProvider.register(stack);
|
||||||
$stateRegistryProvider.register(stackCreation);
|
$stateRegistryProvider.register(stackCreation);
|
||||||
$stateRegistryProvider.register(support);
|
$stateRegistryProvider.register(support);
|
||||||
|
$stateRegistryProvider.register(tags);
|
||||||
$stateRegistryProvider.register(users);
|
$stateRegistryProvider.register(users);
|
||||||
$stateRegistryProvider.register(user);
|
$stateRegistryProvider.register(user);
|
||||||
$stateRegistryProvider.register(teams);
|
$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: {
|
bindings: {
|
||||||
model: '=',
|
model: '=',
|
||||||
availableEndpoints: '=',
|
availableEndpoints: '=',
|
||||||
|
availableTags: '<',
|
||||||
associatedEndpoints: '=',
|
associatedEndpoints: '=',
|
||||||
addLabelAction: '<',
|
addLabelAction: '<',
|
||||||
removeLabelAction: '<',
|
removeLabelAction: '<',
|
||||||
|
|
|
@ -22,33 +22,17 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !description-input -->
|
<!-- !description-input -->
|
||||||
<!-- labels -->
|
<div class="col-sm-12 form-section-title">
|
||||||
<div class="form-group">
|
Metadata
|
||||||
<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>
|
</div>
|
||||||
<!-- !labels -->
|
<!-- tags -->
|
||||||
|
<div class="form-group">
|
||||||
|
<tag-selector
|
||||||
|
tags="$ctrl.availableTags"
|
||||||
|
model="$ctrl.model.Tags"
|
||||||
|
></tag-selector>
|
||||||
|
</div>
|
||||||
|
<!-- !tags -->
|
||||||
<!-- endpoints -->
|
<!-- endpoints -->
|
||||||
<div ng-if="$ctrl.model.Id !== 1">
|
<div ng-if="$ctrl.model.Id !== 1">
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
|
|
8
app/portainer/components/tag-selector/tag-selector.js
Normal file
8
app/portainer/components/tag-selector/tag-selector.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
angular.module('portainer.app').component('tagSelector', {
|
||||||
|
templateUrl: 'app/portainer/components/tag-selector/tagSelector.html',
|
||||||
|
controller: 'TagSelectorController',
|
||||||
|
bindings: {
|
||||||
|
tags: '<',
|
||||||
|
model: '='
|
||||||
|
}
|
||||||
|
});
|
38
app/portainer/components/tag-selector/tagSelector.html
Normal file
38
app/portainer/components/tag-selector/tagSelector.html
Normal file
|
@ -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() {
|
function EndpointGroupDefaultModel() {
|
||||||
this.Name = '';
|
this.Name = '';
|
||||||
this.Description = '';
|
this.Description = '';
|
||||||
this.Labels = [];
|
this.Tags = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function EndpointGroupModel(data) {
|
function EndpointGroupModel(data) {
|
||||||
this.Id = data.Id;
|
this.Id = data.Id;
|
||||||
this.Name = data.Name;
|
this.Name = data.Name;
|
||||||
this.Description = data.Description;
|
this.Description = data.Description;
|
||||||
this.Labels = data.Labels;
|
this.Tags = data.Tags;
|
||||||
this.AuthorizedUsers = data.AuthorizedUsers;
|
this.AuthorizedUsers = data.AuthorizedUsers;
|
||||||
this.AuthorizedTeams = data.AuthorizedTeams;
|
this.AuthorizedTeams = data.AuthorizedTeams;
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ function EndpointGroupModel(data) {
|
||||||
function EndpointGroupCreateRequest(model, endpoints) {
|
function EndpointGroupCreateRequest(model, endpoints) {
|
||||||
this.Name = model.Name;
|
this.Name = model.Name;
|
||||||
this.Description = model.Description;
|
this.Description = model.Description;
|
||||||
this.Labels = model.Labels;
|
this.Tags = model.Tags;
|
||||||
this.AssociatedEndpoints = endpoints;
|
this.AssociatedEndpoints = endpoints;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +24,6 @@ function EndpointGroupUpdateRequest(model, endpoints) {
|
||||||
this.id = model.Id;
|
this.id = model.Id;
|
||||||
this.Name = model.Name;
|
this.Name = model.Name;
|
||||||
this.Description = model.Description;
|
this.Description = model.Description;
|
||||||
this.Labels = model.Labels;
|
this.Tags = model.Tags;
|
||||||
this.AssociatedEndpoints = endpoints;
|
this.AssociatedEndpoints = endpoints;
|
||||||
}
|
}
|
||||||
|
|
4
app/portainer/models/tag.js
Normal file
4
app/portainer/models/tag.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
function TagViewModel(data) {
|
||||||
|
this.Id = data.ID;
|
||||||
|
this.Name = data.Name;
|
||||||
|
}
|
9
app/portainer/rest/tag.js
Normal file
9
app/portainer/rest/tag.js
Normal file
|
@ -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() {
|
service.createLocalEndpoint = function() {
|
||||||
var deferred = $q.defer();
|
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) {
|
.then(function success(response) {
|
||||||
deferred.resolve(response.data);
|
deferred.resolve(response.data);
|
||||||
})
|
})
|
||||||
|
@ -68,10 +68,10 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
||||||
return deferred.promise;
|
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();
|
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) {
|
.then(function success(response) {
|
||||||
deferred.resolve(response.data);
|
deferred.resolve(response.data);
|
||||||
})
|
})
|
||||||
|
@ -82,10 +82,10 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
service.createAzureEndpoint = function(name, applicationId, tenantId, authenticationKey) {
|
service.createAzureEndpoint = function(name, applicationId, tenantId, authenticationKey, groupId, tags) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
FileUploadService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey)
|
FileUploadService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tags)
|
||||||
.then(function success(response) {
|
.then(function success(response) {
|
||||||
deferred.resolve(response.data);
|
deferred.resolve(response.data);
|
||||||
})
|
})
|
||||||
|
|
49
app/portainer/services/api/tagService.js
Normal file
49
app/portainer/services/api/tagService.js
Normal file
|
@ -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({
|
return Upload.upload({
|
||||||
url: 'api/endpoints',
|
url: 'api/endpoints',
|
||||||
data: {
|
data: {
|
||||||
|
@ -61,6 +61,7 @@ angular.module('portainer.app')
|
||||||
URL: URL,
|
URL: URL,
|
||||||
PublicURL: PublicURL,
|
PublicURL: PublicURL,
|
||||||
GroupID: groupID,
|
GroupID: groupID,
|
||||||
|
Tags: Upload.json(tags),
|
||||||
TLS: TLS,
|
TLS: TLS,
|
||||||
TLSSkipVerify: TLSSkipVerify,
|
TLSSkipVerify: TLSSkipVerify,
|
||||||
TLSSkipClientVerify: TLSSkipClientVerify,
|
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({
|
return Upload.upload({
|
||||||
url: 'api/endpoints',
|
url: 'api/endpoints',
|
||||||
data: {
|
data: {
|
||||||
Name: name,
|
Name: name,
|
||||||
EndpointType: 3,
|
EndpointType: 3,
|
||||||
|
GroupID: groupID,
|
||||||
|
Tags: Upload.json(tags),
|
||||||
AzureApplicationID: applicationId,
|
AzureApplicationID: applicationId,
|
||||||
AzureTenantID: tenantId,
|
AzureTenantID: tenantId,
|
||||||
AzureAuthenticationKey: authenticationKey
|
AzureAuthenticationKey: authenticationKey
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('CreateEndpointController', ['$scope', '$state', '$filter', 'EndpointService', 'GroupService', 'Notifications',
|
.controller('CreateEndpointController', ['$q', '$scope', '$state', '$filter', 'EndpointService', 'GroupService', 'TagService', 'Notifications',
|
||||||
function ($scope, $state, $filter, EndpointService, GroupService, Notifications) {
|
function ($q, $scope, $state, $filter, EndpointService, GroupService, TagService, Notifications) {
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
EnvironmentType: 'docker',
|
EnvironmentType: 'docker',
|
||||||
|
@ -15,7 +15,8 @@ function ($scope, $state, $filter, EndpointService, GroupService, Notifications)
|
||||||
SecurityFormData: new EndpointSecurityFormData(),
|
SecurityFormData: new EndpointSecurityFormData(),
|
||||||
AzureApplicationId: '',
|
AzureApplicationId: '',
|
||||||
AzureTenantId: '',
|
AzureTenantId: '',
|
||||||
AzureAuthenticationKey: ''
|
AzureAuthenticationKey: '',
|
||||||
|
Tags: []
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.addDockerEndpoint = function() {
|
$scope.addDockerEndpoint = function() {
|
||||||
|
@ -23,6 +24,7 @@ function ($scope, $state, $filter, EndpointService, GroupService, Notifications)
|
||||||
var URL = $filter('stripprotocol')($scope.formValues.URL);
|
var URL = $filter('stripprotocol')($scope.formValues.URL);
|
||||||
var publicURL = $scope.formValues.PublicURL === '' ? URL.split(':')[0] : $scope.formValues.PublicURL;
|
var publicURL = $scope.formValues.PublicURL === '' ? URL.split(':')[0] : $scope.formValues.PublicURL;
|
||||||
var groupId = $scope.formValues.GroupId;
|
var groupId = $scope.formValues.GroupId;
|
||||||
|
var tags = $scope.formValues.Tags;
|
||||||
|
|
||||||
var securityData = $scope.formValues.SecurityFormData;
|
var securityData = $scope.formValues.SecurityFormData;
|
||||||
var TLS = securityData.TLS;
|
var TLS = securityData.TLS;
|
||||||
|
@ -33,7 +35,7 @@ function ($scope, $state, $filter, EndpointService, GroupService, Notifications)
|
||||||
var TLSCertFile = TLSSkipClientVerify ? null : securityData.TLSCert;
|
var TLSCertFile = TLSSkipClientVerify ? null : securityData.TLSCert;
|
||||||
var TLSKeyFile = TLSSkipClientVerify ? null : securityData.TLSKey;
|
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() {
|
$scope.addAgentEndpoint = function() {
|
||||||
|
@ -41,8 +43,9 @@ function ($scope, $state, $filter, EndpointService, GroupService, Notifications)
|
||||||
var URL = $filter('stripprotocol')($scope.formValues.URL);
|
var URL = $filter('stripprotocol')($scope.formValues.URL);
|
||||||
var publicURL = $scope.formValues.PublicURL === '' ? URL.split(':')[0] : $scope.formValues.PublicURL;
|
var publicURL = $scope.formValues.PublicURL === '' ? URL.split(':')[0] : $scope.formValues.PublicURL;
|
||||||
var groupId = $scope.formValues.GroupId;
|
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() {
|
$scope.addAzureEndpoint = function() {
|
||||||
|
@ -50,15 +53,17 @@ function ($scope, $state, $filter, EndpointService, GroupService, Notifications)
|
||||||
var applicationId = $scope.formValues.AzureApplicationId;
|
var applicationId = $scope.formValues.AzureApplicationId;
|
||||||
var tenantId = $scope.formValues.AzureTenantId;
|
var tenantId = $scope.formValues.AzureTenantId;
|
||||||
var authenticationKey = $scope.formValues.AzureAuthenticationKey;
|
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;
|
var endpoint;
|
||||||
|
|
||||||
$scope.state.actionInProgress = true;
|
$scope.state.actionInProgress = true;
|
||||||
EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey)
|
EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tags)
|
||||||
.then(function success() {
|
.then(function success() {
|
||||||
Notifications.success('Endpoint created', name);
|
Notifications.success('Endpoint created', name);
|
||||||
$state.go('portainer.endpoints', {}, {reload: true});
|
$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;
|
$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() {
|
.then(function success() {
|
||||||
Notifications.success('Endpoint created', name);
|
Notifications.success('Endpoint created', name);
|
||||||
$state.go('portainer.endpoints', {}, {reload: true});
|
$state.go('portainer.endpoints', {}, {reload: true});
|
||||||
|
@ -87,9 +92,13 @@ function ($scope, $state, $filter, EndpointService, GroupService, Notifications)
|
||||||
}
|
}
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
GroupService.groups()
|
$q.all({
|
||||||
|
groups: GroupService.groups(),
|
||||||
|
tags: TagService.tagNames()
|
||||||
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.groups = data;
|
$scope.groups = data.groups;
|
||||||
|
$scope.availableTags = data.tags;
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to load groups');
|
Notifications.error('Failure', err, 'Unable to load groups');
|
||||||
|
|
|
@ -192,6 +192,12 @@
|
||||||
<!-- !authenticationkey-input -->
|
<!-- !authenticationkey-input -->
|
||||||
</div>
|
</div>
|
||||||
<!-- !azure-details -->
|
<!-- !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 -->
|
<!-- group -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="endpoint_group" class="col-sm-3 col-lg-2 control-label text-left">
|
<label for="endpoint_group" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
@ -202,9 +208,17 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !group -->
|
<!-- !group -->
|
||||||
<!-- endpoint-security -->
|
<!-- tags -->
|
||||||
<por-endpoint-security ng-if="state.EnvironmentType === 'docker'" form-data="formValues.SecurityFormData"></por-endpoint-security>
|
<div class="form-group">
|
||||||
<!-- !endpoint-security -->
|
<tag-selector
|
||||||
|
tags="availableTags"
|
||||||
|
model="formValues.Tags"
|
||||||
|
></tag-selector>
|
||||||
|
</div>
|
||||||
|
<!-- !tags -->
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Actions
|
||||||
|
</div>
|
||||||
<!-- actions -->
|
<!-- actions -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
></azure-endpoint-config>
|
></azure-endpoint-config>
|
||||||
<!-- !endpoint-public-url-input -->
|
<!-- !endpoint-public-url-input -->
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Grouping
|
Metadata
|
||||||
</div>
|
</div>
|
||||||
<!-- group -->
|
<!-- group -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -61,6 +61,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !group -->
|
<!-- !group -->
|
||||||
|
<!-- tags -->
|
||||||
|
<div class="form-group">
|
||||||
|
<tag-selector
|
||||||
|
tags="availableTags"
|
||||||
|
model="endpoint.Tags"
|
||||||
|
></tag-selector>
|
||||||
|
</div>
|
||||||
|
<!-- !tags -->
|
||||||
<!-- endpoint-security -->
|
<!-- endpoint-security -->
|
||||||
<div ng-if="endpointType === 'remote' && endpoint.Type !== 3">
|
<div ng-if="endpointType === 'remote' && endpoint.Type !== 3">
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('EndpointController', ['$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, EndpointProvider, Notifications) {
|
function ($q, $scope, $state, $transition$, $filter, EndpointService, GroupService, TagService, EndpointProvider, Notifications) {
|
||||||
|
|
||||||
if (!$scope.applicationState.application.endpointManagement) {
|
if (!$scope.applicationState.application.endpointManagement) {
|
||||||
$state.go('portainer.endpoints');
|
$state.go('portainer.endpoints');
|
||||||
|
@ -27,6 +27,7 @@ function ($q, $scope, $state, $transition$, $filter, EndpointService, GroupServi
|
||||||
Name: endpoint.Name,
|
Name: endpoint.Name,
|
||||||
PublicURL: endpoint.PublicURL,
|
PublicURL: endpoint.PublicURL,
|
||||||
GroupID: endpoint.GroupId,
|
GroupID: endpoint.GroupId,
|
||||||
|
Tags: endpoint.Tags,
|
||||||
TLS: TLS,
|
TLS: TLS,
|
||||||
TLSSkipVerify: TLSSkipVerify,
|
TLSSkipVerify: TLSSkipVerify,
|
||||||
TLSSkipClientVerify: TLSSkipClientVerify,
|
TLSSkipClientVerify: TLSSkipClientVerify,
|
||||||
|
@ -61,7 +62,8 @@ function ($q, $scope, $state, $transition$, $filter, EndpointService, GroupServi
|
||||||
function initView() {
|
function initView() {
|
||||||
$q.all({
|
$q.all({
|
||||||
endpoint: EndpointService.endpoint($transition$.params().id),
|
endpoint: EndpointService.endpoint($transition$.params().id),
|
||||||
groups: GroupService.groups()
|
groups: GroupService.groups(),
|
||||||
|
tags: TagService.tagNames()
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var endpoint = data.endpoint;
|
var endpoint = data.endpoint;
|
||||||
|
@ -73,6 +75,7 @@ function ($q, $scope, $state, $transition$, $filter, EndpointService, GroupServi
|
||||||
endpoint.URL = $filter('stripprotocol')(endpoint.URL);
|
endpoint.URL = $filter('stripprotocol')(endpoint.URL);
|
||||||
$scope.endpoint = endpoint;
|
$scope.endpoint = endpoint;
|
||||||
$scope.groups = data.groups;
|
$scope.groups = data.groups;
|
||||||
|
$scope.availableTags = data.tags;
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve endpoint details');
|
Notifications.error('Failure', err, 'Unable to retrieve endpoint details');
|
||||||
|
|
|
@ -1,19 +1,11 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('CreateGroupController', ['$scope', '$state', 'GroupService', 'EndpointService', 'Notifications',
|
.controller('CreateGroupController', ['$q', '$scope', '$state', 'GroupService', 'EndpointService', 'TagService', 'Notifications',
|
||||||
function ($scope, $state, GroupService, EndpointService, Notifications) {
|
function ($q, $scope, $state, GroupService, EndpointService, TagService, Notifications) {
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
actionInProgress: false
|
actionInProgress: false
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.addLabel = function() {
|
|
||||||
$scope.model.Labels.push({ name: '', value: '' });
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.removeLabel = function(index) {
|
|
||||||
$scope.model.Labels.splice(index, 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.create = function() {
|
$scope.create = function() {
|
||||||
var model = $scope.model;
|
var model = $scope.model;
|
||||||
|
|
||||||
|
@ -40,10 +32,14 @@ function ($scope, $state, GroupService, EndpointService, Notifications) {
|
||||||
function initView() {
|
function initView() {
|
||||||
$scope.model = new EndpointGroupDefaultModel();
|
$scope.model = new EndpointGroupDefaultModel();
|
||||||
|
|
||||||
EndpointService.endpointsByGroup(1)
|
$q.all({
|
||||||
|
endpoints: EndpointService.endpointsByGroup(1),
|
||||||
|
tags: TagService.tagNames()
|
||||||
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.availableEndpoints = data;
|
$scope.availableEndpoints = data.endpoints;
|
||||||
$scope.associatedEndpoints = [];
|
$scope.associatedEndpoints = [];
|
||||||
|
$scope.availableTags = data.tags;
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve endpoints');
|
Notifications.error('Failure', err, 'Unable to retrieve endpoints');
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
<group-form
|
<group-form
|
||||||
model="model"
|
model="model"
|
||||||
available-endpoints="availableEndpoints"
|
available-endpoints="availableEndpoints"
|
||||||
|
available-tags="availableTags"
|
||||||
associated-endpoints="associatedEndpoints"
|
associated-endpoints="associatedEndpoints"
|
||||||
add-label-action="addLabel"
|
add-label-action="addLabel"
|
||||||
remove-label-action="removeLabel"
|
remove-label-action="removeLabel"
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
<group-form
|
<group-form
|
||||||
model="group"
|
model="group"
|
||||||
available-endpoints="availableEndpoints"
|
available-endpoints="availableEndpoints"
|
||||||
|
available-tags="availableTags"
|
||||||
associated-endpoints="associatedEndpoints"
|
associated-endpoints="associatedEndpoints"
|
||||||
add-label-action="addLabel"
|
add-label-action="addLabel"
|
||||||
remove-label-action="removeLabel"
|
remove-label-action="removeLabel"
|
||||||
|
|
|
@ -1,19 +1,11 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('GroupController', ['$q', '$scope', '$state', '$transition$', 'GroupService', 'EndpointService', 'Notifications',
|
.controller('GroupController', ['$q', '$scope', '$state', '$transition$', 'GroupService', 'EndpointService', 'TagService', 'Notifications',
|
||||||
function ($q, $scope, $state, $transition$, GroupService, EndpointService, Notifications) {
|
function ($q, $scope, $state, $transition$, GroupService, EndpointService, TagService, Notifications) {
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
actionInProgress: false
|
actionInProgress: false
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.addLabel = function() {
|
|
||||||
$scope.group.Labels.push({ name: '', value: '' });
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.removeLabel = function(index) {
|
|
||||||
$scope.group.Labels.splice(index, 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.update = function() {
|
$scope.update = function() {
|
||||||
var model = $scope.group;
|
var model = $scope.group;
|
||||||
|
|
||||||
|
@ -42,7 +34,8 @@ function ($q, $scope, $state, $transition$, GroupService, EndpointService, Notif
|
||||||
|
|
||||||
$q.all({
|
$q.all({
|
||||||
group: GroupService.group(groupId),
|
group: GroupService.group(groupId),
|
||||||
endpoints: EndpointService.endpoints()
|
endpoints: EndpointService.endpoints(),
|
||||||
|
tags: TagService.tagNames()
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.group = data.group;
|
$scope.group = data.group;
|
||||||
|
@ -60,6 +53,7 @@ function ($q, $scope, $state, $transition$, GroupService, EndpointService, Notif
|
||||||
|
|
||||||
$scope.availableEndpoints = availableEndpoints;
|
$scope.availableEndpoints = availableEndpoints;
|
||||||
$scope.associatedEndpoints = associatedEndpoints;
|
$scope.associatedEndpoints = associatedEndpoints;
|
||||||
|
$scope.availableTags = data.tags;
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to load view');
|
Notifications.error('Failure', err, 'Unable to load view');
|
||||||
|
|
|
@ -90,7 +90,7 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif
|
||||||
var endpoint;
|
var endpoint;
|
||||||
|
|
||||||
$scope.state.actionInProgress = true;
|
$scope.state.actionInProgress = true;
|
||||||
EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey)
|
EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, 1, [])
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
endpoint = data;
|
endpoint = data;
|
||||||
EndpointProvider.setEndpointID(endpoint.Id);
|
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) {
|
function createRemoteEndpoint(name, type, URL, PublicURL, TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
|
||||||
var endpoint;
|
var endpoint;
|
||||||
$scope.state.actionInProgress = true;
|
$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) {
|
.then(function success(data) {
|
||||||
endpoint = data;
|
endpoint = data;
|
||||||
EndpointProvider.setEndpointID(endpoint.Id);
|
EndpointProvider.setEndpointID(endpoint.Id);
|
||||||
|
|
|
@ -49,9 +49,12 @@
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
|
<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>
|
<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>
|
<a ui-sref="portainer.groups" ui-sref-active="active">Groups</a>
|
||||||
</div>
|
</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>
|
||||||
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
|
<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>
|
<a ui-sref="portainer.registries" ui-sref-active="active">Registries <span class="menu-icon fa fa-database fa-fw"></span></a>
|
||||||
|
|
58
app/portainer/views/tags/tags.html
Normal file
58
app/portainer/views/tags/tags.html
Normal file
|
@ -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>
|
58
app/portainer/views/tags/tagsController.js
Normal file
58
app/portainer/views/tags/tagsController.js
Normal file
|
@ -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;
|
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*/
|
/*bootbox override*/
|
||||||
.modal-open {
|
.modal-open {
|
||||||
padding-right: 0 !important;
|
padding-right: 0 !important;
|
||||||
|
@ -787,12 +799,12 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* json-tree */
|
/* json-tree override */
|
||||||
|
|
||||||
json-tree {
|
json-tree {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #30426a;
|
color: #30426a;
|
||||||
}
|
}
|
||||||
|
|
||||||
json-tree .key {
|
json-tree .key {
|
||||||
color: #738bc0;
|
color: #738bc0;
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
|
@ -803,7 +815,4 @@ json-tree .branch-preview {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
opacity: .5;
|
opacity: .5;
|
||||||
}
|
}
|
||||||
|
/* !json-tree override */
|
||||||
.row.header .meta .page {
|
|
||||||
padding-top: 7px;
|
|
||||||
}
|
|
||||||
|
|
|
@ -55,7 +55,6 @@
|
||||||
"rdash-ui": "1.0.*",
|
"rdash-ui": "1.0.*",
|
||||||
"splitargs": "github:deviantony/splitargs#~0.2.0",
|
"splitargs": "github:deviantony/splitargs#~0.2.0",
|
||||||
"toastr": "github:CodeSeven/toastr#~2.1.3",
|
"toastr": "github:CodeSeven/toastr#~2.1.3",
|
||||||
"ui-select": "~0.19.6",
|
|
||||||
"xterm": "^3.1.0"
|
"xterm": "^3.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -21,7 +21,6 @@ css:
|
||||||
- 'bootstrap/dist/css/bootstrap.css'
|
- 'bootstrap/dist/css/bootstrap.css'
|
||||||
- 'rdash-ui/dist/css/rdash.css'
|
- 'rdash-ui/dist/css/rdash.css'
|
||||||
- 'isteven-angular-multiselect/isteven-multi-select.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-brands.css'
|
||||||
- '@fortawesome/fontawesome-free-webfonts/css/fa-solid.css'
|
- '@fortawesome/fontawesome-free-webfonts/css/fa-solid.css'
|
||||||
- '@fortawesome/fontawesome-free-webfonts/css/fontawesome.css'
|
- '@fortawesome/fontawesome-free-webfonts/css/fontawesome.css'
|
||||||
|
@ -42,7 +41,6 @@ angular:
|
||||||
- 'angular-messages/angular-messages.js'
|
- 'angular-messages/angular-messages.js'
|
||||||
- 'angular-resource/angular-resource.js'
|
- 'angular-resource/angular-resource.js'
|
||||||
- 'angular-sanitize/angular-sanitize.js'
|
- 'angular-sanitize/angular-sanitize.js'
|
||||||
- 'ui-select/dist/select.js'
|
|
||||||
- '@uirouter/angularjs/release/angular-ui-router.js'
|
- '@uirouter/angularjs/release/angular-ui-router.js'
|
||||||
- 'angular-utils-pagination/dirPagination.js'
|
- 'angular-utils-pagination/dirPagination.js'
|
||||||
- 'ng-file-upload/dist/ng-file-upload.js'
|
- 'ng-file-upload/dist/ng-file-upload.js'
|
||||||
|
|
|
@ -4238,10 +4238,6 @@ uglify-to-browserify@~1.0.0:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
|
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:
|
uid-safe@2.1.4:
|
||||||
version "2.1.4"
|
version "2.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.4.tgz#3ad6f38368c6d4c8c75ec17623fb79aa1d071d81"
|
resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.4.tgz#3ad6f38368c6d4c8c75ec17623fb79aa1d071d81"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue