Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c1977e0aa | |||
| c47fd9f9ed | |||
| 767d1d1970 | |||
| ef81e5c0e0 | |||
| 234b7a3d5e | |||
| af49305e64 | |||
| d181d1251c | |||
| 5f7db66e95 | |||
| 17378bdef6 | |||
| 010542ac1e | |||
| 1bb253479a | |||
| f0a13a2ad1 | |||
| f9b28aa0a1 | |||
| d26e1b6983 | |||
| 7b00fdd208 | |||
| 14b998d270 | |||
| 605ff8c1da | |||
| 13f93f4262 | |||
| 16be5ed329 | |||
| c6612898f3 | |||
| 564f34b0ba | |||
| 392fbdb4a7 | |||
| a826c78786 | |||
| a35f0607f1 | |||
| 081d32af0d | |||
| 4cc0b1f567 | |||
| d4da7e1760 | |||
| aced418880 | |||
| 614f42fe5a | |||
| 58736fe93b | |||
| b78330b10d | |||
| eed4a92ca8 | |||
| 0e7468a1e8 | |||
| b807481f1c | |||
| da27de2154 | |||
| 6743e4fbb2 | |||
| b489ffaa63 | |||
| 6e12499d61 | |||
| f7acbe16ba |
+19
-1
@@ -103,8 +103,26 @@ func (store *Store) backupWithOptions(options *BackupOptions) (string, error) {
|
|||||||
store.createBackupFolders()
|
store.createBackupFolders()
|
||||||
|
|
||||||
options = store.setupOptions(options)
|
options = store.setupOptions(options)
|
||||||
|
dbPath := store.databasePath()
|
||||||
|
|
||||||
return options.BackupPath, store.copyDBFile(store.databasePath(), options.BackupPath)
|
if err := store.Close(); err != nil {
|
||||||
|
return options.BackupPath, fmt.Errorf(
|
||||||
|
"error closing datastore before creating backup: %v",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := store.copyDBFile(dbPath, options.BackupPath); err != nil {
|
||||||
|
return options.BackupPath, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := store.Open(); err != nil {
|
||||||
|
return options.BackupPath, fmt.Errorf(
|
||||||
|
"error opening datastore after creating backup: %v",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return options.BackupPath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RestoreWithOptions previously saved backup for the current Edition with options
|
// RestoreWithOptions previously saved backup for the current Edition with options
|
||||||
|
|||||||
@@ -35,6 +35,12 @@
|
|||||||
"TenantID": ""
|
"TenantID": ""
|
||||||
},
|
},
|
||||||
"ComposeSyntaxMaxVersion": "",
|
"ComposeSyntaxMaxVersion": "",
|
||||||
|
"Edge": {
|
||||||
|
"AsyncMode": false,
|
||||||
|
"CommandInterval": 0,
|
||||||
|
"PingInterval": 0,
|
||||||
|
"SnapshotInterval": 0
|
||||||
|
},
|
||||||
"EdgeCheckinInterval": 0,
|
"EdgeCheckinInterval": 0,
|
||||||
"EdgeKey": "",
|
"EdgeKey": "",
|
||||||
"GroupId": 1,
|
"GroupId": 1,
|
||||||
@@ -682,6 +688,12 @@
|
|||||||
"BlackListedLabels": [],
|
"BlackListedLabels": [],
|
||||||
"DisplayDonationHeader": false,
|
"DisplayDonationHeader": false,
|
||||||
"DisplayExternalContributors": false,
|
"DisplayExternalContributors": false,
|
||||||
|
"Edge": {
|
||||||
|
"AsyncMode": false,
|
||||||
|
"CommandInterval": 0,
|
||||||
|
"PingInterval": 0,
|
||||||
|
"SnapshotInterval": 0
|
||||||
|
},
|
||||||
"EdgeAgentCheckinInterval": 5,
|
"EdgeAgentCheckinInterval": 5,
|
||||||
"EdgePortainerUrl": "",
|
"EdgePortainerUrl": "",
|
||||||
"EnableEdgeComputeFeatures": false,
|
"EnableEdgeComputeFeatures": false,
|
||||||
@@ -898,7 +910,7 @@
|
|||||||
],
|
],
|
||||||
"version": {
|
"version": {
|
||||||
"DB_UPDATING": "false",
|
"DB_UPDATING": "false",
|
||||||
"DB_VERSION": "50",
|
"DB_VERSION": "52",
|
||||||
"INSTANCE_ID": "null"
|
"INSTANCE_ID": "null"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
|
|||||||
}
|
}
|
||||||
|
|
||||||
filePaths := stackutils.GetStackFilePaths(stack)
|
filePaths := stackutils.GetStackFilePaths(stack)
|
||||||
err = manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths)
|
err = manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths, "")
|
||||||
return errors.Wrap(err, "failed to remove a stack")
|
return errors.Wrap(err, "failed to remove a stack")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ require (
|
|||||||
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
|
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
|
||||||
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
|
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/portainer/docker-compose-wrapper v0.0.0-20220531190153-c597b853e410
|
github.com/portainer/docker-compose-wrapper v0.0.0-20220708023447-a69a4ebaa021
|
||||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
|
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
|
||||||
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108
|
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108
|
||||||
github.com/portainer/libhttp v0.0.0-20211208103139-07a5f798eb3f
|
github.com/portainer/libhttp v0.0.0-20211208103139-07a5f798eb3f
|
||||||
|
|||||||
+2
-4
@@ -807,10 +807,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/portainer/docker-compose-wrapper v0.0.0-20220526210722-e1574867298e h1:gW1Ooaj7RZ9YkwHxesnNEyOB5nUD71FlZ7cdb5h63vw=
|
github.com/portainer/docker-compose-wrapper v0.0.0-20220708023447-a69a4ebaa021 h1:GFTn2e5AyIoBuK6hXbdVNkuV2m450DQnYmgQDZRU3x8=
|
||||||
github.com/portainer/docker-compose-wrapper v0.0.0-20220526210722-e1574867298e/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c=
|
github.com/portainer/docker-compose-wrapper v0.0.0-20220708023447-a69a4ebaa021/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c=
|
||||||
github.com/portainer/docker-compose-wrapper v0.0.0-20220531190153-c597b853e410 h1:LjxLd8UGR8ae73ov/vLrt/0jedj/nh98XnONkr8DJj8=
|
|
||||||
github.com/portainer/docker-compose-wrapper v0.0.0-20220531190153-c597b853e410/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c=
|
|
||||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a h1:qY8TbocN75n5PDl16o0uVr5MevtM5IhdwSelXEd4nFM=
|
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a h1:qY8TbocN75n5PDl16o0uVr5MevtM5IhdwSelXEd4nFM=
|
||||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a/go.mod h1:n54EEIq+MM0NNtqLeCby8ljL+l275VpolXO0ibHegLE=
|
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a/go.mod h1:n54EEIq+MM0NNtqLeCby8ljL+l275VpolXO0ibHegLE=
|
||||||
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108 h1:5e8KAnDa2G3cEHK7aV/ue8lOaoQwBZUzoALslwWkR04=
|
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108 h1:5e8KAnDa2G3cEHK7aV/ue8lOaoQwBZUzoALslwWkR04=
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ type Handler struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @title PortainerCE API
|
// @title PortainerCE API
|
||||||
// @version 2.14.0
|
// @version 2.14.2
|
||||||
// @description.markdown api-description.md
|
// @description.markdown api-description.md
|
||||||
// @termsOfService
|
// @termsOfService
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package helm
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/portainer/libhelm"
|
"github.com/portainer/libhelm"
|
||||||
@@ -108,7 +107,7 @@ func (handler *Handler) getHelmClusterAccess(r *http.Request) (*options.Kubernet
|
|||||||
|
|
||||||
hostURL := "localhost"
|
hostURL := "localhost"
|
||||||
if !sslSettings.SelfSigned {
|
if !sslSettings.SelfSigned {
|
||||||
hostURL = strings.Split(r.Host, ":")[0]
|
hostURL = r.Host
|
||||||
}
|
}
|
||||||
|
|
||||||
kubeConfigInternal := handler.kubeClusterAccessService.GetData(hostURL, endpoint.ID)
|
kubeConfigInternal := handler.kubeClusterAccessService.GetData(hostURL, endpoint.ID)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
@@ -145,8 +144,7 @@ func (handler *Handler) buildConfig(r *http.Request, tokenData *portainer.TokenD
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) buildCluster(r *http.Request, endpoint portainer.Endpoint) clientV1.NamedCluster {
|
func (handler *Handler) buildCluster(r *http.Request, endpoint portainer.Endpoint) clientV1.NamedCluster {
|
||||||
hostURL := strings.Split(r.Host, ":")[0]
|
kubeConfigInternal := handler.kubeClusterAccessService.GetData(r.Host, endpoint.ID)
|
||||||
kubeConfigInternal := handler.kubeClusterAccessService.GetData(hostURL, endpoint.ID)
|
|
||||||
return clientV1.NamedCluster{
|
return clientV1.NamedCluster{
|
||||||
Name: buildClusterName(endpoint.Name),
|
Name: buildClusterName(endpoint.Name),
|
||||||
Cluster: clientV1.Cluster{
|
Cluster: clientV1.Cluster{
|
||||||
|
|||||||
@@ -30,6 +30,19 @@ type publicSettingsResponse struct {
|
|||||||
KubeconfigExpiry string `example:"24h" default:"0"`
|
KubeconfigExpiry string `example:"24h" default:"0"`
|
||||||
// Whether team sync is enabled
|
// Whether team sync is enabled
|
||||||
TeamSync bool `json:"TeamSync" example:"true"`
|
TeamSync bool `json:"TeamSync" example:"true"`
|
||||||
|
|
||||||
|
Edge struct {
|
||||||
|
// Whether the device has been started in edge async mode
|
||||||
|
AsyncMode bool
|
||||||
|
// The ping interval for edge agent - used in edge async mode [seconds]
|
||||||
|
PingInterval int `json:"PingInterval" example:"60"`
|
||||||
|
// The snapshot interval for edge agent - used in edge async mode [seconds]
|
||||||
|
SnapshotInterval int `json:"SnapshotInterval" example:"60"`
|
||||||
|
// The command list interval for edge agent - used in edge async mode [seconds]
|
||||||
|
CommandInterval int `json:"CommandInterval" example:"60"`
|
||||||
|
// The check in interval for edge agent (in seconds) - used in non async mode [seconds]
|
||||||
|
CheckinInterval int `example:"60"`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @id SettingsPublic
|
// @id SettingsPublic
|
||||||
@@ -61,6 +74,13 @@ func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResp
|
|||||||
KubeconfigExpiry: appSettings.KubeconfigExpiry,
|
KubeconfigExpiry: appSettings.KubeconfigExpiry,
|
||||||
Features: appSettings.FeatureFlagSettings,
|
Features: appSettings.FeatureFlagSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
publicSettings.Edge.AsyncMode = appSettings.Edge.AsyncMode
|
||||||
|
publicSettings.Edge.PingInterval = appSettings.Edge.PingInterval
|
||||||
|
publicSettings.Edge.SnapshotInterval = appSettings.Edge.SnapshotInterval
|
||||||
|
publicSettings.Edge.CommandInterval = appSettings.Edge.CommandInterval
|
||||||
|
publicSettings.Edge.CheckinInterval = appSettings.EdgeAgentCheckinInterval
|
||||||
|
|
||||||
//if OAuth authentication is on, compose the related fields from application settings
|
//if OAuth authentication is on, compose the related fields from application settings
|
||||||
if publicSettings.AuthenticationMethod == portainer.AuthenticationOAuth {
|
if publicSettings.AuthenticationMethod == portainer.AuthenticationOAuth {
|
||||||
publicSettings.OAuthLogoutURI = appSettings.OAuthSettings.LogoutURI
|
publicSettings.OAuthLogoutURI = appSettings.OAuthSettings.LogoutURI
|
||||||
@@ -75,8 +95,10 @@ func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
//if LDAP authentication is on, compose the related fields from application settings
|
//if LDAP authentication is on, compose the related fields from application settings
|
||||||
if publicSettings.AuthenticationMethod == portainer.AuthenticationLDAP {
|
if publicSettings.AuthenticationMethod == portainer.AuthenticationLDAP && appSettings.LDAPSettings.GroupSearchSettings != nil {
|
||||||
publicSettings.TeamSync = len(appSettings.LDAPSettings.GroupSearchSettings) > 0
|
if len(appSettings.LDAPSettings.GroupSearchSettings) > 0 {
|
||||||
|
publicSettings.TeamSync = len(appSettings.LDAPSettings.GroupSearchSettings[0].GroupBaseDN) > 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return publicSettings
|
return publicSettings
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,11 +24,14 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||||||
adminRouter := h.NewRoute().Subrouter()
|
adminRouter := h.NewRoute().Subrouter()
|
||||||
adminRouter.Use(bouncer.AdminAccess)
|
adminRouter.Use(bouncer.AdminAccess)
|
||||||
|
|
||||||
|
restrictedRouter := h.NewRoute().Subrouter()
|
||||||
|
restrictedRouter.Use(bouncer.RestrictedAccess)
|
||||||
|
|
||||||
teamLeaderRouter := h.NewRoute().Subrouter()
|
teamLeaderRouter := h.NewRoute().Subrouter()
|
||||||
teamLeaderRouter.Use(bouncer.TeamLeaderAccess)
|
teamLeaderRouter.Use(bouncer.TeamLeaderAccess)
|
||||||
|
|
||||||
adminRouter.Handle("/teams", httperror.LoggerHandler(h.teamCreate)).Methods(http.MethodPost)
|
adminRouter.Handle("/teams", httperror.LoggerHandler(h.teamCreate)).Methods(http.MethodPost)
|
||||||
teamLeaderRouter.Handle("/teams", httperror.LoggerHandler(h.teamList)).Methods(http.MethodGet)
|
restrictedRouter.Handle("/teams", httperror.LoggerHandler(h.teamList)).Methods(http.MethodGet)
|
||||||
teamLeaderRouter.Handle("/teams/{id}", httperror.LoggerHandler(h.teamInspect)).Methods(http.MethodGet)
|
teamLeaderRouter.Handle("/teams/{id}", httperror.LoggerHandler(h.teamInspect)).Methods(http.MethodGet)
|
||||||
adminRouter.Handle("/teams/{id}", httperror.LoggerHandler(h.teamUpdate)).Methods(http.MethodPut)
|
adminRouter.Handle("/teams/{id}", httperror.LoggerHandler(h.teamUpdate)).Methods(http.MethodPut)
|
||||||
adminRouter.Handle("/teams/{id}", httperror.LoggerHandler(h.teamDelete)).Methods(http.MethodDelete)
|
adminRouter.Handle("/teams/{id}", httperror.LoggerHandler(h.teamDelete)).Methods(http.MethodDelete)
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ func (handler *Handler) userUpdatePassword(w http.ResponseWriter, r *http.Reques
|
|||||||
|
|
||||||
err = handler.CryptoService.CompareHashAndData(user.Password, payload.Password)
|
err = handler.CryptoService.CompareHashAndData(user.Password, payload.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Specified password do not match actual password", httperrors.ErrUnauthorized}
|
return &httperror.HandlerError{http.StatusForbidden, "Current password doesn't match", errors.New("Current password does not match the password provided. Please try again")}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !handler.passwordStrengthChecker.Check(payload.NewPassword) {
|
if !handler.passwordStrengthChecker.Check(payload.NewPassword) {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type (
|
|||||||
}
|
}
|
||||||
|
|
||||||
portainerRegistryAuthenticationHeader struct {
|
portainerRegistryAuthenticationHeader struct {
|
||||||
RegistryId portainer.RegistryID `json:"registryId"`
|
RegistryId *portainer.RegistryID `json:"registryId"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -446,7 +446,20 @@ func (transport *Transport) decorateRegistryAuthenticationHeader(request *http.R
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticationHeader, err := createRegistryAuthenticationHeader(transport.dataStore, originalHeaderData.RegistryId, accessContext)
|
// delete header and exist function without error if Front End
|
||||||
|
// passes empty json. This is to restore original behavior which
|
||||||
|
// never originally passed this header
|
||||||
|
if string(decodedHeaderData) == "{}" {
|
||||||
|
request.Header.Del("X-Registry-Auth")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// only set X-Registry-Auth if registryId is defined
|
||||||
|
if originalHeaderData.RegistryId == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticationHeader, err := createRegistryAuthenticationHeader(transport.dataStore, *originalHeaderData.RegistryId, accessContext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,11 +81,11 @@ func FilterRegistries(registries []portainer.Registry, user *portainer.User, tea
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FilterEndpoints filters environments(endpoints) based on user role and team memberships.
|
// FilterEndpoints filters environments(endpoints) based on user role and team memberships.
|
||||||
// Non administrator and non-team-leader only have access to authorized environments(endpoints) (can be inherited via endpoint groups).
|
// Non administrator only have access to authorized environments(endpoints) (can be inherited via endpoint groups).
|
||||||
func FilterEndpoints(endpoints []portainer.Endpoint, groups []portainer.EndpointGroup, context *RestrictedRequestContext) []portainer.Endpoint {
|
func FilterEndpoints(endpoints []portainer.Endpoint, groups []portainer.EndpointGroup, context *RestrictedRequestContext) []portainer.Endpoint {
|
||||||
filteredEndpoints := endpoints
|
filteredEndpoints := endpoints
|
||||||
|
|
||||||
if !context.IsAdmin && !context.IsTeamLeader {
|
if !context.IsAdmin {
|
||||||
filteredEndpoints = make([]portainer.Endpoint, 0)
|
filteredEndpoints = make([]portainer.Endpoint, 0)
|
||||||
|
|
||||||
for _, endpoint := range endpoints {
|
for _, endpoint := range endpoints {
|
||||||
@@ -101,11 +101,11 @@ func FilterEndpoints(endpoints []portainer.Endpoint, groups []portainer.Endpoint
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FilterEndpointGroups filters environment(endpoint) groups based on user role and team memberships.
|
// FilterEndpointGroups filters environment(endpoint) groups based on user role and team memberships.
|
||||||
// Non administrator users and Non-team-leaders only have access to authorized environment(endpoint) groups.
|
// Non administrator users only have access to authorized environment(endpoint) groups.
|
||||||
func FilterEndpointGroups(endpointGroups []portainer.EndpointGroup, context *RestrictedRequestContext) []portainer.EndpointGroup {
|
func FilterEndpointGroups(endpointGroups []portainer.EndpointGroup, context *RestrictedRequestContext) []portainer.EndpointGroup {
|
||||||
filteredEndpointGroups := endpointGroups
|
filteredEndpointGroups := endpointGroups
|
||||||
|
|
||||||
if !context.IsAdmin && !context.IsTeamLeader {
|
if !context.IsAdmin {
|
||||||
filteredEndpointGroups = make([]portainer.EndpointGroup, 0)
|
filteredEndpointGroups = make([]portainer.EndpointGroup, 0)
|
||||||
|
|
||||||
for _, group := range endpointGroups {
|
for _, group := range endpointGroups {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// KubeClusterAccessService represents a service that is responsible for centralizing kube cluster access data
|
// KubeClusterAccessService represents a service that is responsible for centralizing kube cluster access data
|
||||||
@@ -94,11 +95,20 @@ func (service *kubeClusterAccessService) IsSecure() bool {
|
|||||||
// - pass down params to binaries
|
// - pass down params to binaries
|
||||||
func (service *kubeClusterAccessService) GetData(hostURL string, endpointID portainer.EndpointID) kubernetesClusterAccessData {
|
func (service *kubeClusterAccessService) GetData(hostURL string, endpointID portainer.EndpointID) kubernetesClusterAccessData {
|
||||||
baseURL := service.baseURL
|
baseURL := service.baseURL
|
||||||
|
|
||||||
|
// When the api call is internal, the baseURL should not be used.
|
||||||
|
if hostURL == "localhost" {
|
||||||
|
hostURL = hostURL + service.httpsBindAddr
|
||||||
|
baseURL = "/"
|
||||||
|
}
|
||||||
|
|
||||||
if baseURL != "/" {
|
if baseURL != "/" {
|
||||||
baseURL = fmt.Sprintf("/%s/", strings.Trim(baseURL, "/"))
|
baseURL = fmt.Sprintf("/%s/", strings.Trim(baseURL, "/"))
|
||||||
}
|
}
|
||||||
|
|
||||||
clusterURL := hostURL + service.httpsBindAddr + baseURL
|
logrus.Infof("[kubeconfig] [hostURL: %s, httpsBindAddr: %s, baseURL: %s]", hostURL, service.httpsBindAddr, baseURL)
|
||||||
|
|
||||||
|
clusterURL := hostURL + baseURL
|
||||||
|
|
||||||
clusterServerURL := fmt.Sprintf("https://%sapi/endpoints/%d/kubernetes", clusterURL, endpointID)
|
clusterServerURL := fmt.Sprintf("https://%sapi/endpoints/%d/kubernetes", clusterURL, endpointID)
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ func TestKubeClusterAccessService_GetKubeConfigInternal(t *testing.T) {
|
|||||||
clusterAccessDetails := kcs.GetData("mysite.com", 1)
|
clusterAccessDetails := kcs.GetData("mysite.com", 1)
|
||||||
|
|
||||||
wantClusterAccessDetails := kubernetesClusterAccessData{
|
wantClusterAccessDetails := kubernetesClusterAccessData{
|
||||||
ClusterServerURL: "https://mysite.com:9443/api/endpoints/1/kubernetes",
|
ClusterServerURL: "https://mysite.com/api/endpoints/1/kubernetes",
|
||||||
CertificateAuthorityFile: "",
|
CertificateAuthorityFile: "",
|
||||||
CertificateAuthorityData: "",
|
CertificateAuthorityData: "",
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-2
@@ -345,6 +345,17 @@ type (
|
|||||||
// Whether the device has been trusted or not by the user
|
// Whether the device has been trusted or not by the user
|
||||||
UserTrusted bool
|
UserTrusted bool
|
||||||
|
|
||||||
|
Edge struct {
|
||||||
|
// Whether the device has been started in edge async mode
|
||||||
|
AsyncMode bool
|
||||||
|
// The ping interval for edge agent - used in edge async mode [seconds]
|
||||||
|
PingInterval int `json:"PingInterval" example:"60"`
|
||||||
|
// The snapshot interval for edge agent - used in edge async mode [seconds]
|
||||||
|
SnapshotInterval int `json:"SnapshotInterval" example:"60"`
|
||||||
|
// The command list interval for edge agent - used in edge async mode [seconds]
|
||||||
|
CommandInterval int `json:"CommandInterval" example:"60"`
|
||||||
|
}
|
||||||
|
|
||||||
// Deprecated fields
|
// Deprecated fields
|
||||||
// Deprecated in DBVersion == 4
|
// Deprecated in DBVersion == 4
|
||||||
TLS bool `json:"TLS,omitempty"`
|
TLS bool `json:"TLS,omitempty"`
|
||||||
@@ -837,6 +848,17 @@ type (
|
|||||||
// EdgePortainerURL is the URL that is exposed to edge agents
|
// EdgePortainerURL is the URL that is exposed to edge agents
|
||||||
EdgePortainerURL string `json:"EdgePortainerUrl"`
|
EdgePortainerURL string `json:"EdgePortainerUrl"`
|
||||||
|
|
||||||
|
Edge struct {
|
||||||
|
// The command list interval for edge agent - used in edge async mode (in seconds)
|
||||||
|
CommandInterval int `json:"CommandInterval" example:"5"`
|
||||||
|
// The ping interval for edge agent - used in edge async mode (in seconds)
|
||||||
|
PingInterval int `json:"PingInterval" example:"5"`
|
||||||
|
// The snapshot interval for edge agent - used in edge async mode (in seconds)
|
||||||
|
SnapshotInterval int `json:"SnapshotInterval" example:"5"`
|
||||||
|
// EdgeAsyncMode enables edge async mode by default
|
||||||
|
AsyncMode bool
|
||||||
|
}
|
||||||
|
|
||||||
// Deprecated fields
|
// Deprecated fields
|
||||||
DisplayDonationHeader bool
|
DisplayDonationHeader bool
|
||||||
DisplayExternalContributors bool
|
DisplayExternalContributors bool
|
||||||
@@ -1363,9 +1385,9 @@ type (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// APIVersion is the version number of the Portainer API
|
// APIVersion is the version number of the Portainer API
|
||||||
APIVersion = "2.14.0"
|
APIVersion = "2.14.2"
|
||||||
// DBVersion is the version number of the Portainer database
|
// DBVersion is the version number of the Portainer database
|
||||||
DBVersion = 50
|
DBVersion = 52
|
||||||
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
|
||||||
ComposeSyntaxMaxVersion = "3.9"
|
ComposeSyntaxMaxVersion = "3.9"
|
||||||
// AssetsServerURL represents the URL of the Portainer asset server
|
// AssetsServerURL represents the URL of the Portainer asset server
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import { useSettings } from '@/portainer/settings/queries';
|
import { usePublicSettings } from '@/portainer/settings/queries';
|
||||||
|
|
||||||
const categories = [
|
const categories = [
|
||||||
'docker',
|
'docker',
|
||||||
@@ -64,7 +64,9 @@ export function push(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useAnalytics() {
|
export function useAnalytics() {
|
||||||
const telemetryQuery = useSettings((settings) => settings.EnableTelemetry);
|
const telemetryQuery = usePublicSettings({
|
||||||
|
select: (settings) => settings.EnableTelemetry,
|
||||||
|
});
|
||||||
|
|
||||||
return { trackEvent: handleTrackEvent };
|
return { trackEvent: handleTrackEvent };
|
||||||
|
|
||||||
|
|||||||
@@ -20,3 +20,4 @@ import './app.css';
|
|||||||
|
|
||||||
import './theme.css';
|
import './theme.css';
|
||||||
import './vendor-override.css';
|
import './vendor-override.css';
|
||||||
|
import '../fonts/nomad-icon.css';
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/* created using https://icomoon.io/app */
|
||||||
|
/* https://stackoverflow.com/a/35092005/681629 */
|
||||||
|
/* for additional icons, we should create a new set that includes the existing icons */
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'nomad-icon';
|
||||||
|
src: url('nomad-icon/nomad-icon.eot?6tre2n');
|
||||||
|
src: url('nomad-icon/nomad-icon.eot?6tre2n#iefix') format('embedded-opentype'), url('nomad-icon/nomad-icon.ttf?6tre2n') format('truetype'),
|
||||||
|
url('nomad-icon/nomad-icon.woff?6tre2n') format('woff'), url('nomad-icon/nomad-icon.svg?6tre2n#nomad-icon') format('svg');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nomad-icon {
|
||||||
|
/* use !important to prevent issues with browser extensions that change fonts */
|
||||||
|
font-family: 'nomad-icon' !important;
|
||||||
|
speak: never;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: normal;
|
||||||
|
font-variant: normal;
|
||||||
|
text-transform: none;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
/* Better Font Rendering =========== */
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nomad-icon:before {
|
||||||
|
content: '\e900';
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<metadata>Generated by IcoMoon</metadata>
|
||||||
|
<defs>
|
||||||
|
<font id="icomoon" horiz-adv-x="1024">
|
||||||
|
<font-face units-per-em="1024" ascent="960" descent="-64" />
|
||||||
|
<missing-glyph horiz-adv-x="1024" />
|
||||||
|
<glyph unicode=" " horiz-adv-x="512" d="" />
|
||||||
|
<glyph unicode="" glyph-name="nomad_black" d="M507.999 959.562l-443.079-255.649v-511.675l443.079-255.8 443.079 255.8v511.675l-443.079 255.649zM705.402 396.893l-118.079-67.992-142.631 77.435v-163.256l-134.095-84.839v340.865l106.369 65.121 147.617-77.813v166.202l140.894 84.612-0.076-340.336z" />
|
||||||
|
</font></defs></svg>
|
||||||
|
After Width: | Height: | Size: 738 B |
Binary file not shown.
Binary file not shown.
+2
-8
@@ -1,7 +1,5 @@
|
|||||||
import {
|
import { AccessControlFormData } from '@/portainer/access-control/types';
|
||||||
AccessControlFormData,
|
import { PortainerMetadata } from '@/react/docker/types';
|
||||||
ResourceControlResponse,
|
|
||||||
} from '@/portainer/access-control/types';
|
|
||||||
|
|
||||||
import { PortMapping } from './ContainerInstances/CreateContainerInstanceForm/PortsMappingField';
|
import { PortMapping } from './ContainerInstances/CreateContainerInstanceForm/PortsMappingField';
|
||||||
|
|
||||||
@@ -21,10 +19,6 @@ export interface ContainerInstanceFormValues {
|
|||||||
accessControl: AccessControlFormData;
|
accessControl: AccessControlFormData;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PortainerMetadata {
|
|
||||||
ResourceControl: ResourceControlResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Container {
|
interface Container {
|
||||||
name: string;
|
name: string;
|
||||||
properties: {
|
properties: {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { render } from '@/react-tools/test-utils';
|
import { render } from '@/react-tools/test-utils';
|
||||||
import { UserContext } from '@/portainer/hooks/useUser';
|
import { UserContext } from '@/portainer/hooks/useUser';
|
||||||
import { UserViewModel } from '@/portainer/models/user';
|
import { UserViewModel } from '@/portainer/models/user';
|
||||||
import { ResourceControlOwnership } from '@/portainer/access-control/types';
|
|
||||||
|
|
||||||
import { DockerNetwork } from '../types';
|
import { DockerNetwork } from '../types';
|
||||||
|
|
||||||
@@ -113,9 +112,9 @@ function getNetwork(networkName: string): DockerNetwork {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
TeamAccesses: [],
|
TeamAccesses: [],
|
||||||
Ownership: ResourceControlOwnership.PUBLIC,
|
|
||||||
Public: true,
|
Public: true,
|
||||||
System: false,
|
System: false,
|
||||||
|
AdministratorsOnly: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Scope: 'local',
|
Scope: 'local',
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { confirmDeletionAsync } from '@/portainer/services/modal.service/confirm
|
|||||||
import { AccessControlPanel } from '@/portainer/access-control/AccessControlPanel/AccessControlPanel';
|
import { AccessControlPanel } from '@/portainer/access-control/AccessControlPanel/AccessControlPanel';
|
||||||
import { ResourceControlType } from '@/portainer/access-control/types';
|
import { ResourceControlType } from '@/portainer/access-control/types';
|
||||||
import { DockerContainer } from '@/docker/containers/types';
|
import { DockerContainer } from '@/docker/containers/types';
|
||||||
|
import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel';
|
||||||
|
|
||||||
import { useNetwork, useDeleteNetwork } from '../queries';
|
import { useNetwork, useDeleteNetwork } from '../queries';
|
||||||
import { isSystemNetwork } from '../network.helper';
|
import { isSystemNetwork } from '../network.helper';
|
||||||
@@ -50,6 +51,12 @@ export function NetworkDetailsView() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const network = networkQuery.data;
|
||||||
|
|
||||||
|
const resourceControl = network.Portainer?.ResourceControl
|
||||||
|
? new ResourceControlViewModel(network.Portainer.ResourceControl)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
@@ -77,7 +84,7 @@ export function NetworkDetailsView() {
|
|||||||
networkId,
|
networkId,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
resourceControl={networkQuery.data.Portainer?.ResourceControl}
|
resourceControl={resourceControl}
|
||||||
resourceType={ResourceControlType.Network}
|
resourceType={ResourceControlType.Network}
|
||||||
disableOwnershipChange={isSystemNetwork(networkQuery.data.Name)}
|
disableOwnershipChange={isSystemNetwork(networkQuery.data.Name)}
|
||||||
resourceId={networkId}
|
resourceId={networkId}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel';
|
import { PortainerMetadata } from '@/react/docker/types';
|
||||||
|
|
||||||
import { ContainerId } from '../containers/types';
|
import { ContainerId } from '../containers/types';
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ export interface DockerNetwork {
|
|||||||
Driver: string;
|
Driver: string;
|
||||||
Options: IpamOptions;
|
Options: IpamOptions;
|
||||||
};
|
};
|
||||||
Portainer: { ResourceControl?: ResourceControlViewModel };
|
Portainer?: PortainerMetadata;
|
||||||
Options: NetworkOptions;
|
Options: NetworkOptions;
|
||||||
Containers: NetworkResponseContainers;
|
Containers: NetworkResponseContainers;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,12 +15,5 @@ export const heartbeat: Column<Environment> = {
|
|||||||
export function StatusCell({
|
export function StatusCell({
|
||||||
row: { original: environment },
|
row: { original: environment },
|
||||||
}: CellProps<Environment>) {
|
}: CellProps<Environment>) {
|
||||||
return (
|
return <EdgeIndicator environment={environment} />;
|
||||||
<EdgeIndicator
|
|
||||||
checkInInterval={environment.EdgeCheckinInterval}
|
|
||||||
edgeId={environment.EdgeID}
|
|
||||||
lastCheckInDate={environment.LastCheckInDate}
|
|
||||||
queryDate={environment.QueryDate}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { TableSettingsProvider } from '@/portainer/components/datatables/compone
|
|||||||
import { PageHeader } from '@/portainer/components/PageHeader';
|
import { PageHeader } from '@/portainer/components/PageHeader';
|
||||||
import { useEnvironmentList } from '@/portainer/environments/queries/useEnvironmentList';
|
import { useEnvironmentList } from '@/portainer/environments/queries/useEnvironmentList';
|
||||||
import { r2a } from '@/react-tools/react2angular';
|
import { r2a } from '@/react-tools/react2angular';
|
||||||
|
import { InformationPanel } from '@/portainer/components/InformationPanel';
|
||||||
|
import { TextTip } from '@/portainer/components/Tip/TextTip';
|
||||||
|
|
||||||
import { DataTable } from './Datatable/Datatable';
|
import { DataTable } from './Datatable/Datatable';
|
||||||
import { TableSettings } from './Datatable/types';
|
import { TableSettings } from './Datatable/types';
|
||||||
@@ -29,6 +31,15 @@ export function WaitingRoomView() {
|
|||||||
{ label: 'Waiting Room' },
|
{ label: 'Waiting Room' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<InformationPanel>
|
||||||
|
<TextTip color="blue">
|
||||||
|
Only environments generated from the AEEC script will appear here,
|
||||||
|
manually added environments and edge devices will bypass the waiting
|
||||||
|
room.
|
||||||
|
</TextTip>
|
||||||
|
</InformationPanel>
|
||||||
|
|
||||||
<TableSettingsProvider<TableSettings>
|
<TableSettingsProvider<TableSettings>
|
||||||
defaults={{ pageSize: 10, sortBy: { desc: false, id: 'name' } }}
|
defaults={{ pageSize: 10, sortBy: { desc: false, id: 'name' } }}
|
||||||
storageKey={storageKey}
|
storageKey={storageKey}
|
||||||
|
|||||||
+6
@@ -1,6 +1,7 @@
|
|||||||
import { buildOption } from '@/portainer/components/BoxSelector';
|
import { buildOption } from '@/portainer/components/BoxSelector';
|
||||||
import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel';
|
import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel';
|
||||||
import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils';
|
import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils';
|
||||||
|
import { isBE } from '@/portainer/feature-flags/feature-flags.service';
|
||||||
|
|
||||||
class KubeCreateCustomTemplateViewController {
|
class KubeCreateCustomTemplateViewController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
@@ -13,6 +14,7 @@ class KubeCreateCustomTemplateViewController {
|
|||||||
];
|
];
|
||||||
|
|
||||||
this.templates = null;
|
this.templates = null;
|
||||||
|
this.isTemplateVariablesEnabled = isBE;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
method: 'editor',
|
method: 'editor',
|
||||||
@@ -53,6 +55,10 @@ class KubeCreateCustomTemplateViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parseTemplate(templateStr) {
|
parseTemplate(templateStr) {
|
||||||
|
if (!this.isTemplateVariablesEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const variables = getTemplateVariables(templateStr);
|
const variables = getTemplateVariables(templateStr);
|
||||||
|
|
||||||
const isValid = !!variables;
|
const isValid = !!variables;
|
||||||
|
|||||||
+1
@@ -37,6 +37,7 @@
|
|||||||
</file-upload-form>
|
</file-upload-form>
|
||||||
|
|
||||||
<custom-templates-variables-definition-field
|
<custom-templates-variables-definition-field
|
||||||
|
ng-if="$ctrl.isTemplateVariablesEnabled"
|
||||||
value="$ctrl.formValues.Variables"
|
value="$ctrl.formValues.Variables"
|
||||||
on-change="($ctrl.onVariablesChange)"
|
on-change="($ctrl.onVariablesChange)"
|
||||||
is-variables-names-from-parent="$ctrl.state.method === 'editor'"
|
is-variables-names-from-parent="$ctrl.state.method === 'editor'"
|
||||||
|
|||||||
+7
@@ -1,5 +1,6 @@
|
|||||||
import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel';
|
import { ResourceControlViewModel } from '@/portainer/access-control/models/ResourceControlViewModel';
|
||||||
import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel';
|
import { AccessControlFormData } from '@/portainer/components/accessControlForm/porAccessControlFormModel';
|
||||||
|
import { isBE } from '@/portainer/feature-flags/feature-flags.service';
|
||||||
import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils';
|
import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils';
|
||||||
|
|
||||||
class KubeEditCustomTemplateViewController {
|
class KubeEditCustomTemplateViewController {
|
||||||
@@ -7,6 +8,8 @@ class KubeEditCustomTemplateViewController {
|
|||||||
constructor($async, $state, ModalService, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService) {
|
constructor($async, $state, ModalService, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService) {
|
||||||
Object.assign(this, { $async, $state, ModalService, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService });
|
Object.assign(this, { $async, $state, ModalService, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService });
|
||||||
|
|
||||||
|
this.isTemplateVariablesEnabled = isBE;
|
||||||
|
|
||||||
this.formValues = null;
|
this.formValues = null;
|
||||||
this.state = {
|
this.state = {
|
||||||
formValidationError: '',
|
formValidationError: '',
|
||||||
@@ -60,6 +63,10 @@ class KubeEditCustomTemplateViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parseTemplate(templateStr) {
|
parseTemplate(templateStr) {
|
||||||
|
if (!this.isTemplateVariablesEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const variables = getTemplateVariables(templateStr);
|
const variables = getTemplateVariables(templateStr);
|
||||||
|
|
||||||
const isValid = !!variables;
|
const isValid = !!variables;
|
||||||
|
|||||||
+1
@@ -32,6 +32,7 @@
|
|||||||
</web-editor-form>
|
</web-editor-form>
|
||||||
|
|
||||||
<custom-templates-variables-definition-field
|
<custom-templates-variables-definition-field
|
||||||
|
ng-if="$ctrl.isTemplateVariablesEnabled"
|
||||||
value="$ctrl.formValues.Variables"
|
value="$ctrl.formValues.Variables"
|
||||||
on-change="($ctrl.onVariablesChange)"
|
on-change="($ctrl.onVariablesChange)"
|
||||||
is-variables-names-from-parent="true"
|
is-variables-names-from-parent="true"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ angular.module('portainer.kubernetes').factory('HelmFactory', HelmFactory);
|
|||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
function HelmFactory($resource, API_ENDPOINT_ENDPOINTS) {
|
function HelmFactory($resource, API_ENDPOINT_ENDPOINTS) {
|
||||||
const helmUrl = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/helm';
|
const helmUrl = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/helm';
|
||||||
const templatesUrl = '/api/templates/helm';
|
const templatesUrl = 'api/templates/helm';
|
||||||
|
|
||||||
return $resource(
|
return $resource(
|
||||||
helmUrl,
|
helmUrl,
|
||||||
|
|||||||
@@ -93,7 +93,7 @@
|
|||||||
></custom-template-selector>
|
></custom-template-selector>
|
||||||
|
|
||||||
<custom-templates-variables-field
|
<custom-templates-variables-field
|
||||||
ng-if="ctrl.state.template"
|
ng-if="ctrl.isTemplateVariablesEnabled && ctrl.state.template"
|
||||||
definitions="ctrl.state.template.Variables"
|
definitions="ctrl.state.template.Variables"
|
||||||
value="ctrl.formValues.Variables"
|
value="ctrl.formValues.Variables"
|
||||||
on-change="(ctrl.onChangeTemplateVariables)"
|
on-change="(ctrl.onChangeTemplateVariables)"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import PortainerError from '@/portainer/error';
|
|||||||
import { KubernetesDeployManifestTypes, KubernetesDeployBuildMethods, KubernetesDeployRequestMethods, RepositoryMechanismTypes } from 'Kubernetes/models/deploy';
|
import { KubernetesDeployManifestTypes, KubernetesDeployBuildMethods, KubernetesDeployRequestMethods, RepositoryMechanismTypes } from 'Kubernetes/models/deploy';
|
||||||
import { buildOption } from '@/portainer/components/BoxSelector';
|
import { buildOption } from '@/portainer/components/BoxSelector';
|
||||||
import { renderTemplate } from '@/react/portainer/custom-templates/components/utils';
|
import { renderTemplate } from '@/react/portainer/custom-templates/components/utils';
|
||||||
|
import { isBE } from '@/portainer/feature-flags/feature-flags.service';
|
||||||
|
|
||||||
class KubernetesDeployController {
|
class KubernetesDeployController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
@@ -23,6 +24,8 @@ class KubernetesDeployController {
|
|||||||
this.CustomTemplateService = CustomTemplateService;
|
this.CustomTemplateService = CustomTemplateService;
|
||||||
this.DeployMethod = 'manifest';
|
this.DeployMethod = 'manifest';
|
||||||
|
|
||||||
|
this.isTemplateVariablesEnabled = isBE;
|
||||||
|
|
||||||
this.deployOptions = [
|
this.deployOptions = [
|
||||||
buildOption('method_kubernetes', 'fa fa-cubes', 'Kubernetes', 'Kubernetes manifest format', KubernetesDeployManifestTypes.KUBERNETES),
|
buildOption('method_kubernetes', 'fa fa-cubes', 'Kubernetes', 'Kubernetes manifest format', KubernetesDeployManifestTypes.KUBERNETES),
|
||||||
buildOption('method_compose', 'fab fa-docker', 'Compose', 'docker-compose format', KubernetesDeployManifestTypes.COMPOSE),
|
buildOption('method_compose', 'fab fa-docker', 'Compose', 'docker-compose format', KubernetesDeployManifestTypes.COMPOSE),
|
||||||
@@ -83,6 +86,10 @@ class KubernetesDeployController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderTemplate() {
|
renderTemplate() {
|
||||||
|
if (!this.isTemplateVariablesEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const rendered = renderTemplate(this.state.templateContent, this.formValues.Variables, this.state.template.Variables);
|
const rendered = renderTemplate(this.state.templateContent, this.formValues.Variables, this.state.template.Variables);
|
||||||
this.onChangeFormValues({ EditorContent: rendered });
|
this.onChangeFormValues({ EditorContent: rendered });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,11 +33,11 @@ export class ResourceControlViewModel {
|
|||||||
this.TeamAccesses = data.TeamAccesses;
|
this.TeamAccesses = data.TeamAccesses;
|
||||||
this.Public = data.Public;
|
this.Public = data.Public;
|
||||||
this.System = data.System;
|
this.System = data.System;
|
||||||
this.Ownership = determineOwnership(this);
|
this.Ownership = determineOwnership(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function determineOwnership(resourceControl: ResourceControlViewModel) {
|
function determineOwnership(resourceControl: ResourceControlResponse) {
|
||||||
if (resourceControl.Public) {
|
if (resourceControl.Public) {
|
||||||
return ResourceControlOwnership.PUBLIC;
|
return ResourceControlOwnership.PUBLIC;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { FeatureId } from '@/portainer/feature-flags/enums';
|
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||||
|
|
||||||
@@ -6,11 +7,15 @@ import { getFeatureDetails } from './utils';
|
|||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
featureId?: FeatureId;
|
featureId?: FeatureId;
|
||||||
|
showIcon?: boolean;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BEFeatureIndicator({
|
export function BEFeatureIndicator({
|
||||||
featureId,
|
featureId,
|
||||||
children,
|
children,
|
||||||
|
showIcon = true,
|
||||||
|
className = '',
|
||||||
}: PropsWithChildren<Props>) {
|
}: PropsWithChildren<Props>) {
|
||||||
const { url, limitedToBE } = getFeatureDetails(featureId);
|
const { url, limitedToBE } = getFeatureDetails(featureId);
|
||||||
|
|
||||||
@@ -20,14 +25,18 @@ export function BEFeatureIndicator({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
className="be-indicator"
|
className={clsx('be-indicator', className)}
|
||||||
href={url}
|
href={url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<i className="fas fa-briefcase space-right be-indicator-icon" />
|
{showIcon && (
|
||||||
<span className="be-indicator-label">Business Edition Feature</span>
|
<i className="fas fa-briefcase space-right be-indicator-icon" />
|
||||||
|
)}
|
||||||
|
<span className="be-indicator-label break-words">
|
||||||
|
Business Edition Feature
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Button } from '../Button';
|
|||||||
import { Widget, WidgetBody } from '../widget';
|
import { Widget, WidgetBody } from '../widget';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title?: string;
|
||||||
onDismiss?(): void;
|
onDismiss?(): void;
|
||||||
bodyClassName?: string;
|
bodyClassName?: string;
|
||||||
wrapperStyle?: Record<string, string>;
|
wrapperStyle?: Record<string, string>;
|
||||||
@@ -23,21 +23,23 @@ export function InformationPanel({
|
|||||||
<Widget>
|
<Widget>
|
||||||
<WidgetBody className={bodyClassName}>
|
<WidgetBody className={bodyClassName}>
|
||||||
<div style={wrapperStyle}>
|
<div style={wrapperStyle}>
|
||||||
<div className="col-sm-12 form-section-title">
|
{title && (
|
||||||
<span style={{ float: 'left' }}>{title}</span>
|
<div className="col-sm-12 form-section-title">
|
||||||
{!!onDismiss && (
|
<span style={{ float: 'left' }}>{title}</span>
|
||||||
<span
|
{!!onDismiss && (
|
||||||
className="small"
|
<span
|
||||||
style={{ float: 'right' }}
|
className="small"
|
||||||
ng-if="dismissAction"
|
style={{ float: 'right' }}
|
||||||
>
|
ng-if="dismissAction"
|
||||||
<Button color="link" onClick={() => onDismiss()}>
|
>
|
||||||
<i className="fa fa-times" /> dismiss
|
<Button color="link" onClick={() => onDismiss()}>
|
||||||
</Button>
|
<i className="fa fa-times" /> dismiss
|
||||||
</span>
|
</Button>
|
||||||
)}
|
</span>
|
||||||
</div>
|
)}
|
||||||
<div className="form-group">{children}</div>
|
</div>
|
||||||
|
)}
|
||||||
|
<div>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</WidgetBody>
|
</WidgetBody>
|
||||||
</Widget>
|
</Widget>
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import { react2angular } from '@/react-tools/react2angular';
|
import { react2angular } from '@/react-tools/react2angular';
|
||||||
import { usePublicSettings } from '@/portainer/settings/queries';
|
import { usePublicSettings } from '@/portainer/settings/queries';
|
||||||
|
|
||||||
export function PasswordCheckHint() {
|
interface Props {
|
||||||
|
passwordValid: boolean;
|
||||||
|
forceChangePassword?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PasswordCheckHint({
|
||||||
|
passwordValid,
|
||||||
|
forceChangePassword,
|
||||||
|
}: Props) {
|
||||||
const settingsQuery = usePublicSettings();
|
const settingsQuery = usePublicSettings();
|
||||||
const minPasswordLength = settingsQuery.data?.RequiredPasswordLength;
|
const minPasswordLength = settingsQuery.data?.RequiredPasswordLength;
|
||||||
|
|
||||||
@@ -12,10 +20,18 @@ export function PasswordCheckHint() {
|
|||||||
className="fa fa-exclamation-triangle orange-icon space-right"
|
className="fa fa-exclamation-triangle orange-icon space-right"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
|
{forceChangePassword &&
|
||||||
|
'An administrator has changed your password requirements, '}
|
||||||
The password must be at least {minPasswordLength} characters long.
|
The password must be at least {minPasswordLength} characters long.
|
||||||
|
{passwordValid && (
|
||||||
|
<i className="fa fa-check green-icon space-left" aria-hidden="true" />
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PasswordCheckHintAngular = react2angular(PasswordCheckHint, []);
|
export const PasswordCheckHintAngular = react2angular(PasswordCheckHint, [
|
||||||
|
'passwordValid',
|
||||||
|
'forceChangePassword',
|
||||||
|
]);
|
||||||
|
|||||||
+2
-2
@@ -11,7 +11,7 @@
|
|||||||
loaded="$ctrl.loaded"
|
loaded="$ctrl.loaded"
|
||||||
page-type="$ctrl.pageType"
|
page-type="$ctrl.pageType"
|
||||||
table-type="available"
|
table-type="available"
|
||||||
retrieve-page="$ctrl.getPaginatedEndpoints"
|
retrieve-page="$ctrl.getAvailableEndpoints"
|
||||||
dataset="$ctrl.endpoints.available"
|
dataset="$ctrl.endpoints.available"
|
||||||
entry-click="$ctrl.associateEndpoint"
|
entry-click="$ctrl.associateEndpoint"
|
||||||
pagination-state="$ctrl.state.available"
|
pagination-state="$ctrl.state.available"
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
loaded="$ctrl.loaded"
|
loaded="$ctrl.loaded"
|
||||||
page-type="$ctrl.pageType"
|
page-type="$ctrl.pageType"
|
||||||
table-type="associated"
|
table-type="associated"
|
||||||
retrieve-page="$ctrl.getPaginatedEndpoints"
|
retrieve-page="$ctrl.getAssociatedEndpoints"
|
||||||
dataset="$ctrl.endpoints.associated"
|
dataset="$ctrl.endpoints.associated"
|
||||||
entry-click="$ctrl.dissociateEndpoint"
|
entry-click="$ctrl.dissociateEndpoint"
|
||||||
pagination-state="$ctrl.state.associated"
|
pagination-state="$ctrl.state.associated"
|
||||||
|
|||||||
+3
-3
@@ -27,7 +27,7 @@ class AssoicatedEndpointsSelectorController {
|
|||||||
available: null,
|
available: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.getEndpoints = this.getEndpoints.bind(this);
|
this.getAvailableEndpoints = this.getAvailableEndpoints.bind(this);
|
||||||
this.getEndpointsAsync = this.getEndpointsAsync.bind(this);
|
this.getEndpointsAsync = this.getEndpointsAsync.bind(this);
|
||||||
this.getAssociatedEndpoints = this.getAssociatedEndpoints.bind(this);
|
this.getAssociatedEndpoints = this.getAssociatedEndpoints.bind(this);
|
||||||
this.getAssociatedEndpointsAsync = this.getAssociatedEndpointsAsync.bind(this);
|
this.getAssociatedEndpointsAsync = this.getAssociatedEndpointsAsync.bind(this);
|
||||||
@@ -47,10 +47,10 @@ class AssoicatedEndpointsSelectorController {
|
|||||||
|
|
||||||
loadData() {
|
loadData() {
|
||||||
this.getAssociatedEndpoints();
|
this.getAssociatedEndpoints();
|
||||||
this.getEndpoints();
|
this.getAvailableEndpoints();
|
||||||
}
|
}
|
||||||
|
|
||||||
getEndpoints() {
|
getAvailableEndpoints() {
|
||||||
return this.$async(this.getEndpointsAsync);
|
return this.$async(this.getEndpointsAsync);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
>
|
>
|
||||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-primary" ng-click="$ctrl.setRefferer()" ui-sref="portainer.wizard.endpoints" data-cy="endpoint-addEndpointButton">
|
<button type="button" class="btn btn-sm btn-primary" ng-click="$ctrl.setReferrer()" ui-sref="portainer.wizard.endpoints" data-cy="endpoint-addEndpointButton">
|
||||||
<i class="fa fa-plus-circle space-right" aria-hidden="true"></i>Add environment
|
<i class="fa fa-plus-circle space-right" aria-hidden="true"></i>Add environment
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+2
-2
@@ -34,8 +34,8 @@ angular.module('portainer.app').controller('EndpointsDatatableController', [
|
|||||||
this.paginationChanged();
|
this.paginationChanged();
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setRefferer = function () {
|
this.setReferrer = function () {
|
||||||
window.localStorage.setItem('wizardRefferer', 'environments');
|
window.localStorage.setItem('wizardReferrer', 'environments');
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -29,26 +29,29 @@ export function FormControl({
|
|||||||
required,
|
required,
|
||||||
}: PropsWithChildren<Props>) {
|
}: PropsWithChildren<Props>) {
|
||||||
return (
|
return (
|
||||||
<div className={clsx('form-group', styles.container)}>
|
<>
|
||||||
<label
|
<div className={clsx('form-group', styles.container)}>
|
||||||
htmlFor={inputId}
|
<label
|
||||||
className={clsx(sizeClassLabel(size), 'control-label', 'text-left')}
|
htmlFor={inputId}
|
||||||
>
|
className={clsx(sizeClassLabel(size), 'control-label', 'text-left')}
|
||||||
{label}
|
>
|
||||||
|
{label}
|
||||||
|
|
||||||
{required && <span className="text-danger">*</span>}
|
{required && <span className="text-danger">*</span>}
|
||||||
|
|
||||||
{tooltip && <Tooltip message={tooltip} />}
|
{tooltip && <Tooltip message={tooltip} />}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className={sizeClassChildren(size)}>{children}</div>
|
|
||||||
|
|
||||||
|
<div className={sizeClassChildren(size)}>{children}</div>
|
||||||
|
</div>
|
||||||
{errors && (
|
{errors && (
|
||||||
<div className="col-md-12">
|
<div className="form-group">
|
||||||
<FormError>{errors}</FormError>
|
<div className="col-md-12">
|
||||||
|
<FormError>{errors}</FormError>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,13 @@ export interface KubernetesSettings {
|
|||||||
Snapshots?: KubernetesSnapshot[] | null;
|
Snapshots?: KubernetesSnapshot[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EnvironmentEdge = {
|
||||||
|
AsyncMode: boolean;
|
||||||
|
PingInterval: number;
|
||||||
|
SnapshotInterval: number;
|
||||||
|
CommandInterval: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type Environment = {
|
export type Environment = {
|
||||||
Id: EnvironmentId;
|
Id: EnvironmentId;
|
||||||
Type: EnvironmentType;
|
Type: EnvironmentType;
|
||||||
@@ -73,6 +80,7 @@ export type Environment = {
|
|||||||
IsEdgeDevice?: boolean;
|
IsEdgeDevice?: boolean;
|
||||||
UserTrusted: boolean;
|
UserTrusted: boolean;
|
||||||
AMTDeviceGUID?: string;
|
AMTDeviceGUID?: string;
|
||||||
|
Edge: EnvironmentEdge;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* TS reference of endpoint_create.go#EndpointCreationType iota
|
* TS reference of endpoint_create.go#EndpointCreationType iota
|
||||||
@@ -83,6 +91,7 @@ export enum EnvironmentCreationTypes {
|
|||||||
AzureEnvironment,
|
AzureEnvironment,
|
||||||
EdgeAgentEnvironment,
|
EdgeAgentEnvironment,
|
||||||
LocalKubernetesEnvironment,
|
LocalKubernetesEnvironment,
|
||||||
|
KubeConfigEnvironment,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PlatformType {
|
export enum PlatformType {
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ export enum FeatureState {
|
|||||||
export enum FeatureId {
|
export enum FeatureId {
|
||||||
K8S_RESOURCE_POOL_LB_QUOTA = 'k8s-resourcepool-Ibquota',
|
K8S_RESOURCE_POOL_LB_QUOTA = 'k8s-resourcepool-Ibquota',
|
||||||
K8S_RESOURCE_POOL_STORAGE_QUOTA = 'k8s-resourcepool-storagequota',
|
K8S_RESOURCE_POOL_STORAGE_QUOTA = 'k8s-resourcepool-storagequota',
|
||||||
|
K8S_CREATE_FROM_KUBECONFIG = 'k8s-create-from-kubeconfig',
|
||||||
|
KAAS_PROVISIONING = 'kaas-provisioning',
|
||||||
|
NOMAD = 'nomad',
|
||||||
RBAC_ROLES = 'rbac-roles',
|
RBAC_ROLES = 'rbac-roles',
|
||||||
REGISTRY_MANAGEMENT = 'registry-management',
|
REGISTRY_MANAGEMENT = 'registry-management',
|
||||||
K8S_SETUP_DEFAULT = 'k8s-setup-default',
|
K8S_SETUP_DEFAULT = 'k8s-setup-default',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Edition, FeatureId, FeatureState } from './enums';
|
import { Edition, FeatureId, FeatureState } from './enums';
|
||||||
|
|
||||||
|
export const isBE = process.env.PORTAINER_EDITION === 'BE';
|
||||||
interface ServiceState {
|
interface ServiceState {
|
||||||
currentEdition: Edition;
|
currentEdition: Edition;
|
||||||
features: Record<FeatureId, Edition>;
|
features: Record<FeatureId, Edition>;
|
||||||
@@ -16,6 +17,9 @@ export async function init(edition: Edition) {
|
|||||||
const features = {
|
const features = {
|
||||||
[FeatureId.K8S_RESOURCE_POOL_LB_QUOTA]: Edition.BE,
|
[FeatureId.K8S_RESOURCE_POOL_LB_QUOTA]: Edition.BE,
|
||||||
[FeatureId.K8S_RESOURCE_POOL_STORAGE_QUOTA]: Edition.BE,
|
[FeatureId.K8S_RESOURCE_POOL_STORAGE_QUOTA]: Edition.BE,
|
||||||
|
[FeatureId.K8S_CREATE_FROM_KUBECONFIG]: Edition.BE,
|
||||||
|
[FeatureId.KAAS_PROVISIONING]: Edition.BE,
|
||||||
|
[FeatureId.NOMAD]: Edition.BE,
|
||||||
[FeatureId.ACTIVITY_AUDIT]: Edition.BE,
|
[FeatureId.ACTIVITY_AUDIT]: Edition.BE,
|
||||||
[FeatureId.EXTERNAL_AUTH_LDAP]: Edition.BE,
|
[FeatureId.EXTERNAL_AUTH_LDAP]: Edition.BE,
|
||||||
[FeatureId.HIDE_INTERNAL_AUTH]: Edition.BE,
|
[FeatureId.HIDE_INTERNAL_AUTH]: Edition.BE,
|
||||||
|
|||||||
@@ -22,22 +22,3 @@ export function withFileSize(fileValidation: FileSchema, maxSize: number) {
|
|||||||
return file.size <= maxSize;
|
return file.size <= maxSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withFileType(
|
|
||||||
fileValidation: FileSchema,
|
|
||||||
fileTypes: File['type'][]
|
|
||||||
) {
|
|
||||||
return fileValidation.test(
|
|
||||||
'file-type',
|
|
||||||
'Selected file has unsupported format.',
|
|
||||||
validateFileType
|
|
||||||
);
|
|
||||||
|
|
||||||
function validateFileType(file?: File) {
|
|
||||||
if (!file) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileTypes.includes(file.type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,35 +1,44 @@
|
|||||||
import { render } from '@/react-tools/test-utils';
|
import { createMockEnvironment } from '@/react-tools/test-mocks';
|
||||||
|
import { renderWithQueryClient } from '@/react-tools/test-utils';
|
||||||
|
import { rest, server } from '@/setup-tests/server';
|
||||||
|
|
||||||
import { EdgeIndicator } from './EdgeIndicator';
|
import { EdgeIndicator } from './EdgeIndicator';
|
||||||
|
|
||||||
test('when edge id is not set, should show unassociated label', () => {
|
test('when edge id is not set, should show unassociated label', async () => {
|
||||||
const { queryByLabelText } = renderComponent();
|
const { queryByLabelText } = await renderComponent();
|
||||||
|
|
||||||
const unassociatedLabel = queryByLabelText('unassociated');
|
const unassociatedLabel = queryByLabelText('unassociated');
|
||||||
|
|
||||||
expect(unassociatedLabel).toBeVisible();
|
expect(unassociatedLabel).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('given edge id and last checkin is set, should show heartbeat', () => {
|
// test('given edge id and last checkin is set, should show heartbeat', async () => {
|
||||||
const { queryByLabelText } = renderComponent('id', 1);
|
// const { queryByLabelText } = await renderComponent('id', 1);
|
||||||
|
|
||||||
expect(queryByLabelText('edge-heartbeat')).toBeVisible();
|
// expect(queryByLabelText('edge-heartbeat')).toBeVisible();
|
||||||
expect(queryByLabelText('edge-last-checkin')).toBeVisible();
|
// expect(queryByLabelText('edge-last-checkin')).toBeVisible();
|
||||||
});
|
// });
|
||||||
|
|
||||||
function renderComponent(
|
async function renderComponent(
|
||||||
edgeId = '',
|
edgeId = '',
|
||||||
lastCheckInDate = 0,
|
lastCheckInDate = 0,
|
||||||
checkInInterval = 0,
|
checkInInterval = 0,
|
||||||
queryDate = 0
|
queryDate = 0
|
||||||
) {
|
) {
|
||||||
return render(
|
server.use(rest.get('/api/settings', (req, res, ctx) => res(ctx.json({}))));
|
||||||
<EdgeIndicator
|
|
||||||
edgeId={edgeId}
|
const environment = createMockEnvironment();
|
||||||
lastCheckInDate={lastCheckInDate}
|
|
||||||
checkInInterval={checkInInterval}
|
environment.EdgeID = edgeId;
|
||||||
queryDate={queryDate}
|
environment.LastCheckInDate = lastCheckInDate;
|
||||||
showLastCheckInDate
|
environment.EdgeCheckinInterval = checkInInterval;
|
||||||
/>
|
environment.QueryDate = queryDate;
|
||||||
|
|
||||||
|
const queries = renderWithQueryClient(
|
||||||
|
<EdgeIndicator environment={environment} showLastCheckInDate />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await expect(queries.findByRole('status')).resolves.toBeVisible();
|
||||||
|
|
||||||
|
return queries;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,114 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
|
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
|
||||||
|
import { Environment } from '@/portainer/environments/types';
|
||||||
|
import { usePublicSettings } from '@/portainer/settings/queries';
|
||||||
|
import { PublicSettingsViewModel } from '@/portainer/models/settings';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
checkInInterval?: number;
|
|
||||||
edgeId?: string;
|
|
||||||
queryDate?: number;
|
|
||||||
lastCheckInDate?: number;
|
|
||||||
showLastCheckInDate?: boolean;
|
showLastCheckInDate?: boolean;
|
||||||
|
environment: Environment;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EdgeIndicator({
|
export function EdgeIndicator({
|
||||||
edgeId,
|
environment,
|
||||||
lastCheckInDate,
|
|
||||||
checkInInterval,
|
|
||||||
queryDate,
|
|
||||||
showLastCheckInDate = false,
|
showLastCheckInDate = false,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
if (!edgeId) {
|
const associated = !!environment.EdgeID;
|
||||||
|
|
||||||
|
const isValid = useHasHeartbeat(environment, associated);
|
||||||
|
|
||||||
|
if (isValid === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!associated) {
|
||||||
return (
|
return (
|
||||||
<span className="label label-default" aria-label="unassociated">
|
<span role="status" aria-label="edge-status">
|
||||||
<s>associated</s>
|
<span className="label label-default" aria-label="unassociated">
|
||||||
|
<s>associated</s>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// give checkIn some wiggle room
|
|
||||||
let isCheckValid = false;
|
|
||||||
if (checkInInterval && queryDate && lastCheckInDate) {
|
|
||||||
isCheckValid = queryDate - lastCheckInDate <= checkInInterval * 2 + 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<span role="status" aria-label="edge-status">
|
||||||
<span
|
<span
|
||||||
className={clsx('label', {
|
className={clsx('label', {
|
||||||
'label-danger': !isCheckValid,
|
'label-danger': !isValid,
|
||||||
'label-success': isCheckValid,
|
'label-success': isValid,
|
||||||
})}
|
})}
|
||||||
aria-label="edge-heartbeat"
|
aria-label="edge-heartbeat"
|
||||||
>
|
>
|
||||||
heartbeat
|
heartbeat
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{showLastCheckInDate && !!lastCheckInDate && (
|
{showLastCheckInDate && !!environment.LastCheckInDate && (
|
||||||
<span
|
<span
|
||||||
className="space-left small text-muted"
|
className="space-left small text-muted"
|
||||||
aria-label="edge-last-checkin"
|
aria-label="edge-last-checkin"
|
||||||
>
|
>
|
||||||
{isoDateFromTimestamp(lastCheckInDate)}
|
{isoDateFromTimestamp(environment.LastCheckInDate)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useHasHeartbeat(environment: Environment, associated: boolean) {
|
||||||
|
const settingsQuery = usePublicSettings({ enabled: associated });
|
||||||
|
|
||||||
|
if (!associated) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { LastCheckInDate, QueryDate } = environment;
|
||||||
|
|
||||||
|
const settings = settingsQuery.data;
|
||||||
|
|
||||||
|
if (!settings) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkInInterval = getCheckinInterval(environment, settings);
|
||||||
|
|
||||||
|
if (checkInInterval && QueryDate && LastCheckInDate) {
|
||||||
|
return QueryDate - LastCheckInDate <= checkInInterval * 2 + 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCheckinInterval(
|
||||||
|
environment: Environment,
|
||||||
|
settings: PublicSettingsViewModel
|
||||||
|
) {
|
||||||
|
const asyncMode = environment.Edge.AsyncMode;
|
||||||
|
|
||||||
|
if (asyncMode) {
|
||||||
|
const intervals = [
|
||||||
|
environment.Edge.PingInterval > 0
|
||||||
|
? environment.Edge.PingInterval
|
||||||
|
: settings.Edge.PingInterval,
|
||||||
|
environment.Edge.SnapshotInterval > 0
|
||||||
|
? environment.Edge.SnapshotInterval
|
||||||
|
: settings.Edge.SnapshotInterval,
|
||||||
|
environment.Edge.CommandInterval > 0
|
||||||
|
? environment.Edge.CommandInterval
|
||||||
|
: settings.Edge.CommandInterval,
|
||||||
|
].filter((n) => n > 0);
|
||||||
|
|
||||||
|
return intervals.length > 0 ? Math.min(...intervals) : 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!environment.EdgeCheckinInterval ||
|
||||||
|
environment.EdgeCheckinInterval === 0
|
||||||
|
) {
|
||||||
|
return settings.Edge.CheckinInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
return environment.EdgeCheckinInterval;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
EnvironmentStatus,
|
EnvironmentStatus,
|
||||||
EnvironmentType,
|
EnvironmentType,
|
||||||
} from '@/portainer/environments/types';
|
} from '@/portainer/environments/types';
|
||||||
|
import { createMockEnvironment } from '@/react-tools/test-mocks';
|
||||||
|
|
||||||
import { EnvironmentItem } from './EnvironmentItem';
|
import { EnvironmentItem } from './EnvironmentItem';
|
||||||
|
|
||||||
@@ -57,19 +58,9 @@ KubernetesEdgeEnvironment.args = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function mockEnvironment(type: EnvironmentType): Environment {
|
function mockEnvironment(type: EnvironmentType): Environment {
|
||||||
return {
|
const env = createMockEnvironment();
|
||||||
Id: 1,
|
env.Type = type;
|
||||||
Name: 'environment',
|
env.Status = EnvironmentStatus.Up;
|
||||||
GroupId: 1,
|
|
||||||
Snapshots: [],
|
return env;
|
||||||
Status: EnvironmentStatus.Up,
|
|
||||||
TagIds: [],
|
|
||||||
Type: type,
|
|
||||||
Kubernetes: {
|
|
||||||
Snapshots: [],
|
|
||||||
},
|
|
||||||
URL: 'url',
|
|
||||||
UserTrusted: false,
|
|
||||||
EdgeKey: '',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,25 +6,14 @@ import { Environment } from '@/portainer/environments/types';
|
|||||||
import { UserContext } from '@/portainer/hooks/useUser';
|
import { UserContext } from '@/portainer/hooks/useUser';
|
||||||
import { UserViewModel } from '@/portainer/models/user';
|
import { UserViewModel } from '@/portainer/models/user';
|
||||||
import { Tag } from '@/portainer/tags/types';
|
import { Tag } from '@/portainer/tags/types';
|
||||||
|
import { createMockEnvironment } from '@/react-tools/test-mocks';
|
||||||
import { renderWithQueryClient } from '@/react-tools/test-utils';
|
import { renderWithQueryClient } from '@/react-tools/test-utils';
|
||||||
import { server, rest } from '@/setup-tests/server';
|
import { server, rest } from '@/setup-tests/server';
|
||||||
|
|
||||||
import { EnvironmentItem } from './EnvironmentItem';
|
import { EnvironmentItem } from './EnvironmentItem';
|
||||||
|
|
||||||
test('loads component', async () => {
|
test('loads component', async () => {
|
||||||
const env: Environment = {
|
const env = createMockEnvironment();
|
||||||
TagIds: [],
|
|
||||||
GroupId: 1,
|
|
||||||
Type: 1,
|
|
||||||
Name: 'environment',
|
|
||||||
Status: 1,
|
|
||||||
URL: 'url',
|
|
||||||
Snapshots: [],
|
|
||||||
Kubernetes: { Snapshots: [] },
|
|
||||||
Id: 3,
|
|
||||||
UserTrusted: false,
|
|
||||||
EdgeKey: '',
|
|
||||||
};
|
|
||||||
const { getByText } = renderComponent(env);
|
const { getByText } = renderComponent(env);
|
||||||
|
|
||||||
expect(getByText(env.Name)).toBeInTheDocument();
|
expect(getByText(env.Name)).toBeInTheDocument();
|
||||||
@@ -34,19 +23,8 @@ test('shows group name', async () => {
|
|||||||
const groupName = 'group-name';
|
const groupName = 'group-name';
|
||||||
const groupId: EnvironmentGroupId = 14;
|
const groupId: EnvironmentGroupId = 14;
|
||||||
|
|
||||||
const env: Environment = {
|
const env = createMockEnvironment();
|
||||||
TagIds: [],
|
env.GroupId = groupId;
|
||||||
GroupId: groupId,
|
|
||||||
Type: 1,
|
|
||||||
Name: 'environment',
|
|
||||||
Status: 1,
|
|
||||||
URL: 'url',
|
|
||||||
Snapshots: [],
|
|
||||||
Kubernetes: { Snapshots: [] },
|
|
||||||
Id: 3,
|
|
||||||
UserTrusted: false,
|
|
||||||
EdgeKey: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
const { findByText } = renderComponent(env, { Name: groupName });
|
const { findByText } = renderComponent(env, { Name: groupName });
|
||||||
|
|
||||||
|
|||||||
@@ -68,10 +68,8 @@ export function EnvironmentItem({ environment, onClick, groupName }: Props) {
|
|||||||
<span className="space-left blocklist-item-subtitle">
|
<span className="space-left blocklist-item-subtitle">
|
||||||
{isEdge ? (
|
{isEdge ? (
|
||||||
<EdgeIndicator
|
<EdgeIndicator
|
||||||
edgeId={environment.EdgeID}
|
environment={environment}
|
||||||
checkInInterval={environment.EdgeCheckinInterval}
|
showLastCheckInDate
|
||||||
lastCheckInDate={environment.LastCheckInDate}
|
|
||||||
queryDate={environment.QueryDate}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ export function PublicSettingsViewModel(settings) {
|
|||||||
this.EnableTelemetry = settings.EnableTelemetry;
|
this.EnableTelemetry = settings.EnableTelemetry;
|
||||||
this.OAuthLogoutURI = settings.OAuthLogoutURI;
|
this.OAuthLogoutURI = settings.OAuthLogoutURI;
|
||||||
this.KubeconfigExpiry = settings.KubeconfigExpiry;
|
this.KubeconfigExpiry = settings.KubeconfigExpiry;
|
||||||
|
this.Features = settings.Features;
|
||||||
|
this.Edge = new EdgeSettingsViewModel(settings.Edge);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InternalAuthSettingsViewModel(data) {
|
export function InternalAuthSettingsViewModel(data) {
|
||||||
@@ -75,3 +77,10 @@ export function OAuthSettingsViewModel(data) {
|
|||||||
this.SSO = data.SSO;
|
this.SSO = data.SSO;
|
||||||
this.LogoutURI = data.LogoutURI;
|
this.LogoutURI = data.LogoutURI;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function EdgeSettingsViewModel(data = {}) {
|
||||||
|
this.CheckinInterval = data.CheckinInterval;
|
||||||
|
this.PingInterval = data.PingInterval;
|
||||||
|
this.SnapshotInterval = data.SnapshotInterval;
|
||||||
|
this.CommandInterval = data.CommandInterval;
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ function StateManagerFactory(
|
|||||||
UI: {
|
UI: {
|
||||||
dismissedInfoPanels: {},
|
dismissedInfoPanels: {},
|
||||||
dismissedInfoHash: '',
|
dismissedInfoHash: '',
|
||||||
|
timesPasswordChangeSkipped: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,13 +50,13 @@ function StateManagerFactory(
|
|||||||
};
|
};
|
||||||
|
|
||||||
manager.setPasswordChangeSkipped = function (userID) {
|
manager.setPasswordChangeSkipped = function (userID) {
|
||||||
state.UI.timesPasswordChangeSkipped = state.UI.timesPasswordChangeSkipped || {};
|
state.UI.instanceId = state.UI.instanceId || state.application.instanceId;
|
||||||
state.UI.timesPasswordChangeSkipped[userID] = state.UI.timesPasswordChangeSkipped[userID] + 1 || 1;
|
state.UI.timesPasswordChangeSkipped[userID] = state.UI.timesPasswordChangeSkipped[userID] + 1 || 1;
|
||||||
LocalStorage.storeUIState(state.UI);
|
LocalStorage.storeUIState(state.UI);
|
||||||
};
|
};
|
||||||
|
|
||||||
manager.resetPasswordChangeSkips = function (userID) {
|
manager.resetPasswordChangeSkips = function (userID) {
|
||||||
if (state.UI.timesPasswordChangeSkipped && state.UI.timesPasswordChangeSkipped[userID]) state.UI.timesPasswordChangeSkipped[userID] = 0;
|
if (state.UI.timesPasswordChangeSkipped[userID]) state.UI.timesPasswordChangeSkipped[userID] = 0;
|
||||||
LocalStorage.storeUIState(state.UI);
|
LocalStorage.storeUIState(state.UI);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -141,11 +142,6 @@ function StateManagerFactory(
|
|||||||
manager.initialize = initialize;
|
manager.initialize = initialize;
|
||||||
async function initialize() {
|
async function initialize() {
|
||||||
return $async(async () => {
|
return $async(async () => {
|
||||||
const UIState = LocalStorage.getUIState();
|
|
||||||
if (UIState) {
|
|
||||||
state.UI = UIState;
|
|
||||||
}
|
|
||||||
|
|
||||||
const endpointState = LocalStorage.getEndpointState();
|
const endpointState = LocalStorage.getEndpointState();
|
||||||
if (endpointState) {
|
if (endpointState) {
|
||||||
state.endpoint = endpointState;
|
state.endpoint = endpointState;
|
||||||
@@ -158,6 +154,16 @@ function StateManagerFactory(
|
|||||||
await loadApplicationState();
|
await loadApplicationState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UIState = LocalStorage.getUIState();
|
||||||
|
if (UIState) {
|
||||||
|
state.UI = UIState;
|
||||||
|
if (state.UI.instanceId && state.UI.instanceId !== state.application.instanceId) {
|
||||||
|
state.UI.instanceId = state.application.instanceId;
|
||||||
|
state.UI.timesPasswordChangeSkipped = {};
|
||||||
|
LocalStorage.storeUIState(state.UI);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
$analytics.setPortainerStatus(state.application.instanceId, state.application.version);
|
$analytics.setPortainerStatus(state.application.instanceId, state.application.version);
|
||||||
$analytics.setOptOut(!state.application.enableTelemetry);
|
$analytics.setOptOut(!state.application.enableTelemetry);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { FormSectionTitle } from '@/portainer/components/form-components/FormSectionTitle';
|
import { FormSectionTitle } from '@/portainer/components/form-components/FormSectionTitle';
|
||||||
import { react2angular } from '@/react-tools/react2angular';
|
import { react2angular } from '@/react-tools/react2angular';
|
||||||
|
import { confirm } from '@/portainer/services/modal.service/confirm';
|
||||||
|
|
||||||
import { SaveAuthSettingsButton } from '../components/SaveAuthSettingsButton';
|
import { SaveAuthSettingsButton } from '../components/SaveAuthSettingsButton';
|
||||||
import { Settings } from '../../types';
|
import { Settings } from '../../types';
|
||||||
@@ -19,6 +20,27 @@ export function InternalAuth({
|
|||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
function onSubmit() {
|
||||||
|
if (value.RequiredPasswordLength < 10) {
|
||||||
|
confirm({
|
||||||
|
title: 'Allow weak passwords?',
|
||||||
|
message:
|
||||||
|
'You have set an insecure minimum password length. This could leave your system vulnerable to attack, are you sure?',
|
||||||
|
buttons: {
|
||||||
|
confirm: {
|
||||||
|
label: 'Yes',
|
||||||
|
className: 'btn-danger',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
callback: function onConfirm(confirmed) {
|
||||||
|
if (confirmed) onSaveSettings();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
onSaveSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormSectionTitle>Information</FormSectionTitle>
|
<FormSectionTitle>Information</FormSectionTitle>
|
||||||
@@ -34,7 +56,7 @@ export function InternalAuth({
|
|||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<PasswordLengthSlider
|
<PasswordLengthSlider
|
||||||
min={8}
|
min={1}
|
||||||
max={18}
|
max={18}
|
||||||
step={1}
|
step={1}
|
||||||
value={value.RequiredPasswordLength}
|
value={value.RequiredPasswordLength}
|
||||||
@@ -42,7 +64,7 @@ export function InternalAuth({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SaveAuthSettingsButton onSubmit={onSaveSettings} isLoading={isLoading} />
|
<SaveAuthSettingsButton onSubmit={onSubmit} isLoading={isLoading} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { useMutation, useQuery, useQueryClient } from 'react-query';
|
|||||||
|
|
||||||
import { notifyError } from '@/portainer/services/notifications';
|
import { notifyError } from '@/portainer/services/notifications';
|
||||||
|
|
||||||
|
import { PublicSettingsViewModel } from '../models/settings';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
publicSettings,
|
publicSettings,
|
||||||
getSettings,
|
getSettings,
|
||||||
@@ -9,17 +11,29 @@ import {
|
|||||||
} from './settings.service';
|
} from './settings.service';
|
||||||
import { Settings } from './types';
|
import { Settings } from './types';
|
||||||
|
|
||||||
export function usePublicSettings() {
|
export function usePublicSettings<T = PublicSettingsViewModel>({
|
||||||
|
enabled,
|
||||||
|
select,
|
||||||
|
}: {
|
||||||
|
select?: (settings: PublicSettingsViewModel) => T;
|
||||||
|
enabled?: boolean;
|
||||||
|
} = {}) {
|
||||||
return useQuery(['settings', 'public'], () => publicSettings(), {
|
return useQuery(['settings', 'public'], () => publicSettings(), {
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
notifyError('Failure', err as Error, 'Unable to retrieve settings');
|
notifyError('Failure', err as Error, 'Unable to retrieve settings');
|
||||||
},
|
},
|
||||||
|
select,
|
||||||
|
enabled,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSettings<T = Settings>(select?: (settings: Settings) => T) {
|
export function useSettings<T = Settings>(
|
||||||
|
select?: (settings: Settings) => T,
|
||||||
|
enabled?: boolean
|
||||||
|
) {
|
||||||
return useQuery(['settings'], getSettings, {
|
return useQuery(['settings'], getSettings, {
|
||||||
select,
|
select,
|
||||||
|
enabled,
|
||||||
meta: {
|
meta: {
|
||||||
error: {
|
error: {
|
||||||
title: 'Failure',
|
title: 'Failure',
|
||||||
|
|||||||
@@ -125,4 +125,10 @@ export interface Settings {
|
|||||||
AllowStackManagementForRegularUsers: boolean;
|
AllowStackManagementForRegularUsers: boolean;
|
||||||
AllowDeviceMappingForRegularUsers: boolean;
|
AllowDeviceMappingForRegularUsers: boolean;
|
||||||
AllowContainerCapabilitiesForRegularUsers: boolean;
|
AllowContainerCapabilitiesForRegularUsers: boolean;
|
||||||
|
Edge: {
|
||||||
|
PingInterval: number;
|
||||||
|
SnapshotInterval: number;
|
||||||
|
CommandInterval: number;
|
||||||
|
AsyncMode: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export function CreateAccessToken({
|
|||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<WidgetBody>
|
<WidgetBody>
|
||||||
<div>
|
<div className="form-horizontal">
|
||||||
<FormControl
|
<FormControl
|
||||||
inputId="input"
|
inputId="input"
|
||||||
label={t('Description')}
|
label={t('Description')}
|
||||||
|
|||||||
@@ -42,19 +42,12 @@
|
|||||||
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
||||||
<input type="password" class="form-control" ng-model="formValues.confirmPassword" id="confirm_password" />
|
<input type="password" class="form-control" ng-model="formValues.confirmPassword" id="confirm_password" />
|
||||||
<span class="input-group-addon"
|
<span class="input-group-addon"
|
||||||
><i
|
><i ng-class="{ true: 'fa fa-check green-icon', false: 'fa fa-times red-icon' }[formValues.newPassword === formValues.confirmPassword]" aria-hidden="true"></i
|
||||||
ng-class="
|
|
||||||
{ true: 'fa fa-check green-icon', false: 'fa fa-times red-icon' }[
|
|
||||||
form.new_password.$viewValue !== '' && form.new_password.$viewValue === formValues.confirmPassword
|
|
||||||
]
|
|
||||||
"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i
|
|
||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<password-check-hint ng-if="!form.new_password.$valid || (forceChangePassword && !formValues.newPassword)"></password-check-hint>
|
<password-check-hint password-valid="form.new_password.$valid && formValues.newPassword" force-change-password="forceChangePassword"></password-check-hint>
|
||||||
<div ng-if="userRole === 1">
|
<div ng-if="userRole === 1">
|
||||||
<p class="text-muted">
|
<p class="text-muted">
|
||||||
<i class="fa fa-exclamation-circle blue-icon" aria-hidden="true"></i>
|
<i class="fa fa-exclamation-circle blue-icon" aria-hidden="true"></i>
|
||||||
@@ -67,7 +60,7 @@
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
ng-disabled="isDemoUser || (AuthenticationMethod !== 1 && !initialUser) || !formValues.currentPassword || !formValues.newPassword || !formValues.confirmPassword || form.$invalid || form.new_password.$viewValue !== formValues.confirmPassword"
|
ng-disabled="isDemoUser || (AuthenticationMethod !== 1 && !isInitialAdmin) || !formValues.currentPassword || !formValues.newPassword || form.$invalid || formValues.newPassword !== formValues.confirmPassword"
|
||||||
ng-click="updatePassword()"
|
ng-click="updatePassword()"
|
||||||
>
|
>
|
||||||
Update password
|
Update password
|
||||||
@@ -75,11 +68,11 @@
|
|||||||
<button type="submit" class="btn btn-primary btn-sm" ng-click="skipPasswordChange()" ng-if="forceChangePassword && timesPasswordChangeSkipped < 2"
|
<button type="submit" class="btn btn-primary btn-sm" ng-click="skipPasswordChange()" ng-if="forceChangePassword && timesPasswordChangeSkipped < 2"
|
||||||
>Remind me later</button
|
>Remind me later</button
|
||||||
>
|
>
|
||||||
<span class="text-muted small" style="margin-left: 5px" ng-if="AuthenticationMethod === 2 && !initialUser">
|
<span class="text-muted small" style="margin-left: 5px" ng-if="AuthenticationMethod === 2 && !isInitialAdmin">
|
||||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||||
You cannot change your password when using LDAP authentication.
|
You cannot change your password when using LDAP authentication.
|
||||||
</span>
|
</span>
|
||||||
<span class="text-muted small" style="margin-left: 5px" ng-if="AuthenticationMethod === 3 && !initialUser">
|
<span class="text-muted small" style="margin-left: 5px" ng-if="AuthenticationMethod === 3 && !isInitialAdmin">
|
||||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||||
You cannot change your password when using OAuth authentication.
|
You cannot change your password when using OAuth authentication.
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ angular.module('portainer.app').controller('AccountController', [
|
|||||||
try {
|
try {
|
||||||
await UserService.updateUserPassword($scope.userID, $scope.formValues.currentPassword, $scope.formValues.newPassword);
|
await UserService.updateUserPassword($scope.userID, $scope.formValues.currentPassword, $scope.formValues.newPassword);
|
||||||
Notifications.success('Success', 'Password successfully updated');
|
Notifications.success('Success', 'Password successfully updated');
|
||||||
StateManager.resetPasswordChangeSkips($scope.userID);
|
StateManager.resetPasswordChangeSkips($scope.userID.toString());
|
||||||
$scope.forceChangePassword = false;
|
$scope.forceChangePassword = false;
|
||||||
$state.go('portainer.logout');
|
$state.go('portainer.logout');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -34,7 +34,7 @@ angular.module('portainer.app').controller('AccountController', [
|
|||||||
$scope.skipPasswordChange = async function () {
|
$scope.skipPasswordChange = async function () {
|
||||||
try {
|
try {
|
||||||
if ($scope.userCanSkip()) {
|
if ($scope.userCanSkip()) {
|
||||||
StateManager.setPasswordChangeSkipped($scope.userID);
|
StateManager.setPasswordChangeSkipped($scope.userID.toString());
|
||||||
$scope.forceChangePassword = false;
|
$scope.forceChangePassword = false;
|
||||||
$state.go('portainer.home');
|
$state.go('portainer.home');
|
||||||
}
|
}
|
||||||
@@ -48,8 +48,13 @@ angular.module('portainer.app').controller('AccountController', [
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.uiCanExit = (newTransition) => {
|
this.uiCanExit = (newTransition) => {
|
||||||
if ($scope.userRole === 1 && newTransition.to().name === 'portainer.settings.authentication') {
|
if (newTransition) {
|
||||||
return true;
|
if ($scope.userRole === 1 && newTransition.to().name === 'portainer.settings.authentication') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (newTransition.to().name === 'portainer.logout') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ($scope.forceChangePassword) {
|
if ($scope.forceChangePassword) {
|
||||||
ModalService.confirmForceChangePassword();
|
ModalService.confirmForceChangePassword();
|
||||||
@@ -113,6 +118,7 @@ angular.module('portainer.app').controller('AccountController', [
|
|||||||
$scope.userID = userDetails.ID;
|
$scope.userID = userDetails.ID;
|
||||||
$scope.userRole = Authentication.getUserDetails().role;
|
$scope.userRole = Authentication.getUserDetails().role;
|
||||||
$scope.forceChangePassword = userDetails.forceChangePassword;
|
$scope.forceChangePassword = userDetails.forceChangePassword;
|
||||||
|
$scope.isInitialAdmin = userDetails.ID === 1;
|
||||||
|
|
||||||
if (state.application.demoEnvironment.enabled) {
|
if (state.application.demoEnvironment.enabled) {
|
||||||
$scope.isDemoUser = state.application.demoEnvironment.users.includes($scope.userID);
|
$scope.isDemoUser = state.application.demoEnvironment.users.includes($scope.userID);
|
||||||
@@ -127,11 +133,13 @@ angular.module('portainer.app').controller('AccountController', [
|
|||||||
$scope.AuthenticationMethod = data.AuthenticationMethod;
|
$scope.AuthenticationMethod = data.AuthenticationMethod;
|
||||||
|
|
||||||
if (state.UI.requiredPasswordLength && state.UI.requiredPasswordLength !== data.RequiredPasswordLength) {
|
if (state.UI.requiredPasswordLength && state.UI.requiredPasswordLength !== data.RequiredPasswordLength) {
|
||||||
StateManager.clearPasswordChangeSkips($scope.userID);
|
StateManager.clearPasswordChangeSkips();
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.timesPasswordChangeSkipped =
|
$scope.timesPasswordChangeSkipped =
|
||||||
state.UI.timesPasswordChangeSkipped && state.UI.timesPasswordChangeSkipped[$scope.userID] ? state.UI.timesPasswordChangeSkipped[$scope.userID] : 0;
|
state.UI.timesPasswordChangeSkipped && state.UI.timesPasswordChangeSkipped[$scope.userID.toString()]
|
||||||
|
? state.UI.timesPasswordChangeSkipped[$scope.userID.toString()]
|
||||||
|
: 0;
|
||||||
|
|
||||||
$scope.requiredPasswordLength = data.RequiredPasswordLength;
|
$scope.requiredPasswordLength = data.RequiredPasswordLength;
|
||||||
StateManager.setRequiredPasswordLength(data.RequiredPasswordLength);
|
StateManager.setRequiredPasswordLength(data.RequiredPasswordLength);
|
||||||
|
|||||||
+1
@@ -108,6 +108,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<custom-templates-variables-definition-field
|
<custom-templates-variables-definition-field
|
||||||
|
ng-if="$ctrl.isTemplateVariablesEnabled"
|
||||||
value="$ctrl.formValues.Variables"
|
value="$ctrl.formValues.Variables"
|
||||||
on-change="($ctrl.onVariablesChange)"
|
on-change="($ctrl.onVariablesChange)"
|
||||||
is-variables-names-from-parent="$ctrl.state.Method === 'editor'"
|
is-variables-names-from-parent="$ctrl.state.Method === 'editor'"
|
||||||
|
|||||||
+7
@@ -2,6 +2,7 @@ import _ from 'lodash';
|
|||||||
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
|
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
|
||||||
import { TEMPLATE_NAME_VALIDATION_REGEX } from '@/constants';
|
import { TEMPLATE_NAME_VALIDATION_REGEX } from '@/constants';
|
||||||
import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils';
|
import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils';
|
||||||
|
import { isBE } from '@/portainer/feature-flags/feature-flags.service';
|
||||||
|
|
||||||
class CreateCustomTemplateViewController {
|
class CreateCustomTemplateViewController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
@@ -20,6 +21,8 @@ class CreateCustomTemplateViewController {
|
|||||||
StateManager,
|
StateManager,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.isTemplateVariablesEnabled = isBE;
|
||||||
|
|
||||||
this.formValues = {
|
this.formValues = {
|
||||||
Title: '',
|
Title: '',
|
||||||
FileContent: '',
|
FileContent: '',
|
||||||
@@ -176,6 +179,10 @@ class CreateCustomTemplateViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parseTemplate(templateStr) {
|
parseTemplate(templateStr) {
|
||||||
|
if (!this.isTemplateVariablesEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const variables = getTemplateVariables(templateStr);
|
const variables = getTemplateVariables(templateStr);
|
||||||
|
|
||||||
const isValid = !!variables;
|
const isValid = !!variables;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
>
|
>
|
||||||
<advanced-form>
|
<advanced-form>
|
||||||
<custom-templates-variables-field
|
<custom-templates-variables-field
|
||||||
|
ng-if="$ctrl.isTemplateVariablesEnabled"
|
||||||
definitions="$ctrl.state.selectedTemplate.Variables"
|
definitions="$ctrl.state.selectedTemplate.Variables"
|
||||||
value="$ctrl.formValues.variables"
|
value="$ctrl.formValues.variables"
|
||||||
on-change="($ctrl.onChangeTemplateVariables)"
|
on-change="($ctrl.onChangeTemplateVariables)"
|
||||||
|
|||||||
+7
@@ -2,6 +2,7 @@ import _ from 'lodash-es';
|
|||||||
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
|
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
|
||||||
import { TEMPLATE_NAME_VALIDATION_REGEX } from '@/constants';
|
import { TEMPLATE_NAME_VALIDATION_REGEX } from '@/constants';
|
||||||
import { renderTemplate } from '@/react/portainer/custom-templates/components/utils';
|
import { renderTemplate } from '@/react/portainer/custom-templates/components/utils';
|
||||||
|
import { isBE } from '@/portainer/feature-flags/feature-flags.service';
|
||||||
|
|
||||||
class CustomTemplatesViewController {
|
class CustomTemplatesViewController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
@@ -34,6 +35,8 @@ class CustomTemplatesViewController {
|
|||||||
this.StateManager = StateManager;
|
this.StateManager = StateManager;
|
||||||
this.StackService = StackService;
|
this.StackService = StackService;
|
||||||
|
|
||||||
|
this.isTemplateVariablesEnabled = isBE;
|
||||||
|
|
||||||
this.DOCKER_STANDALONE = 'DOCKER_STANDALONE';
|
this.DOCKER_STANDALONE = 'DOCKER_STANDALONE';
|
||||||
this.DOCKER_SWARM_MODE = 'DOCKER_SWARM_MODE';
|
this.DOCKER_SWARM_MODE = 'DOCKER_SWARM_MODE';
|
||||||
|
|
||||||
@@ -119,6 +122,10 @@ class CustomTemplatesViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderTemplate() {
|
renderTemplate() {
|
||||||
|
if (!this.isTemplateVariablesEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const fileContent = renderTemplate(this.state.templateContent, this.formValues.variables, this.state.selectedTemplate.Variables);
|
const fileContent = renderTemplate(this.state.templateContent, this.formValues.variables, this.state.selectedTemplate.Variables);
|
||||||
this.onChangeFormValues({ fileContent });
|
this.onChangeFormValues({ fileContent });
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -46,6 +46,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<custom-templates-variables-definition-field
|
<custom-templates-variables-definition-field
|
||||||
|
ng-if="$ctrl.isTemplateVariablesEnabled"
|
||||||
value="$ctrl.formValues.Variables"
|
value="$ctrl.formValues.Variables"
|
||||||
on-change="($ctrl.onVariablesChange)"
|
on-change="($ctrl.onVariablesChange)"
|
||||||
is-variables-names-from-parent="true"
|
is-variables-names-from-parent="true"
|
||||||
|
|||||||
+7
@@ -3,12 +3,15 @@ import { ResourceControlViewModel } from '@/portainer/access-control/models/Reso
|
|||||||
|
|
||||||
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
|
import { AccessControlFormData } from 'Portainer/components/accessControlForm/porAccessControlFormModel';
|
||||||
import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils';
|
import { getTemplateVariables, intersectVariables } from '@/react/portainer/custom-templates/components/utils';
|
||||||
|
import { isBE } from '@/portainer/feature-flags/feature-flags.service';
|
||||||
|
|
||||||
class EditCustomTemplateViewController {
|
class EditCustomTemplateViewController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor($async, $state, $window, ModalService, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService) {
|
constructor($async, $state, $window, ModalService, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService) {
|
||||||
Object.assign(this, { $async, $state, $window, ModalService, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService });
|
Object.assign(this, { $async, $state, $window, ModalService, Authentication, CustomTemplateService, FormValidator, Notifications, ResourceControlService });
|
||||||
|
|
||||||
|
this.isTemplateVariablesEnabled = isBE;
|
||||||
|
|
||||||
this.formValues = null;
|
this.formValues = null;
|
||||||
this.state = {
|
this.state = {
|
||||||
formValidationError: '',
|
formValidationError: '',
|
||||||
@@ -127,6 +130,10 @@ class EditCustomTemplateViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parseTemplate(templateStr) {
|
parseTemplate(templateStr) {
|
||||||
|
if (!this.isTemplateVariablesEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const variables = getTemplateVariables(templateStr);
|
const variables = getTemplateVariables(templateStr);
|
||||||
|
|
||||||
const isValid = !!variables;
|
const isValid = !!variables;
|
||||||
|
|||||||
@@ -59,6 +59,7 @@
|
|||||||
edge-info="{ key: endpoint.EdgeKey, id: endpoint.EdgeID }"
|
edge-info="{ key: endpoint.EdgeKey, id: endpoint.EdgeID }"
|
||||||
commands="state.edgeScriptCommands"
|
commands="state.edgeScriptCommands"
|
||||||
is-nomad-token-visible="state.showNomad"
|
is-nomad-token-visible="state.showNomad"
|
||||||
|
hide-async-mode="!endpoint.IsEdgeDevice"
|
||||||
></edge-script-form>
|
></edge-script-form>
|
||||||
|
|
||||||
<span class="small text-muted">
|
<span class="small text-muted">
|
||||||
|
|||||||
@@ -67,10 +67,11 @@
|
|||||||
<!-- !confirm-password-input -->
|
<!-- !confirm-password-input -->
|
||||||
<!-- note -->
|
<!-- note -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12 text-muted" ng-if="!form.password.$valid">
|
<div class="col-sm-12 text-muted">
|
||||||
<p>
|
<p>
|
||||||
<i class="fa fa-exclamation-triangle orange-icon space-right" aria-hidden="true"></i>
|
<i class="fa fa-exclamation-triangle orange-icon space-right" aria-hidden="true"></i>
|
||||||
<span>The password must be at least {{ requiredPasswordLength }} characters long.</span>
|
<span>The password must be at least {{ requiredPasswordLength }} characters long.</span>
|
||||||
|
<i class="fa fa-check green-icon space-left" aria-hidden="true" ng-if="form.password.$valid && formValues.Password"></i>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<por-switch-field
|
<por-switch-field
|
||||||
label="'Use custom logo'"
|
label="'Use custom logo'"
|
||||||
value="formValues.customLogo"
|
checked="formValues.customLogo"
|
||||||
name="'toggle_logo'"
|
name="'toggle_logo'"
|
||||||
disabled="state.isDemo"
|
disabled="state.isDemo"
|
||||||
on-change="(onToggleCustomLogo)"
|
on-change="(onToggleCustomLogo)"
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<por-switch-field
|
<por-switch-field
|
||||||
label="'Allow the collection of anonymous statistics'"
|
label="'Allow the collection of anonymous statistics'"
|
||||||
value="formValues.enableTelemetry"
|
checked="formValues.enableTelemetry"
|
||||||
name="'toggle_enableTelemetry'"
|
name="'toggle_enableTelemetry'"
|
||||||
on-change="(onToggleEnableTelemetry)"
|
on-change="(onToggleEnableTelemetry)"
|
||||||
disabled="state.isDemo"
|
disabled="state.isDemo"
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
children-paths="['portainer.users.user' ,'portainer.teams' ,'portainer.teams.team' ,'portainer.roles' ,'portainer.roles.role' ,'portainer.roles.new']"
|
children-paths="['portainer.users.user' ,'portainer.teams' ,'portainer.teams.team' ,'portainer.roles' ,'portainer.roles.role' ,'portainer.roles.new']"
|
||||||
>
|
>
|
||||||
<sidebar-menu-item path="portainer.teams" class-name="sidebar-sublist" data-cy="portainerSidebar-teams" title="Teams">Teams</sidebar-menu-item>
|
<sidebar-menu-item path="portainer.teams" class-name="sidebar-sublist" data-cy="portainerSidebar-teams" title="Teams">Teams</sidebar-menu-item>
|
||||||
<sidebar-menu-item path="portainer.roles" class-name="sidebar-sublist" data-cy="portainerSidebar-roles" title="Roles">Roles</sidebar-menu-item>
|
<sidebar-menu-item ng-if="isAdmin" path="portainer.roles" class-name="sidebar-sublist" data-cy="portainerSidebar-roles" title="Roles">Roles</sidebar-menu-item>
|
||||||
</sidebar-menu>
|
</sidebar-menu>
|
||||||
|
|
||||||
<div ng-if="isAdmin">
|
<div ng-if="isAdmin">
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { AccessControlFormData } from '@/portainer/components/accessControlForm/
|
|||||||
import { STACK_NAME_VALIDATION_REGEX } from '@/constants';
|
import { STACK_NAME_VALIDATION_REGEX } from '@/constants';
|
||||||
import { RepositoryMechanismTypes } from '@/kubernetes/models/deploy';
|
import { RepositoryMechanismTypes } from '@/kubernetes/models/deploy';
|
||||||
import { FeatureId } from 'Portainer/feature-flags/enums';
|
import { FeatureId } from 'Portainer/feature-flags/enums';
|
||||||
|
import { isBE } from '@/portainer/feature-flags/feature-flags.service';
|
||||||
|
import { renderTemplate } from '@/react/portainer/custom-templates/components/utils';
|
||||||
|
|
||||||
angular
|
angular
|
||||||
.module('portainer.app')
|
.module('portainer.app')
|
||||||
@@ -31,6 +33,8 @@ angular
|
|||||||
endpoint
|
endpoint
|
||||||
) {
|
) {
|
||||||
$scope.onChangeTemplateId = onChangeTemplateId;
|
$scope.onChangeTemplateId = onChangeTemplateId;
|
||||||
|
$scope.onChangeTemplateVariables = onChangeTemplateVariables;
|
||||||
|
$scope.isTemplateVariablesEnabled = isBE;
|
||||||
$scope.buildAnalyticsProperties = buildAnalyticsProperties;
|
$scope.buildAnalyticsProperties = buildAnalyticsProperties;
|
||||||
$scope.stackWebhookFeature = FeatureId.STACK_WEBHOOK;
|
$scope.stackWebhookFeature = FeatureId.STACK_WEBHOOK;
|
||||||
$scope.STACK_NAME_VALIDATION_REGEX = STACK_NAME_VALIDATION_REGEX;
|
$scope.STACK_NAME_VALIDATION_REGEX = STACK_NAME_VALIDATION_REGEX;
|
||||||
@@ -53,6 +57,7 @@ angular
|
|||||||
RepositoryMechanism: RepositoryMechanismTypes.INTERVAL,
|
RepositoryMechanism: RepositoryMechanismTypes.INTERVAL,
|
||||||
RepositoryFetchInterval: '5m',
|
RepositoryFetchInterval: '5m',
|
||||||
RepositoryWebhookURL: WebhookHelper.returnStackWebhookUrl(uuidv4()),
|
RepositoryWebhookURL: WebhookHelper.returnStackWebhookUrl(uuidv4()),
|
||||||
|
Variables: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
@@ -265,11 +270,12 @@ angular
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.onChangeFileContent = function onChangeFileContent(value) {
|
$scope.onChangeFileContent = onChangeFileContent;
|
||||||
|
function onChangeFileContent(value) {
|
||||||
$scope.formValues.StackFileContent = value;
|
$scope.formValues.StackFileContent = value;
|
||||||
$scope.state.editorYamlValidationError = StackHelper.validateYAML($scope.formValues.StackFileContent, $scope.containerNames);
|
$scope.state.editorYamlValidationError = StackHelper.validateYAML($scope.formValues.StackFileContent, $scope.containerNames);
|
||||||
$scope.state.isEditorDirty = true;
|
$scope.state.isEditorDirty = true;
|
||||||
};
|
}
|
||||||
|
|
||||||
async function onFileLoadAsync(event) {
|
async function onFileLoadAsync(event) {
|
||||||
$scope.state.uploadYamlValidationError = StackHelper.validateYAML(event.target.result, $scope.containerNames);
|
$scope.state.uploadYamlValidationError = StackHelper.validateYAML(event.target.result, $scope.containerNames);
|
||||||
@@ -292,18 +298,38 @@ angular
|
|||||||
|
|
||||||
function onChangeTemplateId(templateId, template) {
|
function onChangeTemplateId(templateId, template) {
|
||||||
return $async(async () => {
|
return $async(async () => {
|
||||||
|
if (!template || ($scope.state.selectedTemplateId === templateId && $scope.state.selectedTemplate === template)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$scope.state.selectedTemplateId = templateId;
|
$scope.state.selectedTemplateId = templateId;
|
||||||
$scope.state.selectedTemplate = template;
|
$scope.state.selectedTemplate = template;
|
||||||
|
|
||||||
const fileContent = await CustomTemplateService.customTemplateFile(templateId);
|
const fileContent = await CustomTemplateService.customTemplateFile(templateId);
|
||||||
$scope.onChangeFileContent(fileContent);
|
$scope.state.templateContent = fileContent;
|
||||||
|
onChangeFileContent(fileContent);
|
||||||
|
|
||||||
|
if (template.Variables && template.Variables.length > 0) {
|
||||||
|
const variables = Object.fromEntries(template.Variables.map((variable) => [variable.name, '']));
|
||||||
|
onChangeTemplateVariables(variables);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to retrieve Custom Template file');
|
Notifications.error('Failure', err, 'Unable to retrieve Custom Template file');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onChangeTemplateVariables(value) {
|
||||||
|
onChangeFormValues({ Variables: value });
|
||||||
|
|
||||||
|
if (!$scope.isTemplateVariablesEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rendered = renderTemplate($scope.state.templateContent, $scope.formValues.Variables, $scope.state.selectedTemplate.Variables);
|
||||||
|
onChangeFormValues({ StackFileContent: rendered });
|
||||||
|
}
|
||||||
|
|
||||||
async function initView() {
|
async function initView() {
|
||||||
var endpointMode = $scope.applicationState.endpoint.mode;
|
var endpointMode = $scope.applicationState.endpoint.mode;
|
||||||
$scope.state.StackType = 2;
|
$scope.state.StackType = 2;
|
||||||
@@ -328,8 +354,11 @@ angular
|
|||||||
|
|
||||||
initView();
|
initView();
|
||||||
|
|
||||||
function onChangeFormValues(newValues) {
|
function onChangeFormValues(values) {
|
||||||
$scope.formValues = newValues;
|
$scope.formValues = {
|
||||||
|
...$scope.formValues,
|
||||||
|
...values,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -131,13 +131,21 @@
|
|||||||
path-placeholder="docker-compose.yml"
|
path-placeholder="docker-compose.yml"
|
||||||
></git-form>
|
></git-form>
|
||||||
|
|
||||||
<custom-template-selector
|
<div ng-show="state.Method === 'template'">
|
||||||
ng-show="state.Method === 'template'"
|
<custom-template-selector
|
||||||
new-template-path="docker.templates.custom.new"
|
new-template-path="docker.templates.custom.new"
|
||||||
stack-type="state.StackType"
|
stack-type="state.StackType"
|
||||||
on-change="(onChangeTemplateId)"
|
on-change="(onChangeTemplateId)"
|
||||||
value="state.selectedTemplateId"
|
value="state.selectedTemplateId"
|
||||||
></custom-template-selector>
|
></custom-template-selector>
|
||||||
|
|
||||||
|
<custom-templates-variables-field
|
||||||
|
ng-if="isTemplateVariablesEnabled && state.selectedTemplate"
|
||||||
|
definitions="state.selectedTemplate.Variables"
|
||||||
|
value="formValues.Variables"
|
||||||
|
on-change="(onChangeTemplateVariables)"
|
||||||
|
></custom-templates-variables-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
<web-editor-form
|
<web-editor-form
|
||||||
ng-if="state.Method === 'editor' || (state.Method === 'template' && state.selectedTemplateId)"
|
ng-if="state.Method === 'editor' || (state.Method === 'template' && state.selectedTemplateId)"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>Name</td>
|
<td>Name</td>
|
||||||
<td>
|
<td>
|
||||||
{{ team.Name }}
|
<span ng-if="!settings.TeamSync">{{ team.Name }}</span>
|
||||||
<button class="btn btn-xs btn-danger" ng-if="isAdmin" ng-click="deleteTeam()"
|
<button class="btn btn-xs btn-danger" ng-if="isAdmin" ng-click="deleteTeam()"
|
||||||
><i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Delete this team</button
|
><i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Delete this team</button
|
||||||
>
|
>
|
||||||
@@ -23,7 +23,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Leaders</td>
|
<td>Leaders</td>
|
||||||
<td>{{ leaderCount }}</td>
|
<td>
|
||||||
|
<span ng-if="!settings.TeamSync">{{ leaderCount }}</span>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Total users in team</td>
|
<td>Total users in team</td>
|
||||||
@@ -175,11 +177,11 @@
|
|||||||
<i ng-if="user.TeamRole === 'Leader'" class="fa fa-user-plus" aria-hidden="true" style="margin-right: 2px"></i>
|
<i ng-if="user.TeamRole === 'Leader'" class="fa fa-user-plus" aria-hidden="true" style="margin-right: 2px"></i>
|
||||||
<i ng-if="user.TeamRole === 'Member'" class="fa fa-user" aria-hidden="true" style="margin-right: 2px"></i>
|
<i ng-if="user.TeamRole === 'Member'" class="fa fa-user" aria-hidden="true" style="margin-right: 2px"></i>
|
||||||
{{ user.TeamRole }}
|
{{ user.TeamRole }}
|
||||||
<span style="margin-left: 5px" ng-if="isAdmin">
|
<span style="margin-left: 5px">
|
||||||
<a style="margin-left: 5px" ng-click="promoteToLeader(user)" ng-if="user.TeamRole === 'Member'" ng-class="{ 'btn disabled py-0': settings.TeamSync }"
|
<a style="margin-left: 5px" ng-click="promoteToLeader(user)" ng-if="user.TeamRole === 'Member'" ng-class="{ 'btn disabled py-0': settings.TeamSync }"
|
||||||
><i class="fa fa-user-plus space-right" aria-hidden="true"></i>Leader</a
|
><i class="fa fa-user-plus space-right" aria-hidden="true"></i>Leader</a
|
||||||
>
|
>
|
||||||
<a style="margin-left: 5px" ng-click="demoteToMember(user)" ng-if="user.TeamRole === 'Leader'" ng-class="{ 'btn disabled py-0': settings.TeamSync }"
|
<a style="margin-left: 5px" ng-click="demoteToMember(user)" ng-if="isAdmin && user.TeamRole === 'Leader'" ng-class="{ 'btn disabled py-0': settings.TeamSync }"
|
||||||
><i class="fa fa-user-times space-right" aria-hidden="true"></i>Member</a
|
><i class="fa fa-user-times space-right" aria-hidden="true"></i>Member</a
|
||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !confirm-password-input -->
|
<!-- !confirm-password-input -->
|
||||||
<password-check-hint ng-if="!form.new_password.$valid"></password-check-hint>
|
<password-check-hint password-valid="form.new_password.$valid && formValues.newPassword"></password-check-hint>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-2">
|
<div class="col-sm-2">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -80,10 +80,10 @@
|
|||||||
<!-- !confirm-password-input -->
|
<!-- !confirm-password-input -->
|
||||||
|
|
||||||
<!-- password-check-hint -->
|
<!-- password-check-hint -->
|
||||||
<div class="form-group" ng-if="AuthenticationMethod === 1 && !form.password.$valid">
|
<div class="form-group" ng-if="AuthenticationMethod === 1">
|
||||||
<div class="col-sm-3 col-lg-2"></div>
|
<div class="col-sm-3 col-lg-2"></div>
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
<password-check-hint></password-check-hint>
|
<password-check-hint password-valid="form.password.$valid && formValues.Password"></password-check-hint>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- ! password-check-hint -->
|
<!-- ! password-check-hint -->
|
||||||
@@ -140,7 +140,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
ng-disabled="state.actionInProgress || !state.validUsername || formValues.Username === '' || !formValues.Password || !formValues.ConfirmPassword || (AuthenticationMethod === 1 && form.$invalid) || (AuthenticationMethod === 1 && form.password.$viewValue !== formValues.ConfirmPassword)"
|
ng-disabled="state.actionInProgress || !state.validUsername || formValues.Username === '' || (AuthenticationMethod === 1 && (!formValues.Password || form.$invalid || formValues.Password !== formValues.ConfirmPassword))"
|
||||||
ng-click="addUser()"
|
ng-click="addUser()"
|
||||||
button-spinner="state.actionInProgress"
|
button-spinner="state.actionInProgress"
|
||||||
data-cy="user-createUserButton"
|
data-cy="user-createUserButton"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import _ from 'lodash';
|
|||||||
|
|
||||||
import { Team } from '@/portainer/teams/types';
|
import { Team } from '@/portainer/teams/types';
|
||||||
import { Role, User, UserId } from '@/portainer/users/types';
|
import { Role, User, UserId } from '@/portainer/users/types';
|
||||||
|
import { Environment } from '@/portainer/environments/types';
|
||||||
|
|
||||||
export function createMockUsers(
|
export function createMockUsers(
|
||||||
count: number,
|
count: number,
|
||||||
@@ -59,3 +60,25 @@ export function createMockResourceGroups(subscription: string, count: number) {
|
|||||||
|
|
||||||
return { value: resourceGroups };
|
return { value: resourceGroups };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createMockEnvironment(): Environment {
|
||||||
|
return {
|
||||||
|
TagIds: [],
|
||||||
|
GroupId: 1,
|
||||||
|
Type: 1,
|
||||||
|
Name: 'environment',
|
||||||
|
Status: 1,
|
||||||
|
URL: 'url',
|
||||||
|
Snapshots: [],
|
||||||
|
Kubernetes: { Snapshots: [] },
|
||||||
|
EdgeKey: '',
|
||||||
|
Id: 3,
|
||||||
|
UserTrusted: false,
|
||||||
|
Edge: {
|
||||||
|
AsyncMode: false,
|
||||||
|
PingInterval: 0,
|
||||||
|
CommandInterval: 0,
|
||||||
|
SnapshotInterval: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { ResourceControlResponse } from '@/portainer/access-control/types';
|
||||||
|
|
||||||
|
export interface PortainerMetadata {
|
||||||
|
ResourceControl: ResourceControlResponse;
|
||||||
|
}
|
||||||
@@ -38,12 +38,15 @@ export function intersectVariables(
|
|||||||
) {
|
) {
|
||||||
const oldVariablesWithLabel = oldVariables.filter((v) => !!v.label);
|
const oldVariablesWithLabel = oldVariables.filter((v) => !!v.label);
|
||||||
|
|
||||||
return [
|
return _.uniqBy(
|
||||||
...oldVariablesWithLabel,
|
[
|
||||||
...newVariables.filter(
|
...oldVariablesWithLabel,
|
||||||
(v) => !oldVariablesWithLabel.find(({ name }) => name === v.name)
|
...newVariables.filter(
|
||||||
),
|
(v) => !oldVariablesWithLabel.find(({ name }) => name === v.name)
|
||||||
];
|
),
|
||||||
|
],
|
||||||
|
'name'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderTemplate(
|
export function renderTemplate(
|
||||||
@@ -68,5 +71,5 @@ export function renderTemplate(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
return Mustache.render(template, state);
|
return Mustache.render(template, state, undefined, { escape: (t) => t });
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -23,6 +23,7 @@ export function EnvironmentSelector({ value, onChange }: Props) {
|
|||||||
{environmentTypes.map((eType) => (
|
{environmentTypes.map((eType) => (
|
||||||
<Option
|
<Option
|
||||||
key={eType.id}
|
key={eType.id}
|
||||||
|
featureId={eType.featureId}
|
||||||
title={eType.title}
|
title={eType.title}
|
||||||
description={eType.description}
|
description={eType.description}
|
||||||
icon={eType.icon}
|
icon={eType.icon}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.selected .mask-icon {
|
||||||
|
color: var(--selected-item-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mask-icon {
|
||||||
|
color: var(--bg-boxselector-color);
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import styles from './KaaSIcon.module.css';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
selected?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function KaaSIcon({ selected, className }: Props) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={clsx('fa-stack fa-1x', styles.root, className, {
|
||||||
|
[styles.selected]: selected,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<i className="fas fa-cloud fa-stack-2x" />
|
||||||
|
<i className={clsx('fas fa-dharmachakra fa-stack-1x', styles.maskIcon)} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
+28
-2
@@ -1,4 +1,16 @@
|
|||||||
export const environmentTypes = [
|
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||||
|
|
||||||
|
import { KaaSIcon, Props as KaaSIconProps } from './KaaSIcon';
|
||||||
|
|
||||||
|
interface WizardEnvironmentOption {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
icon: string | { ({ selected, className }: KaaSIconProps): JSX.Element };
|
||||||
|
description: string;
|
||||||
|
featureId?: FeatureId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const environmentTypes: WizardEnvironmentOption[] = [
|
||||||
{
|
{
|
||||||
id: 'docker',
|
id: 'docker',
|
||||||
title: 'Docker',
|
title: 'Docker',
|
||||||
@@ -18,4 +30,18 @@ export const environmentTypes = [
|
|||||||
description: 'Connect to ACI environment via API',
|
description: 'Connect to ACI environment via API',
|
||||||
icon: 'fab fa-microsoft',
|
icon: 'fab fa-microsoft',
|
||||||
},
|
},
|
||||||
] as const;
|
{
|
||||||
|
id: 'nomad',
|
||||||
|
title: 'Nomad',
|
||||||
|
description: 'Connect to HashiCorp Nomad environment via API',
|
||||||
|
icon: 'nomad-icon',
|
||||||
|
featureId: FeatureId.NOMAD,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'kaas',
|
||||||
|
title: 'KaaS',
|
||||||
|
description: 'Provision a Kubernetes environment with a cloud provider',
|
||||||
|
icon: KaaSIcon,
|
||||||
|
featureId: FeatureId.KAAS_PROVISIONING,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
+2
-2
@@ -118,8 +118,8 @@ export function EnvironmentCreationView() {
|
|||||||
])
|
])
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
if (localStorage.getItem('wizardRefferer') === 'environments') {
|
if (localStorage.getItem('wizardReferrer') === 'environments') {
|
||||||
localStorage.removeItem('wizardRefferer');
|
localStorage.removeItem('wizardReferrer');
|
||||||
router.stateService.go('portainer.endpoints');
|
router.stateService.go('portainer.endpoints');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-13
@@ -3,11 +3,7 @@ import { useFormikContext } from 'formik';
|
|||||||
import { FileUploadField } from '@/portainer/components/form-components/FileUpload';
|
import { FileUploadField } from '@/portainer/components/form-components/FileUpload';
|
||||||
import { SwitchField } from '@/portainer/components/form-components/SwitchField';
|
import { SwitchField } from '@/portainer/components/form-components/SwitchField';
|
||||||
import { FormControl } from '@/portainer/components/form-components/FormControl';
|
import { FormControl } from '@/portainer/components/form-components/FormControl';
|
||||||
import {
|
import { file, withFileSize } from '@/portainer/helpers/yup-file-validation';
|
||||||
file,
|
|
||||||
withFileSize,
|
|
||||||
withFileType,
|
|
||||||
} from '@/portainer/helpers/yup-file-validation';
|
|
||||||
|
|
||||||
import { FormValues } from './types';
|
import { FormValues } from './types';
|
||||||
|
|
||||||
@@ -82,16 +78,9 @@ export function TLSFieldset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MAX_FILE_SIZE = 5_242_880; // 5MB
|
const MAX_FILE_SIZE = 5_242_880; // 5MB
|
||||||
const ALLOWED_FILE_TYPES = [
|
|
||||||
'application/x-x509-ca-cert',
|
|
||||||
'application/x-pem-file',
|
|
||||||
];
|
|
||||||
|
|
||||||
function certValidation() {
|
function certValidation() {
|
||||||
return withFileType(
|
return withFileSize(file(), MAX_FILE_SIZE).when(['tls', 'skipVerify'], {
|
||||||
withFileSize(file(), MAX_FILE_SIZE),
|
|
||||||
ALLOWED_FILE_TYPES
|
|
||||||
).when(['tls', 'skipVerify'], {
|
|
||||||
is: (tls: boolean, skipVerify: boolean) => tls && !skipVerify,
|
is: (tls: boolean, skipVerify: boolean) => tls && !skipVerify,
|
||||||
then: (schema) => schema.required('File is required'),
|
then: (schema) => schema.required('File is required'),
|
||||||
});
|
});
|
||||||
|
|||||||
+35
-9
@@ -3,7 +3,6 @@ import { useState } from 'react';
|
|||||||
import { CopyButton } from '@/portainer/components/Button/CopyButton';
|
import { CopyButton } from '@/portainer/components/Button/CopyButton';
|
||||||
import { Code } from '@/portainer/components/Code';
|
import { Code } from '@/portainer/components/Code';
|
||||||
import { NavTabs } from '@/portainer/components/NavTabs/NavTabs';
|
import { NavTabs } from '@/portainer/components/NavTabs/NavTabs';
|
||||||
import { getAgentShortVersion } from '@/portainer/views/endpoints/helpers';
|
|
||||||
import { useAgentDetails } from '@/portainer/environments/queries/useAgentDetails';
|
import { useAgentDetails } from '@/portainer/environments/queries/useAgentDetails';
|
||||||
|
|
||||||
const deployments = [
|
const deployments = [
|
||||||
@@ -28,10 +27,10 @@ export function DeploymentScripts() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { agentVersion } = agentDetailsQuery;
|
const { agentVersion, agentSecret } = agentDetailsQuery;
|
||||||
|
|
||||||
const options = deployments.map((c) => {
|
const options = deployments.map((c) => {
|
||||||
const code = c.command(agentVersion);
|
const code = c.command(agentVersion, agentSecret);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: c.id,
|
id: c.id,
|
||||||
@@ -65,14 +64,41 @@ function DeployCode({ code }: DeployCodeProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function linuxCommand(agentVersion: string) {
|
function linuxCommand(agentVersion: string, agentSecret: string) {
|
||||||
const agentShortVersion = getAgentShortVersion(agentVersion);
|
const secret =
|
||||||
|
agentSecret === '' ? '' : `\\\n -e AGENT_SECRET=${agentSecret} `;
|
||||||
|
|
||||||
return `curl -L https://downloads.portainer.io/ee${agentShortVersion}/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent`;
|
return `docker network create \\
|
||||||
|
--driver overlay \\
|
||||||
|
portainer_agent_network
|
||||||
|
|
||||||
|
docker service create \\
|
||||||
|
--name portainer_agent \\
|
||||||
|
--network portainer_agent_network \\
|
||||||
|
-p 9001:9001/tcp ${secret}\\
|
||||||
|
--mode global \\
|
||||||
|
--constraint 'node.platform.os == linux' \\
|
||||||
|
--mount type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock \\
|
||||||
|
--mount type=bind,src=//var/lib/docker/volumes,dst=/var/lib/docker/volumes \\
|
||||||
|
portainer/agent:${agentVersion}
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function winCommand(agentVersion: string) {
|
function winCommand(agentVersion: string, agentSecret: string) {
|
||||||
const agentShortVersion = getAgentShortVersion(agentVersion);
|
const secret =
|
||||||
|
agentSecret === '' ? '' : `\\\n -e AGENT_SECRET=${agentSecret} `;
|
||||||
|
|
||||||
return `curl -L https://downloads.portainer.io/ee${agentShortVersion}/agent-stack-windows.yml -o agent-stack-windows.yml && docker stack deploy --compose-file=agent-stack-windows.yml portainer-agent `;
|
return `docker network create \\
|
||||||
|
--driver overlay \\
|
||||||
|
portainer_agent_network && \\
|
||||||
|
docker service create \\
|
||||||
|
--name portainer_agent \\
|
||||||
|
--network portainer_agent_network \\
|
||||||
|
-p 9001:9001/tcp ${secret}\\
|
||||||
|
--mode global \\
|
||||||
|
--constraint 'node.platform.os == windows' \\
|
||||||
|
--mount type=npipe,src=\\\\.\\pipe\\docker_engine,dst=\\\\.\\pipe\\docker_engine \\
|
||||||
|
--mount type=bind,src=C:\\ProgramData\\docker\\volumes,dst=C:\\ProgramData\\docker\\volumes \\
|
||||||
|
portainer/agent:${agentVersion}
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-7
@@ -43,7 +43,7 @@ export function WizardEndpointsList({ environmentIds }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<WidgetTitle icon="fa-plug" title="Connected Environments" />
|
<WidgetTitle icon="fa-plug" title="New Environments" />
|
||||||
<WidgetBody>
|
<WidgetBody>
|
||||||
{environments.map((environment) => (
|
{environments.map((environment) => (
|
||||||
<div className={styles.wizardListWrapper} key={environment.Id}>
|
<div className={styles.wizardListWrapper} key={environment.Id}>
|
||||||
@@ -65,12 +65,7 @@ export function WizardEndpointsList({ environmentIds }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
{isEdgeEnvironment(environment.Type) && (
|
{isEdgeEnvironment(environment.Type) && (
|
||||||
<div className={styles.wizardListEdgeStatus}>
|
<div className={styles.wizardListEdgeStatus}>
|
||||||
<EdgeIndicator
|
<EdgeIndicator environment={environment} />
|
||||||
edgeId={environment.EdgeID}
|
|
||||||
checkInInterval={environment.EdgeCheckinInterval}
|
|
||||||
queryDate={environment.QueryDate}
|
|
||||||
lastCheckInDate={environment.LastCheckInDate}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
import { Field, Form, Formik } from 'formik';
|
||||||
|
|
||||||
|
import { LoadingButton } from '@/portainer/components/Button/LoadingButton';
|
||||||
|
import { FormControl } from '@/portainer/components/form-components/FormControl';
|
||||||
|
import { FormSectionTitle } from '@/portainer/components/form-components/FormSectionTitle';
|
||||||
|
import { Input } from '@/portainer/components/form-components/Input';
|
||||||
|
import { Button } from '@/portainer/components/Button';
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
kubeConfig: '',
|
||||||
|
name: '',
|
||||||
|
meta: {
|
||||||
|
groupId: 1,
|
||||||
|
tagIds: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function KubeConfigTeaserForm() {
|
||||||
|
return (
|
||||||
|
<Formik initialValues={initialValues} onSubmit={() => {}} validateOnMount>
|
||||||
|
{() => (
|
||||||
|
<Form className="mt-5">
|
||||||
|
<FormSectionTitle>Environment details</FormSectionTitle>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<div className="col-sm-12">
|
||||||
|
<span className="text-primary">
|
||||||
|
<i
|
||||||
|
className="fa fa-exclamation-circle space-right"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className="text-muted small">
|
||||||
|
Import the
|
||||||
|
<a
|
||||||
|
href="https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/"
|
||||||
|
target="_blank"
|
||||||
|
className="space-right space-left"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
kubeconfig file
|
||||||
|
</a>
|
||||||
|
of an existing Kubernetes cluster located on-premise or on a
|
||||||
|
cloud platform. This will create a corresponding environment in
|
||||||
|
Portainer and install the agent on the cluster. Please ensure:
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="col-sm-12 text-muted small">
|
||||||
|
<ul className="p-2 pl-4">
|
||||||
|
<li>You have a load balancer enabled in your cluster</li>
|
||||||
|
<li>You specify current-context in your kubeconfig</li>
|
||||||
|
<li>
|
||||||
|
The kubeconfig is self-contained - including any required
|
||||||
|
credentials.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
Note: Officially supported cloud providers are Civo, Linode,
|
||||||
|
DigitalOcean and Microsoft Azure (others are not guaranteed to
|
||||||
|
work at present)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormControl label="Name" required>
|
||||||
|
<Field
|
||||||
|
name="name"
|
||||||
|
as={Input}
|
||||||
|
data-cy="endpointCreate-nameInput"
|
||||||
|
placeholder="e.g. docker-prod01 / kubernetes-cluster01"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl
|
||||||
|
label="Kubeconfig file"
|
||||||
|
required
|
||||||
|
inputId="kubeconfig_file"
|
||||||
|
>
|
||||||
|
<Button disabled>Select a file</Button>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<div className="col-sm-12">
|
||||||
|
<LoadingButton
|
||||||
|
className="wizard-connect-button"
|
||||||
|
loadingText="Connecting environment..."
|
||||||
|
isLoading={false}
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<i className="fa fa-plug" aria-hidden="true" /> Connect
|
||||||
|
</LoadingButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
}
|
||||||
+21
@@ -7,11 +7,14 @@ import {
|
|||||||
} from '@/portainer/environments/types';
|
} from '@/portainer/environments/types';
|
||||||
import { BoxSelectorOption } from '@/portainer/components/BoxSelector/types';
|
import { BoxSelectorOption } from '@/portainer/components/BoxSelector/types';
|
||||||
import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
|
import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts';
|
||||||
|
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||||
|
import { BEFeatureIndicator } from '@/portainer/components/BEFeatureIndicator';
|
||||||
|
|
||||||
import { AnalyticsStateKey } from '../types';
|
import { AnalyticsStateKey } from '../types';
|
||||||
import { EdgeAgentTab } from '../shared/EdgeAgentTab';
|
import { EdgeAgentTab } from '../shared/EdgeAgentTab';
|
||||||
|
|
||||||
import { AgentPanel } from './AgentPanel';
|
import { AgentPanel } from './AgentPanel';
|
||||||
|
import { KubeConfigTeaserForm } from './KubeConfigTeaserForm';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onCreate(environment: Environment, analytics: AnalyticsStateKey): void;
|
onCreate(environment: Environment, analytics: AnalyticsStateKey): void;
|
||||||
@@ -20,6 +23,7 @@ interface Props {
|
|||||||
const options: BoxSelectorOption<
|
const options: BoxSelectorOption<
|
||||||
| EnvironmentCreationTypes.AgentEnvironment
|
| EnvironmentCreationTypes.AgentEnvironment
|
||||||
| EnvironmentCreationTypes.EdgeAgentEnvironment
|
| EnvironmentCreationTypes.EdgeAgentEnvironment
|
||||||
|
| EnvironmentCreationTypes.KubeConfigEnvironment
|
||||||
>[] = [
|
>[] = [
|
||||||
{
|
{
|
||||||
id: 'agent_endpoint',
|
id: 'agent_endpoint',
|
||||||
@@ -35,6 +39,14 @@ const options: BoxSelectorOption<
|
|||||||
description: '',
|
description: '',
|
||||||
value: EnvironmentCreationTypes.EdgeAgentEnvironment,
|
value: EnvironmentCreationTypes.EdgeAgentEnvironment,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'kubeconfig_endpoint',
|
||||||
|
icon: 'fas fa-cloud-upload-alt',
|
||||||
|
label: 'Import',
|
||||||
|
value: EnvironmentCreationTypes.KubeConfigEnvironment,
|
||||||
|
description: 'Import an existing Kubernetes config',
|
||||||
|
feature: FeatureId.K8S_CREATE_FROM_KUBECONFIG,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function WizardKubernetes({ onCreate }: Props) {
|
export function WizardKubernetes({ onCreate }: Props) {
|
||||||
@@ -72,6 +84,15 @@ export function WizardKubernetes({ onCreate }: Props) {
|
|||||||
commands={[{ ...commandsTabs.k8sLinux, label: 'Linux' }]}
|
commands={[{ ...commandsTabs.k8sLinux, label: 'Linux' }]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case EnvironmentCreationTypes.KubeConfigEnvironment:
|
||||||
|
return (
|
||||||
|
<div className="px-1 py-5 border border-solid border-orange-1">
|
||||||
|
<BEFeatureIndicator
|
||||||
|
featureId={options.find((o) => o.value === type)?.feature}
|
||||||
|
/>
|
||||||
|
<KubeConfigTeaserForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
throw new Error('Creation type not supported');
|
throw new Error('Creation type not supported');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,34 @@
|
|||||||
.root {
|
.optionTile {
|
||||||
--selected-item-color: var(--blue-2);
|
|
||||||
display: block;
|
|
||||||
width: 200px;
|
|
||||||
height: 300px;
|
|
||||||
border: 1px solid rgb(163, 163, 163);
|
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 25px 20px;
|
padding: 25px 20px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: 0 3px 10px -2px rgb(161 170 166 / 60%);
|
box-shadow: 0 3px 10px -2px rgb(161 170 166 / 60%);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
display: block;
|
||||||
|
width: 200px;
|
||||||
|
min-height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root:hover {
|
.feature {
|
||||||
|
--selected-item-color: var(--blue-2);
|
||||||
|
border: 1px solid rgb(163, 163, 163);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature:hover {
|
||||||
box-shadow: 0 3px 10px -2px rgb(161 170 166 / 80%);
|
box-shadow: 0 3px 10px -2px rgb(161 170 166 / 80%);
|
||||||
border: 1px solid var(--blue-2);
|
border: 1px solid var(--blue-2);
|
||||||
color: #337ab7;
|
color: #337ab7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.teaser {
|
||||||
|
border: 2px solid var(--BE-only) !important;
|
||||||
|
color: var(--text-muted-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.teaser:hover {
|
||||||
|
box-shadow: 0 3px 10px -2px rgb(161 170 166 / 80%);
|
||||||
|
}
|
||||||
|
|
||||||
.active:hover {
|
.active:hover {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { ComponentType } from 'react';
|
import { ComponentType } from 'react';
|
||||||
|
|
||||||
|
import { BEFeatureIndicator } from '@/portainer/components/BEFeatureIndicator';
|
||||||
|
import { FeatureId } from '@/portainer/feature-flags/enums';
|
||||||
|
import { isLimitedToBE } from '@/portainer/feature-flags/feature-flags.service';
|
||||||
|
|
||||||
import styles from './Option.module.css';
|
import styles from './Option.module.css';
|
||||||
|
|
||||||
export interface SelectorItemType {
|
export interface SelectorItemType {
|
||||||
@@ -12,6 +16,7 @@ export interface SelectorItemType {
|
|||||||
interface Props extends SelectorItemType {
|
interface Props extends SelectorItemType {
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
onClick?(): void;
|
onClick?(): void;
|
||||||
|
featureId?: FeatureId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Option({
|
export function Option({
|
||||||
@@ -20,13 +25,22 @@ export function Option({
|
|||||||
description,
|
description,
|
||||||
title,
|
title,
|
||||||
onClick = () => {},
|
onClick = () => {},
|
||||||
|
featureId,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const Icon = typeof icon !== 'string' ? icon : null;
|
const Icon = typeof icon !== 'string' ? icon : null;
|
||||||
|
const isLimited = isLimitedToBE(featureId);
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={clsx('border-0', styles.root, { [styles.active]: active })}
|
className={clsx(
|
||||||
|
styles.optionTile,
|
||||||
|
isLimited ? styles.teaser : styles.feature,
|
||||||
|
'border-0',
|
||||||
|
{
|
||||||
|
[styles.active]: active,
|
||||||
|
}
|
||||||
|
)}
|
||||||
type="button"
|
type="button"
|
||||||
|
disabled={isLimited}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<div className="text-center mt-2">
|
<div className="text-center mt-2">
|
||||||
@@ -37,9 +51,16 @@ export function Option({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 text-center">
|
<div className="mt-3 text-center flex flex-col">
|
||||||
<h3>{title}</h3>
|
<h3>{title}</h3>
|
||||||
<h5>{description}</h5>
|
<h5>{description}</h5>
|
||||||
|
{isLimited && (
|
||||||
|
<BEFeatureIndicator
|
||||||
|
showIcon={false}
|
||||||
|
featureId={featureId}
|
||||||
|
className="!whitespace-normal"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ ARCH=$2
|
|||||||
DOCKER_VERSION=${3:1}
|
DOCKER_VERSION=${3:1}
|
||||||
DOWNLOAD_FOLDER=".tmp/download"
|
DOWNLOAD_FOLDER=".tmp/download"
|
||||||
|
|
||||||
|
if [[ ${PLATFORM} == "darwin" ]]; then
|
||||||
|
PLATFORM="mac"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ ${ARCH} == "amd64" ]]; then
|
if [[ ${ARCH} == "amd64" ]]; then
|
||||||
ARCH="x86_64"
|
ARCH="x86_64"
|
||||||
@@ -18,6 +21,10 @@ elif [[ ${ARCH} == "arm" ]]; then
|
|||||||
ARCH="armhf"
|
ARCH="armhf"
|
||||||
elif [[ ${ARCH} == "arm64" ]]; then
|
elif [[ ${ARCH} == "arm64" ]]; then
|
||||||
ARCH="aarch64"
|
ARCH="aarch64"
|
||||||
|
elif [[ ${ARCH} == "ppc64le" ]]; then
|
||||||
|
DOCKER_VERSION="18.06.3-ce"
|
||||||
|
elif [[ ${ARCH} == "s390x" ]]; then
|
||||||
|
DOCKER_VERSION="18.06.3-ce"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rm -rf "${DOWNLOAD_FOLDER}"
|
rm -rf "${DOWNLOAD_FOLDER}"
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ fi
|
|||||||
|
|
||||||
|
|
||||||
if [[ "$PLATFORM" == "windows" ]]; then
|
if [[ "$PLATFORM" == "windows" ]]; then
|
||||||
wget -O "dist/docker-compose.plugin.exe" "https://github.com/docker/compose/releases/download/$COMPOSE_VERSION/docker-compose-windows-${ARCH}.exe"
|
wget -O "dist/docker-compose.exe" "https://github.com/docker/compose/releases/download/$COMPOSE_VERSION/docker-compose-windows-${ARCH}.exe"
|
||||||
chmod +x "dist/docker-compose.plugin.exe"
|
chmod +x "dist/docker-compose.exe"
|
||||||
else
|
else
|
||||||
wget -O "dist/docker-compose.plugin" "https://github.com/docker/compose/releases/download/$COMPOSE_VERSION/docker-compose-${PLATFORM}-${ARCH}"
|
wget -O "dist/docker-compose" "https://github.com/docker/compose/releases/download/$COMPOSE_VERSION/docker-compose-${PLATFORM}-${ARCH}"
|
||||||
chmod +x "dist/docker-compose.plugin"
|
chmod +x "dist/docker-compose"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"author": "Portainer.io",
|
"author": "Portainer.io",
|
||||||
"name": "portainer",
|
"name": "portainer",
|
||||||
"homepage": "http://portainer.io",
|
"homepage": "http://portainer.io",
|
||||||
"version": "2.14.0",
|
"version": "2.14.2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git@github.com:portainer/portainer.git"
|
"url": "git@github.com:portainer/portainer.git"
|
||||||
|
|||||||
Reference in New Issue
Block a user