1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-23 15:29:42 +02:00

Merge branch 'develop' into feat/EE-809/EE-466/kube-advanced-apps

This commit is contained in:
Felix Han 2021-08-30 13:13:25 +12:00
commit 0979e6ec8f
97 changed files with 1155 additions and 314 deletions

View file

@ -5,7 +5,7 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
endpointutils "github.com/portainer/portainer/api/internal/endpoint"
"github.com/portainer/portainer/api/internal/endpointutils"
snapshotutils "github.com/portainer/portainer/api/internal/snapshot"
)

View file

@ -10,7 +10,7 @@ import (
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/http/security"
endpointutils "github.com/portainer/portainer/api/internal/endpoint"
"github.com/portainer/portainer/api/internal/endpointutils"
)
// GET request on /endpoints/{id}/registries?namespace

View file

@ -48,8 +48,8 @@ type Handler struct {
EndpointGroupHandler *endpointgroups.Handler
EndpointHandler *endpoints.Handler
EndpointProxyHandler *endpointproxy.Handler
FileHandler *file.Handler
KubernetesHandler *kubernetes.Handler
FileHandler *file.Handler
MOTDHandler *motd.Handler
RegistryHandler *registries.Handler
ResourceControlHandler *resourcecontrols.Handler

View file

@ -1,28 +1,68 @@
package kubernetes
import (
"errors"
"net/http"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/middlewares"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/kubernetes/cli"
)
// Handler is the HTTP handler which will natively deal with to external endpoints.
type Handler struct {
*mux.Router
DataStore portainer.DataStore
KubernetesClientFactory *cli.ClientFactory
dataStore portainer.DataStore
kubernetesClientFactory *cli.ClientFactory
authorizationService *authorization.Service
}
// NewHandler creates a handler to process pre-proxied requests to external APIs.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
func NewHandler(bouncer *security.RequestBouncer, authorizationService *authorization.Service, dataStore portainer.DataStore, kubernetesClientFactory *cli.ClientFactory) *Handler {
h := &Handler{
Router: mux.NewRouter(),
Router: mux.NewRouter(),
dataStore: dataStore,
kubernetesClientFactory: kubernetesClientFactory,
authorizationService: authorizationService,
}
h.PathPrefix("/kubernetes/{id}/config").Handler(
kubeRouter := h.PathPrefix("/kubernetes/{id}").Subrouter()
kubeRouter.Use(bouncer.AuthenticatedAccess)
kubeRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
kubeRouter.Use(kubeOnlyMiddleware)
kubeRouter.PathPrefix("/config").Handler(
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.getKubernetesConfig))).Methods(http.MethodGet)
// namespaces
// in the future this piece of code might be in another package (or a few different packages - namespaces/namespace?)
// to keep it simple, we've decided to leave it like this.
namespaceRouter := kubeRouter.PathPrefix("/namespaces/{namespace}").Subrouter()
namespaceRouter.Handle("/system", bouncer.RestrictedAccess(httperror.LoggerHandler(h.namespacesToggleSystem))).Methods(http.MethodPut)
return h
}
func kubeOnlyMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, request *http.Request) {
endpoint, err := middlewares.FetchEndpoint(request)
if err != nil {
httperror.WriteError(rw, http.StatusInternalServerError, "Unable to find an endpoint on request context", err)
return
}
if !endpointutils.IsKubernetesEndpoint(endpoint) {
errMessage := "Endpoint is not a kubernetes endpoint"
httperror.WriteError(rw, http.StatusBadRequest, errMessage, errors.New(errMessage))
return
}
next.ServeHTTP(rw, request)
})
}

View file

@ -39,7 +39,7 @@ func (handler *Handler) getKubernetesConfig(w http.ResponseWriter, r *http.Reque
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
endpoint, err := handler.dataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
@ -56,7 +56,7 @@ func (handler *Handler) getKubernetesConfig(w http.ResponseWriter, r *http.Reque
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint)
cli, err := handler.kubernetesClientFactory.GetKubeClient(endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Kubernetes client", err}
}

View file

@ -0,0 +1,65 @@
package kubernetes
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api/http/middlewares"
)
type namespacesToggleSystemPayload struct {
// Toggle the system state of this namespace to true or false
System bool `example:"true"`
}
func (payload *namespacesToggleSystemPayload) Validate(r *http.Request) error {
return nil
}
// @id KubernetesNamespacesToggleSystem
// @summary Toggle the system state for a namespace
// @description Toggle the system state for a namespace
// @description **Access policy**: administrator or endpoint admin
// @security jwt
// @tags kubernetes
// @accept json
// @param id path int true "Endpoint identifier"
// @param namespace path string true "Namespace name"
// @param body body namespacesToggleSystemPayload true "Update details"
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 404 "Endpoint not found"
// @failure 500 "Server error"
// @router /kubernetes/{id}/namespaces/{namespace}/system [put]
func (handler *Handler) namespacesToggleSystem(rw http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpoint, err := middlewares.FetchEndpoint(r)
if err != nil {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint on request context", err}
}
namespaceName, err := request.RetrieveRouteVariableValue(r, "namespace")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid namespace identifier route variable", err}
}
var payload namespacesToggleSystemPayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
kubeClient, err := handler.kubernetesClientFactory.GetKubeClient(endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create kubernetes client", err}
}
err = kubeClient.ToggleSystemState(namespaceName, payload.System)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to toggle system status", err}
}
return response.Empty(rw)
}

View file

@ -86,17 +86,12 @@ func (handler *Handler) websocketShellPodExec(w http.ResponseWriter, r *http.Req
return nil
}
serviceAccountToken, isAdminToken, err := handler.getToken(r, endpoint, false)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to get user service account token", err}
}
handlerErr := handler.hijackPodExecStartOperation(
w,
r,
cli,
serviceAccountToken,
isAdminToken,
"",
true,
endpoint,
shellPod.Namespace,
shellPod.PodName,

View file

@ -0,0 +1,58 @@
package middlewares
import (
"context"
"errors"
"net/http"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
requesthelpers "github.com/portainer/libhttp/request"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
)
const (
contextEndpoint = "endpoint"
)
func WithEndpoint(endpointService portainer.EndpointService, endpointIDParam string) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, request *http.Request) {
if endpointIDParam == "" {
endpointIDParam = "id"
}
endpointID, err := requesthelpers.RetrieveNumericRouteVariableValue(request, endpointIDParam)
if err != nil {
httperror.WriteError(rw, http.StatusBadRequest, "Invalid endpoint identifier route variable", err)
return
}
endpoint, err := endpointService.Endpoint(portainer.EndpointID(endpointID))
if err != nil {
statusCode := http.StatusInternalServerError
if err == bolterrors.ErrObjectNotFound {
statusCode = http.StatusNotFound
}
httperror.WriteError(rw, statusCode, "Unable to find an endpoint with the specified identifier inside the database", err)
return
}
ctx := context.WithValue(request.Context(), contextEndpoint, endpoint)
next.ServeHTTP(rw, request.WithContext(ctx))
})
}
}
func FetchEndpoint(request *http.Request) (*portainer.Endpoint, error) {
contextData := request.Context().Value(contextEndpoint)
if contextData == nil {
return nil, errors.New("Unable to find endpoint data in request context")
}
return contextData.(*portainer.Endpoint), nil
}

View file

