From c819d4e7f7d57753a53a716b0de241b4f751219e Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Wed, 1 Mar 2023 20:33:05 +0200 Subject: [PATCH] feat(environments): create async edge [EE-4480] (#8527) --- api/http/handler/endpoints/endpoint_create.go | 4 - api/http/handler/endpoints/endpoint_list.go | 4 +- .../handler/endpoints/endpoint_list_test.go | 52 +++-- api/http/handler/endpoints/filter.go | 73 +++---- api/http/handler/endpoints/filter_test.go | 37 ++-- api/http/handler/settings/settings_public.go | 3 - api/portainer.go | 25 +-- app/edge/react/components/index.ts | 2 +- app/portainer/__module.js | 12 ++ .../endpointsDatatable.html | 16 +- .../endpointsDatatableController.js | 3 + app/portainer/models/settings.js | 1 - .../enviroments-list-view-components.ts | 13 ++ app/portainer/react/components/index.ts | 2 + app/portainer/react/views/index.ts | 8 + .../views/endpoints/edit/endpoint.html | 2 +- .../components/BoxSelector/BoxSelector.tsx | 2 +- .../components/TagSelector/TagSelector.tsx | 1 + .../EdgeScriptForm/EdgeScriptForm.tsx | 12 +- .../components/EdgeScriptForm/ScriptTabs.tsx | 22 +-- .../edge/components/edge-agent-async.svg | 10 + .../edge/components/edge-agent-standard.svg | 10 + .../WaitingRoomView/WaitingRoomView.tsx | 1 - .../EnvironmentItem/AgentDetails.tsx | 14 +- .../EnvironmentItem/EnvironmentTypeTag.tsx | 10 +- .../EnvironmentList/EnvironmentList.tsx | 23 ++- .../EnvironmentListFilters.tsx | 20 +- .../HomeView/EnvironmentList/types.ts | 4 +- .../common/PortainerTunnelAddrField.tsx | 65 +++++++ .../PortainerUrlField.tsx | 68 ++++--- .../AutomaticEdgeEnvCreation.tsx | 180 ++++++++++++++++++ .../AutomaticEdgeEnvCreation/index.ts | 1 + .../EdgeAutoCreateScriptView.tsx | 27 +++ .../EdgeAutoCreateScriptView/index.ts | 1 + .../ListView/ImportFdoDeviceButton.tsx | 28 +++ .../environment.service/create.ts | 21 ++ .../environments/environment.service/index.ts | 2 +- .../environments/queries/useAgentDetails.ts | 1 - .../EnvironmentsCreationView.tsx | 8 +- .../WizardDocker/APITab/DeploymentScripts.tsx | 8 - .../WizardDocker/WizardDocker.tsx | 67 +++++-- .../WizardKubernetes/WizardKubernetes.tsx | 75 +++++--- .../EdgeAgentForm/EdgeAgentFieldset.tsx | 22 ++- .../EdgeAgentForm/EdgeAgentForm.tsx | 76 ++++++-- .../EdgeAgentForm/EdgeAgentForm.validation.ts | 16 +- .../EdgeAgentTab/EdgeAgentForm/types.ts | 4 + .../shared/EdgeAgentTab/EdgeAgentTab.tsx | 5 +- .../wizard/EnvironmentsCreationView/types.ts | 8 +- .../AutoEnvCreationSettingsForm.tsx | 84 ++------ .../AutomaticEdgeEnvCreation.tsx | 47 +---- .../EnableWaitingRoomSwitch.tsx | 19 +- .../DeploymentSyncOptions.tsx | 61 ++---- .../DeploymentSyncOptions/types.ts | 1 - .../AddDeviceButton/AddDeviceButton.tsx | 57 ------ .../AddDeviceButton/DeployTypePrompt.tsx | 69 ------- .../AddDeviceButton/index.ts | 1 - .../EdgeComputeSettings.tsx | 47 ++--- .../EdgeComputeSettings/types.ts | 4 +- .../EdgeComputeSettingsView.tsx | 7 +- 59 files changed, 880 insertions(+), 586 deletions(-) create mode 100644 app/portainer/react/components/enviroments-list-view-components.ts create mode 100644 app/react/edge/components/edge-agent-async.svg create mode 100644 app/react/edge/components/edge-agent-standard.svg create mode 100644 app/react/portainer/common/PortainerTunnelAddrField.tsx rename app/react/portainer/{environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm => common}/PortainerUrlField.tsx (50%) create mode 100644 app/react/portainer/environments/EdgeAutoCreateScriptView/AutomaticEdgeEnvCreation/AutomaticEdgeEnvCreation.tsx create mode 100644 app/react/portainer/environments/EdgeAutoCreateScriptView/AutomaticEdgeEnvCreation/index.ts create mode 100644 app/react/portainer/environments/EdgeAutoCreateScriptView/EdgeAutoCreateScriptView.tsx create mode 100644 app/react/portainer/environments/EdgeAutoCreateScriptView/index.ts create mode 100644 app/react/portainer/environments/ListView/ImportFdoDeviceButton.tsx delete mode 100644 app/react/portainer/settings/EdgeComputeView/EdgeComputeSettings/AddDeviceButton/AddDeviceButton.tsx delete mode 100644 app/react/portainer/settings/EdgeComputeView/EdgeComputeSettings/AddDeviceButton/DeployTypePrompt.tsx delete mode 100644 app/react/portainer/settings/EdgeComputeView/EdgeComputeSettings/AddDeviceButton/index.ts diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go index a49ea9188..94abcf113 100644 --- a/api/http/handler/endpoints/endpoint_create.go +++ b/api/http/handler/endpoints/endpoint_create.go @@ -164,9 +164,6 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { checkinInterval, _ := request.RetrieveNumericMultiPartFormValue(r, "CheckinInterval", true) payload.EdgeCheckinInterval = checkinInterval - isEdgeDevice, _ := request.RetrieveBooleanMultiPartFormValue(r, "IsEdgeDevice", true) - payload.IsEdgeDevice = isEdgeDevice - return nil } @@ -196,7 +193,6 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { // @param TagIDs formData []int false "List of tag identifiers to which this environment(endpoint) is associated" // @param EdgeCheckinInterval formData int false "The check in interval for edge agent (in seconds)" // @param EdgeTunnelServerAddress formData string true "URL or IP address that will be used to establish a reverse tunnel" -// @param IsEdgeDevice formData bool false "Is Edge Device" // @param Gpus formData array false "List of GPUs" // @success 200 {object} portainer.Endpoint "Success" // @failure 400 "Invalid request" diff --git a/api/http/handler/endpoints/endpoint_list.go b/api/http/handler/endpoints/endpoint_list.go index 7f7a78d77..a6758882a 100644 --- a/api/http/handler/endpoints/endpoint_list.go +++ b/api/http/handler/endpoints/endpoint_list.go @@ -42,8 +42,8 @@ const ( // @param endpointIds query []int false "will return only these environments(endpoints)" // @param provisioned query bool false "If true, will return environment(endpoint) that were provisioned" // @param agentVersions query []string false "will return only environments with on of these agent versions" -// @param edgeDevice query bool false "if exists true show only edge devices, false show only regular edge endpoints. if missing, will show both types (relevant only for edge endpoints)" -// @param edgeDeviceUntrusted query bool false "if true, show only untrusted endpoints, if false show only trusted (relevant only for edge devices, and if edgeDevice is true)" +// @param edgeAsync query bool false "if exists true show only edge async agents, false show only standard edge agents. if missing, will show both types (relevant only for edge agents)" +// @param edgeDeviceUntrusted query bool false "if true, show only untrusted edge agents, if false show only trusted edge agents (relevant only for edge agents)" // @param name query string false "will return only environments(endpoints) with this name" // @success 200 {array} portainer.Endpoint "Endpoints" // @failure 500 "Server error" diff --git a/api/http/handler/endpoints/endpoint_list_test.go b/api/http/handler/endpoints/endpoint_list_test.go index e94142d0c..cdf5c651d 100644 --- a/api/http/handler/endpoints/endpoint_list_test.go +++ b/api/http/handler/endpoints/endpoint_list_test.go @@ -13,7 +13,6 @@ import ( "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/snapshot" "github.com/portainer/portainer/api/internal/testhelpers" - helper "github.com/portainer/portainer/api/internal/testhelpers" "github.com/stretchr/testify/assert" ) @@ -104,59 +103,58 @@ func Test_EndpointList_AgentVersion(t *testing.T) { } } -func Test_endpointList_edgeDeviceFilter(t *testing.T) { +func Test_endpointList_edgeFilter(t *testing.T) { - trustedEdgeDevice := portainer.Endpoint{ID: 1, UserTrusted: true, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment} - untrustedEdgeDevice := portainer.Endpoint{ID: 2, UserTrusted: false, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment} - regularUntrustedEdgeEndpoint := portainer.Endpoint{ID: 3, UserTrusted: false, IsEdgeDevice: false, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment} - regularTrustedEdgeEndpoint := portainer.Endpoint{ID: 4, UserTrusted: true, IsEdgeDevice: false, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment} - regularEndpoint := portainer.Endpoint{ID: 5, UserTrusted: false, IsEdgeDevice: false, GroupID: 1, Type: portainer.DockerEnvironment} + trustedEdgeAsync := portainer.Endpoint{ID: 1, UserTrusted: true, Edge: portainer.EnvironmentEdgeSettings{AsyncMode: true}, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment} + untrustedEdgeAsync := portainer.Endpoint{ID: 2, UserTrusted: false, Edge: portainer.EnvironmentEdgeSettings{AsyncMode: true}, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment} + regularUntrustedEdgeStandard := portainer.Endpoint{ID: 3, UserTrusted: false, Edge: portainer.EnvironmentEdgeSettings{AsyncMode: false}, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment} + regularTrustedEdgeStandard := portainer.Endpoint{ID: 4, UserTrusted: true, Edge: portainer.EnvironmentEdgeSettings{AsyncMode: false}, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment} + regularEndpoint := portainer.Endpoint{ID: 5, GroupID: 1, Type: portainer.DockerEnvironment} handler, teardown := setup(t, []portainer.Endpoint{ - trustedEdgeDevice, - untrustedEdgeDevice, - regularUntrustedEdgeEndpoint, - regularTrustedEdgeEndpoint, + trustedEdgeAsync, + untrustedEdgeAsync, + regularUntrustedEdgeStandard, + regularTrustedEdgeStandard, regularEndpoint, }) defer teardown() - type endpointListEdgeDeviceTest struct { + type endpointListEdgeTest struct { endpointListTest - edgeDevice *bool + edgeAsync *bool edgeDeviceUntrusted bool } - tests := []endpointListEdgeDeviceTest{ + tests := []endpointListEdgeTest{ { endpointListTest: endpointListTest{ - "should show all endpoints except of the untrusted devices", - []portainer.EndpointID{trustedEdgeDevice.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID, regularEndpoint.ID}, + "should show all endpoints expect of the untrusted devices", + []portainer.EndpointID{trustedEdgeAsync.ID, regularTrustedEdgeStandard.ID, regularEndpoint.ID}, }, - edgeDevice: nil, }, { endpointListTest: endpointListTest{ - "should show only trusted edge devices and regular endpoints", - []portainer.EndpointID{trustedEdgeDevice.ID, regularEndpoint.ID}, + "should show only trusted edge async agents and regular endpoints", + []portainer.EndpointID{trustedEdgeAsync.ID, regularEndpoint.ID}, }, - edgeDevice: BoolAddr(true), + edgeAsync: BoolAddr(true), }, { endpointListTest: endpointListTest{ "should show only untrusted edge devices and regular endpoints", - []portainer.EndpointID{untrustedEdgeDevice.ID, regularEndpoint.ID}, + []portainer.EndpointID{untrustedEdgeAsync.ID, regularEndpoint.ID}, }, - edgeDevice: BoolAddr(true), + edgeAsync: BoolAddr(true), edgeDeviceUntrusted: true, }, { endpointListTest: endpointListTest{ "should show no edge devices", - []portainer.EndpointID{regularEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID}, + []portainer.EndpointID{regularEndpoint.ID, regularTrustedEdgeStandard.ID}, }, - edgeDevice: BoolAddr(false), + edgeAsync: BoolAddr(false), }, } @@ -165,8 +163,8 @@ func Test_endpointList_edgeDeviceFilter(t *testing.T) { is := assert.New(t) query := fmt.Sprintf("edgeDeviceUntrusted=%v&", test.edgeDeviceUntrusted) - if test.edgeDevice != nil { - query += fmt.Sprintf("edgeDevice=%v&", *test.edgeDevice) + if test.edgeAsync != nil { + query += fmt.Sprintf("edgeAsync=%v&", *test.edgeAsync) } req := buildEndpointListRequest(query) @@ -198,7 +196,7 @@ func setup(t *testing.T, endpoints []portainer.Endpoint) (handler *Handler, tear err := store.User().Create(&portainer.User{Username: "admin", Role: portainer.AdministratorRole}) is.NoError(err, "error creating a user") - bouncer := helper.NewTestRequestBouncer() + bouncer := testhelpers.NewTestRequestBouncer() handler = NewHandler(bouncer, nil) handler.DataStore = store handler.ComposeStackManager = testhelpers.NewComposeStackManager() diff --git a/api/http/handler/endpoints/filter.go b/api/http/handler/endpoints/filter.go index f4dec5eb9..248ff2f44 100644 --- a/api/http/handler/endpoints/filter.go +++ b/api/http/handler/endpoints/filter.go @@ -15,14 +15,15 @@ import ( ) type EnvironmentsQuery struct { - search string - types []portainer.EndpointType - tagIds []portainer.TagID - endpointIds []portainer.EndpointID - tagsPartialMatch bool - groupIds []portainer.EndpointGroupID - status []portainer.EndpointStatus - edgeDevice *bool + search string + types []portainer.EndpointType + tagIds []portainer.TagID + endpointIds []portainer.EndpointID + tagsPartialMatch bool + groupIds []portainer.EndpointGroupID + status []portainer.EndpointStatus + // if edgeAsync not nil, will filter edge endpoints based on this value + edgeAsync *bool edgeDeviceUntrusted bool excludeSnapshots bool name string @@ -66,11 +67,10 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) { name, _ := request.RetrieveQueryParameter(r, "name", true) - edgeDeviceParam, _ := request.RetrieveQueryParameter(r, "edgeDevice", true) - - var edgeDevice *bool - if edgeDeviceParam != "" { - edgeDevice = BoolAddr(edgeDeviceParam == "true") + var edgeAsync *bool + edgeAsyncParam, _ := request.RetrieveQueryParameter(r, "edgeAsync", true) + if edgeAsyncParam != "" { + edgeAsync = BoolAddr(edgeAsyncParam == "true") } edgeDeviceUntrusted, _ := request.RetrieveBooleanQueryParameter(r, "edgeDeviceUntrusted", true) @@ -85,7 +85,7 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) { tagsPartialMatch: tagsPartialMatch, groupIds: groupIDs, status: status, - edgeDevice: edgeDevice, + edgeAsync: edgeAsync, edgeDeviceUntrusted: edgeDeviceUntrusted, excludeSnapshots: excludeSnapshots, name: name, @@ -108,15 +108,26 @@ func (handler *Handler) filterEndpointsByQuery(filteredEndpoints []portainer.End filteredEndpoints = filterEndpointsByName(filteredEndpoints, query.name) } - if query.edgeDevice != nil { - filteredEndpoints = filterEndpointsByEdgeDevice(filteredEndpoints, *query.edgeDevice, query.edgeDeviceUntrusted) - } else { - // If the edgeDevice parameter is not set, we need to filter out the untrusted edge devices + // filter async edge environments + if query.edgeAsync != nil { filteredEndpoints = filter(filteredEndpoints, func(endpoint portainer.Endpoint) bool { - return !endpoint.IsEdgeDevice || endpoint.UserTrusted + if !endpointutils.IsEdgeEndpoint(&endpoint) { + return true + } + + return endpoint.Edge.AsyncMode == *query.edgeAsync }) } + // filter edge environments by trusted/untrusted + filteredEndpoints = filter(filteredEndpoints, func(endpoint portainer.Endpoint) bool { + if !endpointutils.IsEdgeEndpoint(&endpoint) { + return true + } + + return endpoint.UserTrusted == !query.edgeDeviceUntrusted + }) + if len(query.status) > 0 { filteredEndpoints = filterEndpointsByStatuses(filteredEndpoints, query.status, settings) } @@ -274,30 +285,6 @@ func filterEndpointsByTypes(endpoints []portainer.Endpoint, endpointTypes []port return endpoints[:n] } -func filterEndpointsByEdgeDevice(endpoints []portainer.Endpoint, edgeDevice bool, untrusted bool) []portainer.Endpoint { - n := 0 - for _, endpoint := range endpoints { - if shouldReturnEdgeDevice(endpoint, edgeDevice, untrusted) { - endpoints[n] = endpoint - n++ - } - } - - return endpoints[:n] -} - -func shouldReturnEdgeDevice(endpoint portainer.Endpoint, edgeDeviceParam bool, untrustedParam bool) bool { - if !endpointutils.IsEdgeEndpoint(&endpoint) { - return true - } - - if !edgeDeviceParam { - return !endpoint.IsEdgeDevice - } - - return endpoint.IsEdgeDevice && endpoint.UserTrusted == !untrustedParam -} - func convertTagIDsToTags(tagsMap map[portainer.TagID]string, tagIDs []portainer.TagID) []string { tags := make([]string, 0, len(tagIDs)) diff --git a/api/http/handler/endpoints/filter_test.go b/api/http/handler/endpoints/filter_test.go index 6e9ca96ff..dd90f9b65 100644 --- a/api/http/handler/endpoints/filter_test.go +++ b/api/http/handler/endpoints/filter_test.go @@ -6,7 +6,6 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/datastore" "github.com/portainer/portainer/api/internal/testhelpers" - helper "github.com/portainer/portainer/api/internal/testhelpers" "github.com/stretchr/testify/assert" ) @@ -74,19 +73,19 @@ func Test_Filter_AgentVersion(t *testing.T) { runTests(tests, t, handler, endpoints) } -func Test_Filter_edgeDeviceFilter(t *testing.T) { +func Test_Filter_edgeFilter(t *testing.T) { - trustedEdgeDevice := portainer.Endpoint{ID: 1, UserTrusted: true, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment} - untrustedEdgeDevice := portainer.Endpoint{ID: 2, UserTrusted: false, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment} - regularUntrustedEdgeEndpoint := portainer.Endpoint{ID: 3, UserTrusted: false, IsEdgeDevice: false, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment} - regularTrustedEdgeEndpoint := portainer.Endpoint{ID: 4, UserTrusted: true, IsEdgeDevice: false, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment} + trustedEdgeAsync := portainer.Endpoint{ID: 1, UserTrusted: true, Edge: portainer.EnvironmentEdgeSettings{AsyncMode: true}, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment} + untrustedEdgeAsync := portainer.Endpoint{ID: 2, UserTrusted: false, Edge: portainer.EnvironmentEdgeSettings{AsyncMode: true}, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment} + regularUntrustedEdgeStandard := portainer.Endpoint{ID: 3, UserTrusted: false, Edge: portainer.EnvironmentEdgeSettings{AsyncMode: false}, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment} + regularTrustedEdgeStandard := portainer.Endpoint{ID: 4, UserTrusted: true, Edge: portainer.EnvironmentEdgeSettings{AsyncMode: false}, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment} regularEndpoint := portainer.Endpoint{ID: 5, GroupID: 1, Type: portainer.DockerEnvironment} endpoints := []portainer.Endpoint{ - trustedEdgeDevice, - untrustedEdgeDevice, - regularUntrustedEdgeEndpoint, - regularTrustedEdgeEndpoint, + trustedEdgeAsync, + untrustedEdgeAsync, + regularUntrustedEdgeStandard, + regularTrustedEdgeStandard, regularEndpoint, } @@ -96,32 +95,32 @@ func Test_Filter_edgeDeviceFilter(t *testing.T) { tests := []filterTest{ { - "should show all edge endpoints except of the untrusted devices", - []portainer.EndpointID{trustedEdgeDevice.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID}, + "should show all edge endpoints except of the untrusted edge", + []portainer.EndpointID{trustedEdgeAsync.ID, regularTrustedEdgeStandard.ID}, EnvironmentsQuery{ types: []portainer.EndpointType{portainer.EdgeAgentOnDockerEnvironment, portainer.EdgeAgentOnKubernetesEnvironment}, }, }, { "should show only trusted edge devices and other regular endpoints", - []portainer.EndpointID{trustedEdgeDevice.ID, regularEndpoint.ID}, + []portainer.EndpointID{trustedEdgeAsync.ID, regularEndpoint.ID}, EnvironmentsQuery{ - edgeDevice: BoolAddr(true), + edgeAsync: BoolAddr(true), }, }, { "should show only untrusted edge devices and other regular endpoints", - []portainer.EndpointID{untrustedEdgeDevice.ID, regularEndpoint.ID}, + []portainer.EndpointID{untrustedEdgeAsync.ID, regularEndpoint.ID}, EnvironmentsQuery{ - edgeDevice: BoolAddr(true), + edgeAsync: BoolAddr(true), edgeDeviceUntrusted: true, }, }, { "should show no edge devices", - []portainer.EndpointID{regularEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID}, + []portainer.EndpointID{regularEndpoint.ID, regularTrustedEdgeStandard.ID}, EnvironmentsQuery{ - edgeDevice: BoolAddr(false), + edgeAsync: BoolAddr(false), }, }, } @@ -168,7 +167,7 @@ func setupFilterTest(t *testing.T, endpoints []portainer.Endpoint) (handler *Han err := store.User().Create(&portainer.User{Username: "admin", Role: portainer.AdministratorRole}) is.NoError(err, "error creating a user") - bouncer := helper.NewTestRequestBouncer() + bouncer := testhelpers.NewTestRequestBouncer() handler = NewHandler(bouncer, nil) handler.DataStore = store handler.ComposeStackManager = testhelpers.NewComposeStackManager() diff --git a/api/http/handler/settings/settings_public.go b/api/http/handler/settings/settings_public.go index adedc7777..aad2b1023 100644 --- a/api/http/handler/settings/settings_public.go +++ b/api/http/handler/settings/settings_public.go @@ -40,8 +40,6 @@ type publicSettingsResponse struct { IsAMTEnabled 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] @@ -86,7 +84,6 @@ func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResp IsAMTEnabled: appSettings.EnableEdgeComputeFeatures && appSettings.OpenAMTConfiguration.Enabled, } - publicSettings.Edge.AsyncMode = appSettings.Edge.AsyncMode publicSettings.Edge.PingInterval = appSettings.Edge.PingInterval publicSettings.Edge.SnapshotInterval = appSettings.Edge.SnapshotInterval publicSettings.Edge.CommandInterval = appSettings.Edge.CommandInterval diff --git a/api/portainer.go b/api/portainer.go index e78af4067..f9430a0f8 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -382,16 +382,7 @@ type ( // Whether we need to run any "post init migrations". PostInitMigrations EndpointPostInitMigrations `json:"PostInitMigrations"` - 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"` - } + Edge EnvironmentEdgeSettings Agent struct { Version string `example:"1.0.0"` @@ -412,6 +403,17 @@ type ( Tags []string `json:"Tags"` } + EnvironmentEdgeSettings 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"` + } + // EndpointAuthorizations represents the authorizations associated to a set of environments(endpoints) EndpointAuthorizations map[EndpointID]Authorizations @@ -917,7 +919,8 @@ type ( 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 + + // Deprecated 2.18 AsyncMode bool } diff --git a/app/edge/react/components/index.ts b/app/edge/react/components/index.ts index a540b42f3..3eee3f259 100644 --- a/app/edge/react/components/index.ts +++ b/app/edge/react/components/index.ts @@ -20,7 +20,7 @@ export const componentsModule = angular 'edgeInfo', 'commands', 'isNomadTokenVisible', - 'hideAsyncMode', + 'asyncMode', ]) ) .component( diff --git a/app/portainer/__module.js b/app/portainer/__module.js index e92a8515f..98d6f21af 100644 --- a/app/portainer/__module.js +++ b/app/portainer/__module.js @@ -148,6 +148,7 @@ angular 'sidebar@': {}, }, }; + const logout = { name: 'portainer.logout', url: '/logout', @@ -200,6 +201,16 @@ angular }, }; + const edgeAutoCreateScript = { + name: 'portainer.endpoints.edgeAutoCreateScript', + url: '/aeec', + views: { + 'content@': { + component: 'edgeAutoCreateScriptView', + }, + }, + }; + var addFDOProfile = { name: 'portainer.endpoints.profile', url: '/profile', @@ -424,6 +435,7 @@ angular $stateRegistryProvider.register(endpoint); $stateRegistryProvider.register(endpointAccess); $stateRegistryProvider.register(endpointKVM); + $stateRegistryProvider.register(edgeAutoCreateScript); $stateRegistryProvider.register(deviceImport); $stateRegistryProvider.register(addFDOProfile); $stateRegistryProvider.register(editFDOProfile); diff --git a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html index fab2526d0..fd9db10cf 100644 --- a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html +++ b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html @@ -33,7 +33,21 @@ Remove - + + diff --git a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatableController.js b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatableController.js index 5447e2b8e..ccdd3178d 100644 --- a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatableController.js +++ b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatableController.js @@ -1,4 +1,5 @@ import _ from 'lodash-es'; +import { isBE } from '@/react/portainer/feature-flags/feature-flags.service'; angular.module('portainer.app').controller('EndpointsDatatableController', [ '$scope', @@ -8,6 +9,8 @@ angular.module('portainer.app').controller('EndpointsDatatableController', [ function ($scope, $controller, DatatableService, PaginationService) { angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); + this.isBE = isBE; + this.state = Object.assign(this.state, { orderBy: this.orderBy, loading: true, diff --git a/app/portainer/models/settings.js b/app/portainer/models/settings.js index 334846cc9..e27c408df 100644 --- a/app/portainer/models/settings.js +++ b/app/portainer/models/settings.js @@ -85,5 +85,4 @@ export function EdgeSettingsViewModel(data = {}) { this.PingInterval = data.PingInterval; this.SnapshotInterval = data.SnapshotInterval; this.CommandInterval = data.CommandInterval; - this.AsyncMode = data.AsyncMode; } diff --git a/app/portainer/react/components/enviroments-list-view-components.ts b/app/portainer/react/components/enviroments-list-view-components.ts new file mode 100644 index 000000000..da3343637 --- /dev/null +++ b/app/portainer/react/components/enviroments-list-view-components.ts @@ -0,0 +1,13 @@ +import angular from 'angular'; + +import { r2a } from '@/react-tools/react2angular'; +import { ImportFdoDeviceButton } from '@/react/portainer/environments/ListView/ImportFdoDeviceButton'; +import { withUIRouter } from '@/react-tools/withUIRouter'; +import { withReactQuery } from '@/react-tools/withReactQuery'; + +export const envListModule = angular + .module('portainer.app.react.components.environments.list-view', []) + .component( + 'importFdoDeviceButton', + r2a(withUIRouter(withReactQuery(ImportFdoDeviceButton)), []) + ).name; diff --git a/app/portainer/react/components/index.ts b/app/portainer/react/components/index.ts index 0ecd4289a..2bab4d0c8 100644 --- a/app/portainer/react/components/index.ts +++ b/app/portainer/react/components/index.ts @@ -32,6 +32,7 @@ import { customTemplatesModule } from './custom-templates'; import { gitFormModule } from './git-form'; import { settingsModule } from './settings'; import { accessControlModule } from './access-control'; +import { envListModule } from './enviroments-list-view-components'; export const componentsModule = angular .module('portainer.app.react.components', [ @@ -39,6 +40,7 @@ export const componentsModule = angular gitFormModule, settingsModule, accessControlModule, + envListModule, ]) .component( 'tagSelector', diff --git a/app/portainer/react/views/index.ts b/app/portainer/react/views/index.ts index 801592b79..6b80ace4a 100644 --- a/app/portainer/react/views/index.ts +++ b/app/portainer/react/views/index.ts @@ -8,6 +8,7 @@ import { withUIRouter } from '@/react-tools/withUIRouter'; import { CreateAccessToken } from '@/react/portainer/account/CreateAccessTokenView'; import { EdgeComputeSettingsView } from '@/react/portainer/settings/EdgeComputeView/EdgeComputeSettingsView'; import { withI18nSuspense } from '@/react-tools/withI18nSuspense'; +import { EdgeAutoCreateScriptView } from '@/react/portainer/environments/EdgeAutoCreateScriptView'; import { wizardModule } from './wizard'; import { teamsModule } from './teams'; @@ -23,6 +24,13 @@ export const viewsModule = angular 'homeView', r2a(withUIRouter(withReactQuery(withCurrentUser(HomeView))), []) ) + .component( + 'edgeAutoCreateScriptView', + r2a( + withUIRouter(withReactQuery(withCurrentUser(EdgeAutoCreateScriptView))), + [] + ) + ) .component( 'createAccessToken', r2a(withI18nSuspense(withUIRouter(CreateAccessToken)), [ diff --git a/app/portainer/views/endpoints/edit/endpoint.html b/app/portainer/views/endpoints/edit/endpoint.html index 97ef69383..7d57c1406 100644 --- a/app/portainer/views/endpoints/edit/endpoint.html +++ b/app/portainer/views/endpoints/edit/endpoint.html @@ -50,7 +50,7 @@ edge-info="{ key: endpoint.EdgeKey, id: endpoint.EdgeID }" commands="state.edgeScriptCommands" is-nomad-token-visible="state.showNomad" - hide-async-mode="!endpoint.IsEdgeDevice" + async-mode="endpoint.Edge.AsyncMode" > diff --git a/app/react/components/BoxSelector/BoxSelector.tsx b/app/react/components/BoxSelector/BoxSelector.tsx index 9c17e6814..624e30649 100644 --- a/app/react/components/BoxSelector/BoxSelector.tsx +++ b/app/react/components/BoxSelector/BoxSelector.tsx @@ -29,7 +29,7 @@ export function BoxSelector({ ...props }: Props) { return ( -
+
{options diff --git a/app/react/components/TagSelector/TagSelector.tsx b/app/react/components/TagSelector/TagSelector.tsx index d50b44a1c..79ee52fca 100644 --- a/app/react/components/TagSelector/TagSelector.tsx +++ b/app/react/components/TagSelector/TagSelector.tsx @@ -60,6 +60,7 @@ export function TagSelector({ value, allowCreate = false, onChange }: Props) { {selectedTags.map((tag) => ( >; isNomadTokenVisible?: boolean; - hideAsyncMode?: boolean; + asyncMode?: boolean; } export function EdgeScriptForm({ edgeInfo, commands, isNomadTokenVisible, - hideAsyncMode, -}: Props) { + asyncMode, + children, +}: PropsWithChildren) { const showOsSelector = !(commands instanceof Array); return ( @@ -41,6 +43,8 @@ export function EdgeScriptForm({ > {({ values, setFieldValue }) => ( <> + {children} + setFieldValue('platform', platform) } - hideAsyncMode={hideAsyncMode} + asyncMode={asyncMode} />
diff --git a/app/react/edge/components/EdgeScriptForm/ScriptTabs.tsx b/app/react/edge/components/EdgeScriptForm/ScriptTabs.tsx index 7b0a8c57f..653d2b5ff 100644 --- a/app/react/edge/components/EdgeScriptForm/ScriptTabs.tsx +++ b/app/react/edge/components/EdgeScriptForm/ScriptTabs.tsx @@ -16,7 +16,7 @@ interface Props { commands: CommandTab[]; platform?: Platform; onPlatformChange?(platform: Platform): void; - hideAsyncMode?: boolean; + asyncMode?: boolean; } export function ScriptTabs({ @@ -25,7 +25,7 @@ export function ScriptTabs({ edgeId, commands, platform, - hideAsyncMode = false, + asyncMode = false, onPlatformChange = () => {}, }: Props) { const agentDetails = useAgentDetails(); @@ -40,14 +40,14 @@ export function ScriptTabs({ return null; } - const { agentSecret, agentVersion, useEdgeAsyncMode } = agentDetails; + const { agentSecret, agentVersion } = agentDetails; const options = commands.map((c) => { const cmd = c.command( agentVersion, edgeKey, values, - !hideAsyncMode && useEdgeAsyncMode, + asyncMode, edgeId, agentSecret ); @@ -67,14 +67,10 @@ export function ScriptTabs({ }); return ( -
-
- onPlatformChange(id)} - /> -
-
+ onPlatformChange(id)} + /> ); } diff --git a/app/react/edge/components/edge-agent-async.svg b/app/react/edge/components/edge-agent-async.svg new file mode 100644 index 000000000..fa1a0b9ee --- /dev/null +++ b/app/react/edge/components/edge-agent-async.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/react/edge/components/edge-agent-standard.svg b/app/react/edge/components/edge-agent-standard.svg new file mode 100644 index 000000000..8590d5130 --- /dev/null +++ b/app/react/edge/components/edge-agent-standard.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/react/edge/edge-devices/WaitingRoomView/WaitingRoomView.tsx b/app/react/edge/edge-devices/WaitingRoomView/WaitingRoomView.tsx index c6aa397df..cce80f494 100644 --- a/app/react/edge/edge-devices/WaitingRoomView/WaitingRoomView.tsx +++ b/app/react/edge/edge-devices/WaitingRoomView/WaitingRoomView.tsx @@ -12,7 +12,6 @@ export default withLimitToBE(WaitingRoomView); function WaitingRoomView() { const { environments, isLoading, totalCount } = useEnvironmentList({ - edgeDevice: true, edgeDeviceUntrusted: true, excludeSnapshots: true, types: EdgeTypes, diff --git a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/AgentDetails.tsx b/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/AgentDetails.tsx index b1abf5899..a292dfeb2 100644 --- a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/AgentDetails.tsx +++ b/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/AgentDetails.tsx @@ -1,5 +1,3 @@ -import { Globe } from 'lucide-react'; - import { Environment } from '@/react/portainer/environments/types'; import { isAgentEnvironment } from '@/react/portainer/environments/utils'; @@ -8,15 +6,5 @@ export function AgentDetails({ environment }: { environment: Environment }) { return null; } - return ( - <> - {environment.Agent.Version} - {environment.Edge.AsyncMode && ( - - - )} - - ); + return {environment.Agent.Version}; } diff --git a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentTypeTag.tsx b/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentTypeTag.tsx index 6bf323e64..bbae9aee2 100644 --- a/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentTypeTag.tsx +++ b/app/react/portainer/HomeView/EnvironmentList/EnvironmentItem/EnvironmentTypeTag.tsx @@ -31,12 +31,12 @@ export function EnvironmentTypeTag({ } function getTypeLabel(environment: Environment) { - if (environment.IsEdgeDevice) { - return 'Edge Device'; - } - if (isEdgeEnvironment(environment.Type)) { - return 'Edge Agent'; + if (environment.Edge.AsyncMode) { + return 'Edge Agent Async'; + } + + return 'Edge Agent Standard'; } if (isLocalEnvironment(environment)) { diff --git a/app/react/portainer/HomeView/EnvironmentList/EnvironmentList.tsx b/app/react/portainer/HomeView/EnvironmentList/EnvironmentList.tsx index 2a452e446..94e3e7eb0 100644 --- a/app/react/portainer/HomeView/EnvironmentList/EnvironmentList.tsx +++ b/app/react/portainer/HomeView/EnvironmentList/EnvironmentList.tsx @@ -110,6 +110,7 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) { tagsPartialMatch: true, agentVersions: agentVersions.map((a) => a.value), updateInformation: isBE, + edgeAsync: getEdgeAsyncValue(connectionTypes), }; const queryWithSort = { @@ -282,8 +283,8 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) { EnvironmentType.AgentOnDocker, EnvironmentType.AgentOnKubernetes, ], - [ConnectionType.EdgeAgent]: EdgeTypes, - [ConnectionType.EdgeDevice]: EdgeTypes, + [ConnectionType.EdgeAgentStandard]: EdgeTypes, + [ConnectionType.EdgeAgentAsync]: EdgeTypes, }; const selectedTypesByPlatform = platformTypes.flatMap( @@ -405,3 +406,21 @@ function renderItems( return items; } + +function getEdgeAsyncValue(connectionTypes: Filter[]) { + const hasEdgeAsync = connectionTypes.some( + (connectionType) => connectionType.value === ConnectionType.EdgeAgentAsync + ); + + const hasEdgeStandard = connectionTypes.some( + (connectionType) => + connectionType.value === ConnectionType.EdgeAgentStandard + ); + + // If both are selected, we don't want to filter on either, and same for if both are not selected + if (hasEdgeAsync === hasEdgeStandard) { + return undefined; + } + + return hasEdgeAsync; +} diff --git a/app/react/portainer/HomeView/EnvironmentList/EnvironmentListFilters.tsx b/app/react/portainer/HomeView/EnvironmentList/EnvironmentListFilters.tsx index 65338698a..8f20ddcd6 100644 --- a/app/react/portainer/HomeView/EnvironmentList/EnvironmentListFilters.tsx +++ b/app/react/portainer/HomeView/EnvironmentList/EnvironmentListFilters.tsx @@ -176,22 +176,26 @@ function getConnectionTypeOptions(platformTypes: Filter[]) { [PlatformType.Docker]: [ ConnectionType.API, ConnectionType.Agent, - ConnectionType.EdgeAgent, - ConnectionType.EdgeDevice, + ConnectionType.EdgeAgentStandard, + ConnectionType.EdgeAgentAsync, ], [PlatformType.Azure]: [ConnectionType.API], [PlatformType.Kubernetes]: [ ConnectionType.Agent, - ConnectionType.EdgeAgent, - ConnectionType.EdgeDevice, + ConnectionType.EdgeAgentStandard, + ConnectionType.EdgeAgentAsync, + ], + [PlatformType.Nomad]: [ + ConnectionType.EdgeAgentStandard, + ConnectionType.EdgeAgentAsync, ], - [PlatformType.Nomad]: [ConnectionType.EdgeAgent, ConnectionType.EdgeDevice], }; const connectionTypesDefaultOptions = [ { value: ConnectionType.API, label: 'API' }, { value: ConnectionType.Agent, label: 'Agent' }, - { value: ConnectionType.EdgeAgent, label: 'Edge Agent' }, + { value: ConnectionType.EdgeAgentStandard, label: 'Edge Agent Standard' }, + { value: ConnectionType.EdgeAgentAsync, label: 'Edge Agent Async' }, ]; if (platformTypes.length === 0) { @@ -226,12 +230,12 @@ function getPlatformTypeOptions(connectionTypes: Filter[]) { const connectionTypePlatformType = { [ConnectionType.API]: [PlatformType.Docker, PlatformType.Azure], [ConnectionType.Agent]: [PlatformType.Docker, PlatformType.Kubernetes], - [ConnectionType.EdgeAgent]: [ + [ConnectionType.EdgeAgentStandard]: [ PlatformType.Kubernetes, PlatformType.Nomad, PlatformType.Docker, ], - [ConnectionType.EdgeDevice]: [ + [ConnectionType.EdgeAgentAsync]: [ PlatformType.Nomad, PlatformType.Docker, PlatformType.Kubernetes, diff --git a/app/react/portainer/HomeView/EnvironmentList/types.ts b/app/react/portainer/HomeView/EnvironmentList/types.ts index 81c81af68..7030c45a3 100644 --- a/app/react/portainer/HomeView/EnvironmentList/types.ts +++ b/app/react/portainer/HomeView/EnvironmentList/types.ts @@ -6,6 +6,6 @@ export interface Filter { export enum ConnectionType { API, Agent, - EdgeAgent, - EdgeDevice, + EdgeAgentStandard, + EdgeAgentAsync, } diff --git a/app/react/portainer/common/PortainerTunnelAddrField.tsx b/app/react/portainer/common/PortainerTunnelAddrField.tsx new file mode 100644 index 000000000..4f03f0a47 --- /dev/null +++ b/app/react/portainer/common/PortainerTunnelAddrField.tsx @@ -0,0 +1,65 @@ +import { Field, useField } from 'formik'; +import { string } from 'yup'; + +import { FormControl } from '@@/form-components/FormControl'; +import { Input } from '@@/form-components/Input'; + +interface Props { + fieldName: string; + readonly?: boolean; + required?: boolean; +} + +export function PortainerTunnelAddrField({ + fieldName, + readonly, + required, +}: Props) { + const [, metaProps] = useField(fieldName); + const id = `${fieldName}-input`; + + return ( + + + + ); +} + +export function validation() { + return string() + .required('Tunnel server address is required') + .test( + 'valid tunnel server URL', + 'The tunnel server address must be a valid address (localhost cannot be used)', + (value) => { + if (!value) { + return false; + } + + return !value.startsWith('localhost'); + } + ); +} + +/** + * Returns an address that can be used as a default value for the Portainer tunnel server address + * based on the current window location. + * Used for Edge Compute. + * + */ +export function buildDefaultValue() { + return `${window.location.hostname}:8000`; +} diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/PortainerUrlField.tsx b/app/react/portainer/common/PortainerUrlField.tsx similarity index 50% rename from app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/PortainerUrlField.tsx rename to app/react/portainer/common/PortainerUrlField.tsx index 4e12c7b2f..826b8fee2 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/PortainerUrlField.tsx +++ b/app/react/portainer/common/PortainerUrlField.tsx @@ -7,36 +7,23 @@ import { Input } from '@@/form-components/Input'; interface Props { fieldName: string; readonly?: boolean; + required?: boolean; + tooltip?: string; } -export function validation() { - return string() - .test( - 'url', - 'URL should be a valid URI and cannot include localhost', - (value) => { - if (!value) { - return false; - } - try { - const url = new URL(value); - return url.hostname !== 'localhost'; - } catch { - return false; - } - } - ) - .required('URL is required'); -} - -export function PortainerUrlField({ fieldName, readonly }: Props) { +export function PortainerUrlField({ + fieldName, + readonly, + required, + tooltip = 'URL of the Portainer instance that the agent will use to initiate the communications.', +}: Props) { const [, metaProps] = useField(fieldName); const id = `${fieldName}-input`; return ( ); } + +export function validation() { + return string() + .required('API server URL is required') + .test( + 'valid API server URL', + 'The API server URL must be a valid URL (localhost cannot be used)', + (value) => { + if (!value) { + return false; + } + + try { + const url = new URL(value); + return !!url.hostname && url.hostname !== 'localhost'; + } catch { + return false; + } + } + ); +} + +/** + * Returns a URL that can be used as a default value for the Portainer server API URL + * based on the current window location. + * Used for Edge Compute. + * + */ +export function buildDefaultValue() { + return `${window.location.protocol}//${window.location.host}`; +} diff --git a/app/react/portainer/environments/EdgeAutoCreateScriptView/AutomaticEdgeEnvCreation/AutomaticEdgeEnvCreation.tsx b/app/react/portainer/environments/EdgeAutoCreateScriptView/AutomaticEdgeEnvCreation/AutomaticEdgeEnvCreation.tsx new file mode 100644 index 000000000..5d5f91d81 --- /dev/null +++ b/app/react/portainer/environments/EdgeAutoCreateScriptView/AutomaticEdgeEnvCreation/AutomaticEdgeEnvCreation.tsx @@ -0,0 +1,180 @@ +import { useMutation } from 'react-query'; +import { useEffect, useState } from 'react'; +import { Laptop } from 'lucide-react'; + +import { generateKey } from '@/react/portainer/environments/environment.service/edge'; +import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm'; +import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts'; +import { useSettings } from '@/react/portainer/settings/queries'; +import EdgeAgentStandardIcon from '@/react/edge/components/edge-agent-standard.svg?c'; +import EdgeAgentAsyncIcon from '@/react/edge/components/edge-agent-async.svg?c'; + +import { Widget, WidgetBody, WidgetTitle } from '@@/Widget'; +import { TextTip } from '@@/Tip/TextTip'; +import { BoxSelector } from '@@/BoxSelector'; +import { FormSection } from '@@/form-components/FormSection'; +import { CopyButton } from '@@/buttons'; +import { Link } from '@@/Link'; +import { FormControl } from '@@/form-components/FormControl'; +import { Input } from '@@/form-components/Input'; + +const commands = { + linux: [ + commandsTabs.k8sLinux, + commandsTabs.swarmLinux, + commandsTabs.standaloneLinux, + commandsTabs.nomadLinux, + ], + win: [commandsTabs.swarmWindows, commandsTabs.standaloneWindow], +}; + +const asyncModeOptions = [ + { + icon: EdgeAgentStandardIcon, + id: 'standard', + label: 'Edge Agent Standard', + value: false, + iconType: 'badge', + }, + { + icon: EdgeAgentAsyncIcon, + id: 'async', + label: 'Edge Agent Async', + value: true, + iconType: 'badge', + }, +] as const; + +export function AutomaticEdgeEnvCreation() { + const edgeKeyMutation = useGenerateKeyMutation(); + const { mutate: generateKey, reset: resetKey } = edgeKeyMutation; + const settingsQuery = useSettings(); + const [asyncMode, setAsyncMode] = useState(false); + + const url = settingsQuery.data?.EdgePortainerUrl; + + const settings = settingsQuery.data; + const edgeKey = edgeKeyMutation.data; + const edgeComputeConfigurationOK = validateConfiguration(); + + useEffect(() => { + if (edgeComputeConfigurationOK) { + generateKey(); + } else { + resetKey(); + } + }, [generateKey, edgeComputeConfigurationOK, resetKey]); + + if (!settingsQuery.data) { + return null; + } + + return ( + + + + {!edgeComputeConfigurationOK ? ( + + In order to use this feature, please turn on Edge Compute features{' '} + here and have + Portainer API server URL and tunnel server address properly + configured. + + ) : ( + <> + + + + + )} + + + ); + + function handleChangeAsyncMode(asyncMode: boolean) { + setAsyncMode(asyncMode); + } + + function validateConfiguration() { + return !!( + settings && + settings.EnableEdgeComputeFeatures && + settings.EdgePortainerUrl && + settings.Edge.TunnelServerAddress + ); + } +} + +// using mutation because we want this action to run only when required +function useGenerateKeyMutation() { + return useMutation(generateKey); +} + +function EdgeKeyInfo({ + isLoading, + edgeKey, + url, + tunnelUrl, + asyncMode, +}: { + isLoading: boolean; + edgeKey?: string; + url?: string; + tunnelUrl?: string; + asyncMode: boolean; +}) { + if (isLoading || !edgeKey) { + return
Generating key for {url} ...
; + } + + return ( + <> +
+ + +
+ {edgeKey} +
+ + Copy token +
+ +
+ + + + + + + {!asyncMode && ( + + + + )} + + + Portainer Server URL{' '} + {!asyncMode ? 'and tunnel server address are' : 'is'} set{' '} + here + + + + ); +} diff --git a/app/react/portainer/environments/EdgeAutoCreateScriptView/AutomaticEdgeEnvCreation/index.ts b/app/react/portainer/environments/EdgeAutoCreateScriptView/AutomaticEdgeEnvCreation/index.ts new file mode 100644 index 000000000..1a10043f7 --- /dev/null +++ b/app/react/portainer/environments/EdgeAutoCreateScriptView/AutomaticEdgeEnvCreation/index.ts @@ -0,0 +1 @@ +export { AutomaticEdgeEnvCreation } from './AutomaticEdgeEnvCreation'; diff --git a/app/react/portainer/environments/EdgeAutoCreateScriptView/EdgeAutoCreateScriptView.tsx b/app/react/portainer/environments/EdgeAutoCreateScriptView/EdgeAutoCreateScriptView.tsx new file mode 100644 index 000000000..de6666511 --- /dev/null +++ b/app/react/portainer/environments/EdgeAutoCreateScriptView/EdgeAutoCreateScriptView.tsx @@ -0,0 +1,27 @@ +import { withLimitToBE } from '@/react/hooks/useLimitToBE'; + +import { PageHeader } from '@@/PageHeader'; + +import { AutomaticEdgeEnvCreation } from './AutomaticEdgeEnvCreation'; + +export const EdgeAutoCreateScriptViewWrapper = withLimitToBE( + EdgeAutoCreateScriptView +); + +function EdgeAutoCreateScriptView() { + return ( + <> + + +
+ +
+ + ); +} diff --git a/app/react/portainer/environments/EdgeAutoCreateScriptView/index.ts b/app/react/portainer/environments/EdgeAutoCreateScriptView/index.ts new file mode 100644 index 000000000..609cb24fe --- /dev/null +++ b/app/react/portainer/environments/EdgeAutoCreateScriptView/index.ts @@ -0,0 +1 @@ +export { EdgeAutoCreateScriptViewWrapper as EdgeAutoCreateScriptView } from './EdgeAutoCreateScriptView'; diff --git a/app/react/portainer/environments/ListView/ImportFdoDeviceButton.tsx b/app/react/portainer/environments/ListView/ImportFdoDeviceButton.tsx new file mode 100644 index 000000000..82a6752f6 --- /dev/null +++ b/app/react/portainer/environments/ListView/ImportFdoDeviceButton.tsx @@ -0,0 +1,28 @@ +import { Plus } from 'lucide-react'; + +import { Button } from '@@/buttons'; +import { Link } from '@@/Link'; + +import { useSettings } from '../../settings/queries'; + +export function ImportFdoDeviceButton() { + const isFDOEnabledQuery = useSettings( + (settings) => settings.fdoConfiguration.enabled + ); + + if (!isFDOEnabledQuery.data) { + return null; + } + + return ( + + ); +} diff --git a/app/react/portainer/environments/environment.service/create.ts b/app/react/portainer/environments/environment.service/create.ts index f565be622..78347d0ef 100644 --- a/app/react/portainer/environments/environment.service/create.ts +++ b/app/react/portainer/environments/environment.service/create.ts @@ -2,6 +2,7 @@ import { Gpu } from '@/react/portainer/environments/wizard/EnvironmentsCreationV import axios, { parseAxiosError } from '@/portainer/services/axios'; import { type EnvironmentGroupId } from '@/react/portainer/environments/environment-groups/types'; import { type TagId } from '@/portainer/tags/types'; +import { EdgeAsyncIntervalsValues } from '@/react/edge/components/EdgeAsyncIntervalsForm'; import { type Environment, EnvironmentCreationTypes } from '../types'; @@ -101,6 +102,10 @@ interface TLSSettings { keyFile?: File; } +interface EdgeSettings extends EdgeAsyncIntervalsValues { + asyncMode: boolean; +} + export interface EnvironmentOptions { url?: string; publicUrl?: string; @@ -110,6 +115,8 @@ export interface EnvironmentOptions { isEdgeDevice?: boolean; gpus?: Gpu[]; pollFrequency?: number; + edge?: EdgeSettings; + tunnelServerAddr?: string; } interface CreateRemoteEnvironment { @@ -163,10 +170,12 @@ export function createAgentEnvironment({ interface CreateEdgeAgentEnvironment { name: string; portainerUrl: string; + tunnelServerAddr?: string; meta?: EnvironmentMetadata; pollFrequency: number; gpus?: Gpu[]; isEdgeDevice?: boolean; + edge: EdgeSettings; } export function createEdgeAgentEnvironment({ @@ -176,6 +185,7 @@ export function createEdgeAgentEnvironment({ gpus = [], isEdgeDevice, pollFrequency, + edge, }: CreateEdgeAgentEnvironment) { return createEnvironment( name, @@ -189,6 +199,7 @@ export function createEdgeAgentEnvironment({ gpus, isEdgeDevice, pollFrequency, + edge, meta, } ); @@ -240,6 +251,16 @@ async function createEnvironment( AzureAuthenticationKey: azure.authenticationKey, }; } + + if (options.edge?.asyncMode) { + payload = { + ...payload, + EdgeAsyncMode: true, + EdgePingInterval: options.edge?.PingInterval, + EdgeSnapshotInterval: options.edge?.SnapshotInterval, + EdgeCommandInterval: options.edge?.CommandInterval, + }; + } } const formPayload = json2formData(payload); diff --git a/app/react/portainer/environments/environment.service/index.ts b/app/react/portainer/environments/environment.service/index.ts index 3131c7c62..4c6763a55 100644 --- a/app/react/portainer/environments/environment.service/index.ts +++ b/app/react/portainer/environments/environment.service/index.ts @@ -22,7 +22,7 @@ export interface EnvironmentsQueryParams { tagsPartialMatch?: boolean; groupIds?: EnvironmentGroupId[]; status?: EnvironmentStatus[]; - edgeDevice?: boolean; + edgeAsync?: boolean; edgeDeviceUntrusted?: boolean; excludeSnapshots?: boolean; provisioned?: boolean; diff --git a/app/react/portainer/environments/queries/useAgentDetails.ts b/app/react/portainer/environments/queries/useAgentDetails.ts index fc066f687..78a51cfa2 100644 --- a/app/react/portainer/environments/queries/useAgentDetails.ts +++ b/app/react/portainer/environments/queries/useAgentDetails.ts @@ -15,6 +15,5 @@ export function useAgentDetails() { return { agentVersion, agentSecret: settingsQuery.data.AgentSecret, - useEdgeAsyncMode: settingsQuery.data.Edge.AsyncMode, }; } diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.tsx index 0d75ade82..bd78764ef 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.tsx @@ -205,12 +205,14 @@ function useAnalyticsState() { dockerAgent: 0, dockerApi: 0, kubernetesAgent: 0, - kubernetesEdgeAgent: 0, + kubernetesEdgeAgentAsync: 0, + kubernetesEdgeAgentStandard: 0, kaasAgent: 0, aciApi: 0, localEndpoint: 0, - nomadEdgeAgent: 0, - dockerEdgeAgent: 0, + nomadEdgeAgentStandard: 0, + dockerEdgeAgentAsync: 0, + dockerEdgeAgentStandard: 0, }); return { analytics, setAnalytics }; diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/DeploymentScripts.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/DeploymentScripts.tsx index 35ce8ef46..bbc8eec12 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/DeploymentScripts.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/APITab/DeploymentScripts.tsx @@ -1,7 +1,5 @@ import { useState } from 'react'; -import { useAgentDetails } from '@/react/portainer/environments/queries/useAgentDetails'; - import { CopyButton } from '@@/buttons/CopyButton'; import { Code } from '@@/Code'; import { NavTabs } from '@@/NavTabs'; @@ -22,12 +20,6 @@ const deployments = [ export function DeploymentScripts() { const [deployType, setDeployType] = useState(deployments[0].id); - const agentDetailsQuery = useAgentDetails(); - - if (!agentDetailsQuery) { - return null; - } - const options = deployments.map((c) => ({ id: c.id, label: c.label, diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/WizardDocker.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/WizardDocker.tsx index 177497b28..4e8fb2746 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/WizardDocker.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardDocker/WizardDocker.tsx @@ -1,8 +1,12 @@ import { useState } from 'react'; -import { Zap, Cloud, Network, Plug2 } from 'lucide-react'; +import { Zap, Network, Plug2 } from 'lucide-react'; +import _ from 'lodash'; import { Environment } from '@/react/portainer/environments/types'; import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts'; +import { isBE } from '@/react/portainer/feature-flags/feature-flags.service'; +import EdgeAgentStandardIcon from '@/react/edge/components/edge-agent-standard.svg?c'; +import EdgeAgentAsyncIcon from '@/react/edge/components/edge-agent-async.svg?c'; import { BoxSelector, type BoxSelectorOption } from '@@/BoxSelector'; import { BadgeIcon } from '@@/BadgeIcon'; @@ -21,8 +25,8 @@ interface Props { } const defaultOptions: BoxSelectorOption< - 'agent' | 'api' | 'socket' | 'edgeAgent' ->[] = [ + 'agent' | 'api' | 'socket' | 'edgeAgentStandard' | 'edgeAgentAsync' +>[] = _.compact([ { id: 'agent', icon: , @@ -45,17 +49,28 @@ const defaultOptions: BoxSelectorOption< value: 'socket', }, { - id: 'edgeAgent', - icon: , - label: 'Edge Agent', + id: 'edgeAgentStandard', + icon: EdgeAgentStandardIcon, + iconType: 'badge', + label: 'Edge Agent Standard', description: '', - value: 'edgeAgent', - hide: window.ddExtension, + value: 'edgeAgentStandard', }, -]; + isBE && { + id: 'edgeAgentAsync', + icon: EdgeAgentAsyncIcon, + iconType: 'badge', + label: 'Edge Agent Async', + description: '', + value: 'edgeAgentAsync', + }, +]); export function WizardDocker({ onCreate, isDockerStandalone }: Props) { - const options = useFilterEdgeOptionsIfNeeded(defaultOptions, 'edgeAgent'); + const options = useFilterEdgeOptionsIfNeeded( + defaultOptions, + 'edgeAgentStandard' + ); const [creationType, setCreationType] = useState(options[0].value); @@ -74,7 +89,14 @@ export function WizardDocker({ onCreate, isDockerStandalone }: Props) {
); - function getTab(creationType: 'agent' | 'api' | 'socket' | 'edgeAgent') { + function getTab( + creationType: + | 'agent' + | 'api' + | 'socket' + | 'edgeAgentStandard' + | 'edgeAgentAsync' + ) { switch (creationType) { case 'agent': return ( @@ -95,10 +117,29 @@ export function WizardDocker({ onCreate, isDockerStandalone }: Props) { onCreate={(environment) => onCreate(environment, 'localEndpoint')} /> ); - case 'edgeAgent': + case 'edgeAgentStandard': return ( onCreate(environment, 'dockerEdgeAgent')} + onCreate={(environment) => + onCreate(environment, 'dockerEdgeAgentStandard') + } + commands={{ + linux: isDockerStandalone + ? [commandsTabs.standaloneLinux] + : [commandsTabs.swarmLinux], + win: isDockerStandalone + ? [commandsTabs.standaloneWindow] + : [commandsTabs.swarmWindows], + }} + /> + ); + case 'edgeAgentAsync': + return ( + + onCreate(environment, 'dockerEdgeAgentAsync') + } commands={{ linux: isDockerStandalone ? [commandsTabs.standaloneLinux] diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/WizardKubernetes.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/WizardKubernetes.tsx index 9b50c98ef..c0af45f76 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/WizardKubernetes.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/WizardKubernetes/WizardKubernetes.tsx @@ -1,17 +1,17 @@ import { useState } from 'react'; -import { Zap, Cloud, UploadCloud } from 'lucide-react'; +import { Zap, UploadCloud } from 'lucide-react'; +import _ from 'lodash'; -import { - Environment, - EnvironmentCreationTypes, -} from '@/react/portainer/environments/types'; +import { Environment } from '@/react/portainer/environments/types'; import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts'; import { FeatureId } from '@/react/portainer/feature-flags/enums'; +import { isBE } from '@/react/portainer/feature-flags/feature-flags.service'; +import EdgeAgentStandardIcon from '@/react/edge/components/edge-agent-standard.svg?c'; +import EdgeAgentAsyncIcon from '@/react/edge/components/edge-agent-async.svg?c'; import { BoxSelectorOption } from '@@/BoxSelector/types'; import { BoxSelector } from '@@/BoxSelector'; import { BEFeatureIndicator } from '@@/BEFeatureIndicator'; -import { BadgeIcon } from '@@/BadgeIcon'; import { AnalyticsStateKey } from '../types'; import { EdgeAgentTab } from '../shared/EdgeAgentTab'; @@ -24,37 +24,50 @@ interface Props { onCreate(environment: Environment, analytics: AnalyticsStateKey): void; } -const defaultOptions: BoxSelectorOption[] = [ +type CreationType = + | 'agent' + | 'edgeAgentStandard' + | 'edgeAgentAsync' + | 'kubeconfig'; + +const defaultOptions: BoxSelectorOption[] = _.compact([ { id: 'agent_endpoint', - icon: , + icon: Zap, + iconType: 'badge', label: 'Agent', - value: EnvironmentCreationTypes.AgentEnvironment, + value: 'agent', description: '', }, { - id: 'edgeAgent', - icon: , - label: 'Edge Agent', + id: 'edgeAgentStandard', + icon: EdgeAgentStandardIcon, + iconType: 'badge', + label: 'Edge Agent Standard', description: '', - value: EnvironmentCreationTypes.EdgeAgentEnvironment, - hide: window.ddExtension, + value: 'edgeAgentStandard', + }, + isBE && { + id: 'edgeAgentAsync', + icon: EdgeAgentAsyncIcon, + iconType: 'badge', + label: 'Edge Agent Async', + description: '', + value: 'edgeAgentAsync', }, { id: 'kubeconfig_endpoint', - icon: , + icon: UploadCloud, + iconType: 'badge', label: 'Import', - value: EnvironmentCreationTypes.KubeConfigEnvironment, + value: 'kubeconfig', description: 'Import an existing Kubernetes config', feature: FeatureId.K8S_CREATE_FROM_KUBECONFIG, }, -]; +]); export function WizardKubernetes({ onCreate }: Props) { - const options = useFilterEdgeOptionsIfNeeded( - defaultOptions, - EnvironmentCreationTypes.EdgeAgentEnvironment - ); + const options = useFilterEdgeOptionsIfNeeded(defaultOptions, 'agent'); const [creationType, setCreationType] = useState(options[0].value); @@ -73,24 +86,34 @@ export function WizardKubernetes({ onCreate }: Props) {
); - function getTab(type: typeof options[number]['value']) { + function getTab(type: CreationType) { switch (type) { - case EnvironmentCreationTypes.AgentEnvironment: + case 'agent': return ( onCreate(environment, 'kubernetesAgent')} /> ); - case EnvironmentCreationTypes.EdgeAgentEnvironment: + case 'edgeAgentStandard': return ( - onCreate(environment, 'kubernetesEdgeAgent') + onCreate(environment, 'kubernetesEdgeAgentStandard') } commands={[{ ...commandsTabs.k8sLinux, label: 'Linux' }]} /> ); - case EnvironmentCreationTypes.KubeConfigEnvironment: + case 'edgeAgentAsync': + return ( + + onCreate(environment, 'kubernetesEdgeAgentAsync') + } + commands={[{ ...commandsTabs.k8sLinux, label: 'Linux' }]} + /> + ); + case 'kubeconfig': return (
- + + {isBE && !asyncMode && ( + + )} ); } diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.tsx index c5b144b26..8d260520c 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.tsx @@ -3,9 +3,15 @@ import { Plug2 } from 'lucide-react'; import { Environment } from '@/react/portainer/environments/types'; import { useCreateEdgeAgentEnvironmentMutation } from '@/react/portainer/environments/queries/useCreateEnvironmentMutation'; -import { baseHref } from '@/portainer/helpers/pathHelper'; +import { Settings } from '@/react/portainer/settings/types'; import { EdgeCheckinIntervalField } from '@/react/edge/components/EdgeCheckInIntervalField'; -import { useCreateEdgeDeviceParam } from '@/react/portainer/environments/wizard/hooks/useCreateEdgeDeviceParam'; +import { + EdgeAsyncIntervalsForm, + EDGE_ASYNC_INTERVAL_USE_DEFAULT, +} from '@/react/edge/components/EdgeAsyncIntervalsForm'; +import { useSettings } from '@/react/portainer/settings/queries'; +import { buildDefaultValue as buildTunnelDefaultValue } from '@/react/portainer/common/PortainerTunnelAddrField'; +import { buildDefaultValue as buildApiUrlDefaultValue } from '@/react/portainer/common/PortainerUrlField'; import { FormSection } from '@@/form-components/FormSection'; import { LoadingButton } from '@@/buttons/LoadingButton'; @@ -21,15 +27,27 @@ interface Props { onCreate(environment: Environment): void; readonly: boolean; showGpus?: boolean; + asyncMode: boolean; } -const initialValues = buildInitialValues(); - -export function EdgeAgentForm({ onCreate, readonly, showGpus = false }: Props) { - const createEdgeDevice = useCreateEdgeDeviceParam(); +export function EdgeAgentForm({ + onCreate, + readonly, + asyncMode, + showGpus = false, +}: Props) { + const settingsQuery = useSettings(); const createMutation = useCreateEdgeAgentEnvironmentMutation(); - const validation = useValidationSchema(); + const validation = useValidationSchema(asyncMode); + + if (!settingsQuery.data) { + return null; + } + + const settings = settingsQuery.data; + + const initialValues = buildInitialValues(settings); return ( @@ -40,15 +58,23 @@ export function EdgeAgentForm({ onCreate, readonly, showGpus = false }: Props) { > {({ isValid, setFieldValue, values }) => (
- + - setFieldValue('pollFrequency', value)} - value={values.pollFrequency} - /> + {asyncMode ? ( + setFieldValue('edge', values)} + /> + ) : ( + setFieldValue('pollFrequency', value)} + value={values.pollFrequency} + /> + )} {showGpus && } @@ -75,7 +101,13 @@ export function EdgeAgentForm({ onCreate, readonly, showGpus = false }: Props) { function handleSubmit(values: typeof initialValues) { createMutation.mutate( - { ...values, isEdgeDevice: createEdgeDevice }, + { + ...values, + edge: { + ...values.edge, + asyncMode, + }, + }, { onSuccess(environment) { onCreate(environment); @@ -85,20 +117,22 @@ export function EdgeAgentForm({ onCreate, readonly, showGpus = false }: Props) { } } -export function buildInitialValues(): FormValues { +export function buildInitialValues(settings: Settings): FormValues { return { name: '', - portainerUrl: defaultPortainerUrl(), + portainerUrl: settings.EdgePortainerUrl || buildApiUrlDefaultValue(), + tunnelServerAddr: + settings.Edge.TunnelServerAddress || buildTunnelDefaultValue(), pollFrequency: 0, meta: { groupId: 1, tagIds: [], }, + edge: { + CommandInterval: EDGE_ASYNC_INTERVAL_USE_DEFAULT, + PingInterval: EDGE_ASYNC_INTERVAL_USE_DEFAULT, + SnapshotInterval: EDGE_ASYNC_INTERVAL_USE_DEFAULT, + }, gpus: [], }; - - function defaultPortainerUrl() { - const baseHREF = baseHref(); - return window.location.origin + (baseHREF !== '/' ? baseHREF : ''); - } } diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.validation.ts b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.validation.ts index b6cb856e8..5a2677777 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.validation.ts +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/EdgeAgentForm.validation.ts @@ -1,21 +1,31 @@ -import { number, object, SchemaOf } from 'yup'; +import { number, object, SchemaOf, string } from 'yup'; +import { + edgeAsyncIntervalsValidation, + EdgeAsyncIntervalsValues, +} from '@/react/edge/components/EdgeAsyncIntervalsForm'; import { gpusListValidation } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList'; +import { validation as urlValidation } from '@/react/portainer/common/PortainerTunnelAddrField'; +import { validation as addressValidation } from '@/react/portainer/common/PortainerUrlField'; +import { isBE } from '@/react/portainer/feature-flags/feature-flags.service'; import { metadataValidation } from '../../MetadataFieldset/validation'; import { useNameValidation } from '../../NameField'; -import { validation as urlValidation } from './PortainerUrlField'; import { FormValues } from './types'; -export function useValidationSchema(): SchemaOf { +export function useValidationSchema(asyncMode: boolean): SchemaOf { const nameValidation = useNameValidation(); return object().shape({ name: nameValidation, portainerUrl: urlValidation(), + tunnelServerAddr: asyncMode ? string() : addressValidation(), pollFrequency: number().required(), meta: metadataValidation(), gpus: gpusListValidation(), + edge: isBE + ? edgeAsyncIntervalsValidation() + : (null as unknown as SchemaOf), }); } diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/types.ts b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/types.ts index bdf05b59a..0ad061cf9 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/types.ts +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentForm/types.ts @@ -1,3 +1,4 @@ +import { EdgeAsyncIntervalsValues } from '@/react/edge/components/EdgeAsyncIntervalsForm'; import { Gpu } from '@/react/portainer/environments/wizard/EnvironmentsCreationView/shared/Hardware/GpusList'; import { EnvironmentMetadata } from '@/react/portainer/environments/environment.service/create'; @@ -5,7 +6,10 @@ export interface FormValues { name: string; portainerUrl: string; + tunnelServerAddr?: string; pollFrequency: number; meta: EnvironmentMetadata; gpus: Gpu[]; + + edge: EdgeAsyncIntervalsValues; } diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentTab.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentTab.tsx index 2133e1ffd..6739c2f26 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentTab.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/shared/EdgeAgentTab/EdgeAgentTab.tsx @@ -16,6 +16,7 @@ interface Props { commands: CommandTab[] | Partial>; isNomadTokenVisible?: boolean; showGpus?: boolean; + asyncMode?: boolean; } export function EdgeAgentTab({ @@ -23,9 +24,9 @@ export function EdgeAgentTab({ commands, isNomadTokenVisible, showGpus = false, + asyncMode = false, }: Props) { const [edgeInfo, setEdgeInfo] = useState(); - const [formKey, clearForm] = useReducer((state) => state + 1, 0); return ( @@ -35,6 +36,7 @@ export function EdgeAgentTab({ readonly={!!edgeInfo} key={formKey} showGpus={showGpus} + asyncMode={asyncMode} /> {edgeInfo && ( @@ -51,6 +53,7 @@ export function EdgeAgentTab({ edgeInfo={edgeInfo} commands={commands} isNomadTokenVisible={isNomadTokenVisible} + asyncMode={asyncMode} />
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/types.ts b/app/react/portainer/environments/wizard/EnvironmentsCreationView/types.ts index 15a275156..9fb76255e 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/types.ts +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/types.ts @@ -1,13 +1,15 @@ export interface AnalyticsState { dockerAgent: number; dockerApi: number; - dockerEdgeAgent: number; + dockerEdgeAgentStandard: number; + dockerEdgeAgentAsync: number; kubernetesAgent: number; - kubernetesEdgeAgent: number; + kubernetesEdgeAgentStandard: number; + kubernetesEdgeAgentAsync: number; kaasAgent: number; aciApi: number; localEndpoint: number; - nomadEdgeAgent: number; + nomadEdgeAgentStandard: number; } export type AnalyticsStateKey = keyof AnalyticsState; diff --git a/app/react/portainer/settings/EdgeComputeView/AutomaticEdgeEnvCreation/AutoEnvCreationSettingsForm.tsx b/app/react/portainer/settings/EdgeComputeView/AutomaticEdgeEnvCreation/AutoEnvCreationSettingsForm.tsx index 25bcb4642..7efb7b016 100644 --- a/app/react/portainer/settings/EdgeComputeView/AutomaticEdgeEnvCreation/AutoEnvCreationSettingsForm.tsx +++ b/app/react/portainer/settings/EdgeComputeView/AutomaticEdgeEnvCreation/AutoEnvCreationSettingsForm.tsx @@ -1,57 +1,29 @@ -import { Field, Form, Formik } from 'formik'; +import { Form, Formik } from 'formik'; import * as yup from 'yup'; -import { useCallback, useEffect } from 'react'; +import { useCallback } from 'react'; -import { baseHref } from '@/portainer/helpers/pathHelper'; import { notifySuccess } from '@/portainer/services/notifications'; import { useUpdateSettingsMutation } from '@/react/portainer/settings/queries'; import { Settings } from '@/react/portainer/settings/types'; import { LoadingButton } from '@@/buttons/LoadingButton'; -import { FormControl } from '@@/form-components/FormControl'; -import { FormSectionTitle } from '@@/form-components/FormSectionTitle'; -import { Input } from '@@/form-components/Input'; import { EnabledWaitingRoomSwitch } from './EnableWaitingRoomSwitch'; interface FormValues { - EdgePortainerUrl: string; - TrustOnFirstConnect: boolean; + EnableWaitingRoom: boolean; } const validation = yup.object({ - TrustOnFirstConnect: yup.boolean(), - EdgePortainerUrl: yup - .string() - .test( - 'url', - 'URL should be a valid URI and cannot include localhost', - (value) => { - if (!value) { - return false; - } - try { - const url = new URL(value); - return !!url.hostname && url.hostname !== 'localhost'; - } catch { - return false; - } - } - ) - .required('URL is required'), + EnableWaitingRoom: yup.boolean(), }); interface Props { settings: Settings; } -const defaultUrl = buildDefaultUrl(); - export function AutoEnvCreationSettingsForm({ settings }: Props) { - const url = settings.EdgePortainerUrl; - - const initialValues = { - EdgePortainerUrl: url || defaultUrl, - TrustOnFirstConnect: settings.TrustOnFirstConnect, + const initialValues: FormValues = { + EnableWaitingRoom: !settings.TrustOnFirstConnect, }; const mutation = useUpdateSettingsMutation(); @@ -60,24 +32,21 @@ export function AutoEnvCreationSettingsForm({ settings }: Props) { const handleSubmit = useCallback( (variables: Partial) => { - updateSettings(variables, { - onSuccess() { - notifySuccess( - 'Success', - 'Successfully updated Automatic Environment Creation settings' - ); - }, - }); + updateSettings( + { TrustOnFirstConnect: !variables.EnableWaitingRoom }, + { + onSuccess() { + notifySuccess( + 'Success', + 'Successfully updated Automatic Environment Creation settings' + ); + }, + } + ); }, [updateSettings] ); - useEffect(() => { - if (!url && validation.isValidSync({ EdgePortainerUrl: defaultUrl })) { - updateSettings({ EdgePortainerUrl: defaultUrl }); - } - }, [updateSettings, url]); - return ( initialValues={initialValues} @@ -86,19 +55,8 @@ export function AutoEnvCreationSettingsForm({ settings }: Props) { validateOnMount enableReinitialize > - {({ errors, isValid, dirty }) => ( + {({ isValid, dirty }) => ( - Configuration - - - - -
@@ -107,6 +65,7 @@ export function AutoEnvCreationSettingsForm({ settings }: Props) { loadingText="generating..." isLoading={mutation.isLoading} disabled={!isValid || !dirty} + className="!ml-0" > Save settings @@ -117,8 +76,3 @@ export function AutoEnvCreationSettingsForm({ settings }: Props) { ); } - -function buildDefaultUrl() { - const baseHREF = baseHref(); - return window.location.origin + (baseHREF !== '/' ? baseHREF : ''); -} diff --git a/app/react/portainer/settings/EdgeComputeView/AutomaticEdgeEnvCreation/AutomaticEdgeEnvCreation.tsx b/app/react/portainer/settings/EdgeComputeView/AutomaticEdgeEnvCreation/AutomaticEdgeEnvCreation.tsx index 3f989151b..813ac5ff0 100644 --- a/app/react/portainer/settings/EdgeComputeView/AutomaticEdgeEnvCreation/AutomaticEdgeEnvCreation.tsx +++ b/app/react/portainer/settings/EdgeComputeView/AutomaticEdgeEnvCreation/AutomaticEdgeEnvCreation.tsx @@ -1,69 +1,26 @@ -import { useMutation } from 'react-query'; -import { useEffect } from 'react'; import { Laptop } from 'lucide-react'; -import { generateKey } from '@/react/portainer/environments/environment.service/edge'; -import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm'; -import { commandsTabs } from '@/react/edge/components/EdgeScriptForm/scripts'; - import { Widget, WidgetBody, WidgetTitle } from '@@/Widget'; import { useSettings } from '../../queries'; import { AutoEnvCreationSettingsForm } from './AutoEnvCreationSettingsForm'; -const commands = { - linux: [ - commandsTabs.k8sLinux, - commandsTabs.swarmLinux, - commandsTabs.standaloneLinux, - commandsTabs.nomadLinux, - ], - win: [commandsTabs.swarmWindows, commandsTabs.standaloneWindow], -}; - export function AutomaticEdgeEnvCreation() { - const edgeKeyMutation = useGenerateKeyMutation(); - const { mutate: generateKey } = edgeKeyMutation; const settingsQuery = useSettings(); - const url = settingsQuery.data?.EdgePortainerUrl; - - useEffect(() => { - if (url) { - generateKey(); - } - }, [generateKey, url]); - if (!settingsQuery.data) { return null; } - const edgeKey = edgeKeyMutation.data; + const settings = settingsQuery.data; return ( - - - {edgeKeyMutation.isLoading ? ( -
Generating key for {url} ...
- ) : ( - edgeKey && ( - - ) - )} +
); } - -// using mutation because we want this action to run only when required -function useGenerateKeyMutation() { - return useMutation(generateKey); -} diff --git a/app/react/portainer/settings/EdgeComputeView/AutomaticEdgeEnvCreation/EnableWaitingRoomSwitch.tsx b/app/react/portainer/settings/EdgeComputeView/AutomaticEdgeEnvCreation/EnableWaitingRoomSwitch.tsx index 5849be771..81c3e6b62 100644 --- a/app/react/portainer/settings/EdgeComputeView/AutomaticEdgeEnvCreation/EnableWaitingRoomSwitch.tsx +++ b/app/react/portainer/settings/EdgeComputeView/AutomaticEdgeEnvCreation/EnableWaitingRoomSwitch.tsx @@ -7,17 +7,18 @@ import { buildConfirmButton } from '@@/modals/utils'; import { ModalType } from '@@/modals'; export function EnabledWaitingRoomSwitch() { - const [inputProps, meta, helpers] = useField('TrustOnFirstConnect'); + const [inputProps, meta, helpers] = useField('EnableWaitingRoom'); return ( ); - async function handleChange(trust: boolean) { - if (!trust) { - helpers.setValue(false); + async function handleChange(enable: boolean) { + if (enable) { + helpers.setValue(true); return; } @@ -39,6 +40,10 @@ export function EnabledWaitingRoomSwitch() { confirmButton: buildConfirmButton('Confirm', 'danger'), }); - helpers.setValue(!!confirmed); + if (!confirmed) { + return; + } + + helpers.setValue(false); } } diff --git a/app/react/portainer/settings/EdgeComputeView/DeploymentSyncOptions/DeploymentSyncOptions.tsx b/app/react/portainer/settings/EdgeComputeView/DeploymentSyncOptions/DeploymentSyncOptions.tsx index 552cd7a03..2951d435e 100644 --- a/app/react/portainer/settings/EdgeComputeView/DeploymentSyncOptions/DeploymentSyncOptions.tsx +++ b/app/react/portainer/settings/EdgeComputeView/DeploymentSyncOptions/DeploymentSyncOptions.tsx @@ -5,9 +5,8 @@ import { Laptop } from 'lucide-react'; import { EdgeCheckinIntervalField } from '@/react/edge/components/EdgeCheckInIntervalField'; import { EdgeAsyncIntervalsForm } from '@/react/edge/components/EdgeAsyncIntervalsForm'; import { notifySuccess } from '@/portainer/services/notifications'; +import { isBE } from '@/react/portainer/feature-flags/feature-flags.service'; -import { FormControl } from '@@/form-components/FormControl'; -import { Switch } from '@@/form-components/SwitchField/Switch'; import { Widget, WidgetBody, WidgetTitle } from '@@/Widget'; import { FormSection } from '@@/form-components/FormSection'; import { LoadingButton } from '@@/buttons/LoadingButton'; @@ -45,7 +44,6 @@ export function DeploymentSyncOptions() { const initialValues: FormValues = { Edge: { - AsyncMode: settingsQuery.data.Edge.AsyncMode, CommandInterval: settingsQuery.data.Edge.CommandInterval, PingInterval: settingsQuery.data.Edge.PingInterval, SnapshotInterval: settingsQuery.data.Edge.SnapshotInterval, @@ -63,8 +61,13 @@ export function DeploymentSyncOptions() { onSubmit={handleSubmit} key={formKey} > - {({ errors, setFieldValue, values, isValid, dirty }) => ( + {({ setFieldValue, values, isValid, dirty }) => ( + + Default values set here will be available to choose as an + option for edge environment creation + + - - - setFieldValue('Edge.AsyncMode', e.valueOf()) - } - /> - - - - Enabling Async disables the tunnel function. - - - {values.Edge.AsyncMode && ( + {isBE && ( )} - -
-
- - Save settings - -
+
+
+ + Save settings +
- +
)} diff --git a/app/react/portainer/settings/EdgeComputeView/DeploymentSyncOptions/types.ts b/app/react/portainer/settings/EdgeComputeView/DeploymentSyncOptions/types.ts index b58c80f66..ce0b852e8 100644 --- a/app/react/portainer/settings/EdgeComputeView/DeploymentSyncOptions/types.ts +++ b/app/react/portainer/settings/EdgeComputeView/DeploymentSyncOptions/types.ts @@ -3,7 +3,6 @@ export interface FormValues { PingInterval: number; SnapshotInterval: number; CommandInterval: number; - AsyncMode: boolean; }; EdgeAgentCheckinInterval: number; } diff --git a/app/react/portainer/settings/EdgeComputeView/EdgeComputeSettings/AddDeviceButton/AddDeviceButton.tsx b/app/react/portainer/settings/EdgeComputeView/EdgeComputeSettings/AddDeviceButton/AddDeviceButton.tsx deleted file mode 100644 index f8a9609c5..000000000 --- a/app/react/portainer/settings/EdgeComputeView/EdgeComputeSettings/AddDeviceButton/AddDeviceButton.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { useRouter } from '@uirouter/react'; -import { Plus } from 'lucide-react'; - -import { usePublicSettings } from '@/react/portainer/settings/queries'; - -import { Button } from '@@/buttons'; -import { openModal } from '@@/modals'; - -import { DeployTypePrompt } from './DeployTypePrompt'; - -enum DeployType { - FDO = 'FDO', - MANUAL = 'MANUAL', -} - -export function AddDeviceButton() { - const router = useRouter(); - const isFDOEnabledQuery = usePublicSettings({ - select: (settings) => settings.IsFDOEnabled, - }); - const isFDOEnabled = !!isFDOEnabledQuery.data; - - return ( - - ); - - async function handleNewDeviceClick() { - const result = await getDeployType(); - - switch (result) { - case DeployType.FDO: - router.stateService.go('portainer.endpoints.importDevice'); - break; - case DeployType.MANUAL: - router.stateService.go('portainer.wizard.endpoints', { - edgeDevice: true, - }); - break; - default: - break; - } - } - - function getDeployType() { - if (!isFDOEnabled) { - return Promise.resolve(DeployType.MANUAL); - } - - return askForDeployType(); - } -} - -function askForDeployType() { - return openModal(DeployTypePrompt, {}); -} diff --git a/app/react/portainer/settings/EdgeComputeView/EdgeComputeSettings/AddDeviceButton/DeployTypePrompt.tsx b/app/react/portainer/settings/EdgeComputeView/EdgeComputeSettings/AddDeviceButton/DeployTypePrompt.tsx deleted file mode 100644 index ffa11f9b9..000000000 --- a/app/react/portainer/settings/EdgeComputeView/EdgeComputeSettings/AddDeviceButton/DeployTypePrompt.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { useState } from 'react'; - -import { DeployType } from '@/react/nomad/jobs/JobsView/JobsDatatable/types'; - -import { OnSubmit } from '@@/modals'; -import { Dialog } from '@@/modals/Dialog'; -import { buildCancelButton, buildConfirmButton } from '@@/modals/utils'; - -export function DeployTypePrompt({ - onSubmit, -}: { - onSubmit: OnSubmit; -}) { - const [deployType, setDeployType] = useState(DeployType.FDO); - return ( - - - - - - } - buttons={[buildCancelButton(), buildConfirmButton()]} - onSubmit={(confirm) => onSubmit(confirm ? deployType : undefined)} - /> - ); -} - -function RadioInput({ - value, - onChange, - label, - groupValue, - name, -}: { - value: T; - onChange: (value: T) => void; - label: string; - groupValue: T; - name: string; -}) { - return ( - - ); -} diff --git a/app/react/portainer/settings/EdgeComputeView/EdgeComputeSettings/AddDeviceButton/index.ts b/app/react/portainer/settings/EdgeComputeView/EdgeComputeSettings/AddDeviceButton/index.ts deleted file mode 100644 index 0e5cf0515..000000000 --- a/app/react/portainer/settings/EdgeComputeView/EdgeComputeSettings/AddDeviceButton/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AddDeviceButton } from './AddDeviceButton'; diff --git a/app/react/portainer/settings/EdgeComputeView/EdgeComputeSettings/EdgeComputeSettings.tsx b/app/react/portainer/settings/EdgeComputeView/EdgeComputeSettings/EdgeComputeSettings.tsx index dd96bcb9f..cdd687ac4 100644 --- a/app/react/portainer/settings/EdgeComputeView/EdgeComputeSettings/EdgeComputeSettings.tsx +++ b/app/react/portainer/settings/EdgeComputeView/EdgeComputeSettings/EdgeComputeSettings.tsx @@ -1,19 +1,19 @@ import { Formik, Form } from 'formik'; import { Laptop } from 'lucide-react'; -import { EdgeCheckinIntervalField } from '@/react/edge/components/EdgeCheckInIntervalField'; import { Settings } from '@/react/portainer/settings/types'; +import { PortainerUrlField } from '@/react/portainer/common/PortainerUrlField'; +import { PortainerTunnelAddrField } from '@/react/portainer/common/PortainerTunnelAddrField'; +import { isBE } from '@/react/portainer/feature-flags/feature-flags.service'; import { Switch } from '@@/form-components/SwitchField/Switch'; import { FormControl } from '@@/form-components/FormControl'; import { Widget, WidgetBody, WidgetTitle } from '@@/Widget'; import { LoadingButton } from '@@/buttons/LoadingButton'; import { TextTip } from '@@/Tip/TextTip'; -import { FormSectionTitle } from '@@/form-components/FormSectionTitle'; import { validationSchema } from './EdgeComputeSettings.validation'; import { FormValues } from './types'; -import { AddDeviceButton } from './AddDeviceButton'; interface Props { settings?: Settings; @@ -28,22 +28,16 @@ export function EdgeComputeSettings({ settings, onSubmit }: Props) { const initialValues: FormValues = { EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures, EdgePortainerUrl: settings.EdgePortainerUrl, - EdgeAgentCheckinInterval: settings.EdgeAgentCheckinInterval, + Edge: { + TunnelServerAddress: settings.Edge.TunnelServerAddress, + }, EnforceEdgeID: settings.EnforceEdgeID, }; return (
- - Edge Compute settings - {settings.EnableEdgeComputeFeatures && } - - } - /> + - When enabled, this will enable Portainer to execute Edge - Device features. + Enable this setting to use Portainer Edge Compute + capabilities. + {isBE && values.EnableEdgeComputeFeatures && ( + <> + + + + + )} + - Check-in Intervals - - - setFieldValue('EdgeAgentCheckinInterval', value) - } - isDefaultHidden - label="Edge agent default poll frequency" - tooltip="Interval used by default by each Edge agent to check in with the Portainer instance. Affects Edge environment management and Edge compute features." - /> -
- {process.env.PORTAINER_EDITION === 'BE' && } + + + {isBE && }
); }