Compare commits

...

18 Commits

Author SHA1 Message Date
Sven Dowideit
891ebf8e13 Merge branch 'feat/EE-882/be-highlight' into feat/EE-882/EE-1672/registry-view 2021-09-28 12:48:16 +10:00
fhanportainer
24198e8ab4 feat(activity-log): added activity log (#5760)
* feat(datatable): backport datatable children components to CE.

* feat(sidebar): added auth logs to the side bar

* feat(auth-log): added auth log component

* feat(auth-log): added BE feature only to datatable title bar

* feat(activity-log): added activity log

Co-authored-by: Sven Dowideit <SvenDowideit@home.org.au>
2021-09-28 12:46:31 +10:00
Sven Dowideit
4f0c71e323 Merge branch 'feat/EE-882/be-highlight' into feat/EE-882/EE-1672/registry-view 2021-09-28 12:19:51 +10:00
Chaim Lev-Ari
b370193ca3 feat(roles): highlight BE added value (#5673)
* refactor(settings): backport auth views (#5705)

* feat(roles): highlight BE added value

* feat(roles): show default for standard user

* fix(feature-flags): replace feature id

* feat(roles): show no users when limited to be
2021-09-28 14:23:59 +13:00
Felix Han
105042e1b1 fix(registry): using <ng-transclude> 2021-09-27 03:53:22 +13:00
Felix Han
6c5a20146e feat(registry): highlight BE added value [EE-1672] 2021-09-27 03:37:52 +13:00
Chaim Lev-Ari
fc94c679c0 refactor(settings): backport auth views (#5705) 2021-09-24 14:29:17 +03:00
Chaim Lev-Ari
70e3614345 feat(k8s): highlight BE added value [EE-1673] (#5674) 2021-09-24 14:19:36 +03:00
Chaim Lev-Ari
49d6bb4786 feat(featureflags): expose is feature limited to BE 2021-09-24 14:18:54 +03:00
Chaim Lev-Ari
fa87f808f0 feat(app): introduce feature flags framework (#5622) 2021-09-24 14:18:54 +03:00
Chaim Lev-Ari
db04bc9f38 fix(k8s/ns): validate ingress ctrl host pattern (#5663)
* fix(k8s/ns): validate ingress ctrl host pattern

* feat(kube/ns): validate ingress hostname
2021-09-24 14:02:06 +03:00
zees-dev
7d40a83d03 feat(kubectl-shell): page state refreshes in k8s endpoint do not close shell EE-1628 (#5685)
* converting all kubernetes view reload to partial state heirarchy refresh

* updated helm and kube kustom templates headers to use the reusable k8s page header component
2021-09-24 20:21:50 +12:00
Chaim Lev-Ari
d4f581a596 feat(kube): use local kubectl for all deployments (#5488) 2021-09-24 16:56:22 +12:00
testA113
5ad3cacefd Xt 321 automate k8s smoke test data cy attributes (#5734)
* added data-cy attributes for robust ui test automation
2021-09-24 13:00:55 +12:00
Richard Wei
6ac9c4367e show ip address of pod (#5613) 2021-09-23 14:34:24 +12:00
Felix Han
8c4acb7f04 feat(registry): highlight BE added value [EE-1672] 2021-09-22 10:34:46 +12:00
Chaim Lev-Ari
665b4794a3 feat(featureflags): expose is feature limited to BE 2021-09-19 11:15:48 +03:00
Chaim Lev-Ari
443e1ecd23 feat(app): introduce feature flags framework (#5622) 2021-09-19 11:15:47 +03:00
227 changed files with 4613 additions and 1240 deletions

View File

@@ -56,6 +56,32 @@ func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) *porta
}
}
// GetActiveTunnel retrieves an active tunnel which allows communicating with edge agent
func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (*portainer.TunnelDetails, error) {
tunnel := service.GetTunnelDetails(endpoint.ID)
if tunnel.Status == portainer.EdgeAgentIdle || tunnel.Status == portainer.EdgeAgentManagementRequired {
err := service.SetTunnelStatusToRequired(endpoint.ID)
if err != nil {
return nil, fmt.Errorf("failed opening tunnel to endpoint: %w", err)
}
if endpoint.EdgeCheckinInterval == 0 {
settings, err := service.dataStore.Settings().Settings()
if err != nil {
return nil, fmt.Errorf("failed fetching settings from db: %w", err)
}
endpoint.EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
}
waitForAgentToConnect := time.Duration(endpoint.EdgeCheckinInterval) * time.Second
time.Sleep(waitForAgentToConnect * 2)
}
tunnel = service.GetTunnelDetails(endpoint.ID)
return tunnel, nil
}
// SetTunnelStatusToActive update the status of the tunnel associated to the specified environment(endpoint).
// It sets the status to ACTIVE.
func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID) {

View File

@@ -99,8 +99,8 @@ func initSwarmStackManager(assetsPath string, configPath string, signatureServic
return exec.NewSwarmStackManager(assetsPath, configPath, signatureService, fileService, reverseTunnelService)
}
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, assetsPath string) portainer.KubernetesDeployer {
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, assetsPath)
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, assetsPath string) portainer.KubernetesDeployer {
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager, assetsPath)
}
func initHelmPackageManager(assetsPath string) (libhelm.HelmPackageManager, error) {
@@ -469,7 +469,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
log.Fatalf("failed initializing swarm stack manager: %s", err)
}
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, *flags.Assets)
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, proxyManager, *flags.Assets)
helmPackageManager, err := initHelmPackageManager(*flags.Assets)
if err != nil {

View File

@@ -47,7 +47,7 @@ func (manager *ComposeStackManager) ComposeSyntaxMaxVersion() string {
func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
url, proxy, err := manager.fetchEndpointProxy(endpoint)
if err != nil {
return errors.Wrap(err, "failed to featch environment proxy")
return errors.Wrap(err, "failed to fetch endpoint proxy")
}
if proxy != nil {
@@ -90,12 +90,12 @@ func (manager *ComposeStackManager) fetchEndpointProxy(endpoint *portainer.Endpo
return "", nil, nil
}
proxy, err := manager.proxyManager.CreateComposeProxyServer(endpoint)
proxy, err := manager.proxyManager.CreateAgentProxyServer(endpoint)
if err != nil {
return "", nil, err
}
return fmt.Sprintf("tcp://127.0.0.1:%d", proxy.Port), proxy, nil
return fmt.Sprintf("http://127.0.0.1:%d", proxy.Port), proxy, nil
}
func createEnvFile(stack *portainer.Stack) (string, error) {

View File

@@ -2,24 +2,21 @@ package exec
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os/exec"
"path"
"runtime"
"strings"
"time"
"github.com/pkg/errors"
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/proxy/factory"
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/kubernetes/cli"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
)
// KubernetesDeployer represents a service to deploy resources inside a Kubernetes environment(endpoint).
@@ -30,10 +27,11 @@ type KubernetesDeployer struct {
signatureService portainer.DigitalSignatureService
kubernetesClientFactory *cli.ClientFactory
kubernetesTokenCacheManager *kubernetes.TokenCacheManager
proxyManager *proxy.Manager
}
// NewKubernetesDeployer initializes a new KubernetesDeployer service.
func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, kubernetesClientFactory *cli.ClientFactory, datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, binaryPath string) *KubernetesDeployer {
func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, kubernetesClientFactory *cli.ClientFactory, datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, binaryPath string) *KubernetesDeployer {
return &KubernetesDeployer{
binaryPath: binaryPath,
dataStore: datastore,
@@ -41,6 +39,7 @@ func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheMan
signatureService: signatureService,
kubernetesClientFactory: kubernetesClientFactory,
kubernetesTokenCacheManager: kubernetesTokenCacheManager,
proxyManager: proxyManager,
}
}
@@ -50,14 +49,14 @@ func (deployer *KubernetesDeployer) getToken(request *http.Request, endpoint *po
return "", err
}
kubecli, err := deployer.kubernetesClientFactory.GetKubeClient(endpoint)
kubeCLI, err := deployer.kubernetesClientFactory.GetKubeClient(endpoint)
if err != nil {
return "", err
}
tokenCache := deployer.kubernetesTokenCacheManager.GetOrCreateTokenCache(int(endpoint.ID))
tokenManager, err := kubernetes.NewTokenManager(kubecli, deployer.dataStore, tokenCache, setLocalAdminToken)
tokenManager, err := kubernetes.NewTokenManager(kubeCLI, deployer.dataStore, tokenCache, setLocalAdminToken)
if err != nil {
return "", err
}
@@ -80,153 +79,44 @@ func (deployer *KubernetesDeployer) getToken(request *http.Request, endpoint *po
// Deploy will deploy a Kubernetes manifest inside a specific namespace in a Kubernetes environment(endpoint).
// Otherwise it will use kubectl to deploy the manifest.
func (deployer *KubernetesDeployer) Deploy(request *http.Request, endpoint *portainer.Endpoint, stackConfig string, namespace string) (string, error) {
if endpoint.Type == portainer.KubernetesLocalEnvironment {
token, err := deployer.getToken(request, endpoint, true)
command := path.Join(deployer.binaryPath, "kubectl")
if runtime.GOOS == "windows" {
command = path.Join(deployer.binaryPath, "kubectl.exe")
}
args := make([]string, 0)
if endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
url, proxy, err := deployer.getAgentURL(endpoint)
if err != nil {
return "", err
return "", errors.WithMessage(err, "failed generating endpoint URL")
}
command := path.Join(deployer.binaryPath, "kubectl")
if runtime.GOOS == "windows" {
command = path.Join(deployer.binaryPath, "kubectl.exe")
}
args := make([]string, 0)
args = append(args, "--server", endpoint.URL)
defer proxy.Close()
args = append(args, "--server", url)
args = append(args, "--insecure-skip-tls-verify")
args = append(args, "--token", token)
args = append(args, "--namespace", namespace)
args = append(args, "apply", "-f", "-")
var stderr bytes.Buffer
cmd := exec.Command(command, args...)
cmd.Stderr = &stderr
cmd.Stdin = strings.NewReader(stackConfig)
output, err := cmd.Output()
if err != nil {
return "", errors.New(stderr.String())
}
return string(output), nil
}
// agent
endpointURL := endpoint.URL
if endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
tunnel := deployer.reverseTunnelService.GetTunnelDetails(endpoint.ID)
if tunnel.Status == portainer.EdgeAgentIdle {
err := deployer.reverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
if err != nil {
return "", err
}
settings, err := deployer.dataStore.Settings().Settings()
if err != nil {
return "", err
}
waitForAgentToConnect := time.Duration(settings.EdgeAgentCheckinInterval) * time.Second
time.Sleep(waitForAgentToConnect * 2)
}
endpointURL = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
}
transport := &http.Transport{}
if endpoint.TLSConfig.TLS {
tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
if err != nil {
return "", err
}
transport.TLSClientConfig = tlsConfig
}
httpCli := &http.Client{
Transport: transport,
}
if !strings.HasPrefix(endpointURL, "http") {
endpointURL = fmt.Sprintf("https://%s", endpointURL)
}
url, err := url.Parse(fmt.Sprintf("%s/v2/kubernetes/stack", endpointURL))
token, err := deployer.getToken(request, endpoint, endpoint.Type == portainer.KubernetesLocalEnvironment)
if err != nil {
return "", err
}
reqPayload, err := json.Marshal(
struct {
StackConfig string
Namespace string
}{
StackConfig: stackConfig,
Namespace: namespace,
})
args = append(args, "--token", token)
args = append(args, "--namespace", namespace)
args = append(args, "apply", "-f", "-")
var stderr bytes.Buffer
cmd := exec.Command(command, args...)
cmd.Stderr = &stderr
cmd.Stdin = strings.NewReader(stackConfig)
output, err := cmd.Output()
if err != nil {
return "", err
return "", errors.Wrapf(err, "failed to execute kubectl command: %q", stderr.String())
}
req, err := http.NewRequest(http.MethodPost, url.String(), bytes.NewReader(reqPayload))
if err != nil {
return "", err
}
signature, err := deployer.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
if err != nil {
return "", err
}
token, err := deployer.getToken(request, endpoint, false)
if err != nil {
return "", err
}
req.Header.Set(portainer.PortainerAgentPublicKeyHeader, deployer.signatureService.EncodedPublicKey())
req.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
req.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
resp, err := httpCli.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var errorResponseData struct {
Message string
Details string
}
err = json.NewDecoder(resp.Body).Decode(&errorResponseData)
if err != nil {
output, parseStringErr := ioutil.ReadAll(resp.Body)
if parseStringErr != nil {
return "", parseStringErr
}
return "", fmt.Errorf("Failed parsing, body: %s, error: %w", output, err)
}
return "", fmt.Errorf("Deployment to agent failed: %s", errorResponseData.Details)
}
var responseData struct{ Output string }
err = json.NewDecoder(resp.Body).Decode(&responseData)
if err != nil {
parsedOutput, parseStringErr := ioutil.ReadAll(resp.Body)
if parseStringErr != nil {
return "", parseStringErr
}
return "", fmt.Errorf("Failed decoding, body: %s, err: %w", parsedOutput, err)
}
return responseData.Output, nil
return string(output), nil
}
// ConvertCompose leverages the kompose binary to deploy a compose compliant manifest.
@@ -251,3 +141,12 @@ func (deployer *KubernetesDeployer) ConvertCompose(data []byte) ([]byte, error)
return output, nil
}
func (deployer *KubernetesDeployer) getAgentURL(endpoint *portainer.Endpoint) (string, *factory.ProxyServer, error) {
proxy, err := deployer.proxyManager.CreateAgentProxyServer(endpoint)
if err != nil {
return "", nil, err
}
return fmt.Sprintf("http://127.0.0.1:%d/kubernetes", proxy.Port), proxy, nil
}

View File

@@ -6,29 +6,36 @@ import (
"net"
"net/http"
"net/url"
"strings"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/proxy/factory/dockercompose"
"github.com/portainer/portainer/api/http/proxy/factory/agent"
"github.com/portainer/portainer/api/internal/endpointutils"
)
// ProxyServer provide an extedned proxy with a local server to forward requests
// ProxyServer provide an extended proxy with a local server to forward requests
type ProxyServer struct {
server *http.Server
Port int
}
func (factory *ProxyFactory) NewDockerComposeAgentProxy(endpoint *portainer.Endpoint) (*ProxyServer, error) {
// NewAgentProxy creates a new instance of ProxyServer that wrap http requests with agent headers
func (factory *ProxyFactory) NewAgentProxy(endpoint *portainer.Endpoint) (*ProxyServer, error) {
urlString := endpoint.URL
if endpointutils.IsEdgeEndpoint((endpoint)) {
tunnel, err := factory.reverseTunnelService.GetActiveTunnel(endpoint)
if err != nil {
return nil, errors.Wrap(err, "failed starting tunnel")
}
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
return &ProxyServer{
Port: factory.reverseTunnelService.GetTunnelDetails(endpoint.ID).Port,
}, nil
urlString = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
}
endpointURL, err := url.Parse(endpoint.URL)
endpointURL, err := parseURL(urlString)
if err != nil {
return nil, err
return nil, errors.Wrapf(err, "failed parsing url %s", endpoint.URL)
}
endpointURL.Scheme = "http"
@@ -37,7 +44,7 @@ func (factory *ProxyFactory) NewDockerComposeAgentProxy(endpoint *portainer.Endp
if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify {
config, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
if err != nil {
return nil, err
return nil, errors.WithMessage(err, "failed generating tls configuration")
}
httpTransport.TLSClientConfig = config
@@ -46,7 +53,7 @@ func (factory *ProxyFactory) NewDockerComposeAgentProxy(endpoint *portainer.Endp
proxy := newSingleHostReverseProxyWithHostHeader(endpointURL)
proxy.Transport = dockercompose.NewAgentTransport(factory.signatureService, httpTransport)
proxy.Transport = agent.NewTransport(factory.signatureService, httpTransport)
proxyServer := &ProxyServer{
server: &http.Server{
@@ -57,7 +64,7 @@ func (factory *ProxyFactory) NewDockerComposeAgentProxy(endpoint *portainer.Endp
err = proxyServer.start()
if err != nil {
return nil, err
return nil, errors.Wrap(err, "failed starting proxy server")
}
return proxyServer, nil
@@ -91,3 +98,15 @@ func (proxy *ProxyServer) Close() {
proxy.server.Close()
}
}
// parseURL parses the endpointURL using url.Parse.
//
// to prevent an error when url has port but no protocol prefix
// we add `//` prefix if needed
func parseURL(endpointURL string) (*url.URL, error) {
if !strings.HasPrefix(endpointURL, "http") && !strings.HasPrefix(endpointURL, "tcp") && !strings.HasPrefix(endpointURL, "//") {
endpointURL = fmt.Sprintf("//%s", endpointURL)
}
return url.Parse(endpointURL)
}

View File

@@ -1,4 +1,4 @@
package dockercompose
package agent
import (
"net/http"
@@ -7,17 +7,17 @@ import (
)
type (
// AgentTransport is an http.Transport wrapper that adds custom http headers to communicate to an Agent
AgentTransport struct {
// Transport is an http.Transport wrapper that adds custom http headers to communicate to an Agent
Transport struct {
httpTransport *http.Transport
signatureService portainer.DigitalSignatureService
endpointIdentifier portainer.EndpointID
}
)
// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer agent
func NewAgentTransport(signatureService portainer.DigitalSignatureService, httpTransport *http.Transport) *AgentTransport {
transport := &AgentTransport{
// NewTransport returns a new transport that can be used to send signed requests to a Portainer agent
func NewTransport(signatureService portainer.DigitalSignatureService, httpTransport *http.Transport) *Transport {
transport := &Transport{
httpTransport: httpTransport,
signatureService: signatureService,
}
@@ -26,8 +26,7 @@ func NewAgentTransport(signatureService portainer.DigitalSignatureService, httpT
}
// RoundTrip is the implementation of the the http.RoundTripper interface
func (transport *AgentTransport) RoundTrip(request *http.Request) (*http.Response, error) {
func (transport *Transport) RoundTrip(request *http.Request) (*http.Response, error) {
signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
if err != nil {
return nil, err

View File

@@ -48,10 +48,10 @@ func (manager *Manager) CreateAndRegisterEndpointProxy(endpoint *portainer.Endpo
return proxy, nil
}
// CreateComposeProxyServer creates a new HTTP reverse proxy based on environment(endpoint) properties and and adds it to the registered proxies.
// CreateAgentProxyServer creates a new HTTP reverse proxy based on environment(endpoint) properties and and adds it to the registered proxies.
// It can also be used to create a new HTTP reverse proxy and replace an already registered proxy.
func (manager *Manager) CreateComposeProxyServer(endpoint *portainer.Endpoint) (*factory.ProxyServer, error) {
return manager.proxyFactory.NewDockerComposeAgentProxy(endpoint)
func (manager *Manager) CreateAgentProxyServer(endpoint *portainer.Endpoint) (*factory.ProxyServer, error) {
return manager.proxyFactory.NewAgentProxy(endpoint)
}
// GetEndpointProxy returns the proxy associated to a key

View File

@@ -24,3 +24,7 @@ func IsDockerEndpoint(endpoint *portainer.Endpoint) bool {
endpoint.Type == portainer.AgentOnDockerEnvironment ||
endpoint.Type == portainer.EdgeAgentOnDockerEnvironment
}
func IsEdgeEndpoint(endpoint *portainer.Endpoint) bool {
return endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment
}

View File

@@ -1,14 +1,13 @@
package cli
import (
"errors"
"fmt"
"net/http"
"strconv"
"sync"
"time"
cmap "github.com/orcaman/concurrent-map"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"k8s.io/client-go/kubernetes"
@@ -116,36 +115,18 @@ func (rt *agentHeaderRoundTripper) RoundTrip(req *http.Request) (*http.Response,
func (factory *ClientFactory) buildAgentClient(endpoint *portainer.Endpoint) (*kubernetes.Clientset, error) {
endpointURL := fmt.Sprintf("https://%s/kubernetes", endpoint.URL)
return factory.createRemoteClient(endpointURL);
return factory.createRemoteClient(endpointURL)
}
func (factory *ClientFactory) buildEdgeClient(endpoint *portainer.Endpoint) (*kubernetes.Clientset, error) {
tunnel := factory.reverseTunnelService.GetTunnelDetails(endpoint.ID)
if tunnel.Status == portainer.EdgeAgentIdle {
err := factory.reverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
if err != nil {
return nil, fmt.Errorf("failed opening tunnel to environment: %w", err)
}
if endpoint.EdgeCheckinInterval == 0 {
settings, err := factory.dataStore.Settings().Settings()
if err != nil {
return nil, fmt.Errorf("failed fetching settings from db: %w", err)
}
endpoint.EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
}
waitForAgentToConnect := time.Duration(endpoint.EdgeCheckinInterval) * time.Second
time.Sleep(waitForAgentToConnect * 2)
tunnel = factory.reverseTunnelService.GetTunnelDetails(endpoint.ID)
tunnel, err := factory.reverseTunnelService.GetActiveTunnel(endpoint)
if err != nil {
return nil, errors.Wrap(err, "failed activating tunnel")
}
endpointURL := fmt.Sprintf("http://127.0.0.1:%d/kubernetes", tunnel.Port)
return factory.createRemoteClient(endpointURL);
return factory.createRemoteClient(endpointURL)
}
func (factory *ClientFactory) createRemoteClient(endpointURL string) (*kubernetes.Clientset, error) {

View File

@@ -1326,6 +1326,7 @@ type (
SetTunnelStatusToIdle(endpointID EndpointID)
KeepTunnelAlive(endpointID EndpointID, ctx context.Context, maxKeepAlive time.Duration)
GetTunnelDetails(endpointID EndpointID) *TunnelDetails
GetActiveTunnel(endpoint *Endpoint) (*TunnelDetails, error)
AddEdgeJob(endpointID EndpointID, edgeJob *EdgeJob)
RemoveEdgeJob(edgeJobID EdgeJobID)
}

View File

@@ -71,6 +71,15 @@ body,
font-size: 0.9em;
}
.form-control.limited-be {
border-color: var(--BE-only);
}
.btn.limited-be {
background-color: var(--BE-only);
border-color: var(--BE-only);
}
input[type='checkbox'] {
margin-top: 1px;
vertical-align: middle;

View File

@@ -88,8 +88,13 @@ html {
--green-1: #164;
--green-2: #1ec863;
--orange-1: #e86925;
--BE-only: var(--orange-1);
}
/* Default Theme */
:root {
--bg-card-color: var(--grey-10);
--bg-main-color: var(--white-color);

View File

@@ -101,7 +101,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -136,7 +136,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -102,7 +102,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -37,7 +37,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -295,7 +295,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -68,7 +68,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -204,7 +204,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -97,7 +97,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -189,7 +189,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -88,7 +88,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -153,7 +153,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -136,7 +136,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -219,7 +219,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -109,7 +109,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -201,7 +201,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -10,6 +10,7 @@
ng-model="$ctrl.model.Registry"
id="image_registry"
class="form-control"
data-cy="component-registrySelect"
></select>
</div>
<label for="image_name" ng-class="$ctrl.labelClass" class="margin-sm-top control-label text-left">Image</label>

View File

@@ -65,7 +65,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -65,7 +65,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -79,7 +79,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -70,7 +70,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -142,7 +142,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -94,7 +94,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -76,7 +76,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -124,7 +124,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -98,7 +98,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -68,7 +68,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -83,7 +83,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -99,6 +99,13 @@
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Node' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('IP')">
Pod IP
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'IP' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'IP' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('CreationDate')">
Creation date
@@ -129,6 +136,7 @@
</span>
<span ng-if="!item.Node">-</span>
</td>
<td>{{ item.PodIP }}</td>
<td>{{ item.CreationDate | getisodate }}</td>
<td>
<a
@@ -161,7 +169,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -269,7 +269,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -243,7 +243,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -171,7 +171,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -110,7 +110,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -141,7 +141,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -152,7 +152,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -130,7 +130,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -163,7 +163,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -179,7 +179,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -21,7 +21,7 @@ export default class HelmAddRepositoryController {
try {
await this.HelmService.addHelmRepository(this.EndpointProvider.currentEndpoint().Id, { url: this.state.repository });
this.Notifications.success('Helm repository added successfully');
this.$state.reload();
this.$state.reload(this.$state.current);
} catch (err) {
this.Notifications.error('Installation error', err);
} finally {

View File

@@ -1,11 +1,6 @@
<rd-header id="view-top">
<rd-header-title title-text="Helm">
<a data-toggle="tooltip" title="Refresh" ui-sref="kubernetes.templates.helm" ui-sref-opts="{reload: true}">
<i class="fa fa-sync" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>Charts</rd-header-content>
</rd-header>
<kubernetes-view-header title="Helm" state="kubernetes.templates.helm" view-ready="true">
Charts
</kubernetes-view-header>
<information-panel title-text="Information" ng-if="!$ctrl.state.chart">
<span class="small text-muted">

View File

@@ -10,6 +10,12 @@ export default class KubectlShellController {
this.EndpointProvider = EndpointProvider;
this.LocalStorage = LocalStorage;
this.Notifications = Notifications;
$window.onbeforeunload = () => {
if (this.state.shell.connected) {
return '';
}
};
}
disconnect() {

View File

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

View File

@@ -1,11 +1,6 @@
<rd-header id="view-top">
<rd-header-title title-text="Custom Templates">
<a data-toggle="tooltip" title="Refresh" ui-sref="kubernetes.templates.custom" ui-sref-opts="{reload: true}">
<i class="fa fa-sync" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>Custom Templates</rd-header-content>
</rd-header>
<kubernetes-view-header title="Custom Templates" state="kubernetes.templates.custom" view-ready="true">
Custom Templates
</kubernetes-view-header>
<div class="row">
<div class="col-sm-12">

View File

@@ -68,6 +68,7 @@ function computeContainers(data) {
const res = new KubernetesPodContainer();
res.Type = KubernetesPodContainerTypes.APP;
res.PodName = data.metadata.name;
res.PodIP = data.status.podIP;
res.Name = item.name;
res.Image = item.image;
res.ImagePullPolicy = item.imagePullPolicy;

View File

@@ -44,7 +44,7 @@ export default class KubernetesRegistryAccessController {
await this.EndpointService.updateRegistryAccess(this.endpoint.Id, this.registry.Id, {
namespaces,
});
this.$state.reload();
this.$state.reload(this.$state.current);
} catch (err) {
this.Notifications.error('Failure', err, 'Failed saving registry access');
}

View File

@@ -45,7 +45,7 @@ class KubernetesApplicationsController {
} finally {
--actionCount;
if (actionCount === 0) {
this.$state.reload();
this.$state.reload(this.$state.current);
}
}
}
@@ -79,7 +79,7 @@ class KubernetesApplicationsController {
} finally {
--actionCount;
if (actionCount === 0) {
this.$state.reload();
this.$state.reload(this.$state.current);
}
}
}

View File

@@ -36,6 +36,7 @@
ng-options="resourcePool.Namespace.Name for resourcePool in ctrl.resourcePools"
ng-change="ctrl.onResourcePoolSelectionChange()"
ng-disabled="ctrl.state.isEdit"
data-cy="k8sAppCreate-nsSelect"
></select>
</div>
</div>
@@ -178,6 +179,7 @@
uib-typeahead="stack for stack in ctrl.stacks | filter:$viewValue | limitTo:7"
typeahead-show-hint="true"
typeahead-min-length="0"
data-cy="k8sAppCreate-stackName"
/>
</div>
</div>
@@ -190,7 +192,13 @@
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">Environment variables</label>
<span ng-if="ctrl.formValues.Containers.length <= 1" class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addEnvironmentVariable()">
<span
ng-if="ctrl.formValues.Containers.length <= 1"
class="label label-default interactive"
style="margin-left: 10px;"
ng-click="ctrl.addEnvironmentVariable()"
data-cy="k8sAppCreate-addEnvVarButton"
>
<i class="fa fa-plus-circle" aria-hidden="true"></i> add environment variable
</span>
</div>
@@ -210,6 +218,7 @@
ng-pattern="/^[-._a-zA-Z][-._a-zA-Z0-9]*$/"
placeholder="foo"
ng-disabled="ctrl.formValues.Containers.length > 1"
data-cy="k8sAppCreate-envVarName_{{ $index }}"
required
/>
</div>
@@ -224,6 +233,7 @@
ng-model="envVar.Value"
placeholder="bar"
ng-disabled="ctrl.formValues.Containers.length > 1"
data-cy="k8sAppCreate-envVarValue_{{ $index }}"
/>
</div>
@@ -231,7 +241,13 @@
<button ng-if="!envVar.NeedsDeletion" class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeEnvironmentVariable(envVar)">
<i class="fa fa-trash-alt" aria-hidden="true"></i>
</button>
<button ng-if="envVar.NeedsDeletion" class="btn btn-sm btn-primary" type="button" ng-click="ctrl.restoreEnvironmentVariable(envVar)">
<button
ng-if="envVar.NeedsDeletion"
class="btn btn-sm btn-primary"
type="button"
ng-click="ctrl.restoreEnvironmentVariable(envVar)"
data-cy="k8sAppCreate-removeEnvVarButton_{{ $index }}"
>
<i class="fa fa-trash-restore" aria-hidden="true"></i>
</button>
</div>
@@ -277,14 +293,20 @@
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">Configurations</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addConfiguration()" ng-if="ctrl.formValues.Containers.length <= 1">
<span
class="label label-default interactive"
style="margin-left: 10px;"
ng-click="ctrl.addConfiguration()"
ng-if="ctrl.formValues.Containers.length <= 1"
data-cy="k8sAppCreate-addConfigButton"
>
<i class="fa fa-plus-circle" aria-hidden="true"></i> add configuration
</span>
</div>
<div class="col-sm-12 small text-muted" style="margin-top: 15px;" ng-if="ctrl.formValues.Configurations.length">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Portainer will automatically expose all the keys of a configuration as environment variables. This behavior can be overriden to filesystem mounts for each key via
the override button.
Portainer will automatically expose all the keys of a configuration as environment variables. This behavior can be overriden to filesystem mounts for each key
via the override button.
</div>
</div>
@@ -298,6 +320,7 @@
ng-options="c as c.Name for c in ctrl.configurations track by c.Name"
ng-change="ctrl.resetConfiguration(index)"
ng-disabled="ctrl.formValues.Containers.length > 1"
data-cy="k8sAppCreate-addConfigSelect_{{ $index }}"
></select>
</div>
<div class="col-sm-6" style="margin-top: 2px;">
@@ -307,6 +330,7 @@
ng-if="!config.Overriden"
ng-click="ctrl.overrideConfiguration(index)"
ng-disabled="!config.SelectedConfiguration || ctrl.formValues.Containers.length > 1"
data-cy="k8sAppCreate-configOverrideButton_{{ $index }}"
>
<i class="fa fa-list" aria-hidden="true"></i> Override
</button>
@@ -316,10 +340,17 @@
ng-if="config.Overriden"
ng-click="ctrl.resetConfiguration(index)"
ng-disabled="ctrl.formValues.Containers.length > 1"
data-cy="k8sAppCreate-configAutoButton_{{ $index }}"
>
<i class="fa fa-undo" aria-hidden="true"></i> Auto
</button>
<button class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeConfiguration(index)" ng-if="ctrl.formValues.Containers.length <= 1">
<button
class="btn btn-sm btn-danger"
type="button"
ng-click="ctrl.removeConfiguration(index)"
ng-if="ctrl.formValues.Containers.length <= 1"
data-cy="k8sAppCreate-configRemoveButton"
>
<i class="fa fa-trash-alt" aria-hidden="true"></i> Remove
</button>
</div>
@@ -358,6 +389,7 @@
ng-disabled="ctrl.formValues.Containers.length > 1"
required
ng-change="ctrl.onChangeConfigurationPath()"
data-cy="k8sAppCreate-pathOnDiskInput"
/>
</div>
</div>
@@ -420,7 +452,13 @@
<div class="form-group" ng-if="ctrl.storageClassAvailable()">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Persisted folders</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addPersistedFolder()" ng-if="ctrl.isAddPersistentFolderButtonShowed()">
<span
class="label label-default interactive"
style="margin-left: 10px;"
ng-click="ctrl.addPersistedFolder()"
ng-if="ctrl.isAddPersistentFolderButtonShowed()"
data-cy="k8sAppCreate-addPersistentFolderButton"
>
<i class="fa fa-plus-circle" aria-hidden="true"></i> add persisted folder
</span>
</div>
@@ -438,6 +476,7 @@
ng-disabled="ctrl.isEditAndExistingPersistedFolder($index) || ctrl.formValues.Containers.length > 1"
placeholder="/data"
required
data-cy="k8sAppCreate-containerPathInput_{{ $index }}"
/>
</div>
@@ -500,8 +539,16 @@
ng-model="persistedFolder.StorageClass"
ng-options="storageClass as storageClass.Name for storageClass in ctrl.storageClasses"
ng-disabled="ctrl.state.isEdit || ctrl.formValues.Containers.length > 1"
data-cy="k8sAppCreate-storageSelect_{{ $index }}"
></select>
<input ng-if="!ctrl.hasMultipleStorageClassesAvailable()" type="text" class="form-control" disabled ng-model="persistedFolder.StorageClass.Name" />
<input
ng-if="!ctrl.hasMultipleStorageClassesAvailable()"
type="text"
class="form-control"
disabled
ng-model="persistedFolder.StorageClass.Name"
data-cy="k8sAppCreate-storageClassNameInput_{{ $index }}"
/>
</div>
<div class="input-group col-sm-5 input-group-sm" ng-if="!persistedFolder.UseNewVolume" ng-class="{ striked: persistedFolder.NeedsDeletion }">
@@ -521,10 +568,22 @@
<div class="input-group col-sm-1 input-group-sm">
<div ng-if="!ctrl.isEditAndStatefulSet() && !ctrl.state.useExistingVolume[$index] && ctrl.formValues.Containers.length <= 1">
<button ng-if="!persistedFolder.NeedsDeletion" class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removePersistedFolder($index)">
<button
ng-if="!persistedFolder.NeedsDeletion"
class="btn btn-sm btn-danger"
type="button"
ng-click="ctrl.removePersistedFolder($index)"
data-cy="k8sAppCreate-rmPersistentFolderButton"
>
<i class="fa fa-trash-alt" aria-hidden="true"></i>
</button>
<button ng-if="persistedFolder.NeedsDeletion" class="btn btn-sm btn-primary" type="button" ng-click="ctrl.restorePersistedFolder($index)">
<button
ng-if="persistedFolder.NeedsDeletion"
class="btn btn-sm btn-primary"
type="button"
ng-click="ctrl.restorePersistedFolder($index)"
data-cy="k8sAppCreate-restorePersistentButton"
>
<i class="fa fa-trash-restore" aria-hidden="true"></i>
</button>
</div>
@@ -544,7 +603,9 @@
<div
class="small text-warning"
style="margin-top: 5px;"
ng-show="kubernetesApplicationCreationForm['persisted_folder_path_' + $index].$invalid || ctrl.state.duplicates.persistedFolders.refs[$index] !== undefined"
ng-show="
kubernetesApplicationCreationForm['persisted_folder_path_' + $index].$invalid || ctrl.state.duplicates.persistedFolders.refs[$index] !== undefined
"
>
<ng-messages for="kubernetesApplicationCreationForm['persisted_folder_path_' + $index].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Path is required.</p>
@@ -623,7 +684,9 @@
</div>
<div
style="color: #767676;"
ng-if="(ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.SHARED) || ctrl.state.persistedFoldersUseExistingVolumes"
ng-if="
(ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.SHARED) || ctrl.state.persistedFoldersUseExistingVolumes
"
>
<input type="radio" id="data_access_isolated" disabled />
<label
@@ -694,8 +757,8 @@
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && !ctrl.resourceQuotaCapacityExceeded()">
<div class="col-sm-12 small text-muted">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
A resource quota is set on this namespace, you must specify resource reservations. Resource reservations are applied per instance of the application. Maximums are
inherited from the namespace quota.
A resource quota is set on this namespace, you must specify resource reservations. Resource reservations are applied per instance of the application. Maximums
are inherited from the namespace quota.
</div>
</div>
@@ -733,6 +796,7 @@
class="form-control"
id="memory-limit"
required
data-cy="k8sAppCreate-memoryLimit"
/>
</div>
<div class="col-sm-4">
@@ -798,7 +862,13 @@
<div class="form-group" style="margin-bottom: 0;">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="deployment_replicated" ng-value="ctrl.ApplicationDeploymentTypes.REPLICATED" ng-model="ctrl.formValues.DeploymentType" />
<input
type="radio"
id="deployment_replicated"
ng-value="ctrl.ApplicationDeploymentTypes.REPLICATED"
ng-model="ctrl.formValues.DeploymentType"
data-cy="k8sAppCreate-replicatedDeploymentButton"
/>
<label for="deployment_replicated">
<div class="boxselector_header">
<i class="fa fa-cube" aria-hidden="true" style="margin-right: 2px;"></i>
@@ -831,6 +901,7 @@
ng-value="ctrl.ApplicationDeploymentTypes.GLOBAL"
ng-model="ctrl.formValues.DeploymentType"
ng-click="ctrl.unselectAutoScaler()"
data-cy="k8sAppCreate-globalDeployButton"
/>
<label for="deployment_global">
<div class="boxselector_header">
@@ -862,6 +933,7 @@
ng-disabled="!ctrl.supportScalableReplicaDeployment()"
ng-change="ctrl.enforceReplicaCountMinimum()"
required
data-cy="k8sAppCreate-replicaCountInput"
/>
</div>
</div>
@@ -914,7 +986,13 @@
Enable auto scaling for this application
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" class="form-control" name="enable_auto_scaling" ng-model="ctrl.formValues.AutoScaler.IsUsed" />
<input
type="checkbox"
class="form-control"
name="enable_auto_scaling"
ng-model="ctrl.formValues.AutoScaler.IsUsed"
data-cy="k8sAppCreate-autoScaleCheckbox"
/>
<i></i>
</label>
</div>
@@ -957,6 +1035,7 @@
min="0"
ng-max="ctrl.formValues.AutoScaler.MaxReplicas"
ng-model="ctrl.formValues.AutoScaler.MinReplicas"
data-cy="k8sAppCreate-autoScaleMin"
required
/>
</div>
@@ -991,7 +1070,16 @@
</td>
<td style="padding: 8px 5px 5px 0; border: none;">
<div class="input-group input-group-sm" style="width: 100%;">
<input type="number" class="form-control" name="auto_scaler_cpu" ng-model="ctrl.formValues.AutoScaler.TargetCPUUtilization" min="1" max="100" required />
<input
type="number"
class="form-control"
name="auto_scaler_cpu"
ng-model="ctrl.formValues.AutoScaler.TargetCPUUtilization"
min="1"
max="100"
required
data-cy="k8sAppCreate-targetCPUInput"
/>
</div>
<div class="input-group input-group-sm" ng-show="kubernetesApplicationCreationForm['auto_scaler_cpu'].$invalid">
<div class="small text-warning" style="margin-top: 5px;">
@@ -1044,6 +1132,7 @@
ng-options="label as (label.Key | kubernetesNodeLabelHumanReadbleText) for label in ctrl.nodesLabels"
ng-change="ctrl.onChangePlacementLabel($index)"
ng-disabled="ctrl.isEditAndNotNewPlacement($index)"
data-cy="k8sAppCreate-placementLabel_{{ $index }}"
>
</select>
</div>
@@ -1053,15 +1142,28 @@
ng-model="placement.Value"
ng-options="value for value in placement.Label.Values"
ng-disabled="ctrl.isEditAndNotNewPlacement($index)"
data-cy="k8sAppCreate-placementName_{{ $index }}"
>
</select>
</div>
<div class="col-sm-1 input-group">
<button ng-if="!placement.NeedsDeletion" class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removePlacement($index)">
<button
ng-if="!placement.NeedsDeletion"
class="btn btn-sm btn-danger"
type="button"
ng-click="ctrl.removePlacement($index)"
data-cy="k8sAppCreate-deletePlacementButton"
>
<i class="fa fa-trash-alt" aria-hidden="true"></i>
</button>
<button ng-if="placement.NeedsDeletion" class="btn btn-sm btn-primary" type="button" ng-click="ctrl.restorePlacement($index)">
<button
ng-if="placement.NeedsDeletion"
class="btn btn-sm btn-primary"
type="button"
ng-click="ctrl.restorePlacement($index)"
data-cy="k8sAppCreate-restorePlacementButton"
>
<i class="fa fa-trash-restore" aria-hidden="true"></i>
</button>
</div>
@@ -1094,7 +1196,13 @@
<div class="form-group" style="margin-bottom: 0;" ng-if="ctrl.formValues.Placements.length">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="placement_hard" ng-value="ctrl.ApplicationPlacementTypes.MANDATORY" ng-model="ctrl.formValues.PlacementType" />
<input
type="radio"
id="placement_hard"
ng-value="ctrl.ApplicationPlacementTypes.MANDATORY"
ng-model="ctrl.formValues.PlacementType"
data-cy="k8sAppCreate-mandatoryPlacementButton"
/>
<label for="placement_hard">
<div class="boxselector_header">
<i class="fa fa-tasks" aria-hidden="true" style="margin-right: 2px;"></i>
@@ -1104,7 +1212,13 @@
</label>
</div>
<div>
<input type="radio" id="placement_soft" ng-value="ctrl.ApplicationPlacementTypes.PREFERRED" ng-model="ctrl.formValues.PlacementType" />
<input
type="radio"
id="placement_soft"
ng-value="ctrl.ApplicationPlacementTypes.PREFERRED"
ng-model="ctrl.formValues.PlacementType"
data-cy="k8sAppCreate-prefferedPlacementButton"
/>
<label for="placement_soft">
<div class="boxselector_header">
<i class="fa fa-list" aria-hidden="true" style="margin-right: 2px;"></i>
@@ -1141,11 +1255,13 @@
ng-model="ctrl.formValues.PublishingType"
ng-change="ctrl.onChangePublishedPorts()"
ng-disabled="ctrl.isPublishingTypeEditDisabled()"
data-cy="k8sAppCreate-internalPublishButton"
/>
<label
for="publishing_internal"
ng-if="
!ctrl.isPublishingTypeEditDisabled() || (ctrl.isPublishingTypeEditDisabled() && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INTERNAL)
!ctrl.isPublishingTypeEditDisabled() ||
(ctrl.isPublishingTypeEditDisabled() && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INTERNAL)
"
>
<div class="boxselector_header">
@@ -1179,11 +1295,13 @@
ng-model="ctrl.formValues.PublishingType"
ng-change="ctrl.onChangePublishedPorts()"
ng-disabled="ctrl.isPublishingTypeEditDisabled()"
data-cy="k8sAppCreate-clusterPublishButton"
/>
<label
for="publishing_cluster"
ng-if="
!ctrl.isPublishingTypeEditDisabled() || (ctrl.isPublishingTypeEditDisabled() && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.CLUSTER)
!ctrl.isPublishingTypeEditDisabled() ||
(ctrl.isPublishingTypeEditDisabled() && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.CLUSTER)
"
>
<div class="boxselector_header">
@@ -1216,11 +1334,13 @@
ng-model="ctrl.formValues.PublishingType"
ng-change="ctrl.onChangePublishedPorts()"
ng-disabled="ctrl.isPublishingTypeEditDisabled()"
data-cy="k8sAppCreate-ingressPublishButton"
/>
<label
for="publishing_ingress"
ng-if="
!ctrl.isPublishingTypeEditDisabled() || (ctrl.isPublishingTypeEditDisabled() && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS)
!ctrl.isPublishingTypeEditDisabled() ||
(ctrl.isPublishingTypeEditDisabled() && ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS)
"
>
<div class="boxselector_header">
@@ -1253,6 +1373,7 @@
ng-model="ctrl.formValues.PublishingType"
ng-change="ctrl.onChangePublishedPorts()"
ng-disabled="ctrl.isPublishingTypeEditDisabled()"
data-cy="k8sAppCreate-lbPublichButton"
/>
<label
for="publishing_loadbalancer"
@@ -1291,7 +1412,7 @@
<div class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Published ports</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addPublishedPort()" ng-if="!ctrl.disableLoadBalancerEdit()">
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addPublishedPort()" data-cy="k8sAppCreate-addNewPortButton">
<i class="fa fa-plus-circle" aria-hidden="true"></i> publish a new port
</span>
</div>
@@ -1340,6 +1461,7 @@
ng-required="!publishedPort.NeedsDeletion"
ng-change="ctrl.onChangePortMappingContainerPort()"
ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
data-cy="k8sAppCreate-containerPort_{{ $index }}"
/>
</div>
@@ -1362,7 +1484,8 @@
ng-max="32767"
ng-change="ctrl.onChangePortMappingNodePort()"
ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
/>
data-cy="k8sAppCreate-nodePort_{{ $index }}"
/>
</div>
<div
@@ -1386,6 +1509,7 @@
ng-required="!publishedPort.NeedsDeletion"
ng-change="ctrl.onChangePortMappingLoadBalancer()"
ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
data-cy="k8sAppCreate-lbPortInput_{{ $index }}"
/>
</div>
@@ -1406,6 +1530,7 @@
ng-required="!publishedPort.NeedsDeletion"
ng-change="ctrl.onChangePortMappingIngress($index)"
ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
data-cy="k8sAppCreate-ingressSelect_{{ $index }}"
>
<option selected disabled hidden value="">Select an ingress</option>
</select>
@@ -1427,6 +1552,7 @@
ng-options="host as (host | kubernetesApplicationIngressEmptyHostname) for host in publishedPort.IngressHosts"
ng-change="ctrl.onChangePublishedPorts()"
ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
data-cy="k8sAppCreate-hostnameSelect_{{ $index }}"
>
<option selected disabled hidden value="">Select a hostname</option>
</select>
@@ -1450,6 +1576,7 @@
ng-change="ctrl.onChangePortMappingIngressRoute()"
ng-pattern="/^(\/?[a-zA-Z0-9]+([a-zA-Z0-9-/_]*[a-zA-Z0-9])?|[a-zA-Z0-9]+)|(\/){1}$/"
ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)"
data-cy="k8sAppCreate-ingressRoute_{{ $index }}"
/>
</div>
@@ -1461,6 +1588,7 @@
uib-btn-radio="'TCP'"
ng-change="ctrl.onChangePortProtocol($index)"
ng-disabled="ctrl.isProtocolOptionDisabled($index, 'TCP')"
data-cy="k8sAppCreate-TCPButton_{{ $index }}"
>TCP</label
>
<label
@@ -1469,6 +1597,7 @@
uib-btn-radio="'UDP'"
ng-change="ctrl.onChangePortProtocol($index)"
ng-disabled="ctrl.isProtocolOptionDisabled($index, 'UDP')"
data-cy="k8sAppCreate-UDPButton_{{ $index }}"
>UDP</label
>
</div>
@@ -1477,6 +1606,7 @@
class="btn btn-sm btn-danger"
type="button"
ng-click="ctrl.removePublishedPort($index)"
data-cy="k8sAppCreate-rmPortButton_{{ $index }}"
>
<i class="fa fa-trash-alt" aria-hidden="true"></i>
</button>
@@ -1485,6 +1615,7 @@
class="btn btn-sm btn-primary"
type="button"
ng-click="ctrl.restorePublishedPort($index)"
data-cy="k8sAppCreate-restorePortButton_{{ $index }}"
>
<i class="fa fa-trash-restore" aria-hidden="true"></i>
</button>
@@ -1564,8 +1695,8 @@
<div ng-messages="kubernetesApplicationCreationForm['ingress_route_'+$index].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Route is required.</p>
<p ng-message="pattern"
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field must consist of alphanumeric characters or the special characters: '-', '_' or
'/'. It must start and end with an alphanumeric character (e.g. 'my-route', or 'route-123').</p
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field must consist of alphanumeric characters or the special characters: '-', '_'
or '/'. It must start and end with an alphanumeric character (e.g. 'my-route', or 'route-123').</p
>
</div>
<p ng-if="ctrl.state.duplicates.publishedPorts.ingressRoutes.refs[$index] !== undefined">
@@ -1608,7 +1739,7 @@
form-values="ctrl.formValues"
old-form-values="ctrl.savedFormValues"
></kubernetes-summary-view>
</div>
</div>
</div>
<div class="col-sm-12 form-section-title" ng-if="ctrl.state.appType !== ctrl.KubernetesDeploymentTypes.GIT">
@@ -1636,6 +1767,7 @@
type="button"
class="btn btn-sm btn-default"
ui-sref="kubernetes.applications.application({ name: ctrl.application.Name, namespace: ctrl.application.ResourcePool })"
data-cy="k8sAppCreate-appCancelButton"
>
Cancel
</button>

View File

@@ -169,7 +169,7 @@ class KubernetesCreateApplicationController {
this.state.updateWebEditorInProgress = true;
await this.StackService.updateKubeStack({ EndpointId: this.endpoint.Id, Id: this.application.StackId }, this.stackFileContent, null);
this.state.isEditorDirty = false;
await this.$state.reload();
await this.$state.reload(this.$state.current);
} catch (err) {
this.Notifications.error('Failure', err, 'Failed redeploying application');
} finally {

View File

@@ -26,7 +26,7 @@
</tr>
<tr>
<td>Stack</td>
<td>{{ ctrl.application.StackName || '-' }}</td>
<td data-cy="k8sAppDetail-stackName">{{ ctrl.application.StackName || '-' }}</td>
</tr>
<tr>
<td>Namespace</td>
@@ -37,15 +37,15 @@
</tr>
<tr>
<td>Application Type</td>
<td>
<td data-cy="k8sAppDetail-appType">
{{ ctrl.application.ApplicationType | kubernetesApplicationTypeText }}
</td>
</tr>
<tr>
<td>Status</td>
<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>
<span ng-if="ctrl.application.DeploymentType === ctrl.KubernetesApplicationDeploymentTypes.REPLICATED" data-cy="k8sAppDetail-deployType">Replicated</span>
<span ng-if="ctrl.application.DeploymentType === ctrl.KubernetesApplicationDeploymentTypes.GLOBAL" data-cy="k8sAppDetail-appType">Global</span>
<code data-cy="k8sAppDetail-runningPods">{{ ctrl.application.RunningPodsCount }}</code> /
<code data-cy="k8sAppDetail-totalPods">{{ ctrl.application.TotalPodsCount }}</code>
</td>
@@ -59,8 +59,10 @@
<div ng-if="ctrl.application.ApplicationType !== ctrl.KubernetesApplicationTypes.POD" class="text-muted small"> per instance </div>
</td>
<td>
<div ng-if="ctrl.application.Requests.Cpu">CPU {{ ctrl.application.Requests.Cpu | kubernetesApplicationCPUValue }}</div>
<div ng-if="ctrl.application.Requests.Memory">Memory {{ ctrl.application.Requests.Memory | humansize }}</div>
<div ng-if="ctrl.application.Requests.Cpu" data-cy="k8sAppDetail-cpuReservation"
>CPU {{ ctrl.application.Requests.Cpu | kubernetesApplicationCPUValue }}</div
>
<div ng-if="ctrl.application.Requests.Memory" data-cy="k8sAppDetail-memoryReservation">Memory {{ ctrl.application.Requests.Memory | humansize }}</div>
</td>
</tr>
<tr>
@@ -79,7 +81,7 @@
<div class="form-group">
<div class="col-sm-12">
<i class="fa fa-edit" aria-hidden="true"></i> Note
<button class="btn btn-xs btn-primary" ng-click="ctrl.state.expandedNote = !ctrl.state.expandedNote;"
<button class="btn btn-xs btn-primary" ng-click="ctrl.state.expandedNote = !ctrl.state.expandedNote;" data-cy="k8sAppDetail-expandNoteButton"
>{{ ctrl.state.expandedNote ? 'Collapse' : 'Expand' }}
<i class="fas {{ ctrl.state.expandedNote ? 'fa-angle-up' : 'fa-angle-down' }}" aria-hidden="true"></i
></button>
@@ -105,6 +107,7 @@
type="button"
ng-click="ctrl.updateApplication()"
ng-disabled="ctrl.formValues.Note === ctrl.application.Note"
data-cy="k8sAppDetail-saveNoteButton"
>{{ ctrl.application.Note ? 'Update' : 'Save' }} note</button
>
</div>
@@ -195,7 +198,14 @@
<rd-widget>
<rd-widget-body>
<div ng-if="!ctrl.isSystemNamespace()" style="margin-bottom: 15px;">
<button ng-if="!ctrl.isExternalApplication()" type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.applications.application.edit" style="margin-left: 0;">
<button
ng-if="!ctrl.isExternalApplication()"
type="button"
class="btn btn-sm btn-primary"
ui-sref="kubernetes.applications.application.edit"
style="margin-left: 0;"
data-cy="k8sAppDetail-editAppButton"
>
<i class="fa fa-file-code space-right" aria-hidden="true"></i>Edit this application
</button>
<button
@@ -204,6 +214,7 @@
class="btn btn-sm btn-primary"
style="margin-left: 0;"
ng-click="ctrl.redeployApplication()"
data-cy="k8sAppDetail-redeployButton"
>
<i class="fa fa-redo space-right" aria-hidden="true"></i>Redeploy
</button>
@@ -214,6 +225,7 @@
style="margin-left: 0;"
ng-click="ctrl.rollbackApplication()"
ng-disabled="ctrl.application.Revisions.length < 2 || ctrl.state.appType !== ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"
data-cy="k8sAppDetail-rollbackButton"
>
<i class="fas fa-history space-right" aria-hidden="true"></i>Rollback to previous configuration
</button>
@@ -327,7 +339,7 @@
<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">
<span ng-if="ctrl.application.ServiceType !== ctrl.KubernetesServiceTypes.NODE_PORT" data-cy="k8sAppDetail-containerPort">
{{ port.Port }}
</span>
<a
@@ -335,6 +347,7 @@
ng-href="http://{{ ctrl.application.LoadBalancerIPAddress }}:{{ port.Port }}"
target="_blank"
style="margin-left: 5px;"
data-cy="k8sAppDetail-accessLink"
>
<i class="fa fa-external-link-alt" aria-hidden="true"></i> access
</a>
@@ -342,12 +355,12 @@
<td ng-if="!ctrl.portHasIngressRules(port)">-</td>
</tr>
<tr ng-repeat-end ng-repeat="rule in port.IngressRules">
<td>{{ port.TargetPort }}/{{ port.Protocol }}</td>
<td data-cy="k8sAppDetail-httpRoute">{{ port.TargetPort }}/{{ port.Protocol }}</td>
<td>
<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">
<span ng-if="ctrl.application.ServiceType !== ctrl.KubernetesServiceTypes.NODE_PORT" data-cy="k8sAppDetail-port">
{{ port.Port }}
</span>
<a
@@ -371,7 +384,7 @@
>pending
</span>
<span ng-if="ctrl.ruleCanBeDisplayed(rule)">
<a ng-href="{{ ctrl.buildIngressRuleURL(rule) }}" target="_blank">
<a ng-href="{{ ctrl.buildIngressRuleURL(rule) }}" target="_blank" data-cy="k8sAppDetail-httpRouteLink">
{{ ctrl.buildIngressRuleURL(rule) | stripprotocol }}
</a>
</span>
@@ -404,9 +417,9 @@
</td>
</tr>
<tr>
<td>{{ ctrl.application.AutoScaler.MinReplicas }}</td>
<td>{{ ctrl.application.AutoScaler.MaxReplicas }}</td>
<td>{{ ctrl.application.AutoScaler.TargetCPUUtilization }}%</td>
<td data-cy="k8sAppDetail-minReplicas">{{ ctrl.application.AutoScaler.MinReplicas }}</td>
<td data-cy="k8sAppDetail-maxReplicas">{{ ctrl.application.AutoScaler.MaxReplicas }}</td>
<td data-cy="k8sAppDetail-targetCPU">{{ ctrl.application.AutoScaler.TargetCPUUtilization }}%</td>
</tr>
</tbody>
</table>
@@ -431,7 +444,7 @@
</tr>
<tbody ng-repeat="container in ctrl.application.Containers" style="border-top: 0;">
<tr ng-repeat="envvar in container.Env | orderBy: 'name'">
<td>
<td data-cy="k8sAppDetail-containerName">
{{ container.Name }}
<span ng-if="container.Type === ctrl.KubernetesPodContainerTypes.INIT"
><i class="fa fa-asterisk" aria-hidden="true"></i> {{ envvar.valueFrom.fieldRef.fieldPath }} (<a
@@ -441,12 +454,16 @@
>)</span
>
</td>
<td>{{ envvar.name }}</td>
<td data-cy="k8sAppDetail-envVarName">{{ envvar.name }}</td>
<td>
<span ng-if="envvar.value">{{ envvar.value }}</span>
<span ng-if="envvar.valueFrom.configMapKeyRef"><i class="fa fa-key" aria-hidden="true"></i> {{ envvar.valueFrom.configMapKeyRef.key }}</span>
<span ng-if="envvar.valueFrom.secretKeyRef"><i class="fa fa-key" aria-hidden="true"></i> {{ envvar.valueFrom.secretKeyRef.key }}</span>
<span ng-if="envvar.valueFrom.fieldRef"
<span ng-if="envvar.value" data-cy="k8sAppDetail-envVarValue">{{ envvar.value }}</span>
<span ng-if="envvar.valueFrom.configMapKeyRef" data-cy="k8sAppDetail-envVarValue"
><i class="fa fa-key" aria-hidden="true"></i> {{ envvar.valueFrom.configMapKeyRef.key }}</span
>
<span ng-if="envvar.valueFrom.secretKeyRef" data-cy="k8sAppDetail-envVarValue"
><i class="fa fa-key" aria-hidden="true"></i> {{ envvar.valueFrom.secretKeyRef.key }}</span
>
<span ng-if="envvar.valueFrom.fieldRef" data-cy="k8sAppDetail-envVarValue"
><i class="fa fa-asterisk" aria-hidden="true"></i> {{ envvar.valueFrom.fieldRef.fieldPath }} (<a
href="https://kubernetes.io/docs/tasks/inject-data-application/downward-api-volume-expose-pod-information/#capabilities-of-the-downward-api"
target="_blank"
@@ -457,12 +474,12 @@
</td>
<td>
<span ng-if="envvar.value || envvar.valueFrom.fieldRef || (!envvar.valueFrom.secretKeyRef && !envvar.valueFrom.configMapKeyRef)">-</span>
<span ng-if="envvar.valueFrom.configMapKeyRef"
<span ng-if="envvar.valueFrom.configMapKeyRef" data-cy="k8sAppDetail-configName"
><a ui-sref="kubernetes.configurations.configuration({ name: envvar.valueFrom.configMapKeyRef.name, namespace: ctrl.application.ResourcePool })"
><i class="fa fa-file-code" aria-hidden="true"></i> {{ envvar.valueFrom.configMapKeyRef.name }}</a
></span
>
<span ng-if="envvar.valueFrom.secretKeyRef"
<span ng-if="envvar.valueFrom.secretKeyRef" data-cy="k8sAppDetail-configName"
><a ui-sref="kubernetes.configurations.configuration({ name: envvar.valueFrom.secretKeyRef.name, namespace: ctrl.application.ResourcePool })"
><i class="fa fa-file-code" aria-hidden="true"></i> {{ envvar.valueFrom.secretKeyRef.name }}</a
></span
@@ -529,11 +546,11 @@
</tr>
<tbody ng-repeat="container in ctrl.application.Containers" style="border-top: 0;">
<tr ng-repeat="volume in container.PersistedFolders track by $index">
<td>
<td data-cy="k8sAppDetail-volMountPath">
{{ volume.MountPath }}
</td>
<td ng-if="volume.PersistentVolumeClaimName">
<a ui-sref="kubernetes.volumes.volume({ name: volume.PersistentVolumeClaimName, namespace: ctrl.application.ResourcePool })"
<a ui-sref="kubernetes.volumes.volume({ name: volume.PersistentVolumeClaimName, namespace: ctrl.application.ResourcePool })" data-cy="k8sAppDetail-volClaimName"
><i class="fa fa-database" aria-hidden="true"></i> {{ volume.PersistentVolumeClaimName }}</a
>
</td>

View File

@@ -215,7 +215,7 @@ class KubernetesApplicationController {
const revision = _.nth(this.application.Revisions, -2);
await this.KubernetesApplicationService.rollback(this.application, revision);
this.Notifications.success('Application successfully rolled back');
this.$state.reload();
this.$state.reload(this.$state.current);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to rollback the application');
}
@@ -236,7 +236,7 @@ class KubernetesApplicationController {
const promises = _.map(this.application.Pods, (item) => this.KubernetesPodService.delete(item));
await Promise.all(promises);
this.Notifications.success('Application successfully redeployed');
this.$state.reload();
this.$state.reload(this.$state.current);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to redeploy the application');
}
@@ -259,7 +259,7 @@ class KubernetesApplicationController {
application.Note = this.formValues.Note;
await this.KubernetesApplicationService.patch(this.application, application, true);
this.Notifications.success('Application successfully updated');
this.$state.reload();
this.$state.reload(this.$state.current);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to update application');
}

View File

@@ -179,7 +179,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -20,7 +20,7 @@ export default class KubernetesHelmApplicationController {
if (releases.length > 0) {
this.state.release = releases[0];
} else {
throw PortainerError(`Release ${this.state.params.name} not found`);
throw new PortainerError(`Release ${this.state.params.name} not found`);
}
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve helm application details');

View File

@@ -253,7 +253,7 @@ class KubernetesNodeController {
await this.drainNode();
}
this.Notifications.success('Node updated successfully');
this.$state.reload();
this.$state.reload(this.$state.current);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to update node');
}

View File

@@ -51,7 +51,7 @@ class KubernetesConfigurationsController {
} finally {
--actionCount;
if (actionCount === 0) {
this.$state.reload();
this.$state.reload(this.$state.current);
}
}
}

View File

@@ -107,7 +107,7 @@ class KubernetesConfigurationController {
} else {
await this.KubernetesConfigurationService.update(this.formValues, this.configuration);
this.Notifications.success('Configuration succesfully updated');
this.$state.reload();
this.$state.reload(this.$state.current);
}
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to update configuration');

View File

@@ -27,7 +27,9 @@
<label class="control-label text-left">
Allow users to use external load balancer
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ctrl.formValues.UseLoadBalancer" /><i></i> </label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="ctrl.formValues.UseLoadBalancer" /><i data-cy="kubeSetup-loadBalancerToggle"></i>
</label>
</div>
</div>
@@ -43,7 +45,7 @@
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">Ingress controller</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addIngressClass()">
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addIngressClass()" data-cy="kubeSetup-congifIngressButton">
<i class="fa fa-plus-circle" aria-hidden="true"></i> configure ingress controller
</span>
</div>
@@ -61,6 +63,7 @@
ng-pattern="/^[a-z]([-a-z0-9]*[a-z0-9])?$/"
ng-change="ctrl.onChangeIngressClassName($index)"
required
data-cy="kubeSetup-ingressClassName"
/>
</div>
<div class="col-sm-3 input-group input-group-sm" ng-class="{ striked: ingressClass.NeedsDeletion }">
@@ -71,12 +74,19 @@
ng-model="ingressClass.Type"
ng-options="value as value for (key, value) in ctrl.IngressClassTypes"
required
data-cy="kubeSetup-ingressType"
>
<option selected disabled hidden value="">Select a type</option>
</select>
</div>
<div class="col-sm-1 input-group input-group-sm">
<button ng-if="!ingressClass.NeedsDeletion" class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeIngressClass($index)">
<button
ng-if="!ingressClass.NeedsDeletion"
class="btn btn-sm btn-danger"
type="button"
ng-click="ctrl.removeIngressClass($index)"
data-cy="kubeSetup-deleteIngress"
>
<i class="fa fa-trash-alt" aria-hidden="true"></i>
</button>
<button ng-if="ingressClass.NeedsDeletion" class="btn btn-sm btn-primary" type="button" ng-click="ctrl.restoreIngressClass($index)">
@@ -145,7 +155,9 @@
<label class="control-label text-left">
Restrict access to the default namespace
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ctrl.formValues.RestrictDefaultNamespace" /><i></i> </label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="ctrl.formValues.RestrictDefaultNamespace" /><i data-cy="kubeSetup-restrictDefaultNsToggle"></i>
</label>
</div>
</div>
@@ -166,14 +178,14 @@
</div>
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Allow resource over-commit
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" checked 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-setup-overcommit" target="_blank"> Portainer Business Edition</a>.
</span>
<por-switch-field
label="Allow resource over-commit"
name="resource-over-commit-switch"
feature="'k8s-setup-default'"
ng-model="ctrl.formValues.EnableResourceOverCommit"
ng-change="ctrl.onChangeEnableResourceOverCommit()"
ng-data-cy="kubeSetup-resourceOverCommitToggle"
></por-switch-field>
</div>
</div>
@@ -193,7 +205,7 @@
Enable features using the metrics API
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="ctrl.formValues.UseServerMetrics" ng-change="ctrl.enableMetricsServer()" /><i></i>
<input type="checkbox" ng-model="ctrl.formValues.UseServerMetrics" ng-change="ctrl.enableMetricsServer()" /><i data-cy="kubeSetup-metricsToggle"></i>
</label>
</div>
<div ng-if="ctrl.state.metrics.pending && ctrl.state.metrics.userClick" class="col-sm-12 small text-muted" style="margin-top: 5px;">
@@ -252,7 +264,9 @@
<tr ng-repeat="class in ctrl.StorageClasses">
<td>
<div style="margin: 5px;">
<label class="switch" style="margin-right: 10px;"> <input type="checkbox" ng-model="class.selected" /><i></i> </label>
<label class="switch" style="margin-right: 10px;">
<input type="checkbox" ng-model="class.selected" /><i data-cy="kubeSetup-storageToggle{{ class.Name }}"></i>
</label>
<span>{{ class.Name }}</span>
</div>
</td>
@@ -267,12 +281,15 @@
directive-id="{{ class.Name }}"
helper-elements=""
translation="{nothingSelected: 'Not configured'}"
data-cy="kubeSetup-storageAccessSelect{{ class.Name }}"
>
</span>
</td>
<td>
<div style="margin: 5px;">
<label class="switch"><input type="checkbox" ng-model="class.AllowVolumeExpansion" /><i></i> </label>
<label class="switch"
><input type="checkbox" ng-model="class.AllowVolumeExpansion" /><i data-cy="kubeSetup-storageExpansionToggle{{ class.Name }}"></i>
</label>
</div>
</td>
</tr>
@@ -304,6 +321,7 @@
analytics-category="kubernetes"
analytics-event="kubernetes-configure"
analytics-properties="{ metadata: { restrictAccessToDefaultNamespace: ctrl.formValues.RestrictDefaultNamespace } }"
data-cy="kubeSetup-saveConfigurationButton"
>
<span ng-hide="ctrl.state.actionInProgress">Save configuration</span>
<span ng-show="ctrl.state.actionInProgress">Saving configuration...</span>

View File

@@ -98,7 +98,7 @@ class KubernetesResourcePoolAccessController {
const accessConfigMap = KubernetesConfigMapHelper.modifiyNamespaceAccesses(angular.copy(this.accessConfigMap), this.pool.Namespace.Name, newAccesses);
await this.KubernetesConfigMapService.updateAccess(accessConfigMap);
this.Notifications.success('Access successfully created');
this.$state.reload();
this.$state.reload(this.$state.current);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to create accesses');
}
@@ -118,7 +118,7 @@ class KubernetesResourcePoolAccessController {
const accessConfigMap = KubernetesConfigMapHelper.modifiyNamespaceAccesses(angular.copy(this.accessConfigMap), this.pool.Namespace.Name, newAccesses);
await this.KubernetesConfigMapService.updateAccess(accessConfigMap);
this.Notifications.success('Access successfully removed');
this.$state.reload();
this.$state.reload(this.$state.current);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to remove accesses');
} finally {

View File

@@ -162,14 +162,13 @@
</div>
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Load Balancer quota
</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>.
</span>
<por-switch-field
ng-data-cy="k8sNamespaceCreate-loadBalancerQuotaToggle"
label="Load Balancer quota"
name="k8s-resourcepool-Ibquota"
feature="'k8s-resourcepool-Ibquota'"
ng-model="lbquota"
></por-switch-field>
</div>
</div>
<!-- #endregion -->
@@ -192,15 +191,13 @@
</div>
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Enable quota
</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
<a href="https://www.portainer.io/business-upsell?from=k8s-resourcepool-storagequota" target="_blank"> Portainer Business Edition</a>.
</span>
<por-switch-field
ng-data-cy="k8sNamespaceCreate-enableQuotaToggle"
label="Enable quota"
name="k8s-resourcepool-storagequota"
feature="'k8s-resourcepool-storagequota'"
ng-model="storagequota"
></por-switch-field>
</div>
</div>
<!-- #endregion -->
@@ -213,8 +210,8 @@
<div class="form-group" ng-if="$ctrl.formValues.IngressClasses.length === 0">
<div class="col-sm-12 small text-muted">
The ingress feature must be enabled in the
<a ui-sref="portainer.endpoints.endpoint.kubernetesConfig({id: $ctrl.endpoint.Id})">environment configuration view</a> to be able to register ingresses inside this
namespace.
<a ui-sref="portainer.endpoints.endpoint.kubernetesConfig({id: $ctrl.endpoint.Id})">environment configuration view</a> to be able to register ingresses inside
this namespace.
</div>
</div>
@@ -238,7 +235,9 @@
<label class="control-label text-left">
Allow users to use this ingress
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ic.Selected" /><i></i> </label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="ic.Selected" /><i data-cy="namespaceCreate-ingressToggle{{ ic.IngressClass.Name }}"></i>
</label>
</div>
</div>
@@ -253,7 +252,12 @@
>
</portainer-tooltip>
</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addHostname(ic)">
<span
class="label label-default interactive"
style="margin-left: 10px;"
ng-click="$ctrl.addHostname(ic)"
data-cy="namespaceCreate-addHostButton{{ ic.IngressClass.Name }}"
>
<i class="fa fa-plus-circle" aria-hidden="true"></i> add hostname
</span>
</div>
@@ -269,7 +273,9 @@
ng-model="item.Host"
ng-change="$ctrl.onChangeIngressHostname()"
placeholder="foo"
pattern="[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*"
required
data-cy="namespaceCreate-hostnameInput{{ ic.IngressClass.Name }}_{{ $index }}"
/>
</div>
<div class="col-sm-1 input-group input-group-sm" ng-if="$index > 0">
@@ -288,6 +294,10 @@
>
<ng-messages for="resourcePoolCreationForm['hostname_' + ic.IngressClass.Name + '_' + $index].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Hostname is required.</p>
<p ng-message="pattern">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
This field must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com').
</p>
</ng-messages>
<p ng-if="$ctrl.state.duplicates.ingressHosts.refs[ic.IngressClass.Name][$index] !== undefined">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This hostname is already used.
@@ -306,17 +316,19 @@
>
</portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ic.RewriteTarget" /><i></i> </label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="ic.RewriteTarget" /><i data-cy="namespaceCreate-redirectRoutesToggle{{ ic.IngressClass.Name }}"></i>
</label>
</div>
</div>
</div>
<div ng-repeat-end class="form-group" ng-if="ic.Selected" style="margin-bottom: 20px;">
<div class="col-sm-12">
<p>
<a class="small interactive" ng-if="!ic.AdvancedConfig" ng-click="ic.AdvancedConfig = true">
<a class="small interactive" ng-if="!ic.AdvancedConfig" ng-click="ic.AdvancedConfig = true" data-cy="namespaceCreate-advancedConfig{{ ic.IngressClass.Name }}">
<i class="fa fa-plus space-right" aria-hidden="true"></i> Advanced configuration
</a>
<a class="small interactive" ng-if="ic.AdvancedConfig" ng-click="ic.AdvancedConfig = false">
<a class="small interactive" ng-if="ic.AdvancedConfig" ng-click="ic.AdvancedConfig = false" data-cy="namespaceCreate-hideConfig{{ ic.IngressClass.Name }}">
<i class="fa fa-minus space-right" aria-hidden="true"></i> Hide configuration
</a>
</p>
@@ -331,7 +343,12 @@
<div class="col-sm-12" ng-if="ic.AdvancedConfig">
<label class="control-label text-left">Annotations</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addAnnotation(ic)">
<span
class="label label-default interactive"
style="margin-left: 10px;"
ng-click="$ctrl.addAnnotation(ic)"
data-cy="namespaceCreate-addAnnotation{{ ic.IngressClass.Name }}"
>
<i class="fa fa-plus-circle" aria-hidden="true"></i> add annotation
</span>
</div>
@@ -340,14 +357,33 @@
<div ng-repeat="annotation in ic.Annotations track by $index" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">Key</span>
<input type="text" class="form-control" ng-model="annotation.Key" placeholder="nginx.ingress.kubernetes.io/rewrite-target" required />
<input
type="text"
class="form-control"
ng-model="annotation.Key"
placeholder="nginx.ingress.kubernetes.io/rewrite-target"
required
data-cy="namespaceCreate-annotationKey{{ ic.IngressClass.Name }}"
/>
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">Value</span>
<input type="text" class="form-control" ng-model="annotation.Value" placeholder="/$1" required />
<input
type="text"
class="form-control"
ng-model="annotation.Value"
placeholder="/$1"
required
data-cy="namespaceCreate-annotationValue{{ ic.IngressClass.Name }}"
/>
</div>
<div class="col-sm-1 input-group input-group-sm">
<button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removeAnnotation(ic, $index)">
<button
class="btn btn-sm btn-danger"
type="button"
ng-click="$ctrl.removeAnnotation(ic, $index)"
data-cy="namespaceCreate-deleteAnnotationButton{{ ic.IngressClass.Name }}"
>
<i class="fa fa-trash-alt" aria-hidden="true"></i>
</button>
</div>
@@ -392,6 +428,7 @@
helper-elements="filter"
search-property="Name"
translation="{nothingSelected: 'Select one or more registry', search: 'Search...'}"
data-cy="namespaceCreate-registrySelect"
>
</span>
</div>

View File

@@ -34,7 +34,7 @@ class KubernetesCreateResourcePoolController {
onChangeIngressHostname() {
const state = this.state.duplicates.ingressHosts;
const hosts = _.flatMap(this.formValues.IngressClasses, 'Hosts');
const hostnames = _.map(hosts, 'Host');
const hostnames = _.compact(hosts.map((h) => h.Host));
const hostnamesWithoutRemoved = _.filter(hostnames, (h) => !h.NeedsDeletion);
const allHosts = _.flatMap(this.allIngresses, 'Hosts');
const formDuplicates = KubernetesFormValidationHelper.getDuplicates(hostnamesWithoutRemoved);

View File

@@ -117,7 +117,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -221,6 +221,7 @@
ng-model="item.Host"
ng-change="ctrl.onChangeIngressHostname()"
placeholder="foo"
pattern="[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*"
required
/>
</div>
@@ -240,6 +241,11 @@
>
<ng-messages for="resourcePoolEditForm['hostname_' + ic.IngressClass.Name + '_' + $index].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Hostname is required.</p>
<p ng-message="pattern">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
This field must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g.
'example.com').
</p>
</ng-messages>
<p ng-if="item.Duplicate">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>

View File

@@ -176,7 +176,7 @@ class KubernetesResourcePoolController {
this.checkDefaults();
await this.KubernetesResourcePoolService.patch(oldFormValues, newFormValues);
this.Notifications.success('Namespace successfully updated', this.pool.Namespace.Name);
this.$state.reload();
this.$state.reload(this.$state.current);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to create namespace');
} finally {
@@ -238,7 +238,7 @@ class KubernetesResourcePoolController {
await this.KubernetesResourcePoolService.toggleSystem(this.endpoint.Id, namespaceName, !this.isSystem);
this.Notifications.success('Namespace successfully updated', namespaceName);
this.$state.reload();
this.$state.reload(this.$state.current);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to create namespace');
} finally {

View File

@@ -29,7 +29,7 @@ class KubernetesResourcePoolsController {
} finally {
--actionCount;
if (actionCount === 0) {
this.$state.reload();
this.$state.reload(this.$state.current);
}
}
}

View File

@@ -127,7 +127,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -13,14 +13,14 @@
<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-heading> <i class="fa fa-database space-right" aria-hidden="true"></i> Volume </uib-tab-heading>
<uib-tab-heading data-cy="k8sVolDetail-volTab"> <i class="fa fa-database space-right" aria-hidden="true"></i> Volume </uib-tab-heading>
<div style="padding: 20px;">
<table class="table">
<tbody>
<tr>
<td>Name</td>
<td>
<td data-cy="k8sVolDetail-volName">
{{ ctrl.volume.PersistentVolumeClaim.Name }}
<span class="label label-primary image-tag label-margins" ng-if="!ctrl.isSystemNamespace() && ctrl.isExternalVolume()">external</span>
<span class="label label-warning image-tag label-margins" ng-if="!ctrl.isSystemNamespace() && !ctrl.isUsed()">unused</span>
@@ -29,17 +29,19 @@
<tr>
<td>Namespace</td>
<td>
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.volume.ResourcePool.Namespace.Name })">{{ ctrl.volume.ResourcePool.Namespace.Name }}</a>
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.volume.ResourcePool.Namespace.Name })" data-cy="k8sVolDetail-volNamespace">{{
ctrl.volume.ResourcePool.Namespace.Name
}}</a>
<span style="margin-left: 5px;" class="label label-info image-tag" ng-if="ctrl.isSystemNamespace()">system</span>
</td>
</tr>
<tr>
<td>Storage</td>
<td>{{ ctrl.volume.PersistentVolumeClaim.StorageClass.Name }}</td>
<td data-cy="k8sVolDetail-volStorageClassname">{{ ctrl.volume.PersistentVolumeClaim.StorageClass.Name }}</td>
</tr>
<tr>
<td>Shared Access Policy</td>
<td
<td data-cy="k8sVolDetail-volAccessPolicy"
>{{ ctrl.state.volumeSharedAccessPolicy }}
<portainer-tooltip
position="bottom"
@@ -50,11 +52,13 @@
</tr>
<tr>
<td>Provisioner</td>
<td>{{ ctrl.volume.PersistentVolumeClaim.StorageClass.Provisioner ? ctrl.volume.PersistentVolumeClaim.StorageClass.Provisioner : '-' }}</td>
<td data-cy="k8sVolDetail-volProvisioner">{{
ctrl.volume.PersistentVolumeClaim.StorageClass.Provisioner ? ctrl.volume.PersistentVolumeClaim.StorageClass.Provisioner : '-'
}}</td>
</tr>
<tr>
<td>Creation date</td>
<td>{{ ctrl.volume.PersistentVolumeClaim.CreationDate | getisodate }}</td>
<td data-cy="k8sVolDetail-volCreatedAt">{{ ctrl.volume.PersistentVolumeClaim.CreationDate | getisodate }}</td>
</tr>
<tr>
<td>Size</td>
@@ -65,6 +69,7 @@
class="btn btn-sm btn-primary"
ng-click="ctrl.state.increaseSize = true"
ng-if="ctrl.volume.PersistentVolumeClaim.StorageClass.AllowVolumeExpansion"
data-cy="k8sVolDetail-increaseSizeButton"
>Increase size</button
>
</td>
@@ -82,6 +87,7 @@
min="0"
ng-change="ctrl.onChangeSize()"
required
data-cy="k8sVolDetail-increaseSizeInput"
/>
<span class="input-group-addon" style="padding: 0;">
<select
@@ -89,13 +95,20 @@
ng-change="ctrl.onChangeSize()"
ng-options="unit for unit in ctrl.state.availableSizeUnits"
style="width: 100%; height: 100%;"
data-cy="k8sVolDetail-increaseSizeUnits"
></select>
</span>
</div>
<button type="button" class="btn btn-sm btn-primary" ng-disabled="!ctrl.sizeIsValid()" ng-click="ctrl.updateVolume()">
<button
type="button"
class="btn btn-sm btn-primary"
ng-disabled="!ctrl.sizeIsValid()"
ng-click="ctrl.updateVolume()"
data-cy="k8sVolDetail-updateSizeConfirm"
>
Update size
</button>
<button type="button" class="btn btn-sm btn-default" ng-click="ctrl.state.increaseSize = false">
<button type="button" class="btn btn-sm btn-default" ng-click="ctrl.state.increaseSize = false" data-cy="k8sVolDetail-cancelUpdateSize">
Cancel
</button>
</div>
@@ -119,7 +132,7 @@
</uib-tab>
<uib-tab index="1" classes="btn-sm" select="ctrl.selectTab(1)">
<uib-tab-heading>
<uib-tab-heading data-cy="k8sVolDetail-volEventsTab">
<i class="fa fa-history space-right" aria-hidden="true"></i> Events
<div ng-if="ctrl.hasEventWarnings()">
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
@@ -140,7 +153,7 @@
</uib-tab>
<uib-tab index="2" ng-if="ctrl.volume.PersistentVolumeClaim.Yaml" select="ctrl.showEditor()" classes="btn-sm">
<uib-tab-heading> <i class="fa fa-code space-right" aria-hidden="true"></i> YAML </uib-tab-heading>
<uib-tab-heading data-cy="k8sVolDetail-volYamlTab"> <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="volume-yaml" data="ctrl.volume.PersistentVolumeClaim.Yaml"></kubernetes-yaml-inspector>
</div>

View File

@@ -96,7 +96,7 @@ class KubernetesVolumeController {
this.Notifications.success('Applications successfully redeployed');
}
this.$state.reload();
this.$state.reload(this.$state.current);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to update volume.');
}

View File

@@ -68,7 +68,7 @@ class KubernetesVolumesController {
} finally {
--actionCount;
if (actionCount === 0) {
this.$state.reload();
this.$state.reload(this.$state.current);
}
}
}

View File

@@ -1,7 +1,10 @@
import _ from 'lodash-es';
import './rbac';
import componentsModule from './components';
import settingsModule from './settings';
import featureFlagModule from './feature-flags';
import userActivityModule from './user-activity';
async function initAuthentication(authManager, Authentication, $rootScope, $state) {
authManager.checkAuthOnRefresh();
@@ -18,7 +21,7 @@ async function initAuthentication(authManager, Authentication, $rootScope, $stat
return await Authentication.init();
}
angular.module('portainer.app', ['portainer.oauth', componentsModule, settingsModule]).config([
angular.module('portainer.app', ['portainer.oauth', 'portainer.rbac', componentsModule, settingsModule, featureFlagModule, userActivityModule, 'portainer.shared.datatable']).config([
'$stateRegistryProvider',
function ($stateRegistryProvider) {
'use strict';
@@ -51,6 +54,18 @@ angular.module('portainer.app', ['portainer.oauth', componentsModule, settingsMo
controller: 'SidebarController',
},
},
resolve: {
featuresServiceInitialized: /* @ngInject */ function featuresServiceInitialized($async, featureService, Notifications) {
return $async(async () => {
try {
await featureService.init();
} catch (e) {
Notifications.error('Failed initializing features service', e);
throw e;
}
});
},
},
};
var endpointRoot = {
@@ -403,16 +418,6 @@ angular.module('portainer.app', ['portainer.oauth', componentsModule, settingsMo
},
};
var roles = {
name: 'portainer.roles',
url: '/roles',
views: {
'content@': {
templateUrl: './views/roles/roles.html',
},
},
};
$stateRegistryProvider.register(root);
$stateRegistryProvider.register(endpointRoot);
$stateRegistryProvider.register(portainer);
@@ -444,7 +449,6 @@ angular.module('portainer.app', ['portainer.oauth', componentsModule, settingsMo
$stateRegistryProvider.register(user);
$stateRegistryProvider.register(teams);
$stateRegistryProvider.register(team);
$stateRegistryProvider.register(roles);
},
]);

View File

@@ -81,7 +81,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -0,0 +1,18 @@
const BE_URL = 'https://www.portainer.io/business-upsell?from=';
export default class BeIndicatorController {
/* @ngInject */
constructor(featureService) {
Object.assign(this, { featureService });
this.limitedToBE = false;
}
$onInit() {
if (this.feature) {
this.url = `${BE_URL}${this.feature}`;
this.limitedToBE = this.featureService.isLimitedToBE(this.feature);
}
}
}

View File

@@ -0,0 +1,23 @@
.be-indicator {
border: solid 1px var(--BE-only);
border-radius: 15px;
padding: 5px 10px;
font-weight: 400;
}
.be-indicator .icon {
color: #000000;
}
.be-indicator:hover {
text-decoration: none;
}
.be-indicator:hover .be-indicator-label {
text-decoration: underline;
}
.be-indicator-container {
border: solid 1px var(--BE-only);
margin: 15px;
}

View File

@@ -0,0 +1,5 @@
<a class="be-indicator" href="{{ $ctrl.url }}" target="_blank" rel="noopener" ng-if="$ctrl.limitedToBE">
<ng-transclude></ng-transclude>
<i class="fas fa-briefcase space-right"></i>
<span class="be-indicator-label">Business Edition Feature</span>
</a>

View File

@@ -0,0 +1,15 @@
import angular from 'angular';
import controller from './be-feature-indicator.controller.js';
import './be-feature-indicator.css';
export const beFeatureIndicator = {
templateUrl: './be-feature-indicator.html',
controller,
bindings: {
feature: '<',
},
transclude: true,
};
angular.module('portainer.app').component('beFeatureIndicator', beFeatureIndicator);

View File

@@ -0,0 +1,14 @@
export default class BoxSelectorItemController {
/* @ngInject */
constructor(featureService) {
Object.assign(this, { featureService });
this.limitedToBE = false;
}
$onInit() {
if (this.option.feature) {
this.limitedToBE = this.featureService.isLimitedToBE(this.feature);
}
}
}

View File

@@ -0,0 +1,111 @@
.boxselector_wrapper > div,
.boxselector_wrapper box-selector-item {
--selected-item-color: var(--blue-2);
flex: 1;
padding: 0.5rem;
}
.boxselector_wrapper .boxselector_header {
font-size: 14px;
margin-bottom: 5px;
font-weight: bold;
user-select: none;
}
.boxselector_header .fa,
.fab {
font-weight: normal;
}
.boxselector_wrapper input[type='radio'] {
display: none;
}
.boxselector_wrapper input[type='radio']:not(:disabled) ~ label {
cursor: pointer;
background-color: var(--bg-boxselector-wrapper-disabled-color);
}
.boxselector_wrapper input[type='radio']:not(:disabled):hover ~ label:hover {
cursor: pointer;
}
.boxselector_wrapper label {
font-weight: normal;
font-size: 12px;
display: block;
background: var(--bg-boxselector-color);
border: 1px solid var(--border-boxselector-color);
border-radius: 2px;
padding: 10px 10px 0 10px;
text-align: center;
box-shadow: var(--shadow-boxselector-color);
position: relative;
}
.box-selector-item input:disabled + label,
.boxselector_wrapper label.boxselector_disabled {
background: var(--bg-boxselector-disabled-color) !important;
border-color: #787878;
color: #787878;
cursor: not-allowed;
pointer-events: none;
}
.boxselector_wrapper input[type='radio']:checked + label {
background: var(--selected-item-color);
color: white;
padding-top: 2rem;
border-color: var(--selected-item-color);
}
.boxselector_wrapper input[type='radio']:checked + label::after {
color: var(--selected-item-color);
font-family: 'Font Awesome 5 Free';
border: 2px solid var(--selected-item-color);
content: '\f00c';
font-size: 16px;
font-weight: bold;
position: absolute;
top: -15px;
left: 50%;
transform: translateX(-50%);
height: 30px;
width: 30px;
line-height: 26px;
text-align: center;
border-radius: 50%;
background: white;
box-shadow: 0 2px 5px -2px rgba(0, 0, 0, 0.25);
}
@media only screen and (max-width: 700px) {
.boxselector_wrapper {
flex-direction: column;
}
}
.box-selector-item-description {
height: 1em;
}
.box-selector-item.limited.business {
--selected-item-color: var(--BE-only);
}
.box-selector-item.limited.business label {
border-color: var(--BE-only);
border-width: 2px;
}
.box-selector-item .limited-icon {
position: absolute;
left: 3em;
top: calc(50% - 0.5em);
height: 1em;
}
.box-selector-item.limited.business :checked + label {
background-color: initial;
color: initial;
}

View File

@@ -1,5 +1,6 @@
<div
class="box-selector-item"
ng-class="{ business: $ctrl.limitedToBE, limited: $ctrl.limitedToBE }"
tooltip-append-to-body="true"
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
@@ -14,11 +15,13 @@
ng-value="$ctrl.option.value"
ng-disabled="$ctrl.disabled"
/>
<label for="{{ $ctrl.option.id }}" ng-click="$ctrl.onChange($ctrl.option.value)" t>
<label for="{{ $ctrl.option.id }}" ng-click="$ctrl.onChange($ctrl.option.value, $ctrl.limitedToBE)">
<i class="fas fa-briefcase limited-icon" ng-if="$ctrl.limitedToBE"></i>
<div class="boxselector_header">
<i ng-class="$ctrl.option.icon" aria-hidden="true" style="margin-right: 2px;"></i>
{{ $ctrl.option.label }}
</div>
<p ng-if="$ctrl.option.description">{{ $ctrl.option.description }}</p>
<p class="box-selector-item-description">{{ $ctrl.option.description }}</p>
</label>
</div>

View File

@@ -1,7 +1,12 @@
import angular from 'angular';
import './box-selector-item.css';
import controller from './box-selector-item.controller';
angular.module('portainer.app').component('boxSelectorItem', {
templateUrl: './box-selector-item.html',
controller,
bindings: {
radioName: '@',
isChecked: '<',

View File

@@ -4,10 +4,10 @@ export default class BoxSelectorController {
this.change = this.change.bind(this);
}
change(value) {
change(value, limited) {
this.ngModel = value;
if (this.onChange) {
this.onChange(value);
this.onChange(value, limited);
}
}

View File

@@ -3,89 +3,3 @@
flex-flow: row wrap;
margin: 0.5rem;
}
.boxselector_wrapper > div,
.boxselector_wrapper box-selector-item {
flex: 1;
padding: 0.5rem;
}
.boxselector_wrapper .boxselector_header {
font-size: 14px;
margin-bottom: 5px;
font-weight: bold;
user-select: none;
}
.boxselector_header .fa,
.fab {
font-weight: normal;
}
.boxselector_wrapper input[type='radio'] {
display: none;
}
.boxselector_wrapper input[type='radio']:not(:disabled) ~ label {
cursor: pointer;
background-color: var(--bg-boxselector-wrapper-disabled-color);
}
.boxselector_wrapper input[type='radio']:not(:disabled):hover ~ label:hover {
cursor: pointer;
}
.boxselector_wrapper label {
font-weight: normal;
font-size: 12px;
display: block;
background: var(--bg-boxselector-color);
border: 1px solid var(--border-boxselector-color);
border-radius: 2px;
padding: 10px 10px 0 10px;
text-align: center;
box-shadow: var(--shadow-boxselector-color);
position: relative;
}
.box-selector-item input:disabled + label,
.boxselector_wrapper label.boxselector_disabled {
background: var(--bg-boxselector-disabled-color) !important;
border-color: #787878;
color: #787878;
cursor: not-allowed;
pointer-events: none;
}
.boxselector_wrapper input[type='radio']:checked + label {
background: #337ab7;
color: white;
padding-top: 2rem;
border-color: #337ab7;
}
.boxselector_wrapper input[type='radio']:checked + label::after {
color: #337ab7;
font-family: 'Font Awesome 5 Free';
border: 2px solid #337ab7;
content: '\f00c';
font-size: 16px;
font-weight: bold;
position: absolute;
top: -15px;
left: 50%;
transform: translateX(-50%);
height: 30px;
width: 30px;
line-height: 26px;
text-align: center;
border-radius: 50%;
background: white;
box-shadow: 0 2px 5px -2px rgba(0, 0, 0, 0.25);
}
@media only screen and (max-width: 700px) {
.boxselector_wrapper {
flex-direction: column;
}
}

View File

@@ -15,6 +15,6 @@ angular.module('portainer.app').component('boxSelector', {
},
});
export function buildOption(id, icon, label, description, value) {
return { id, icon, label, description, value };
export function buildOption(id, icon, label, description, value, feature) {
return { id, icon, label, description, value, feature };
}

View File

@@ -1,26 +0,0 @@
<div class="datatable">
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" placeholder="Search..." ng-model-options="{ debounce: 300 }" />
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<thead>
<tr>
<th>
Environment
</th>
<th>
Role
</th>
<th>Access origin</th>
</tr>
</thead>
<tbody>
<tr ng-if="!$ctrl.dataset">
<td colspan="3" class="text-center text-muted">Select a user to show associated access and role</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@@ -92,6 +92,10 @@
float: none;
}
.datatable.datatable-empty .table > tbody > tr > td {
padding: 15px 0;
}
.tableMenu {
color: #767676;
padding: 10px;

View File

@@ -115,7 +115,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -0,0 +1,19 @@
export default class DatatableFilterController {
isEnabled() {
return 0 < this.state.length && this.state.length < this.labels.length;
}
onChangeItem(filterValue) {
if (this.isChecked(filterValue)) {
return this.onChange(
this.filterKey,
this.state.filter((v) => v !== filterValue)
);
}
return this.onChange(this.filterKey, [...this.state, filterValue]);
}
isChecked(filterValue) {
return this.state.includes(filterValue);
}
}

View File

@@ -0,0 +1,32 @@
<div uib-dropdown dropdown-append-to-body auto-close="outsideClick" is-open="$ctrl.isOpen">
<span ng-transclude></span>
<div class="filter-button">
<span uib-dropdown-toggle class="table-filter" ng-class="{ 'filter-active': $ctrl.isEnabled() }">
Filter
<i class="fa" ng-class="[$ctrl.isEnabled() ? 'fa-check' : 'fa-filter']" aria-hidden="true"></i>
</span>
</div>
<div class="dropdown-menu" style="min-width: 0;" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuContent">
<div class="md-checkbox" ng-repeat="filter in $ctrl.labels track by filter.value">
<input
id="filter_{{ $ctrl.filterKey }}_{{ $index }}"
type="checkbox"
ng-value="filter.value"
ng-checked="$ctrl.state.includes(filter.value)"
ng-click="$ctrl.onChangeItem(filter.value)"
/>
<label for="filter_{{ $ctrl.filterKey }}_{{ $index }}">
{{ filter.label }}
</label>
</div>
</div>
<div>
<a class="btn btn-default btn-sm" ng-click="$ctrl.isOpen = false;">
Close
</a>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,13 @@
import controller from './datatable-filter.controller';
export const datatableFilter = {
bindings: {
labels: '<', // [{label, value}]
state: '<', // [filterValue]
filterKey: '@',
onChange: '<',
},
controller,
templateUrl: './datatable-filter.html',
transclude: true,
};

View File

@@ -128,7 +128,11 @@ angular.module('portainer.app').controller('GenericDatatableController', [
* https://github.com/portainer/portainer/pull/2877#issuecomment-503333425
* https://github.com/portainer/portainer/pull/2877#issuecomment-503537249
*/
this.$onInit = function () {
this.$onInit = function $onInit() {
this.$onInitGeneric();
};
this.$onInitGeneric = function $onInitGeneric() {
this.setDefaults();
this.prepareTableFromDataset();

View File

@@ -82,7 +82,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="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>

View File

@@ -0,0 +1,16 @@
import angular from 'angular';
import 'angular-utils-pagination';
import { datatableTitlebar } from './titlebar';
import { datatableSearchbar } from './searchbar';
import { datatableSortIcon } from './sort-icon';
import { datatablePagination } from './pagination';
import { datatableFilter } from './filter';
export default angular
.module('portainer.shared.datatable', ['angularUtils.directives.dirPagination'])
.component('datatableTitlebar', datatableTitlebar)
.component('datatableSearchbar', datatableSearchbar)
.component('datatableSortIcon', datatableSortIcon)
.component('datatablePagination', datatablePagination)
.component('datatableFilter', datatableFilter).name;

Some files were not shown because too many files have changed in this diff Show More