@ -26,7 +26,7 @@ import (
"github.com/portainer/portainer/api/http/handler/endpointproxy"
"github.com/portainer/portainer/api/http/handler/endpoints"
"github.com/portainer/portainer/api/http/handler/file"
kube "github.com/portainer/portainer/api/http/handler/kubernetes"
kubehandler "github.com/portainer/portainer/api/http/handler/kubernetes"
"github.com/portainer/portainer/api/http/handler/motd"
"github.com/portainer/portainer/api/http/handler/registries"
"github.com/portainer/portainer/api/http/handler/resourcecontrols"
@ -160,11 +160,9 @@ func (server *Server) Start() error {
endpointProxyHandler.ProxyManager = server.ProxyManager
endpointProxyHandler.ReverseTunnelService = server.ReverseTunnelService
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
var kubernetesHandler = kubehandler.NewHandler(requestBouncer, server.AuthorizationService, server.DataStore, server.KubernetesClientFactory)
var kubernetesHandler = kube.NewHandler(requestBouncer)
kubernetesHandler.DataStore = server.DataStore
kubernetesHandler.KubernetesClientFactory = server.KubernetesClientFactory
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
var motdHandler = motd.NewHandler(requestBouncer)
@ -244,8 +242,8 @@ func (server *Server) Start() error {
EndpointHandler: endpointHandler,
EndpointEdgeHandler: endpointEdgeHandler,
EndpointProxyHandler: endpointProxyHandler,
FileHandler: fileHandler,
KubernetesHandler: kubernetesHandler,
FileHandler: fileHandler,
MOTDHandler: motdHandler,
RegistryHandler: registryHandler,
ResourceControlHandler: resourceControlHandler,

View file

@ -1,17 +0,0 @@
package endpoint
import portainer "github.com/portainer/portainer/api"
// IsKubernetesEndpoint returns true if this is a kubernetes endpoint
func IsKubernetesEndpoint(endpoint *portainer.Endpoint) bool {
return endpoint.Type == portainer.KubernetesLocalEnvironment ||
endpoint.Type == portainer.AgentOnKubernetesEnvironment ||
endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment
}
// IsDockerEndpoint returns true if this is a docker endpoint
func IsDockerEndpoint(endpoint *portainer.Endpoint) bool {
return endpoint.Type == portainer.DockerEnvironment ||
endpoint.Type == portainer.AgentOnDockerEnvironment ||
endpoint.Type == portainer.EdgeAgentOnDockerEnvironment
}

View file

@ -6,6 +6,7 @@ import (
portainer "github.com/portainer/portainer/api"
)
// IsLocalEndpoint returns true if this is a local endpoint
func IsLocalEndpoint(endpoint *portainer.Endpoint) bool {
return strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") || endpoint.Type == 5
}

View file

@ -0,0 +1,73 @@
package cli
import (
"strconv"
"github.com/pkg/errors"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
systemNamespaceLabel = "io.portainer.kubernetes.namespace.system"
)
func defaultSystemNamespaces() map[string]struct{} {
return map[string]struct{}{
"kube-system": {},
"kube-public": {},
"kube-node-lease": {},
"portainer": {},
}
}
func isSystemNamespace(namespace v1.Namespace) bool {
systemLabelValue, hasSystemLabel := namespace.Labels[systemNamespaceLabel]
if hasSystemLabel {
return systemLabelValue == "true"
}
systemNamespaces := defaultSystemNamespaces()
_, isSystem := systemNamespaces[namespace.Name]
return isSystem
}
// ToggleSystemState will set a namespace as a system namespace, or remove this state
// if isSystem is true it will set `systemNamespaceLabel` to "true" and false otherwise
// this will skip if namespace is "default" or if the required state is already set
func (kcl *KubeClient) ToggleSystemState(namespaceName string, isSystem bool) error {
if namespaceName == "default" {
return nil
}
nsService := kcl.cli.CoreV1().Namespaces()
namespace, err := nsService.Get(namespaceName, metav1.GetOptions{})
if err != nil {
return errors.Wrap(err, "failed fetching namespace object")
}
if isSystemNamespace(*namespace) == isSystem {
return nil
}
if namespace.Labels == nil {
namespace.Labels = map[string]string{}
}
namespace.Labels[systemNamespaceLabel] = strconv.FormatBool(isSystem)
_, err = nsService.Update(namespace)
if err != nil {
return errors.Wrap(err, "failed updating namespace object")
}
if isSystem {
return kcl.NamespaceAccessPoliciesDeleteNamespace(namespaceName)
}
return nil
}

View file

@ -0,0 +1,185 @@
package cli
import (
"strconv"
"sync"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert"
core "k8s.io/api/core/v1"
ktypes "k8s.io/api/core/v1"
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kfake "k8s.io/client-go/kubernetes/fake"
)
func Test_ToggleSystemState(t *testing.T) {
t.Run("should skip is default (exit without error)", func(t *testing.T) {
nsName := "default"
kcl := &KubeClient{
cli: kfake.NewSimpleClientset(&core.Namespace{ObjectMeta: meta.ObjectMeta{Name: nsName}}),
instanceID: "instance",
lock: &sync.Mutex{},
}
err := kcl.ToggleSystemState(nsName, true)
assert.NoError(t, err)
ns, err := kcl.cli.CoreV1().Namespaces().Get(nsName, meta.GetOptions{})
assert.NoError(t, err)
_, exists := ns.Labels[systemNamespaceLabel]
assert.False(t, exists, "system label should not exists")
})
t.Run("should fail if namespace doesn't exist", func(t *testing.T) {
nsName := "not-exist"
kcl := &KubeClient{
cli: kfake.NewSimpleClientset(),
instanceID: "instance",
lock: &sync.Mutex{},
}
err := kcl.ToggleSystemState(nsName, true)
assert.Error(t, err)
})
t.Run("if called with the same state, should skip (exit without error)", func(t *testing.T) {
nsName := "namespace"
tests := []struct {
isSystem bool
}{
{isSystem: true},
{isSystem: false},
}
for _, test := range tests {
t.Run(strconv.FormatBool(test.isSystem), func(t *testing.T) {
kcl := &KubeClient{
cli: kfake.NewSimpleClientset(&core.Namespace{ObjectMeta: meta.ObjectMeta{Name: nsName, Labels: map[string]string{
systemNamespaceLabel: strconv.FormatBool(test.isSystem),
}}}),
instanceID: "instance",
lock: &sync.Mutex{},
}
err := kcl.ToggleSystemState(nsName, test.isSystem)
assert.NoError(t, err)
ns, err := kcl.cli.CoreV1().Namespaces().Get(nsName, meta.GetOptions{})
assert.NoError(t, err)
assert.Equal(t, test.isSystem, isSystemNamespace(*ns))
})
}
})
t.Run("for regular namespace if isSystem is true and doesn't have a label, should set the label to true", func(t *testing.T) {
nsName := "namespace"
kcl := &KubeClient{
cli: kfake.NewSimpleClientset(&core.Namespace{ObjectMeta: meta.ObjectMeta{Name: nsName}}),
instanceID: "instance",
lock: &sync.Mutex{},
}
err := kcl.ToggleSystemState(nsName, true)
assert.NoError(t, err)
ns, err := kcl.cli.CoreV1().Namespaces().Get(nsName, meta.GetOptions{})
assert.NoError(t, err)
labelValue, exists := ns.Labels[systemNamespaceLabel]
assert.True(t, exists, "system label should exists")
assert.Equal(t, "true", labelValue)
})
t.Run("for default system namespace if isSystem is false and doesn't have a label, should set the label to false", func(t *testing.T) {
nsName := "portainer"
kcl := &KubeClient{
cli: kfake.NewSimpleClientset(&core.Namespace{ObjectMeta: meta.ObjectMeta{Name: nsName}}),
instanceID: "instance",
lock: &sync.Mutex{},
}
err := kcl.ToggleSystemState(nsName, false)
assert.NoError(t, err)
ns, err := kcl.cli.CoreV1().Namespaces().Get(nsName, meta.GetOptions{})
assert.NoError(t, err)
labelValue, exists := ns.Labels[systemNamespaceLabel]
assert.True(t, exists, "system label should exists")
assert.Equal(t, "false", labelValue)
})
t.Run("for system namespace (with label), if called with false, should set the label", func(t *testing.T) {
nsName := "namespace"
kcl := &KubeClient{
cli: kfake.NewSimpleClientset(&core.Namespace{ObjectMeta: meta.ObjectMeta{Name: nsName, Labels: map[string]string{
systemNamespaceLabel: "true",
}}}),
instanceID: "instance",
lock: &sync.Mutex{},
}
err := kcl.ToggleSystemState(nsName, false)
assert.NoError(t, err)
ns, err := kcl.cli.CoreV1().Namespaces().Get(nsName, meta.GetOptions{})
assert.NoError(t, err)
labelValue, exists := ns.Labels[systemNamespaceLabel]
assert.True(t, exists, "system label should exists")
assert.Equal(t, "false", labelValue)
})
t.Run("for non system namespace (with label), if called with true, should set the label, and remove accesses", func(t *testing.T) {
nsName := "ns1"
namespace := &core.Namespace{ObjectMeta: meta.ObjectMeta{Name: nsName, Labels: map[string]string{
systemNamespaceLabel: "false",
}}}
config := &ktypes.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: portainerConfigMapName,
Namespace: portainerNamespace,
},
Data: map[string]string{
"NamespaceAccessPolicies": `{"ns1":{"UserAccessPolicies":{"2":{"RoleId":0}}}, "ns2":{"UserAccessPolicies":{"2":{"RoleId":0}}}}`,
},
}
kcl := &KubeClient{
cli: kfake.NewSimpleClientset(namespace, config),
instanceID: "instance",
lock: &sync.Mutex{},
}
err := kcl.ToggleSystemState(nsName, true)
assert.NoError(t, err)
ns, err := kcl.cli.CoreV1().Namespaces().Get(nsName, meta.GetOptions{})
assert.NoError(t, err)
labelValue, exists := ns.Labels[systemNamespaceLabel]
assert.True(t, exists, "system label should exists")
assert.Equal(t, "true", labelValue)
expectedPolicies := map[string]portainer.K8sNamespaceAccessPolicy{
"ns2": {UserAccessPolicies: portainer.UserAccessPolicies{2: {RoleID: 0}}},
}
actualPolicies, err := kcl.GetNamespaceAccessPolicies()
assert.NoError(t, err, "failed to fetch policies")
assert.Equal(t, expectedPolicies, actualPolicies)
})
}

View file

@ -1230,6 +1230,7 @@ type (
CreateRegistrySecret(registry *Registry, namespace string) error
IsRegistrySecret(namespace, secretName string) (bool, error)
GetKubeConfig(ctx context.Context, apiServerURL string, bearerToken string, tokenData *TokenData) (*clientV1.Config, error)
ToggleSystemState(namespace string, isSystem bool) error
}
// KubernetesDeployer represents a service to deploy a manifest inside a Kubernetes endpoint

View file

@ -1,7 +1,19 @@
<sidebar-menu-item path="azure.dashboard" path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-tachometer-alt fa-fw" class-name="sidebar-list">
<sidebar-menu-item
path="azure.dashboard"
path-params="{ endpointId: $ctrl.endpointId }"
icon-class="fa-tachometer-alt fa-fw"
class-name="sidebar-list"
data-cy="azureSidebar-dashboard"
>
Dashboard
</sidebar-menu-item>
<sidebar-menu-item path="azure.containerinstances" path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-cubes fa-fw" class-name="sidebar-list">
<sidebar-menu-item
path="azure.containerinstances"
path-params="{ endpointId: $ctrl.endpointId }"
icon-class="fa-cubes fa-fw"
class-name="sidebar-list"
data-cy="azureSidebar-containerInstances"
>
Container instances
</sidebar-menu-item>

View file

@ -27,8 +27,6 @@ angular
.constant('PAGINATION_MAX_ITEMS', 10)
.constant('APPLICATION_CACHE_VALIDITY', 3600)
.constant('CONSOLE_COMMANDS_LABEL_PREFIX', 'io.portainer.commands.')
.constant('PREDEFINED_NETWORKS', ['host', 'bridge', 'none'])
.constant('KUBERNETES_DEFAULT_NAMESPACE', 'default')
.constant('KUBERNETES_SYSTEM_NAMESPACES', ['kube-system', 'kube-public', 'kube-node-lease', 'portainer']);
.constant('PREDEFINED_NETWORKS', ['host', 'bridge', 'none']);
export const PORTAINER_FADEOUT = 1500;

View file

@ -1,4 +1,10 @@
<sidebar-menu-item path="docker.dashboard" path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-tachometer-alt fa-fw" class-name="sidebar-list">
<sidebar-menu-item
path="docker.dashboard"
path-params="{ endpointId: $ctrl.endpointId }"
icon-class="fa-tachometer-alt fa-fw"
class-name="sidebar-list"
data-cy="dockerSidebar-dashboard"
>
Dashboard
</sidebar-menu-item>
@ -11,32 +17,46 @@
is-sidebar-open="$ctrl.isSidebarOpen"
children-paths="[]"
>
<sidebar-menu-item path="docker.templates.custom" path-params="{ endpointId: $ctrl.endpointId }" class-name="sidebar-sublist">
<sidebar-menu-item path="docker.templates.custom" path-params="{ endpointId: $ctrl.endpointId }" class-name="sidebar-sublist" data-cy="dockerSidebar-customTemplates">
Custom Templates
</sidebar-menu-item>
</sidebar-menu>
<sidebar-menu-item ng-if="$ctrl.showStacks" path="docker.stacks" path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-th-list fa-fw" class-name="sidebar-list">
<sidebar-menu-item
ng-if="$ctrl.showStacks"
path="docker.stacks"
path-params="{ endpointId: $ctrl.endpointId }"
icon-class="fa-th-list fa-fw"
class-name="sidebar-list"
data-cy="dockerSidebar-stacks"
>
Stacks
</sidebar-menu-item>
<sidebar-menu-item ng-if="$ctrl.swarmManagement" path="docker.services" path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-list-alt fa-fw" class-name="sidebar-list">
<sidebar-menu-item
ng-if="$ctrl.swarmManagement"
path="docker.services"
path-params="{ endpointId: $ctrl.endpointId }"
icon-class="fa-list-alt fa-fw"
class-name="sidebar-list"
data-cy="dockerSidebar-services"
>
Services
</sidebar-menu-item>
<sidebar-menu-item path="docker.containers" path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-cubes fa-fw" class-name="sidebar-list">
<sidebar-menu-item path="docker.containers" path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-cubes fa-fw" class-name="sidebar-list" data-cy="dockerSidebar-containers">
Containers
</sidebar-menu-item>
<sidebar-menu-item path="docker.images" path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-clone fa-fw" class-name="sidebar-list">
<sidebar-menu-item path="docker.images" path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-clone fa-fw" class-name="sidebar-list" data-cy="dockerSidebar-images">
Images
</sidebar-menu-item>
<sidebar-menu-item path="docker.networks" path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-sitemap fa-fw" class-name="sidebar-list">
<sidebar-menu-item path="docker.networks" path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-sitemap fa-fw" class-name="sidebar-list" data-cy="dockerSidebar-networks">
Networks
</sidebar-menu-item>
<sidebar-menu-item path="docker.volumes" path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-hdd fa-fw" class-name="sidebar-list">
<sidebar-menu-item path="docker.volumes" path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-hdd fa-fw" class-name="sidebar-list" data-cy="dockerSidebar-volumes">
Volumes
</sidebar-menu-item>
@ -46,6 +66,7 @@
path-params="{ endpointId: $ctrl.endpointId }"
icon-class="fa-file-code fa-fw"
class-name="sidebar-list"
data-cy="dockerSidebar-configs"
>
Configs
</sidebar-menu-item>
@ -56,6 +77,7 @@
path-params="{ endpointId: $ctrl.endpointId }"
icon-class="fa-user-secret fa-fw"
class-name="sidebar-list"
data-cy="dockerSidebar-secrets"
>
Secrets
</sidebar-menu-item>
@ -66,6 +88,7 @@
path-params="{ endpointId: $ctrl.endpointId }"
icon-class="fa-history fa-fw"
class-name="sidebar-list"
data-cy="dockerSidebar-events"
>
Events
</sidebar-menu-item>
@ -85,11 +108,18 @@
path="docker.featuresConfiguration"
path-params="{ endpointId: $ctrl.endpointId }"
class-name="sidebar-sublist"
data-cy="dockerSidebar-setup"
>
Setup
</sidebar-menu-item>
<sidebar-menu-item authorization="PortainerRegistryList" path="docker.registries" path-params="{ endpointId: $ctrl.endpointId }" class-name="sidebar-sublist">
<sidebar-menu-item
authorization="PortainerRegistryList"
path="docker.registries"
path-params="{ endpointId: $ctrl.endpointId }"
class-name="sidebar-sublist"
data-cy="dockerSidebar-registries"
>
Registries
</sidebar-menu-item>
</div>
@ -110,11 +140,18 @@
path="docker.featuresConfiguration"
path-params="{ endpointId: $ctrl.endpointId }"
class-name="sidebar-sublist"
data-cy="swarmSidebar-setup"
>
Setup
</sidebar-menu-item>
<sidebar-menu-item authorization="PortainerRegistryList" path="docker.registries" path-params="{ endpointId: $ctrl.endpointId }" class-name="sidebar-sublist">
<sidebar-menu-item
authorization="PortainerRegistryList"
path="docker.registries"
path-params="{ endpointId: $ctrl.endpointId }"
class-name="sidebar-sublist"
data-cy="swarmSidebar-registries"
>
Registries
</sidebar-menu-item>
</div>

View file

@ -26,6 +26,7 @@
placeholder="e.g. myImage:myTag"
ng-change="$ctrl.onImageChange()"
required
data-cy="component-imageInput"
/>
<span ng-if="$ctrl.isDockerHubRegistry()" class="input-group-btn">
<a

View file

@ -1,4 +1,4 @@
<ui-select multiple ng-model="$ctrl.model" close-on-select="false">
<ui-select multiple ng-model="$ctrl.model" close-on-select="false" data-cy="edgeGroupCreate-edgeGroupsSelector">
<ui-select-match placeholder="Select one or multiple group(s)">
<span>
{{ $item.Name }}

View file

@ -6,7 +6,7 @@
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i>
Edge Stacks
</div>
<div class="settings">
<div class="settings" data-cy="edgeStack-stackTableSettings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle> <i class="fa fa-cog" aria-hidden="true"></i> Settings </span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
@ -17,7 +17,13 @@
<div class="menuContent">
<div>
<div class="md-checkbox">
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()" />
<input
id="setting_auto_refresh"
type="checkbox"
ng-model="$ctrl.settings.repeater.autoRefresh"
ng-change="$ctrl.onSettingsRepeaterChange()"
data-cy="edgeStack-autoRefreshCheckbox"
/>
<label for="setting_auto_refresh">Auto refresh</label>
</div>
<div ng-if="$ctrl.settings.repeater.autoRefresh">
@ -48,10 +54,18 @@
</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)">
<button
type="button"
class="btn btn-sm btn-danger"
ng-disabled="$ctrl.state.selectedItemCount === 0"
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
data-cy="edgeStack-removeStackButton"
>
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="edge.stacks.new"> <i class="fa fa-plus space-right" aria-hidden="true"></i>Add stack </button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="edge.stacks.new" data-cy="edgeStack-addStackButton">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add stack
</button>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
@ -66,12 +80,12 @@
/>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<table class="table table-hover nowrap-cells" data-cy="edgeStack-stackTable">
<thead>
<tr>
<th>
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" data-cy="edgeStack-selectAllCheckbox" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.changeOrderBy('Name')">
@ -109,10 +123,10 @@
<td><edge-stack-status stack-status="item.Status"></edge-stack-status></td>
<td>{{ item.CreationDate | getisodatefromtimestamp }}</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<tr ng-if="!$ctrl.dataset" data-cy="edgeStack-loadingRow">
<td colspan="4" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<tr ng-if="$ctrl.state.filteredDataSet.length === 0" data-cy="edgeStack-noStackRow">
<td colspan="4" class="text-center text-muted">
No stack available.
</td>

View file

@ -4,7 +4,7 @@
Name
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="group_name" name="group_name" ng-model="$ctrl.model.Name" required auto-focus />
<input type="text" class="form-control" id="group_name" name="group_name" ng-model="$ctrl.model.Name" required auto-focus data-cy="edgeGroupCreate-groupNameInput" />
</div>
</div>
<div class="form-group" ng-show="EdgeGroupForm.group_name.$invalid">
@ -138,6 +138,7 @@
class="btn btn-primary btn-sm"
ng-disabled="$ctrl.actionInProgress || !EdgeGroupForm.$valid || (!$ctrl.model.Dynamic && !$ctrl.model.Endpoints.length) || ($ctrl.model.Dynamic && !$ctrl.model.TagIds.length)"
button-spinner="$ctrl.actionInProgress"
data-cy="edgeGroupCreate-addGroupButton"
>
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
<span ng-show="$ctrl.actionInProgress">In progress...</span>

View file

@ -3,10 +3,18 @@
<rd-widget-header icon="{{ $ctrl.titleIcon }}" title-text="Edge Groups"> </rd-widget-header>
<rd-widget-body classes="no-padding">
<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)">
<button
type="button"
class="btn btn-sm btn-danger"
ng-disabled="$ctrl.state.selectedItemCount === 0"
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
data-cy="edgeGroup-removeEdgeGroupButton"
>
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="edge.groups.new"> <i class="fa fa-plus space-right" aria-hidden="true"></i>Add Edge group </button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="edge.groups.new" data-cy="edgeGroup-addEdgeGroupButton">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add Edge group
</button>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
@ -17,15 +25,16 @@
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
ng-model-options="{ debounce: 300 }"
data-cy="edgeGroup-searchInput"
/>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<table class="table table-hover nowrap-cells" data-cy="edgeGroup-edgeGroupTable">
<thead>
<tr>
<th>
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" data-cy="edgeGroup-selectAllCheckbox" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.changeOrderBy('Name')">

View file

@ -14,7 +14,7 @@
Name
</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="$ctrl.formValues.Name" id="stack_name" placeholder="e.g. mystack" auto-focus />
<input type="text" class="form-control" ng-model="$ctrl.formValues.Name" id="stack_name" placeholder="e.g. mystack" auto-focus data-cy="edgeStackCreate-nameInput" />
</div>
</div>
<!-- !name-input -->
@ -39,7 +39,7 @@
<div class="boxselector_wrapper">
<div>
<input type="radio" id="method_editor" ng-model="$ctrl.state.Method" value="editor" ng-change="$ctrl.onChangeMethod()" />
<label for="method_editor">
<label for="method_editor" data-cy="edgeStackCreate-webEditorButton">
<div class="boxselector_header">
<i class="fa fa-edit" aria-hidden="true" style="margin-right: 2px;"></i>
Web editor
@ -49,7 +49,7 @@
</div>
<div>
<input type="radio" id="method_upload" ng-model="$ctrl.state.Method" value="upload" ng-change="$ctrl.onChangeMethod()" />
<label for="method_upload">
<label for="method_upload" data-cy="edgeStackCreate-uploadButton">
<div class="boxselector_header">
<i class="fa fa-upload" aria-hidden="true" style="margin-right: 2px;"></i>
Upload
@ -59,7 +59,7 @@
</div>
<div>
<input type="radio" id="method_repository" ng-model="$ctrl.state.Method" value="repository" ng-change="$ctrl.onChangeMethod()" />
<label for="method_repository">
<label for="method_repository" data-cy="edgeStackCreate-repoButton">
<div class="boxselector_header">
<i class="fab fa-git" aria-hidden="true" style="margin-right: 2px;"></i>
Repository
@ -69,7 +69,7 @@
</div>
<div>
<input type="radio" id="method_template" ng-model="$ctrl.state.Method" value="template" ng-change="$ctrl.onChangeMethod()" />
<label for="method_template">
<label for="method_template" data-cy="edgeStackCreate-templateButton">
<div class="boxselector_header">
<i class="fas fa-rocket" aria-hidden="true" style="margin-right: 2px;"></i>
Template
@ -198,6 +198,7 @@
|| !$ctrl.formValues.Name"
ng-click="$ctrl.createStack()"
button-spinner="$ctrl.state.actionInProgress"
data-cy="edgeStackCreate-createStackButton"
>
<span ng-hide="$ctrl.state.actionInProgress">Deploy the stack</span>
<span ng-show="$ctrl.state.actionInProgress">Deployment in progress...</span>

