diff --git a/api/http/handler/endpoints/filter.go b/api/http/handler/endpoints/filter.go index a0006a48d..0397ee52d 100644 --- a/api/http/handler/endpoints/filter.go +++ b/api/http/handler/endpoints/filter.go @@ -34,6 +34,7 @@ type EnvironmentsQuery struct { edgeCheckInPassedSeconds int edgeStackId portainer.EdgeStackID edgeStackStatus *portainer.EdgeStackStatusType + excludeIds []portainer.EndpointID } func parseQuery(r *http.Request) (EnvironmentsQuery, error) { @@ -69,6 +70,11 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) { return EnvironmentsQuery{}, err } + excludeIDs, err := getNumberArrayQueryParameter[portainer.EndpointID](r, "excludeIds") + if err != nil { + return EnvironmentsQuery{}, err + } + agentVersions := getArrayQueryParameter(r, "agentVersions") name, _ := request.RetrieveQueryParameter(r, "name", true) @@ -97,6 +103,7 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) { types: endpointTypes, tagIds: tagIDs, endpointIds: endpointIDs, + excludeIds: excludeIDs, tagsPartialMatch: tagsPartialMatch, groupIds: groupIDs, status: status, @@ -118,6 +125,12 @@ func (handler *Handler) filterEndpointsByQuery(filteredEndpoints []portainer.End filteredEndpoints = filteredEndpointsByIds(filteredEndpoints, query.endpointIds) } + if len(query.excludeIds) > 0 { + filteredEndpoints = filter(filteredEndpoints, func(endpoint portainer.Endpoint) bool { + return !slices.Contains(query.excludeIds, endpoint.ID) + }) + } + if len(query.groupIds) > 0 { filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, query.groupIds) } diff --git a/api/http/handler/endpoints/filter_test.go b/api/http/handler/endpoints/filter_test.go index e54af0aea..f98ddd20f 100644 --- a/api/http/handler/endpoints/filter_test.go +++ b/api/http/handler/endpoints/filter_test.go @@ -5,6 +5,7 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/datastore" + "github.com/portainer/portainer/api/internal/slices" "github.com/portainer/portainer/api/internal/testhelpers" "github.com/stretchr/testify/assert" ) @@ -124,6 +125,28 @@ func Test_Filter_edgeFilter(t *testing.T) { runTests(tests, t, handler, endpoints) } +func Test_Filter_excludeIDs(t *testing.T) { + ids := []portainer.EndpointID{1, 2, 3, 4, 5, 6, 7, 8, 9} + + environments := slices.Map(ids, func(id portainer.EndpointID) portainer.Endpoint { + return portainer.Endpoint{ID: id, GroupID: 1, Type: portainer.DockerEnvironment} + }) + + handler := setupFilterTest(t, environments) + + tests := []filterTest{ + { + title: "should exclude IDs 2,5,8", + expected: []portainer.EndpointID{1, 3, 4, 6, 7, 9}, + query: EnvironmentsQuery{ + excludeIds: []portainer.EndpointID{2, 5, 8}, + }, + }, + } + + runTests(tests, t, handler, environments) +} + func runTests(tests []filterTest, t *testing.T, handler *Handler, endpoints []portainer.Endpoint) { for _, test := range tests { t.Run(test.title, func(t *testing.T) { diff --git a/api/internal/slices/slices.go b/api/internal/slices/slices.go index 7eb486398..7d0fa21c7 100644 --- a/api/internal/slices/slices.go +++ b/api/internal/slices/slices.go @@ -63,3 +63,12 @@ func RemoveIndex[T any](s []T, index int) []T { s[index] = s[len(s)-1] return s[:len(s)-1] } + +// Map applies the given function to each element of the slice and returns a new slice with the results +func Map[T, U any](s []T, f func(T) U) []U { + result := make([]U, len(s)) + for i, v := range s { + result[i] = f(v) + } + return result +} diff --git a/app/edge/react/components/index.ts b/app/edge/react/components/index.ts index a40b04ea6..608fe3a0b 100644 --- a/app/edge/react/components/index.ts +++ b/app/edge/react/components/index.ts @@ -92,7 +92,6 @@ export const componentsModule = angular 'query', 'title', 'data-cy', - 'hideEnvironmentIds', ]) ) .component( diff --git a/app/react/edge/components/AssociatedEdgeEnvironmentsSelector.tsx b/app/react/edge/components/AssociatedEdgeEnvironmentsSelector.tsx index 7de8938b6..79e795852 100644 --- a/app/react/edge/components/AssociatedEdgeEnvironmentsSelector.tsx +++ b/app/react/edge/components/AssociatedEdgeEnvironmentsSelector.tsx @@ -28,6 +28,7 @@ export function AssociatedEdgeEnvironmentsSelector({ emptyContentLabel="No environment available" query={{ types: EdgeTypes, + excludeIds: value, }} onClickRow={(env) => { if (!value.includes(env.Id)) { @@ -35,7 +36,6 @@ export function AssociatedEdgeEnvironmentsSelector({ } }} data-cy="edgeGroupCreate-availableEndpoints" - hideEnvironmentIds={value} />