View file

@ -11,7 +11,7 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule]).conf
parent: 'endpoint',
abstract: true,
onEnter: /* @ngInject */ function onEnter($async, $state, endpoint, EndpointProvider, KubernetesHealthService, Notifications, StateManager) {
onEnter: /* @ngInject */ function onEnter($async, $state, endpoint, EndpointProvider, KubernetesHealthService, KubernetesNamespaceService, Notifications, StateManager) {
return $async(async () => {
if (![5, 6, 7].includes(endpoint.Type)) {
$state.go('portainer.home');
@ -34,6 +34,8 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule]).conf
if (endpoint.Type === 7 && endpoint.Status === 2) {
throw new Error('Unable to contact Edge agent, please ensure that the agent is properly running on the remote environment.');
}
await KubernetesNamespaceService.get();
} catch (e) {
Notifications.error('Failed loading endpoint', e);
$state.go('portainer.home', {}, { reload: true });

View file

@ -54,7 +54,7 @@
/>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<table class="table table-hover nowrap-cells" data-cy="k8sAppDetail-containerTable">
<thead>
<tr>
<th ng-if="!$ctrl.isPod">

View file

@ -7,7 +7,7 @@
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
System resources are hidden, this can be changed in the table settings.
</span>
<div class="settings">
<div class="settings" data-cy="k8sApp-tableSettings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Table settings</span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
@ -22,14 +22,26 @@
<label for="applications_setting_show_system">Show system resources</label>
</div>
<div class="md-checkbox">
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()" />
<input
id="setting_auto_refresh"
type="checkbox"
ng-model="$ctrl.settings.repeater.autoRefresh"
ng-change="$ctrl.onSettingsRepeaterChange()"
data-cy="k8sApp-autoRefreshCheckbox"
/>
<label for="setting_auto_refresh">Auto refresh</label>
</div>
<div ng-if="$ctrl.settings.repeater.autoRefresh">
<label for="settings_refresh_rate">
Refresh rate
</label>
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
<select
id="settings_refresh_rate"
ng-model="$ctrl.settings.repeater.refreshRate"
ng-change="$ctrl.onSettingsRepeaterChange()"
class="small-select"
data-cy="k8sApp-refreshRateDropdown"
>
<option value="10">10s</option>
<option value="30">30s</option>
<option value="60">1min</option>
@ -43,7 +55,7 @@
</div>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;" data-cy="k8sApp-tableSettingsCloseButton">Close</a>
</div>
</div>
</div>
@ -51,10 +63,16 @@
</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)">
<button
type="button"
class="btn btn-sm btn-danger"
ng-disabled="$ctrl.state.selectedItemCount === 0"
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
data-cy="k8sApp-removeAppButton"
>
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.applications.new">
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.applications.new" data-cy="k8sApp-addApplicationButton">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add application
</button>
</div>
@ -68,15 +86,16 @@
placeholder="Search..."
auto-focus
ng-model-options="{ debounce: 300 }"
data-cy="k8sApp-searchApplicationsInput"
/>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<table class="table table-hover nowrap-cells" data-cy="k8sApp-appTable">
<thead>
<tr>
<th>
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" data-cy="k8sApp-selectAllCheckbox" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.changeOrderBy('Name')">

View file

@ -1,13 +1,13 @@
import { KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
angular.module('portainer.docker').controller('KubernetesApplicationsDatatableController', [
'$scope',
'$controller',
'KubernetesNamespaceHelper',
'DatatableService',
'Authentication',
function ($scope, $controller, KubernetesNamespaceHelper, DatatableService, Authentication) {
function ($scope, $controller, DatatableService, Authentication) {
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
var ctrl = this;

View file

@ -1,15 +1,15 @@
import _ from 'lodash-es';
import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
angular.module('portainer.docker').controller('KubernetesApplicationsPortsDatatableController', [
'$scope',
'$controller',
'KubernetesNamespaceHelper',
'DatatableService',
'Authentication',
function ($scope, $controller, KubernetesNamespaceHelper, DatatableService, Authentication) {
function ($scope, $controller, DatatableService, Authentication) {
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
this.state = Object.assign(this.state, {
expandedItems: [],

View file

@ -134,7 +134,7 @@
</td>
<td>
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: item.ResourcePool })" ng-click="$event.stopPropagation();">{{ item.ResourcePool }}</a>
<span class="label label-info image-tag label-margins" ng-if="$ctrl.isSystemNamespace(item)">system</span>
<span class="label label-info image-tag label-margins" ng-if="$ctrl.isSystemNamespace(item.ResourcePool)">system</span>
</td>
<td>{{ item.Applications.length }}</td>
<td>

View file

@ -1,14 +1,14 @@
import _ from 'lodash-es';
import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
angular.module('portainer.docker').controller('KubernetesApplicationsStacksDatatableController', [
angular.module('portainer.kubernetes').controller('KubernetesApplicationsStacksDatatableController', [
'$scope',
'$controller',
'KubernetesNamespaceHelper',
'DatatableService',
'Authentication',
function ($scope, $controller, KubernetesNamespaceHelper, DatatableService, Authentication) {
function ($scope, $controller, DatatableService, Authentication) {
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
this.state = Object.assign(this.state, {
expandedItems: [],
@ -33,15 +33,19 @@ angular.module('portainer.docker').controller('KubernetesApplicationsStacksDatat
* Do not allow applications in system namespaces to be selected
*/
this.allowSelection = function (item) {
return !this.isSystemNamespace(item);
return !this.isSystemNamespace(item.ResourcePool);
};
this.isSystemNamespace = function (item) {
return KubernetesNamespaceHelper.isSystemNamespace(item.ResourcePool);
/**
* @param {String} namespace Namespace (string name)
* @returns Boolean
*/
this.isSystemNamespace = function (namespace) {
return KubernetesNamespaceHelper.isSystemNamespace(namespace);
};
this.isDisplayed = function (item) {
return !ctrl.isSystemNamespace(item) || ctrl.settings.showSystem;
return !ctrl.isSystemNamespace(item.ResourcePool) || ctrl.settings.showSystem;
};
this.expandItem = function (item, expanded) {

View file

@ -9,7 +9,7 @@
</span>
<div class="settings">
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Table settings</span>
<span uib-dropdown-toggle data-cy="k8sConfig-configSettingsButton"><i class="fa fa-cog" aria-hidden="true"></i> Table settings</span>
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuHeader">
@ -18,7 +18,13 @@
<div class="menuContent">
<div>
<div class="md-checkbox" ng-if="$ctrl.isAdmin">
<input id="setting_show_system" type="checkbox" ng-model="$ctrl.settings.showSystem" ng-change="$ctrl.onSettingsShowSystemChange()" />
<input
id="setting_show_system"
type="checkbox"
ng-model="$ctrl.settings.showSystem"
ng-change="$ctrl.onSettingsShowSystemChange()"
data-cy="k8sConfig-systemResourceCheckbox"
/>
<label for="setting_show_system">Show system resources</label>
</div>
<div class="md-checkbox">
@ -43,7 +49,7 @@
</div>
</div>
<div>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;" data-cy="k8sConfig-closeSettingsButton">Close</a>
</div>
</div>
</div>
@ -51,10 +57,16 @@
</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)">
<button
type="button"
class="btn btn-sm btn-danger"
ng-disabled="$ctrl.state.selectedItemCount === 0"
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
data-cy="k8sConfig-removeConfigButton"
>
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.configurations.new">
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.configurations.new" data-cy="k8sConfig-addConfigButton">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add configuration
</button>
</div>
@ -68,10 +80,11 @@
placeholder="Search..."
auto-focus
ng-model-options="{ debounce: 300 }"
data-cy="k8sConfig-searchInput"
/>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<table class="table table-hover nowrap-cells" data-cy="k8sConfig-tableSettingsButtonconfigsTable">
<thead>
<tr>
<th>
@ -145,7 +158,7 @@
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()" data-cy="k8sConfig-paginationDropdown">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View file

@ -1,12 +1,12 @@
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
angular.module('portainer.docker').controller('KubernetesConfigurationsDatatableController', [
'$scope',
'$controller',
'KubernetesNamespaceHelper',
'DatatableService',
'Authentication',
function ($scope, $controller, KubernetesNamespaceHelper, DatatableService, Authentication) {
function ($scope, $controller, DatatableService, Authentication) {
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
const ctrl = this;

View file

@ -52,10 +52,11 @@
placeholder="Search..."
auto-focus
ng-model-options="{ debounce: 300 }"
data-cy="k8sConfigDetail-eventsTableSearchInput"
/>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<table class="table table-hover nowrap-cells" data-cy="k8sConfigDetail-eventsTable">
<thead>
<tr>
<th>
@ -117,7 +118,12 @@
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<select
class="form-control"
ng-model="$ctrl.state.paginatedItemLimit"
ng-change="$ctrl.changePaginationLimit()"
data-cy="k8sConfigDetail-eventsTablePaginationDropdown"
>
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View file

@ -1,12 +1,12 @@
import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
angular.module('portainer.docker').controller('KubernetesNodeApplicationsDatatableController', [
'$scope',
'$controller',
'KubernetesNamespaceHelper',
'DatatableService',
function ($scope, $controller, KubernetesNamespaceHelper, DatatableService) {
function ($scope, $controller, DatatableService) {
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
this.isSystemNamespace = function (item) {

View file

@ -51,10 +51,16 @@
</div>
</div>
<div ng-if="$ctrl.isAdmin" class="actionBar">
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<button
type="button"
class="btn btn-sm btn-danger"
ng-disabled="$ctrl.state.selectedItemCount === 0"
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
data-cy="k8sNamespace-removeNamespaceButton"
>
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.resourcePools.new">
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.resourcePools.new" data-cy="k8sNamespace-addNamespaceButton">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add namespace
</button>
</div>
@ -68,6 +74,7 @@
placeholder="Search..."
auto-focus
ng-model-options="{ debounce: 300 }"
data-cy="k8sNamespace-namespaceSearchInput"
/>
</div>
<div class="table-responsive">

View file

@ -1,10 +1,11 @@
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
angular.module('portainer.docker').controller('KubernetesResourcePoolsDatatableController', [
'$scope',
'$controller',
'Authentication',
'KubernetesNamespaceHelper',
'DatatableService',
function ($scope, $controller, Authentication, KubernetesNamespaceHelper, DatatableService) {
function ($scope, $controller, Authentication, DatatableService) {
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
var ctrl = this;
@ -19,14 +20,14 @@ angular.module('portainer.docker').controller('KubernetesResourcePoolsDatatableC
this.canManageAccess = function (item) {
if (!this.endpoint.Kubernetes.Configuration.RestrictDefaultNamespace) {
return item.Namespace.Name !== 'default' && !this.isSystemNamespace(item);
return !KubernetesNamespaceHelper.isDefaultNamespace(item.Namespace.Name) && !this.isSystemNamespace(item);
} else {
return !this.isSystemNamespace(item);
}
};
this.disableRemove = function (item) {
return KubernetesNamespaceHelper.isSystemNamespace(item.Namespace.Name) || item.Namespace.Name === 'default';
return this.isSystemNamespace(item) || KubernetesNamespaceHelper.isDefaultNamespace(item.Namespace.Name);
};
this.isSystemNamespace = function (item) {

View file

@ -1,14 +1,14 @@
import angular from 'angular';
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
// TODO: review - refactor to use `extends GenericDatatableController`
class KubernetesVolumesDatatableController {
/* @ngInject */
constructor($async, $controller, Authentication, KubernetesNamespaceHelper, DatatableService) {
constructor($async, $controller, Authentication, DatatableService) {
this.$async = $async;
this.$controller = $controller;
this.Authentication = Authentication;
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
this.DatatableService = DatatableService;
this.onInit = this.onInit.bind(this);
@ -29,7 +29,7 @@ class KubernetesVolumesDatatableController {
}
isSystemNamespace(item) {
return this.KubernetesNamespaceHelper.isSystemNamespace(item.ResourcePool.Namespace.Name);
return KubernetesNamespaceHelper.isSystemNamespace(item.ResourcePool.Namespace.Name);
}
isDisplayed(item) {

View file

@ -1,4 +1,4 @@
<button type="button" class="btn btn-xs btn-primary" ng-click="$ctrl.connectConsole()" ng-disabled="$ctrl.state.shell.connected">
<button type="button" class="btn btn-xs btn-primary" ng-click="$ctrl.connectConsole()" ng-disabled="$ctrl.state.shell.connected" data-cy="k8sSidebar-shellButton">
<i class="fa fa-terminal" style="margin-right: 2px;"></i>
kubectl shell
</button>
@ -10,9 +10,13 @@
<a href="" ng-click="$ctrl.downloadKubeconfig()"><i class="fas fa-file-download" style="margin-right: 5px;"></i>Download Kubeconfig</a>
</div>
<div class="shell-item-right">
<i class="fas fa-redo-alt" ng-click="$ctrl.screenClear();"></i>
<i class="fas {{ $ctrl.state.icon }}" ng-click="$ctrl.miniRestore();"></i>
<i class="fas fa-times" ng-click="$ctrl.disconnect()"></i>
<i class="fas fa-redo-alt" ng-click="$ctrl.screenClear();" data-cy="k8sShell-refreshButton"></i>
<i
class="fas {{ $ctrl.state.icon }}"
ng-click="$ctrl.miniRestore();"
data-cy="{{ $ctrl.state.icon === '.fa-window-minimize' ? 'k8sShell-restore' : 'k8sShell-minimise' }}"
></i>
<i class="fas fa-times" ng-click="$ctrl.disconnect()" data-cy="k8sShell-closeButton"></i>
</div>
</div>
<div>

View file

@ -26,10 +26,10 @@
<div class="form-group" ng-if="$ctrl.formValues.IsSimple">
<div class="col-sm-12">
<button type="button" class="btn btn-sm btn-default" style="margin-left: 0;" ng-click="$ctrl.addEntry()">
<button type="button" class="btn btn-sm btn-default" style="margin-left: 0;" ng-click="$ctrl.addEntry()" data-cy="k8sConfigCreate-createEntryButton">
<i class="fa fa-plus-circle" aria-hidden="true"></i> Create entry
</button>
<button type="button" class="btn btn-sm btn-default" ngf-select="$ctrl.addEntryFromFile($file)" style="margin-left: 0;">
<button type="button" class="btn btn-sm btn-default" ngf-select="$ctrl.addEntryFromFile($file)" style="margin-left: 0;" data-cy="k8sConfigCreate-createConfigsFromFileButton">
<i class="fa fa-file-upload" aria-hidden="true"></i> Create key/value from file
</button>
</div>
@ -98,7 +98,14 @@
<div class="form-group" ng-if="$ctrl.formValues.IsSimple">
<div class="col-sm-1"></div>
<div class="col-sm-11">
<button type="button" class="btn btn-sm btn-danger space-right" style="margin-left: 0;" ng-disabled="entry.Used" ng-click="$ctrl.removeEntry(index, entry)">
<button
type="button"
class="btn btn-sm btn-danger space-right"
style="margin-left: 0;"
ng-disabled="entry.Used"
ng-click="$ctrl.removeEntry(index, entry)"
data-cy="k8sConfigDetail-removeEntryButton{{ index }}"
>
<i class="fa fa-trash-alt" aria-hidden="true"></i> Remove entry
</button>
<span class="small text-muted" ng-if="entry.Used">

View file

@ -5,5 +5,6 @@ angular.module('portainer.kubernetes').component('kubernetesSidebar', {
bindings: {
endpointId: '<',
isSidebarOpen: '<',
adminAccess: '<',
},
});

View file

@ -1,20 +1,44 @@
<sidebar-menu-item path="kubernetes.dashboard" path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-tachometer-alt fa-fw" class-name="sidebar-list">
<sidebar-menu-item
path="kubernetes.dashboard"
path-params="{ endpointId: $ctrl.endpointId }"
icon-class="fa-tachometer-alt fa-fw"
class-name="sidebar-list"
data-cy="k8sSidebar-dashboard"
>
Dashboard
</sidebar-menu-item>
<sidebar-menu-item path="kubernetes.resourcePools" path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-layer-group fa-fw" class-name="sidebar-list">
<sidebar-menu-item
path="kubernetes.resourcePools"
path-params="{ endpointId: $ctrl.endpointId }"
icon-class="fa-layer-group fa-fw"
class-name="sidebar-list"
data-cy="k8sSidebar-namespaces"
>
Namespaces
</sidebar-menu-item>
<sidebar-menu-item path="kubernetes.applications" path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-laptop-code fa-fw" class-name="sidebar-list">
<sidebar-menu-item
path="kubernetes.applications"
path-params="{ endpointId: $ctrl.endpointId }"
icon-class="fa-laptop-code fa-fw"
class-name="sidebar-list"
data-cy="k8sSidebar-applications"
>
Applications
</sidebar-menu-item>
<sidebar-menu-item path="kubernetes.configurations" path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-file-code fa-fw" class-name="sidebar-list">
<sidebar-menu-item
path="kubernetes.configurations"
path-params="{ endpointId: $ctrl.endpointId }"
icon-class="fa-file-code fa-fw"
class-name="sidebar-list"
data-cy="k8sSidebar-configurations"
>
Configurations
</sidebar-menu-item>
<sidebar-menu-item path="kubernetes.volumes" path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-database fa-fw" class-name="sidebar-list">
<sidebar-menu-item path="kubernetes.volumes" path-params="{ endpointId: $ctrl.endpointId }" icon-class="fa-database fa-fw" class-name="sidebar-list" data-cy="k8sSidebar-volumes">
Volumes
</sidebar-menu-item>
@ -27,11 +51,23 @@
children-paths="['kubernetes.cluster', 'portainer.endpoints.endpoint.kubernetesConfig', 'kubernetes.registries', 'kubernetes.registries.access']"
>
<div ng-if="$ctrl.adminAccess">
<sidebar-menu-item authorization="K8sClusterSetupRW" path="portainer.endpoints.endpoint.kubernetesConfig" path-params="{ id: $ctrl.endpointId }" class-name="sidebar-sublist">
<sidebar-menu-item
authorization="K8sClusterSetupRW"
path="portainer.endpoints.endpoint.kubernetesConfig"
path-params="{ id: $ctrl.endpointId }"
class-name="sidebar-sublist"
data-cy="k8sSidebar-setup"
>
Setup
</sidebar-menu-item>
<sidebar-menu-item authorization="PortainerRegistryList" path="kubernetes.registries" path-params="{ endpointId: $ctrl.endpointId }" class-name="sidebar-sublist">
<sidebar-menu-item
authorization="PortainerRegistryList"
path="kubernetes.registries"
path-params="{ endpointId: $ctrl.endpointId }"
class-name="sidebar-sublist"
data-cy="k8sSidebar-registries"
>
Registries
</sidebar-menu-item>
</div>

View file

@ -14,7 +14,12 @@
Memory reservation
</label>
<div class="col-sm-9" style="margin-top: 4px;">
<uib-progressbar animate="false" value="$ctrl.memoryReservationPercent" type="{{ $ctrl.memoryReservationPercent | kubernetesUsageLevelInfo }}">
<uib-progressbar
animate="false"
value="$ctrl.memoryReservationPercent"
type="{{ $ctrl.memoryReservationPercent | kubernetesUsageLevelInfo }}"
data-cy="k8sNamespaceDetail-memoryUsage"
>
<b style="white-space: nowrap;"> {{ $ctrl.memoryReservation }} / {{ $ctrl.memoryLimit }} MB - {{ $ctrl.memoryReservationPercent }}% </b>
</uib-progressbar>
</div>

View file

@ -1,7 +1,7 @@
<rd-header ng-if="$ctrl.viewReady">
<rd-header-title title-text="{{ $ctrl.title }}">
<a data-toggle="tooltip" title="refresh the view" ui-sref="{{ $ctrl.state }}" ui-sref-opts="{reload: true}" ng-if="$ctrl.viewReady">
<i class="fa fa-sm fa-sync" aria-hidden="true"></i>
<i class="fa fa-sm fa-sync" aria-hidden="true" data-cy="component-refreshTableButton"></i>
</a>
</rd-header-title>
<rd-header-content>

View file

@ -1,9 +1,14 @@
import _ from 'lodash-es';
import { KubernetesNamespace } from 'Kubernetes/models/namespace/models';
import { KubernetesNamespaceCreatePayload } from 'Kubernetes/models/namespace/payloads';
import { KubernetesPortainerResourcePoolNameLabel, KubernetesPortainerResourcePoolOwnerLabel } from 'Kubernetes/models/resource-pool/models';
import {
KubernetesPortainerResourcePoolNameLabel,
KubernetesPortainerResourcePoolOwnerLabel,
KubernetesPortainerNamespaceSystemLabel,
} from 'Kubernetes/models/resource-pool/models';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
class KubernetesNamespaceConverter {
export default class KubernetesNamespaceConverter {
static apiToNamespace(data, yaml) {
const res = new KubernetesNamespace();
res.Id = data.metadata.uid;
@ -13,6 +18,14 @@ class KubernetesNamespaceConverter {
res.Yaml = yaml ? yaml.data : '';
res.ResourcePoolName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerResourcePoolNameLabel] : '';
res.ResourcePoolOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] : '';
res.IsSystem = KubernetesNamespaceHelper.isDefaultSystemNamespace(data.metadata.name);
if (data.metadata.labels) {
const systemLabel = data.metadata.labels[KubernetesPortainerNamespaceSystemLabel];
if (!_.isEmpty(systemLabel)) {
res.IsSystem = systemLabel === 'true';
}
}
return res;
}
@ -20,6 +33,7 @@ class KubernetesNamespaceConverter {
const res = new KubernetesNamespaceCreatePayload();
res.metadata.name = namespace.Name;
res.metadata.labels[KubernetesPortainerResourcePoolNameLabel] = namespace.ResourcePoolName;
if (namespace.ResourcePoolOwner) {
const resourcePoolOwner = _.truncate(namespace.ResourcePoolOwner, { length: 63, omission: '' });
res.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] = resourcePoolOwner;
@ -27,5 +41,3 @@ class KubernetesNamespaceConverter {
return res;
}
}
export default KubernetesNamespaceConverter;

View file

@ -18,6 +18,7 @@ class KubernetesResourcePoolConverter {
namespace.Name = formValues.Name;
namespace.ResourcePoolName = formValues.Name;
namespace.ResourcePoolOwner = formValues.Owner;
namespace.IsSystem = formValues.IsSystem;
const quota = KubernetesResourceQuotaConverter.resourcePoolFormValuesToResourceQuota(formValues);

View file

@ -1,21 +1,33 @@
import _ from 'lodash-es';
import angular from 'angular';
class KubernetesNamespaceHelper {
/* @ngInject */
constructor(KUBERNETES_SYSTEM_NAMESPACES, KUBERNETES_DEFAULT_NAMESPACE) {
this.KUBERNETES_SYSTEM_NAMESPACES = KUBERNETES_SYSTEM_NAMESPACES;
this.KUBERNETES_DEFAULT_NAMESPACE = KUBERNETES_DEFAULT_NAMESPACE;
import { KUBERNETES_DEFAULT_NAMESPACE, KUBERNETES_DEFAULT_SYSTEM_NAMESPACES } from 'Kubernetes/models/namespace/models';
import { isSystem } from 'Kubernetes/store/namespace';
export default class KubernetesNamespaceHelper {
/**
* Check if namespace is system or not
* @param {String} namespace Namespace (string name) to evaluate
* @returns Boolean
*/
static isSystemNamespace(namespace) {
return isSystem(namespace);
}
isSystemNamespace(namespace) {
return _.includes(this.KUBERNETES_SYSTEM_NAMESPACES, namespace);
/**
* Check if namespace is default or not
* @param {String} namespace Namespace (string name) to evaluate
* @returns Boolean
*/
static isDefaultNamespace(namespace) {
return namespace === KUBERNETES_DEFAULT_NAMESPACE;
}
isDefaultNamespace(namespace) {
return namespace === this.KUBERNETES_DEFAULT_NAMESPACE;
/**
* Check if namespace is one of the default system namespaces
* @param {String} namespace Namespace (string name) to evaluate
* @returns Boolean
*/
static isDefaultSystemNamespace(namespace) {
return _.includes(KUBERNETES_DEFAULT_SYSTEM_NAMESPACES, namespace);
}
}
export default KubernetesNamespaceHelper;
angular.module('portainer.app').service('KubernetesNamespaceHelper', KubernetesNamespaceHelper);

View file

@ -1,11 +1,15 @@
export function KubernetesNamespace() {
return {
Id: '',
Name: '',
CreationDate: '',
Status: '',
Yaml: '',
ResourcePoolName: '',
ResourcePoolOwner: '',
};
export class KubernetesNamespace {
constructor() {
this.Id = '';
this.Name = '';
this.CreationDate = '';
this.Status = '';
this.Yaml = '';
this.ResourcePoolName = '';
this.ResourcePoolOwner = '';
this.IsSystem = false;
}
}
export const KUBERNETES_DEFAULT_SYSTEM_NAMESPACES = ['kube-system', 'kube-public', 'kube-node-lease', 'portainer'];
export const KUBERNETES_DEFAULT_NAMESPACE = 'default';

View file

@ -1,13 +1,12 @@
export function KubernetesResourcePoolFormValues(defaults) {
return {
Name: '',
MemoryLimit: defaults.MemoryLimit,
CpuLimit: defaults.CpuLimit,
HasQuota: false,
IngressClasses: [], // KubernetesResourcePoolIngressClassFormValue
Registries: [], // RegistryViewModel
EndpointId: 0,
};
this.Name = '';
this.MemoryLimit = defaults.MemoryLimit;
this.CpuLimit = defaults.CpuLimit;
this.HasQuota = false;
this.IngressClasses = []; // KubernetesResourcePoolIngressClassFormValue
this.Registries = []; // RegistryViewModel
this.EndpointId = 0;
this.IsSystem = false;
}
/**

View file

@ -2,6 +2,8 @@ export const KubernetesPortainerResourcePoolNameLabel = 'io.portainer.kubernetes
export const KubernetesPortainerResourcePoolOwnerLabel = 'io.portainer.kubernetes.resourcepool.owner';
export const KubernetesPortainerNamespaceSystemLabel = 'io.portainer.kubernetes.namespace.system';
/**
* KubernetesResourcePool Model
*/

View file

@ -1,11 +1,12 @@
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
export default class KubernetesRegistryAccessController {
/* @ngInject */
constructor($async, $state, EndpointService, Notifications, KubernetesResourcePoolService, KubernetesNamespaceHelper) {
constructor($async, $state, EndpointService, Notifications, KubernetesResourcePoolService) {
this.$async = $async;
this.$state = $state;
this.Notifications = Notifications;
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
this.EndpointService = EndpointService;
this.state = {
@ -60,7 +61,7 @@ export default class KubernetesRegistryAccessController {
const resourcePools = await this.KubernetesResourcePoolService.get();
this.resourcePools = resourcePools
.filter((pool) => !this.KubernetesNamespaceHelper.isSystemNamespace(pool.Namespace.Name) && !this.savedResourcePools.find(({ value }) => value === pool.Namespace.Name))
.filter((pool) => !KubernetesNamespaceHelper.isSystemNamespace(pool.Namespace.Name) && !this.savedResourcePools.find(({ value }) => value === pool.Namespace.Name))
.map((pool) => ({ name: pool.Namespace.Name, id: pool.Namespace.Id }));
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve namespaces');

View file

@ -0,0 +1,12 @@
angular.module('portainer.kubernetes').factory('KubernetesPortainerNamespaces', KubernetesPortainerNamespacesFactory);
function KubernetesPortainerNamespacesFactory($resource) {
const url = '/api/kubernetes/:endpointId/namespaces/:namespaceName/:action';
return $resource(
url,
{},
{
toggleSystem: { method: 'PUT', params: { action: 'system' } },
}
);
}

View file

@ -4,6 +4,7 @@ import angular from 'angular';
import PortainerError from 'Portainer/error';
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
import KubernetesNamespaceConverter from 'Kubernetes/converters/namespace';
import { updateNamespaces } from 'Kubernetes/store/namespace';
import $allSettled from 'Portainer/services/allSettled';
class KubernetesNamespaceService {
@ -27,7 +28,9 @@ class KubernetesNamespaceService {
params.id = name;
await this.KubernetesNamespaces().status(params).$promise;
const [raw, yaml] = await Promise.all([this.KubernetesNamespaces().get(params).$promise, this.KubernetesNamespaces().getYaml(params).$promise]);
return KubernetesNamespaceConverter.apiToNamespace(raw, yaml);
const ns = KubernetesNamespaceConverter.apiToNamespace(raw, yaml);
updateNamespaces([ns]);
return ns;
} catch (err) {
throw new PortainerError('Unable to retrieve namespace', err);
}
@ -43,7 +46,9 @@ class KubernetesNamespaceService {
return KubernetesNamespaceConverter.apiToNamespace(item);
}
});
return _.without(visibleNamespaces, undefined);
const res = _.without(visibleNamespaces, undefined);
updateNamespaces(res);
return res;
} catch (err) {
throw new PortainerError('Unable to retrieve namespaces', err);
}

View file

@ -5,12 +5,20 @@ import KubernetesResourcePoolConverter from 'Kubernetes/converters/resourcePool'
import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper';
/* @ngInject */
export function KubernetesResourcePoolService($async, EndpointService, KubernetesNamespaceService, KubernetesResourceQuotaService, KubernetesIngressService) {
export function KubernetesResourcePoolService(
$async,
EndpointService,
KubernetesNamespaceService,
KubernetesResourceQuotaService,
KubernetesIngressService,
KubernetesPortainerNamespaces
) {
return {
get,
create,
patch,
delete: _delete,
toggleSystem,
};
async function getOne(name) {
@ -67,9 +75,8 @@ export function KubernetesResourcePoolService($async, EndpointService, Kubernete
function patch(oldFormValues, newFormValues) {
return $async(async () => {
const [oldNamespace, oldQuota, oldIngresses, oldRegistries] = KubernetesResourcePoolConverter.formValuesToResourcePool(oldFormValues);
const [newNamespace, newQuota, newIngresses, newRegistries] = KubernetesResourcePoolConverter.formValuesToResourcePool(newFormValues);
void oldNamespace, newNamespace;
const [, oldQuota, oldIngresses, oldRegistries] = KubernetesResourcePoolConverter.formValuesToResourcePool(oldFormValues);
const [, newQuota, newIngresses, newRegistries] = KubernetesResourcePoolConverter.formValuesToResourcePool(newFormValues);
if (oldQuota && newQuota) {
await KubernetesResourceQuotaService.patch(oldQuota, newQuota);
@ -114,6 +121,10 @@ export function KubernetesResourcePoolService($async, EndpointService, Kubernete
await KubernetesNamespaceService.delete(pool.Namespace);
});
}
function toggleSystem(endpointId, namespaceName, system) {
return KubernetesPortainerNamespaces.toggleSystem({ namespaceName, endpointId }, { system }).$promise;
}
}
angular.module('portainer.kubernetes').service('KubernetesResourcePoolService', KubernetesResourcePoolService);

View file

@ -0,0 +1,21 @@
// singleton pattern as:
// * we don't want to use AngularJS DI to fetch the single instance
// * we need to use the Store in static functions / non-instanciated classes
const storeNamespaces = {};
/**
* Check if a namespace of the store is system or not
* @param {String} name Namespace name
* @returns Boolean
*/
export function isSystem(name) {
return storeNamespaces[name] && storeNamespaces[name].IsSystem;
}
/**
* Called from KubernetesNamespaceService.get()
* @param {KubernetesNamespace[]} namespaces list of namespaces to update in Store
*/
export function updateNamespaces(namespaces) {
namespaces.forEach((ns) => (storeNamespaces[ns.Name] = ns));
}

View file

@ -5,7 +5,10 @@
Advanced deployment allows you to deploy any Kubernetes manifest inside your cluster.
</p>
<p>
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.deploy"> <i class="fa fa-file-code space-right" aria-hidden="true"></i>Advanced deployment </button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.deploy" data-cy="k8sApp-advancedDeployButton">
<i class="fa fa-file-code space-right" aria-hidden="true"></i>
Advanced deployment
</button>
</p>
</span>
</information-panel>

View file

@ -8,7 +8,7 @@
<div ng-include="'app/kubernetes/templates/advancedDeploymentPanel.html'"></div>
<div class="row">
<div class="col-sm-12">
<div class="col-sm-12" data-cy="k8sApp-appList">
<rd-widget>
<rd-widget-body classes="no-padding">
<uib-tabset active="ctrl.state.activeTab" justified="true" type="pills">

View file

@ -1602,6 +1602,7 @@
ng-disabled="!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled() || !ctrl.state.pullImageValidity"
ng-click="ctrl.deployApplication()"
button-spinner="ctrl.state.actionInProgress"
data-cy="k8sAppCreate-deployButton"
>
<span ng-show="!ctrl.state.isEdit && !ctrl.state.actionInProgress">Deploy application</span>
<span ng-show="!ctrl.state.isEdit && ctrl.state.actionInProgress">Deployment in progress...</span>

View file

@ -29,6 +29,7 @@ import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceRese
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application/index';
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
import { KubernetesNodeHelper } from 'Kubernetes/node/helper';
class KubernetesCreateApplicationController {
@ -48,7 +49,6 @@ class KubernetesCreateApplicationController {
KubernetesNodeService,
KubernetesIngressService,
KubernetesPersistentVolumeClaimService,
KubernetesNamespaceHelper,
KubernetesVolumeService,
RegistryService,
StackService
@ -66,7 +66,6 @@ class KubernetesCreateApplicationController {
this.KubernetesVolumeService = KubernetesVolumeService;
this.KubernetesIngressService = KubernetesIngressService;
this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService;
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
this.RegistryService = RegistryService;
this.StackService = StackService;
@ -996,7 +995,7 @@ class KubernetesCreateApplicationController {
]);
this.ingresses = ingresses;
this.resourcePools = _.filter(resourcePools, (resourcePool) => !this.KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
this.resourcePools = _.filter(resourcePools, (resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
this.formValues.ResourcePool = this.resourcePools[0];
if (!this.formValues.ResourcePool) {
return;

View file

@ -19,7 +19,7 @@
<tbody>
<tr>
<td>Name</td>
<td>
<td data-cy="k8sAppDetail-appName">
{{ ctrl.application.Name }}
<span class="label label-primary image-tag label-margins" ng-if="!ctrl.isSystemNamespace() && ctrl.isExternalApplication()">external</span>
</td>
@ -30,9 +30,9 @@
</tr>
<tr>
<td>Namespace</td>
<td>
<td data-cy="k8sAppDetail-resourcePoolName">
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.application.ResourcePool })">{{ ctrl.application.ResourcePool }}</a>
<span style="margin-left: 5px;" class="label label-info image-tag" ng-if="ctrl.isSystemNamespace(item)">system</span>
<span style="margin-left: 5px;" class="label label-info image-tag" ng-if="ctrl.isSystemNamespace()">system</span>
</td>
</tr>
<tr>
@ -46,7 +46,8 @@
<td ng-if="ctrl.application.ApplicationType !== ctrl.KubernetesApplicationTypes.POD">
<span ng-if="ctrl.application.DeploymentType === ctrl.KubernetesApplicationDeploymentTypes.REPLICATED">Replicated</span>
<span ng-if="ctrl.application.DeploymentType === ctrl.KubernetesApplicationDeploymentTypes.GLOBAL">Global</span>
<code>{{ ctrl.application.RunningPodsCount }}</code> / <code>{{ ctrl.application.TotalPodsCount }}</code>
<code data-cy="k8sAppDetail-runningPods">{{ ctrl.application.RunningPodsCount }}</code> /
<code data-cy="k8sAppDetail-totalPods">{{ ctrl.application.TotalPodsCount }}</code>
</td>
<td ng-if="ctrl.application.ApplicationType === ctrl.KubernetesApplicationTypes.POD">
{{ ctrl.application.Pods[0].Status }}
@ -318,9 +319,9 @@
<td style="width: 50%;">HTTP route</td>
</tr>
<tr ng-repeat-start="port in ctrl.application.PublishedPorts">
<td ng-if="!ctrl.portHasIngressRules(port)">{{ port.TargetPort }}/{{ port.Protocol }}</td>
<td ng-if="!ctrl.portHasIngressRules(port)" data-cy="k8sAppDetail-containerPort">{{ port.TargetPort }}/{{ port.Protocol }}</td>
<td ng-if="!ctrl.portHasIngressRules(port)">
<span ng-if="ctrl.application.ServiceType === ctrl.KubernetesServiceTypes.NODE_PORT">
<span ng-if="ctrl.application.ServiceType === ctrl.KubernetesServiceTypes.NODE_PORT" data-cy="k8sAppDetail-nodePort">
{{ port.NodePort }}
</span>
<span ng-if="ctrl.application.ServiceType !== ctrl.KubernetesServiceTypes.NODE_PORT">

View file

@ -12,6 +12,7 @@ import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
import { KubernetesPodNodeAffinityNodeSelectorRequirementOperators } from 'Kubernetes/pod/models';
import { KubernetesPodContainerTypes } from 'Kubernetes/pod/models/index';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
function computeTolerations(nodes, application) {
const pod = application.Pods[0];
@ -111,7 +112,6 @@ class KubernetesApplicationController {
KubernetesStackService,
KubernetesPodService,
KubernetesNodeService,
KubernetesNamespaceHelper,
EndpointProvider
) {
this.$async = $async;
@ -127,8 +127,6 @@ class KubernetesApplicationController {
this.KubernetesPodService = KubernetesPodService;
this.KubernetesNodeService = KubernetesNodeService;
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
this.KubernetesApplicationTypes = KubernetesApplicationTypes;
this.EndpointProvider = EndpointProvider;
@ -160,7 +158,7 @@ class KubernetesApplicationController {
}
isSystemNamespace() {
return this.KubernetesNamespaceHelper.isSystemNamespace(this.application.ResourcePool);
return KubernetesNamespaceHelper.isSystemNamespace(this.application.ResourcePool);
}
isExternalApplication() {

View file

@ -24,6 +24,7 @@
placeholder="my-configuration"
auto-focus
required
data-cy="k8sConfigCreate-nameInput"
/>
</div>
</div>
@ -56,6 +57,7 @@
id="resource-pool-selector"
ng-model="ctrl.formValues.ResourcePool"
ng-options="resourcePool.Namespace.Name for resourcePool in ctrl.resourcePools"
data-cy="k8sConfigCreate-namespaceDropdown"
></select>
</div>
</div>
@ -83,7 +85,7 @@
<div class="boxselector_wrapper">
<div>
<input type="radio" id="type_basic" ng-value="ctrl.KubernetesConfigurationTypes.CONFIGMAP" ng-model="ctrl.formValues.Type" />
<label for="type_basic">
<label for="type_basic" data-cy="k8sConfigCreate-nonSensitiveButton">
<div class="boxselector_header">
<i class="fa fa-file-code" aria-hidden="true" style="margin-right: 2px;"></i>
Non-sensitive
@ -93,7 +95,7 @@
</div>
<div>
<input type="radio" id="type_secret" ng-value="ctrl.KubernetesConfigurationTypes.SECRET" ng-model="ctrl.formValues.Type" />
<label for="type_secret">
<label for="type_secret" data-cy="k8sConfigCreate-sensitiveButton">
<div class="boxselector_header">
<i class="fa fa-user-secret" aria-hidden="true" style="margin-right: 2px;"></i>
Sensitive
@ -141,6 +143,7 @@
ng-disabled="!kubernetesConfigurationCreationForm.$valid || !ctrl.isFormValid() || ctrl.state.actionInProgress"
ng-click="ctrl.createConfiguration()"
button-spinner="ctrl.state.actionInProgress"
data-cy="k8sConfigCreate-CreateConfigButton"
>
<span ng-hide="ctrl.state.actionInProgress">Create configuration</span>
<span ng-show="ctrl.state.actionInProgress">Creation in progress...</span>

View file

@ -3,10 +3,11 @@ import _ from 'lodash-es';
import { KubernetesConfigurationFormValues, KubernetesConfigurationFormValuesEntry } from 'Kubernetes/models/configuration/formvalues';
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
class KubernetesCreateConfigurationController {
/* @ngInject */
constructor($async, $state, $window, ModalService, Notifications, Authentication, KubernetesConfigurationService, KubernetesResourcePoolService, KubernetesNamespaceHelper) {
constructor($async, $state, $window, ModalService, Notifications, Authentication, KubernetesConfigurationService, KubernetesResourcePoolService) {
this.$async = $async;
this.$state = $state;
this.$window = $window;
@ -16,7 +17,6 @@ class KubernetesCreateConfigurationController {
this.KubernetesConfigurationService = KubernetesConfigurationService;
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
this.KubernetesConfigurationTypes = KubernetesConfigurationTypes;
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
this.onInit = this.onInit.bind(this);
this.createConfigurationAsync = this.createConfigurationAsync.bind(this);
@ -94,7 +94,7 @@ class KubernetesCreateConfigurationController {
try {
const resourcePools = await this.KubernetesResourcePoolService.get();
this.resourcePools = _.filter(resourcePools, (resourcePool) => !this.KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
this.resourcePools = _.filter(resourcePools, (resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
this.formValues.ResourcePool = this.resourcePools[0];
await this.getConfigurations();

View file

@ -12,10 +12,10 @@
<rd-widget>
<rd-widget-body classes="no-padding">
<uib-tabset active="ctrl.state.activeTab" justified="true" type="pills">
<uib-tab index="0" classes="btn-sm" select="ctrl.selectTab(0)">
<uib-tab index="0" classes="btn-sm" select="ctrl.selectTab(0)" data-cy="k8sConfigDetail-configTab">
<uib-tab-heading> <i class="fa fa-file-code space-right" aria-hidden="true"></i> Configuration </uib-tab-heading>
<div style="padding: 20px;">
<table class="table">
<table class="table" data-cy="k8sConfigDetail-configTable">
<tbody>
<tr>
<td>Name</td>
@ -41,7 +41,7 @@
</table>
</div>
</uib-tab>
<uib-tab index="1" classes="btn-sm" select="ctrl.selectTab(1)">
<uib-tab index="1" classes="btn-sm" select="ctrl.selectTab(1)" data-cy="k8sConfigDetail-eventsTab">
<uib-tab-heading>
<i class="fa fa-history space-right" aria-hidden="true"></i> Events
<div ng-if="ctrl.hasEventWarnings()">
@ -61,7 +61,7 @@
>
</kubernetes-events-datatable>
</uib-tab>
<uib-tab index="2" ng-if="ctrl.configuration.Yaml" classes="btn-sm" select="ctrl.showEditor()">
<uib-tab index="2" ng-if="ctrl.configuration.Yaml" classes="btn-sm" select="ctrl.showEditor()" data-cy="k8sConfigDetail-yamlTab">
<uib-tab-heading> <i class="fa fa-code space-right" aria-hidden="true"></i> YAML </uib-tab-heading>
<div style="padding-right: 25px;" ng-if="ctrl.state.showEditorTab">
<kubernetes-yaml-inspector key="configuration-yaml" data="ctrl.configuration.Yaml"> </kubernetes-yaml-inspector>
@ -104,6 +104,7 @@
ng-disabled="!ctrl.isFormValid() || !kubernetesConfigurationCreationForm.$valid || ctrl.state.actionInProgress"
ng-click="ctrl.updateConfiguration()"
button-spinner="ctrl.state.actionInProgress"
data-cy="k8sConfigDetail-updateConfig"
>
<span ng-hide="ctrl.state.actionInProgress">Update configuration</span>
<span ng-show="ctrl.state.actionInProgress">Update in progress...</span>

View file

@ -1,10 +1,12 @@
import angular from 'angular';
import _ from 'lodash-es';
import { KubernetesConfigurationFormValues } from 'Kubernetes/models/configuration/formvalues';
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
import KubernetesConfigurationConverter from 'Kubernetes/converters/configuration';
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
import _ from 'lodash-es';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
class KubernetesConfigurationController {
/* @ngInject */
@ -21,8 +23,7 @@ class KubernetesConfigurationController {
KubernetesResourcePoolService,
ModalService,
KubernetesApplicationService,
KubernetesEventService,
KubernetesNamespaceHelper
KubernetesEventService
) {
this.$async = $async;
this.$state = $state;
@ -36,7 +37,6 @@ class KubernetesConfigurationController {
this.KubernetesApplicationService = KubernetesApplicationService;
this.KubernetesEventService = KubernetesEventService;
this.KubernetesConfigurationTypes = KubernetesConfigurationTypes;
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
this.KubernetesConfigMapService = KubernetesConfigMapService;
this.KubernetesSecretService = KubernetesSecretService;
@ -52,7 +52,7 @@ class KubernetesConfigurationController {
}
isSystemNamespace() {
return this.KubernetesNamespaceHelper.isSystemNamespace(this.configuration.Namespace);
return KubernetesNamespaceHelper.isSystemNamespace(this.configuration.Namespace);
}
isSystemConfig() {

View file

@ -5,6 +5,7 @@ import { KubernetesFormValidationReferences } from 'Kubernetes/models/applicatio
import { KubernetesIngressClass } from 'Kubernetes/ingress/models';
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
class KubernetesConfigureController {
/* #region CONSTRUCTOR */
@ -18,7 +19,6 @@ class KubernetesConfigureController {
EndpointService,
EndpointProvider,
ModalService,
KubernetesNamespaceHelper,
KubernetesResourcePoolService,
KubernetesIngressService,
KubernetesMetricsService
@ -30,7 +30,6 @@ class KubernetesConfigureController {
this.EndpointService = EndpointService;
this.EndpointProvider = EndpointProvider;
this.ModalService = ModalService;
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
this.KubernetesIngressService = KubernetesIngressService;
this.KubernetesMetricsService = KubernetesMetricsService;
@ -147,8 +146,7 @@ class KubernetesConfigureController {
const allResourcePools = await this.KubernetesResourcePoolService.get();
const resourcePools = _.filter(
allResourcePools,
(resourcePool) =>
!this.KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) && !this.KubernetesNamespaceHelper.isDefaultNamespace(resourcePool.Namespace.Name)
(resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name) && !KubernetesNamespaceHelper.isDefaultNamespace(resourcePool.Namespace.Name)
);
ingressesToDel.forEach((ingress) => {

View file

@ -34,7 +34,7 @@
</div>
<div class="row">
<div class="col-xs-12 col-md-6" ng-if="ctrl.pools">
<div class="col-xs-12 col-md-6" ng-if="ctrl.pools" data-cy="k8sDashboard-namespaces">
<a ui-sref="kubernetes.resourcePools">
<rd-widget>
<rd-widget-body>
@ -47,7 +47,7 @@
</rd-widget>
</a>
</div>
<div class="col-xs-12 col-md-6" ng-if="ctrl.applications">
<div class="col-xs-12 col-md-6" ng-if="ctrl.applications" data-cy="k8sDashboard-applications">
<a ui-sref="kubernetes.applications">
<rd-widget>
<rd-widget-body>
@ -60,7 +60,7 @@
</rd-widget>
</a>
</div>
<div class="col-xs-12 col-md-6" ng-if="ctrl.configurations">
<div class="col-xs-12 col-md-6" ng-if="ctrl.configurations" data-cy="k8sDashboard-configurations">
<a ui-sref="kubernetes.configurations">
<rd-widget>
<rd-widget-body>
@ -73,7 +73,7 @@
</rd-widget>
</a>
</div>
<div class="col-xs-12 col-md-6" ng-if="ctrl.volumes">
<div class="col-xs-12 col-md-6" ng-if="ctrl.volumes" data-cy="k8sDashboard-volumes">
<a ui-sref="kubernetes.volumes">
<rd-widget>
<rd-widget-body>

View file

@ -1,6 +1,7 @@
import angular from 'angular';
import _ from 'lodash-es';
import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
class KubernetesDashboardController {
/* @ngInject */
@ -13,7 +14,6 @@ class KubernetesDashboardController {
KubernetesApplicationService,
KubernetesConfigurationService,
KubernetesVolumeService,
KubernetesNamespaceHelper,
Authentication,
TagService
) {
@ -25,7 +25,6 @@ class KubernetesDashboardController {
this.KubernetesApplicationService = KubernetesApplicationService;
this.KubernetesConfigurationService = KubernetesConfigurationService;
this.KubernetesVolumeService = KubernetesVolumeService;
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
this.Authentication = Authentication;
this.TagService = TagService;
@ -65,13 +64,8 @@ class KubernetesDashboardController {
: '-';
if (!isAdmin) {
this.pools = _.filter(pools, (pool) => {
return !this.KubernetesNamespaceHelper.isSystemNamespace(pool.Namespace.Name);
});
this.configurations = _.filter(configurations, (config) => {
return !KubernetesConfigurationHelper.isSystemToken(config);
});
this.pools = _.filter(pools, (pool) => !KubernetesNamespaceHelper.isSystemNamespace(pool.Namespace.Name));
this.configurations = _.filter(configurations, (config) => !KubernetesConfigurationHelper.isSystemToken(config));
} else {
this.pools = pools;
this.configurations = configurations;

View file

@ -26,12 +26,12 @@
<div class="col-sm-12 form-section-title">
Deployment type
</div>
<box-selector radio-name="deploy" ng-model="ctrl.state.DeployType" options="ctrl.deployOptions"></box-selector>
<box-selector radio-name="deploy" ng-model="ctrl.state.DeployType" options="ctrl.deployOptions" data-cy="k8sAppDeploy-deploymentSelector"></box-selector>
<div class="col-sm-12 form-section-title">
Build method
</div>
<box-selector radio-name="method" ng-model="ctrl.state.BuildMethod" options="ctrl.methodOptions"></box-selector>
<box-selector radio-name="method" ng-model="ctrl.state.BuildMethod" options="ctrl.methodOptions" data-cy="k8sAppDeploy-buildSelector"></box-selector>
<!-- repository -->
<div ng-show="ctrl.state.BuildMethod === ctrl.BuildMethods.GIT">
@ -48,7 +48,14 @@
<div class="form-group">
<label for="stack_repository_path" class="col-sm-2 control-label text-left">Manifest path</label>
<div class="col-sm-10">
<input type="text" class="form-control" ng-model="ctrl.formValues.FilePathInRepository" id="stack_manifest_path" placeholder="deployment.yml" />
<input
type="text"
class="form-control"
ng-model="ctrl.formValues.FilePathInRepository"
id="stack_manifest_path"
placeholder="deployment.yml"
data-cy="k8sAppDeploy-gitManifestPath"
/>
</div>
</div>
<git-form-auth-fieldset model="ctrl.formValues" on-change="(ctrl.onChangeFormValues)"></git-form-auth-fieldset>
@ -100,7 +107,14 @@
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="ctrl.disableDeploy()" ng-click="ctrl.deploy()" button-spinner="ctrl.state.actionInProgress">
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="ctrl.disableDeploy()"
ng-click="ctrl.deploy()"
button-spinner="ctrl.state.actionInProgress"
data-cy="k8sAppDeploy-deployButton"
>
<span ng-hide="ctrl.state.actionInProgress">Deploy</span>
<span ng-show="ctrl.state.actionInProgress">Deployment in progress...</span>
</button>

View file

@ -22,6 +22,7 @@
ng-pattern="/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/"
ng-change="$ctrl.onChangeName()"
placeholder="my-project"
data-cy="k8sNamespaceCreate-namespaceNameInput"
required
auto-focus
/>
@ -58,7 +59,9 @@
<label class="control-label text-left">
Resource assignment
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="$ctrl.formValues.HasQuota" /><i></i> </label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="$ctrl.formValues.HasQuota" /><i data-cy="k8sNamespaceCreate-resourceAssignmentToggle"></i>
</label>
</div>
</div>
<div class="form-group" ng-if="$ctrl.formValues.HasQuota && !$ctrl.isQuotaValid()">
@ -84,6 +87,7 @@
ceil="$ctrl.state.sliderMaxMemory"
step="128"
ng-if="$ctrl.state.sliderMaxMemory"
data-cy="k8sNamespaceCreate-memoryLimitSlider"
>
</slider>
</div>
@ -96,6 +100,7 @@
class="form-control"
ng-model="$ctrl.formValues.MemoryLimit"
id="memory-limit"
data-cy="k8sNamespaceCreate-memoryLimitInput"
required
/>
</div>
@ -128,6 +133,7 @@
step="0.1"
precision="2"
ng-if="$ctrl.state.sliderMaxCpu"
data-cy="k8sNamespaceCreate-cpuLimitSlider"
>
</slider>
</div>
@ -159,7 +165,7 @@
<label class="control-label text-left">
Load Balancer quota
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" disabled /><i></i> </label>
<label class="switch" style="margin-left: 20px;" data-cy="k8sNamespaceCreate-loadBalancerQuotaToggle"> <input type="checkbox" disabled /><i></i> </label>
<span class="text-muted small" style="margin-left: 15px;">
<i class="fa fa-user" aria-hidden="true"></i>
This feature is available in <a href="https://www.portainer.io/business-upsell?from=k8s-resourcepool-lbquota" target="_blank"> Portainer Business Edition</a>.
@ -189,7 +195,7 @@
<label class="control-label text-left">
Enable quota
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" disabled /><i></i> </label>
<label class="switch" style="margin-left: 20px;" data-cy="k8sNamespaceCreate-enableQuotaToggle"> <input type="checkbox" disabled /><i></i> </label>
<span class="text-muted small" style="margin-left: 15px;">
<i class="fa fa-user" aria-hidden="true"></i>
This feature is available in
@ -409,7 +415,7 @@
ng-click="$ctrl.createResourcePool()"
button-spinner="$ctrl.state.actionInProgress"
>
<span ng-hide="$ctrl.state.actionInProgress">Create namespace</span>
<span ng-hide="$ctrl.state.actionInProgress" data-cy="k8sNamespace-createNamespaceButton">Create namespace</span>
<span ng-show="$ctrl.state.actionInProgress">Creation in progress...</span>
</button>
</div>

View file

@ -15,9 +15,18 @@
<form class="form-horizontal" autocomplete="off" name="resourcePoolEditForm" style="padding: 20px; margin-top: 10px;">
<!-- name-input -->
<div class="form-group">
<label for="pool_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
<input type="text" class="form-control" name="pool_name" ng-model="ctrl.pool.Namespace.Name" disabled />
<div class="col-sm-12">
<table class="table">
<tbody>
<tr>
<td>Name</td>
<td>
{{ ctrl.pool.Namespace.Name }}
<span class="label label-info image-tag label-margins" ng-if="ctrl.isSystem">system</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- !name-input -->
@ -396,21 +405,26 @@
<!-- !summary -->
<!-- actions -->
<div ng-if="ctrl.isAdmin && ctrl.isEditable" class="col-sm-12 form-section-title">
<div ng-if="ctrl.isAdmin && !ctrl.isDefaultNamespace" class="col-sm-12 form-section-title">
Actions
</div>
<div ng-if="ctrl.isAdmin && ctrl.isEditable" class="form-group">
<div ng-if="ctrl.isAdmin && !ctrl.isDefaultNamespace" class="form-group">
<div class="col-sm-12">
<button
type="button"
ng-if="ctrl.isEditable"
class="btn btn-primary btn-sm"
ng-disabled="!resourcePoolEditForm.$valid || ctrl.isUpdateButtonDisabled()"
ng-click="ctrl.updateResourcePool()"
button-spinner="ctrl.state.actionInProgress"
>
<span ng-hide="ctrl.state.actionInProgress">Update namespace</span>
<span ng-hide="ctrl.state.actionInProgress" data-cy="k8sNamespaceEdit-updateNamespaceButton">Update namespace</span>
<span ng-show="ctrl.state.actionInProgress">Update in progress...</span>
</button>
<button type="button" class="btn btn-primary btn-sm" ng-click="ctrl.markUnmarkAsSystem()" button-spinner="ctrl.state.actionInProgress">
<span ng-if="ctrl.isSystem">Unmark as system</span>
<span ng-if="!ctrl.isSystem">Mark as system</span>
</button>
</div>
</div>
<!-- !actions -->

View file

@ -15,6 +15,7 @@ import { KubernetesFormValidationReferences } from 'Kubernetes/models/applicatio
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
import KubernetesResourceQuotaConverter from 'Kubernetes/converters/resourceQuota';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
class KubernetesResourcePoolController {
/* #region CONSTRUCTOR */
@ -34,7 +35,6 @@ class KubernetesResourcePoolController {
KubernetesEventService,
KubernetesPodService,
KubernetesApplicationService,
KubernetesNamespaceHelper,
KubernetesIngressService,
KubernetesVolumeService
) {
@ -53,7 +53,6 @@ class KubernetesResourcePoolController {
KubernetesEventService,
KubernetesPodService,
KubernetesApplicationService,
KubernetesNamespaceHelper,
KubernetesIngressService,
KubernetesVolumeService,
});
@ -171,11 +170,11 @@ class KubernetesResourcePoolController {
}
/* #region UPDATE NAMESPACE */
async updateResourcePoolAsync() {
async updateResourcePoolAsync(oldFormValues, newFormValues) {
this.state.actionInProgress = true;
try {
this.checkDefaults();
await this.KubernetesResourcePoolService.patch(this.savedFormValues, this.formValues);
await this.KubernetesResourcePoolService.patch(oldFormValues, newFormValues);
this.Notifications.success('Namespace successfully updated', this.pool.Namespace.Name);
this.$state.reload();
} catch (err) {
@ -202,13 +201,45 @@ class KubernetesResourcePoolController {
${warnings.ingress ? messages.ingress : ''}<br/><br/>Do you wish to continue?`;
this.ModalService.confirmUpdate(displayedMessage, (confirmed) => {
if (confirmed) {
return this.$async(this.updateResourcePoolAsync);
return this.$async(this.updateResourcePoolAsync, this.savedFormValues, this.formValues);
}
});
} else {
return this.$async(this.updateResourcePoolAsync);
return this.$async(this.updateResourcePoolAsync, this.savedFormValues, this.formValues);
}
}
async confirmMarkUnmarkAsSystem() {
const message = this.isSystem
? 'Unmarking this namespace as system will allow non administrator users to manage it and the resources in contains depending on the access control settings. Are you sure?'
: 'Marking this namespace as a system namespace will prevent non administrator users from managing it and the resources it contains. Are you sure?';
return new Promise((resolve) => {
this.ModalService.confirmUpdate(message, resolve);
});
}
markUnmarkAsSystem() {
return this.$async(async () => {
try {
const namespaceName = this.$state.params.id;
this.state.actionInProgress = true;
const confirmed = await this.confirmMarkUnmarkAsSystem();
if (!confirmed) {
return;
}
await this.KubernetesResourcePoolService.toggleSystem(this.endpoint.Id, namespaceName, !this.isSystem);
this.Notifications.success('Namespace successfully updated', namespaceName);
this.$state.reload();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to create namespace');
} finally {
this.state.actionInProgress = false;
}
});
}
/* #endregion */
hasEventWarnings() {
@ -361,6 +392,7 @@ class KubernetesResourcePoolController {
this.formValues = new KubernetesResourcePoolFormValues(KubernetesResourceQuotaDefaults);
this.formValues.Name = this.pool.Namespace.Name;
this.formValues.EndpointId = this.endpoint.Id;
this.formValues.IsSystem = this.pool.Namespace.IsSystem;
_.forEach(nodes, (item) => {
this.state.sliderMaxMemory += filesizeParser(item.Memory);
@ -377,11 +409,9 @@ class KubernetesResourcePoolController {
this.state.resourceReservation.CPU = quota.CpuLimitUsed;
this.state.resourceReservation.Memory = KubernetesResourceReservationHelper.megaBytesValue(quota.MemoryLimitUsed);
}
this.isEditable = !this.KubernetesNamespaceHelper.isSystemNamespace(this.pool.Namespace.Name);
if (this.pool.Namespace.Name === 'default') {
this.isEditable = false;
}
this.isSystem = KubernetesNamespaceHelper.isSystemNamespace(this.pool.Namespace.Name);
this.isDefaultNamespace = KubernetesNamespaceHelper.isDefaultNamespace(this.pool.Namespace.Name);
this.isEditable = !this.isSystem && !this.isDefaultNamespace;
await this.getEvents();
await this.getApplications();

View file

@ -14,6 +14,7 @@
remove-action="ctrl.removeAction"
refresh-callback="ctrl.getResourcePools"
endpoint="ctrl.endpoint"
data-cy="k8sNamespace-namespaceTable"
></kubernetes-resource-pools-datatable>
</div>
</div>

View file

@ -30,7 +30,7 @@
<td>Namespace</td>
<td>
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.volume.ResourcePool.Namespace.Name })">{{ ctrl.volume.ResourcePool.Namespace.Name }}</a>
<span style="margin-left: 5px;" class="label label-info image-tag" ng-if="ctrl.isSystemNamespace(item)">system</span>
<span style="margin-left: 5px;" class="label label-info image-tag" ng-if="ctrl.isSystemNamespace()">system</span>
</td>
</tr>
<tr>

View file

@ -4,6 +4,7 @@ import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
import { KubernetesStorageClassAccessPolicies } from 'Kubernetes/models/storage-class/models';
import filesizeParser from 'filesize-parser';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
class KubernetesVolumeController {
/* @ngInject */
@ -14,7 +15,6 @@ class KubernetesVolumeController {
LocalStorage,
KubernetesVolumeService,
KubernetesEventService,
KubernetesNamespaceHelper,
KubernetesApplicationService,
KubernetesPersistentVolumeClaimService,
ModalService,
@ -27,7 +27,6 @@ class KubernetesVolumeController {
this.KubernetesVolumeService = KubernetesVolumeService;
this.KubernetesEventService = KubernetesEventService;
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
this.KubernetesApplicationService = KubernetesApplicationService;
this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService;
this.ModalService = ModalService;
@ -55,7 +54,7 @@ class KubernetesVolumeController {
}
isSystemNamespace() {
return this.KubernetesNamespaceHelper.isSystemNamespace(this.volume.ResourcePool.Namespace.Name);
return KubernetesNamespaceHelper.isSystemNamespace(this.volume.ResourcePool.Namespace.Name);
}
isUsed() {

View file

@ -17,6 +17,7 @@
helper-elements="filter"
search-property="Name"
translation="{nothingSelected: 'Select one or more users and/or teams', search: 'Search...'}"
data-cy="component-selectUser"
>
</span>
</div>

View file

@ -27,6 +27,7 @@
ng-disabled="(ctrl.availableUsersAndTeams | filter:{ticked:true}).length === 0 || ctrl.actionInProgress"
ng-click="ctrl.authorizeAccess()"
button-spinner="ctrl.actionInProgress"
data-cy="access-createAccess"
>
<span ng-hide="ctrl.state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Create access</span>
<span ng-show="ctrl.state.actionInProgress">Creating access...</span>

View file

@ -21,6 +21,7 @@
groups="$ctrl.groups"
show-groups="true"
has-backend-pagination="true"
data-cy="edgeGroupCreate-availableEndpoints"
></group-association-table>
</div>
</div>
@ -43,6 +44,7 @@
groups="$ctrl.groups"
show-groups="true"
has-backend-pagination="true"
data-cy="edgeGroupCreate-associatedEndpoints"
></group-association-table>
</div>
</div>

View file

@ -5,10 +5,18 @@
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </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)">
<button
type="button"
class="btn btn-sm btn-danger"
ng-disabled="$ctrl.state.selectedItemCount === 0"
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
data-cy="endpoint-removeEndpointButton"
>
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="portainer.endpoints.new"> <i class="fa fa-plus space-right" aria-hidden="true"></i>Add endpoint </button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="portainer.endpoints.new" data-cy="endpoint-addEndpointButton">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add endpoint
</button>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
@ -20,10 +28,11 @@
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
ng-model-options="{ debounce: 300 }"
data-cy="endpoint-searchInput"
/>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<table class="table table-hover nowrap-cells" data-cy="endpoint-endpointTable">
<thead>
<tr>
<th>

View file

@ -5,10 +5,18 @@
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </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)">
<button
type="button"
class="btn btn-sm btn-danger"
ng-disabled="$ctrl.state.selectedItemCount === 0"
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
data-cy="endpointGroup-removeGroupButton"
>
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="portainer.groups.new"> <i class="fa fa-plus space-right" aria-hidden="true"></i>Add group </button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="portainer.groups.new" data-cy="endpointGroup-addGroupButton">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add group
</button>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
@ -20,15 +28,16 @@
placeholder="Search..."
auto-focus
ng-model-options="{ debounce: 300 }"
data-cy="endpointGroup-searchInput"
/>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<table class="table table-hover nowrap-cells" data-cy="endpointGroup-endpointGroupTable">
<thead>
<tr>
<th>
<span class="md-checkbox">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" data-cy="endpointGroup-selectAllCheckbox" />
<label for="select_all"></label>
</span>
<a ng-click="$ctrl.changeOrderBy('Name')">

View file

@ -54,10 +54,18 @@
authorization="PortainerStackDelete"
ng-disabled="$ctrl.state.selectedItemCount === 0"
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
data-cy="stack-removeStackButton"
>
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</button>
<button ng-disabled="!$ctrl.createEnabled" type="button" class="btn btn-sm btn-primary" ui-sref="docker.stacks.newstack" authorization="PortainerStackCreate">
<button
ng-disabled="!$ctrl.createEnabled"
type="button"
class="btn btn-sm btn-primary"
ui-sref="docker.stacks.newstack"
authorization="PortainerStackCreate"
data-cy="stack-addStackButton"
>
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add stack
</button>
</div>
@ -71,10 +79,11 @@
placeholder="Search..."
auto-focus
ng-model-options="{ debounce: 300 }"
data-cy="stack-searchInput"
/>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<table class="table table-hover nowrap-cells" data-cy="stack-stackTable">
<thead>
<tr>
<th uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.filters.state.open">
@ -202,10 +211,10 @@
</span>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<tr ng-if="!$ctrl.dataset" data-cy="stacks-loadingTableRow">
<td colspan="6" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<tr ng-if="$ctrl.state.filteredDataSet.length === 0" data-cy="stacks-noStackTableRow">
<td colspan="6" class="text-center text-muted">No stack available.</td>
</tr>
</tbody>

View file

@ -5,7 +5,13 @@
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </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)">
<button
type="button"
class="btn btn-sm btn-danger"
ng-disabled="$ctrl.state.selectedItemCount === 0"
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
data-cy="user-removeUserButton"
>
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</button>
</div>
@ -21,7 +27,7 @@
/>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<table class="table table-hover nowrap-cells" data-cy="user-userTable">
<thead>
<tr>
<th>

View file

@ -38,7 +38,7 @@ class EndpointItemController {
const now = Math.floor(Date.now() / 1000);
// give checkIn some wiggle room
return now - this.model.LastCheckInDate <= checkInInterval * 2;
return now - this.model.LastCheckInDate <= checkInInterval * 2 + 20;
}
$onInit() {

View file

@ -6,7 +6,9 @@
</div>
<div class="actionBar" ng-if="$ctrl.showSnapshotAction">
<button type="button" class="btn btn-sm btn-primary" ng-click="$ctrl.snapshotAction()"> <i class="fa fa-sync space-right" aria-hidden="true"></i>Refresh </button>
<button type="button" class="btn btn-sm btn-primary" ng-click="$ctrl.snapshotAction()" data-cy="home-refreshEndpointsButton">
<i class="fa fa-sync space-right" aria-hidden="true"></i>Refresh
</button>
</div>
<div class="searchBar">
@ -19,10 +21,11 @@
ng-model-options="{ debounce: 300 }"
placeholder="Search by name, group, tag, status, URL..."
auto-focus
data-cy="home-endpointsSearchInput"
/>
</div>
<div class="blocklist">
<div class="blocklist" data-cy="home-endpointList">
<endpoint-item
ng-if="$ctrl.hasBackendPagination()"
dir-paginate="endpoint in $ctrl.state.filteredEndpoints | itemsPerPage: $ctrl.state.paginatedItemLimit"
@ -42,10 +45,10 @@
is-admin="$ctrl.isAdmin"
tags="$ctrl.tags"
></endpoint-item>
<div ng-if="$ctrl.state.loading" class="text-center text-muted">
<div ng-if="$ctrl.state.loading" class="text-center text-muted" data-cy="home-loadingEndpoints">
Loading...
</div>
<div ng-if="!$ctrl.state.loading && !$ctrl.state.filteredEndpoints.length" class="text-center text-muted">
<div ng-if="!$ctrl.state.loading && !$ctrl.state.filteredEndpoints.length" class="text-center text-muted" data-cy="home-noEndpoints">
No endpoint available.
</div>
</div>
@ -57,7 +60,7 @@
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()" data-cy="home-paginationSelect">
<option value="0" ng-if="!$ctrl.hasBackendPagination()">All</option>
<option value="10">10</option>
<option value="25">25</option>

View file

@ -1,6 +1,6 @@
<div class="form-group">
<div class="col-sm-12">
<por-switch-field ng-model="$ctrl.model.RepositoryAuthentication" label="Authentication" on-change="($ctrl.onChangeAuth)"></por-switch-field>
<por-switch-field ng-model="$ctrl.model.RepositoryAuthentication" label="Authentication" on-change="($ctrl.onChangeAuth)" data-cy="component-gitAuthToggle"></por-switch-field>
</div>
</div>
<div class="small text-warning" style="margin: 5px 0 15px 0;" ng-if="$ctrl.model.RepositoryAuthentication && $ctrl.showAuthExplanation">
@ -18,6 +18,7 @@
name="repository_username"
placeholder="git username"
ng-change="$ctrl.onChangeUsername($ctrl.model.RepositoryUsername)"
data-cy="component-gitUsernameInput"
/>
</div>
</div>
@ -35,6 +36,7 @@
placeholder="personal access token"
ng-change="$ctrl.onChangePassword($ctrl.model.RepositoryPassword)"
ng-required="!$ctrl.isEdit"
data-cy="component-gitPasswordInput"
/>
</div>
</div>

View file

@ -7,6 +7,14 @@
<div class="form-group">
<label for="stack_repository_reference_name" class="col-sm-1 control-label text-left">Repository reference</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="$ctrl.value" id="stack_repository_reference_name" placeholder="refs/heads/master" ng-change="$ctrl.onChange($ctrl.value)" />
<input
type="text"
class="form-control"
ng-model="$ctrl.value"
id="stack_repository_reference_name"
placeholder="refs/heads/master"
ng-change="$ctrl.onChange($ctrl.value)"
data-cy="component-gitRefInput"
/>
</div>
</div>

View file

@ -13,6 +13,7 @@
ng-change="$ctrl.onChange($ctrl.value)"
id="stack_repository_url"
placeholder="https://github.com/portainer/portainer-compose"
data-cy="component-gitUrlInput"
/>
</div>
</div>

View file

@ -1,3 +1,4 @@
<label class="switch" ng-class="$ctrl.className" style="margin-bottom: 0;">
<input type="checkbox" name="{{::$ctrl.name}}" id="{{::$ctrl.id}}" ng-model="$ctrl.ngModel" ng-disabled="$ctrl.disabled" ng-change="$ctrl.onChange($ctrl.ngModel)" /><i></i>
<input type="checkbox" name="{{::$ctrl.name}}" id="{{::$ctrl.id}}" ng-model="$ctrl.ngModel" ng-disabled="$ctrl.disabled" ng-change="$ctrl.onChange($ctrl.ngModel)" />
<i></i>
</label>

View file

@ -8,7 +8,7 @@ angular.module('portainer.app').directive('rdHeaderContent', [
scope.username = Authentication.getUserDetails().username;
},
template:
'<div class="breadcrumb-links"><div class="pull-left" ng-transclude></div><div class="pull-right" ng-if="username"><a ui-sref="portainer.account" style="margin-right: 5px;"><u><i class="fa fa-wrench" aria-hidden="true"></i> my account </u></a><a ui-sref="portainer.logout({performApiLogout: true})" class="text-danger" style="margin-right: 25px;"><u><i class="fa fa-sign-out-alt" aria-hidden="true"></i> log out</u></a></div></div>',
'<div class="breadcrumb-links"><div class="pull-left" ng-transclude></div><div class="pull-right" ng-if="username"><a ui-sref="portainer.account" style="margin-right: 5px;"><u><i class="fa fa-wrench" aria-hidden="true"></i> my account </u></a><a ui-sref="portainer.logout({performApiLogout: true})" class="text-danger" style="margin-right: 25px;" data-cy="template-logoutButton"><u><i class="fa fa-sign-out-alt" aria-hidden="true"></i> log out</u></a></div></div>',
restrict: 'E',
};
return directive;

View file

@ -1,5 +1,5 @@
<li class="sidebar-list sidebar-menu">
<sidebar-menu-item path="{{::$ctrl.path }}" path-params="$ctrl.pathParams" icon-class="{{::$ctrl.iconClass}}">
<sidebar-menu-item path="{{::$ctrl.path }}" path-params="$ctrl.pathParams" icon-class="{{::$ctrl.iconClass}}" data-cy="portainerSidebar-{{ ::$ctrl.label }}">
<div class="sidebar-menu-head">
<button ng-click="$ctrl.onClickArrow($event)" class="small sidebar-menu-indicator">
<i class="fas" ng-class="$ctrl.isOpen() ? 'fa-chevron-down' : 'fa-chevron-right'"></i>

View file

@ -27,6 +27,7 @@
typeahead-no-results="$ctrl.state.noResult"
typeahead-show-hint="true"
typeahead-min-length="0"
data-cy="tags-tagInput"
/>
</div>
<div class="col-sm-9 col-lg-10" ng-if="!$ctrl.allowCreate && $ctrl.tags.length === 0">

View file

@ -52,13 +52,13 @@
<!-- !username input -->
<div class="input-group" ng-if="ctrl.state.showStandardLogin">
<span class="input-group-addon"><i class="fa fa-user" aria-hidden="true"></i></span>
<input id="username" type="text" class="form-control" name="username" ng-model="ctrl.formValues.Username" auto-focus />
<input id="username" type="text" class="form-control" name="username" ng-model="ctrl.formValues.Username" auto-focus data-cy="auth-usernameInput" />
</div>
<!-- password input -->
<div class="input-group" ng-if="ctrl.state.showStandardLogin">
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<input id="password" type="password" class="form-control" name="password" ng-model="ctrl.formValues.Password" />
<input id="password" type="password" class="form-control" name="password" ng-model="ctrl.formValues.Password" data-cy="auth-passwordInput" />
</div>
<div class="form-group" ng-if="ctrl.state.showStandardLogin">
@ -70,6 +70,7 @@
ng-click="ctrl.authenticateUser()"
button-spinner="ctrl.state.loginInProgress"
ng-disabled="ctrl.state.loginInProgress"
data-cy="auth-loginButton"
>
<span ng-hide="ctrl.state.loginInProgress"><i class="fa fa-sign-in-alt" aria-hidden="true"></i> Login</span>
<span ng-show="ctrl.state.loginInProgress">Login in progress...</span>

View file

@ -16,7 +16,7 @@
<div class="boxselector_wrapper">
<div ng-click="resetEndpointURL()">
<input type="radio" id="agent_endpoint" ng-model="state.EnvironmentType" value="agent" />
<label for="agent_endpoint">
<label for="agent_endpoint" data-cy="endpointCreate-agentSelectButton">
<div class="boxselector_header">
<i class="fa fa-bolt" aria-hidden="true" style="margin-right: 2px;"></i>
Agent
@ -26,7 +26,7 @@
</div>
<div ng-click="setDefaultPortainerInstanceURL()">
<input type="radio" id="edge_agent_endpoint" ng-model="state.EnvironmentType" value="edge_agent" />
<label for="edge_agent_endpoint">
<label for="edge_agent_endpoint" data-cy="endpointCreate-edgeAgentSelectButton">
<div class="boxselector_header">
<i class="fa fa-cloud" aria-hidden="true" style="margin-right: 2px;"></i>
Edge Agent
@ -36,7 +36,7 @@
</div>
<div ng-click="resetEndpointURL()">
<input type="radio" id="docker_endpoint" ng-model="state.EnvironmentType" value="docker" />
<label for="docker_endpoint">
<label for="docker_endpoint" data-cy="endpointCreate-dockerSelectButton">
<div class="boxselector_header">
<i class="fab fa-docker" aria-hidden="true" style="margin-right: 2px;"></i>
Docker
@ -46,7 +46,7 @@
</div>
<div ng-click="resetEndpointURL()">
<input type="radio" id="kubernetes_endpoint" ng-model="state.EnvironmentType" value="kubernetes" />
<label for="kubernetes_endpoint">
<label for="kubernetes_endpoint" data-cy="endpointCreate-kubeSelectButton">
<div class="boxselector_header">
<i class="fas fa-dharmachakra" aria-hidden="true" style="margin-right: 2px;"></i>
Kubernetes
@ -56,7 +56,7 @@
</div>
<div>
<input type="radio" id="azure_endpoint" ng-model="state.EnvironmentType" value="azure" />
<label for="azure_endpoint">
<label for="azure_endpoint" data-cy="endpointCreate-azureSelectButton">
<div class="boxselector_header">
<i class="fab fa-microsoft" aria-hidden="true" style="margin-right: 2px;"></i>
Azure
@ -190,6 +190,7 @@
placeholder="e.g. docker-prod01 / kubernetes-cluster01"
required
auto-focus
data-cy="endpointCreate-nameInput"
/>
</div>
</div>
@ -207,7 +208,9 @@
<label for="connect_socket" class="col-sm_12 control-label text-left">
Connect via socket
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="formValues.ConnectSocket" /><i></i> </label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="formValues.ConnectSocket" /><i data-cy="endpointCreate-connectSocketSwitch"></i>
</label>
</div>
</div>
@ -216,7 +219,9 @@
<label for="override_socket" class="col-sm_12 control-label text-left">
Override default socket path
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="formValues.OverrideSocket" /><i></i> </label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="formValues.OverrideSocket" /><i data-cy="endpointCreate-overrideSocketSwitch"></i>
</label>
</div>
<div ng-if="formValues.OverrideSocket">
@ -234,6 +239,7 @@
ng-model="formValues.SocketPath"
placeholder="e.g. /var/run/docker.sock (on Linux) or //./pipe/docker_engine (on Windows)"
required
data-cy="endpointCreate-socketPathInput"
/>
</div>
</div>
@ -267,6 +273,7 @@
ng-model="formValues.URL"
placeholder="e.g. 10.0.0.10:2375 or mydocker.mydomain.com:2375"
required
data-cy="endpointCreate-endpointUrlDockerInput"
/>
<input
ng-if="state.EnvironmentType === 'agent'"
@ -276,6 +283,7 @@
ng-model="formValues.URL"
placeholder="e.g. 10.0.0.10:9001 or tasks.portainer_agent:9001"
required
data-cy="endpointCreate-endpointUrlAgentInput"
/>
</div>
</div>
@ -296,7 +304,15 @@
<portainer-tooltip position="bottom" message="URL of the Portainer instance that the agent will use to initiate the communications."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" name="endpoint_url" ng-model="formValues.URL" placeholder="e.g. 10.0.0.10:9000 or portainer.mydomain.com" required />
<input
type="text"
class="form-control"
name="endpoint_url"
ng-model="formValues.URL"
placeholder="e.g. 10.0.0.10:9000 or portainer.mydomain.com"
required
data-cy="endpointCreate-portainerServerUrlInput"
/>
</div>
</div>
<div class="form-group" ng-show="endpointCreationForm.endpoint_url.$invalid">
@ -322,6 +338,7 @@
class="form-control"
ng-model="formValues.CheckinInterval"
ng-options="+(opt.value) as opt.key for opt in state.availableEdgeAgentCheckinOptions"
data-cy="endpointCreate-pollFrequencySelect"
></select>
</div>
</div>
@ -338,7 +355,14 @@
</portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="endpoint_public_url" ng-model="formValues.PublicURL" placeholder="e.g. 10.0.0.10 or mydocker.mydomain.com" />
<input
type="text"
class="form-control"
id="endpoint_public_url"
ng-model="formValues.PublicURL"
placeholder="e.g. 10.0.0.10 or mydocker.mydomain.com"
data-cy="endpointCreate-publicIpInput"
/>
</div>
</div>
</div>
@ -356,6 +380,7 @@
ng-model="formValues.AzureApplicationId"
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
required
data-cy="endpointCreate-azureAppIdInput"
/>
</div>
</div>
@ -378,6 +403,7 @@
ng-model="formValues.AzureTenantId"
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
required
data-cy="endpointCreate-azureTenantIdInput"
/>
</div>
</div>
@ -400,6 +426,7 @@
ng-model="formValues.AzureAuthenticationKey"
placeholder="cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk="
required
data-cy="endpointCreate-azureAuthKeyInput"
/>
</div>
</div>
@ -425,7 +452,13 @@
Group
</label>
<div class="col-sm-9 col-lg-10">
<select ng-options="group.Id as group.Name for group in groups" ng-model="formValues.GroupId" id="endpoint_group" class="form-control"></select>
<select
ng-options="group.Id as group.Name for group in groups"
ng-model="formValues.GroupId"
id="endpoint_group"
class="form-control"
data-cy="endpointCreate-endpointGroup"
></select>
</div>
</div>
<!-- !group -->
@ -448,6 +481,7 @@
ng-disabled="state.actionInProgress || !endpointCreationForm.$valid || (formValues.TLS && ((formValues.TLSVerify && !formValues.TLSCACert) || (formValues.TLSClientCert && (!formValues.TLSCert || !formValues.TLSKey))))"
ng-click="addDockerEndpoint()"
button-spinner="state.actionInProgress"
data-cy="endpointCreate-createDockerEndpoint"
>
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
<span ng-show="state.actionInProgress">Creating endpoint...</span>
@ -459,6 +493,7 @@
ng-disabled="state.actionInProgress || !endpointCreationForm.$valid"
ng-click="addAgentEndpoint()"
button-spinner="state.actionInProgress"
data-cy="endpointCreate-createAgentEndpoint"
>
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
<span ng-show="state.actionInProgress">Creating endpoint...</span>
@ -470,6 +505,7 @@
ng-disabled="state.actionInProgress || !endpointCreationForm.$valid"
ng-click="addEdgeAgentEndpoint()"
button-spinner="state.actionInProgress"
data-cy="endpointCreate-createEdgeAgentEndpoint"
>
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
<span ng-show="state.actionInProgress">Creating endpoint...</span>
@ -492,6 +528,7 @@
ng-disabled="state.actionInProgress || !endpointCreationForm.$valid"
ng-click="addAzureEndpoint()"
button-spinner="state.actionInProgress"
data-cy="endpointCreate-createAzureEndpoint"
>
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
<span ng-show="state.actionInProgress">Creating endpoint...</span>

View file

@ -70,7 +70,15 @@
URL
</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="settings.TemplatesURL" id="templates_url" placeholder="https://myserver.mydomain/templates.json" required />
<input
type="text"
class="form-control"
ng-model="settings.TemplatesURL"
id="templates_url"
placeholder="https://myserver.mydomain/templates.json"
required
data-cy="settings-templateUrl"
/>
</div>
</div>
</div>
@ -95,6 +103,7 @@
class="form-control"
ng-model="settings.EdgeAgentCheckinInterval"
ng-options="+(opt.value) as opt.key for opt in state.availableEdgeAgentCheckinOptions"
data-cy="settings-pollFrequencySelect"
></select>
</div>
</div>
@ -103,7 +112,7 @@
<label for="toggle_enableEdgeComputeFeatures" class="control-label text-left">
Enable edge compute features
</label>
<label class="switch" style="margin-left: 20px;">
<label class="switch" style="margin-left: 20px;" data-cy="settings-edgeToggle">
<input type="checkbox" name="toggle_enableEdgeComputeFeatures" ng-model="formValues.enableEdgeComputeFeatures" /><i></i>
</label>
</div>
@ -118,6 +127,7 @@
ng-click="saveApplicationSettings()"
ng-disabled="state.actionInProgress || !settings.TemplatesURL"
button-spinner="state.actionInProgress"
data-cy="settings-saveSettingsButton"
>
<span ng-hide="state.actionInProgress">Save settings</span>
<span ng-show="state.actionInProgress">Saving...</span>

View file

@ -1,7 +1,7 @@
<!-- Sidebar -->
<div id="sidebar-wrapper">
<div class="sidebar-header">
<a ui-sref="portainer.home">
<a ui-sref="portainer.home" data-cy="portainerSidebar-homeImage">
<img ng-if="logo" ng-src="{{ logo }}" class="img-responsive logo" />
<img ng-if="!logo" src="~@/assets/images/logo.png" class="img-responsive logo" alt="Portainer" />
</a>
@ -9,14 +9,19 @@
</div>
<div class="sidebar-content">
<ul class="sidebar">
<sidebar-menu-item path="portainer.home" icon-class="fa-home fa-fw" class-name="sidebar-list">Home</sidebar-menu-item>
<sidebar-menu-item path="portainer.home" icon-class="fa-home fa-fw" class-name="sidebar-list" data-cy="portainerSidebar-home">Home</sidebar-menu-item>
<li class="sidebar-title endpoint-name" ng-if="applicationState.endpoint.name">
<span class="fa fa-plug space-right"></span>{{ applicationState.endpoint.name }}
<kubectl-shell ng-if="applicationState.endpoint.mode && applicationState.endpoint.mode.provider === 'KUBERNETES'" class="kubectl-shell"></kubectl-shell>
</li>
<div ng-if="applicationState.endpoint.mode">
<kubernetes-sidebar ng-if="applicationState.endpoint.mode.provider === 'KUBERNETES'" is-sidebar-open="toggle" endpoint-id="endpointId"></kubernetes-sidebar>
<kubernetes-sidebar
ng-if="applicationState.endpoint.mode.provider === 'KUBERNETES'"
is-sidebar-open="toggle"
endpoint-id="endpointId"
admin-access="isAdmin"
></kubernetes-sidebar>
<azure-sidebar ng-if="applicationState.endpoint.mode.provider === 'AZURE'" endpoint-id="endpointId"> </azure-sidebar>
@ -55,9 +60,9 @@
</sidebar-section>
<sidebar-section title="Edge compute" ng-if="isAdmin && applicationState.application.enableEdgeComputeFeatures">
<sidebar-menu-item path="edge.groups" icon-class="fa-object-group fa-fw" class-name="sidebar-list">Edge Groups</sidebar-menu-item>
<sidebar-menu-item path="edge.stacks" icon-class="fa-layer-group fa-fw" class-name="sidebar-list">Edge Stacks</sidebar-menu-item>
<sidebar-menu-item path="edge.jobs" icon-class="fa-clock fa-fw" class-name="sidebar-list">Edge Jobs</sidebar-menu-item>
<sidebar-menu-item path="edge.groups" icon-class="fa-object-group fa-fw" class-name="sidebar-list" data-cy="portainerSidebar-edgeGroups">Edge Groups</sidebar-menu-item>
<sidebar-menu-item path="edge.stacks" icon-class="fa-layer-group fa-fw" class-name="sidebar-list" data-cy="portainerSidebar-edgeStacks">Edge Stacks</sidebar-menu-item>
<sidebar-menu-item path="edge.jobs" icon-class="fa-clock fa-fw" class-name="sidebar-list" data-cy="portainerSidebar-edgeJobs">Edge Jobs</sidebar-menu-item>
</sidebar-section>
<sidebar-section ng-if="isAdmin || isTeamLeader" title="Settings">
@ -68,8 +73,8 @@
is-sidebar-open="toggle"
children-paths="['portainer.users.user' ,'portainer.teams' ,'portainer.teams.team' ,'portainer.roles' ,'portainer.roles.role' ,'portainer.roles.new']"
>
<sidebar-menu-item path="portainer.teams" class-name="sidebar-sublist">Teams</sidebar-menu-item>
<sidebar-menu-item path="portainer.roles" class-name="sidebar-sublist">Roles</sidebar-menu-item>
<sidebar-menu-item path="portainer.teams" class-name="sidebar-sublist" data-cy="portainerSidebar-teams">Teams</sidebar-menu-item>
<sidebar-menu-item path="portainer.roles" class-name="sidebar-sublist" data-cy="portainerSidebar-roles">Roles</sidebar-menu-item>
</sidebar-menu>
<div ng-if="isAdmin">
@ -80,17 +85,19 @@
is-sidebar-open="toggle"
children-paths="['portainer.endpoints.endpoint', 'portainer.endpoints.new', 'portainer.endpoints.endpoint.access', 'portainer.groups', 'portainer.groups.group', 'portainer.groups.group.access', 'portainer.groups.new', 'portainer.tags']"
>
<sidebar-menu-item path="portainer.groups" class-name="sidebar-sublist">Groups</sidebar-menu-item>
<sidebar-menu-item path="portainer.tags" class-name="sidebar-sublist">Tags</sidebar-menu-item>
<sidebar-menu-item path="portainer.groups" class-name="sidebar-sublist" data-cy="portainerSidebar-endpointGroups">Groups</sidebar-menu-item>
<sidebar-menu-item path="portainer.tags" class-name="sidebar-sublist" data-cy="portainerSidebar-endpointTags">Tags</sidebar-menu-item>
</sidebar-menu>
<sidebar-menu-item path="portainer.registries" icon-class="fa-database fa-fw" class-name="sidebar-list">Registries</sidebar-menu-item>
<sidebar-menu-item path="portainer.registries" icon-class="fa-database fa-fw" class-name="sidebar-list" data-cy="portainerSidebar-registries">
Registries</sidebar-menu-item
>
<sidebar-menu label="Settings" icon-class="fa-cogs fa-fw" path="portainer.settings" is-sidebar-open="toggle" children-paths="['portainer.settings.authentication']">
<sidebar-menu-item path="portainer.settings.authentication" class-name="sidebar-sublist">Authentication</sidebar-menu-item>
<sidebar-menu-item path="portainer.settings.authentication" class-name="sidebar-sublist" data-cy="portainerSidebar-authentication">Authentication</sidebar-menu-item>
<div class="sidebar-sublist">
<a href="http://www.portainer.io/help_about" target="_blank">Help / About</a>
<a href="http://www.portainer.io/help_about" target="_blank" data-cy="portainerSidebar-help">Help / About</a>
</div>
</sidebar-menu>
</div>
@ -104,7 +111,7 @@
</div>
<div>
<img src="~@/assets/images/logo_small.png" class="img-responsive logo" alt="Portainer" />
<span class="version">{{ uiVersion }}</span>
<span class="version" data-cy="portainerSidebar-versionNumber">{{ uiVersion }}</span>
</div>
</div>
</div>

View file

@ -27,6 +27,7 @@
placeholder="e.g. development"
auto-focus
required
data-cy="team-teamNameInput"
/>
</div>
</div>
@ -61,6 +62,7 @@
search-property="Username"
translation="{nothingSelected: 'Select one or more team leaders', search: 'Search...'}"
style="margin-left: 20px;"
data-cy="team-teamLeaderSelect"
>
</span>
</div>
@ -74,6 +76,7 @@
ng-disabled="state.actionInProgress || !teamCreationForm.$valid"
ng-click="addTeam()"
button-spinner="state.actionInProgress"
data-cy="team-createTeamButton"
>
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Create team</span>
<span ng-show="state.actionInProgress">Creating team...</span>

View file

@ -25,7 +25,16 @@
</label>
<div class="col-sm-8">
<div class="input-group">
<input type="text" class="form-control" id="username" ng-model="formValues.Username" ng-change="checkUsernameValidity()" placeholder="e.g. jdoe" auto-focus />
<input
type="text"
class="form-control"
id="username"
ng-model="formValues.Username"
ng-change="checkUsernameValidity()"
placeholder="e.g. jdoe"
auto-focus
data-cy="user-usernameInput"
/>
<span class="input-group-addon"><i ng-class="{ true: 'fa fa-check green-icon', false: 'fa fa-times red-icon' }[state.validUsername]" aria-hidden="true"></i></span>
</div>
</div>
@ -37,7 +46,7 @@
<div class="col-sm-8">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<input type="password" class="form-control" ng-model="formValues.Password" id="password" />
<input type="password" class="form-control" ng-model="formValues.Password" id="password" data-cy="user-passwordInput" />
</div>
</div>
</div>
@ -48,7 +57,7 @@
<div class="col-sm-8">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<input type="password" class="form-control" ng-model="formValues.ConfirmPassword" id="confirm_password" />
<input type="password" class="form-control" ng-model="formValues.ConfirmPassword" id="confirm_password" data-cy="user-passwordConfirmInput" />
<span class="input-group-addon"
><i
ng-class="{ true: 'fa fa-check green-icon', false: 'fa fa-times red-icon' }[formValues.Password !== '' && formValues.Password === formValues.ConfirmPassword]"
@ -69,7 +78,7 @@
message="Administrators have access to Portainer settings management as well as full control over all defined endpoints and their resources."
></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="formValues.Administrator" /><i></i> </label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="formValues.Administrator" data-cy="user-adminCheckbox" /><i></i> </label>
</div>
</div>
<!-- !admin-checkbox -->
@ -94,6 +103,7 @@
search-property="Name"
translation="{nothingSelected: 'Select one or more teams', search: 'Search...'}"
style="margin-left: 20px;"
data-cy="user-teamSelect"
>
</span>
</div>
@ -115,6 +125,7 @@
ng-disabled="state.actionInProgress || !state.validUsername || formValues.Username === '' || (AuthenticationMethod === 1 && formValues.Password === '') || (AuthenticationMethod === 1 && formValues.Password !== formValues.ConfirmPassword)"
ng-click="addUser()"
button-spinner="state.actionInProgress"
data-cy="user-createUserButton"
>
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Create user</span>
<span ng-show="state.actionInProgress">Creating user...</span>