mirror of https://github.com/portainer/portainer
fix(edge/groups): filter selected environments [EE-5891] (#10050)
parent
a27cc6c0e5
commit
a1e610a39a
|
@ -34,6 +34,7 @@ type EnvironmentsQuery struct {
|
||||||
edgeCheckInPassedSeconds int
|
edgeCheckInPassedSeconds int
|
||||||
edgeStackId portainer.EdgeStackID
|
edgeStackId portainer.EdgeStackID
|
||||||
edgeStackStatus *portainer.EdgeStackStatusType
|
edgeStackStatus *portainer.EdgeStackStatusType
|
||||||
|
excludeIds []portainer.EndpointID
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
|
func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
|
||||||
|
@ -69,6 +70,11 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
|
||||||
return EnvironmentsQuery{}, err
|
return EnvironmentsQuery{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
excludeIDs, err := getNumberArrayQueryParameter[portainer.EndpointID](r, "excludeIds")
|
||||||
|
if err != nil {
|
||||||
|
return EnvironmentsQuery{}, err
|
||||||
|
}
|
||||||
|
|
||||||
agentVersions := getArrayQueryParameter(r, "agentVersions")
|
agentVersions := getArrayQueryParameter(r, "agentVersions")
|
||||||
|
|
||||||
name, _ := request.RetrieveQueryParameter(r, "name", true)
|
name, _ := request.RetrieveQueryParameter(r, "name", true)
|
||||||
|
@ -97,6 +103,7 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
|
||||||
types: endpointTypes,
|
types: endpointTypes,
|
||||||
tagIds: tagIDs,
|
tagIds: tagIDs,
|
||||||
endpointIds: endpointIDs,
|
endpointIds: endpointIDs,
|
||||||
|
excludeIds: excludeIDs,
|
||||||
tagsPartialMatch: tagsPartialMatch,
|
tagsPartialMatch: tagsPartialMatch,
|
||||||
groupIds: groupIDs,
|
groupIds: groupIDs,
|
||||||
status: status,
|
status: status,
|
||||||
|
@ -118,6 +125,12 @@ func (handler *Handler) filterEndpointsByQuery(filteredEndpoints []portainer.End
|
||||||
filteredEndpoints = filteredEndpointsByIds(filteredEndpoints, query.endpointIds)
|
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 {
|
if len(query.groupIds) > 0 {
|
||||||
filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, query.groupIds)
|
filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, query.groupIds)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/datastore"
|
"github.com/portainer/portainer/api/datastore"
|
||||||
|
"github.com/portainer/portainer/api/internal/slices"
|
||||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
@ -124,6 +125,28 @@ func Test_Filter_edgeFilter(t *testing.T) {
|
||||||
runTests(tests, t, handler, endpoints)
|
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) {
|
func runTests(tests []filterTest, t *testing.T, handler *Handler, endpoints []portainer.Endpoint) {
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.title, func(t *testing.T) {
|
t.Run(test.title, func(t *testing.T) {
|
||||||
|
|
|
@ -63,3 +63,12 @@ func RemoveIndex[T any](s []T, index int) []T {
|
||||||
s[index] = s[len(s)-1]
|
s[index] = s[len(s)-1]
|
||||||
return 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
|
||||||
|
}
|
||||||
|
|
|
@ -92,7 +92,6 @@ export const componentsModule = angular
|
||||||
'query',
|
'query',
|
||||||
'title',
|
'title',
|
||||||
'data-cy',
|
'data-cy',
|
||||||
'hideEnvironmentIds',
|
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
.component(
|
.component(
|
||||||
|
|
|
@ -28,6 +28,7 @@ export function AssociatedEdgeEnvironmentsSelector({
|
||||||
emptyContentLabel="No environment available"
|
emptyContentLabel="No environment available"
|
||||||
query={{
|
query={{
|
||||||
types: EdgeTypes,
|
types: EdgeTypes,
|
||||||
|
excludeIds: value,
|
||||||
}}
|
}}
|
||||||
onClickRow={(env) => {
|
onClickRow={(env) => {
|
||||||
if (!value.includes(env.Id)) {
|
if (!value.includes(env.Id)) {
|
||||||
|
@ -35,7 +36,6 @@ export function AssociatedEdgeEnvironmentsSelector({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
data-cy="edgeGroupCreate-availableEndpoints"
|
data-cy="edgeGroupCreate-availableEndpoints"
|
||||||
hideEnvironmentIds={value}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-1/2">
|
<div className="w-1/2">
|
||||||
|
|
|
@ -3,10 +3,7 @@ import { truncate } from 'lodash';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { useEnvironmentList } from '@/react/portainer/environments/queries';
|
import { useEnvironmentList } from '@/react/portainer/environments/queries';
|
||||||
import {
|
import { Environment } from '@/react/portainer/environments/types';
|
||||||
Environment,
|
|
||||||
EnvironmentId,
|
|
||||||
} from '@/react/portainer/environments/types';
|
|
||||||
import { useGroups } from '@/react/portainer/environments/environment-groups/queries';
|
import { useGroups } from '@/react/portainer/environments/environment-groups/queries';
|
||||||
import { useTags } from '@/portainer/tags/queries';
|
import { useTags } from '@/portainer/tags/queries';
|
||||||
import { EnvironmentsQueryParams } from '@/react/portainer/environments/environment.service';
|
import { EnvironmentsQueryParams } from '@/react/portainer/environments/environment.service';
|
||||||
|
@ -47,13 +44,11 @@ export function EdgeGroupAssociationTable({
|
||||||
emptyContentLabel,
|
emptyContentLabel,
|
||||||
onClickRow,
|
onClickRow,
|
||||||
'data-cy': dataCy,
|
'data-cy': dataCy,
|
||||||
hideEnvironmentIds = [],
|
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
query: EnvironmentsQueryParams;
|
query: EnvironmentsQueryParams;
|
||||||
emptyContentLabel: string;
|
emptyContentLabel: string;
|
||||||
onClickRow: (env: Environment) => void;
|
onClickRow: (env: Environment) => void;
|
||||||
hideEnvironmentIds?: EnvironmentId[];
|
|
||||||
} & AutomationTestingProps) {
|
} & AutomationTestingProps) {
|
||||||
const tableState = useTableStateWithoutStorage('Name');
|
const tableState = useTableStateWithoutStorage('Name');
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
@ -74,25 +69,17 @@ export function EdgeGroupAssociationTable({
|
||||||
|
|
||||||
const environments: Array<DecoratedEnvironment> = useMemo(
|
const environments: Array<DecoratedEnvironment> = useMemo(
|
||||||
() =>
|
() =>
|
||||||
environmentsQuery.environments
|
environmentsQuery.environments.map((env) => ({
|
||||||
.filter((e) => !hideEnvironmentIds.includes(e.Id))
|
...env,
|
||||||
.map((env) => ({
|
Group: groupsQuery.data?.find((g) => g.Id === env.GroupId)?.Name || '',
|
||||||
...env,
|
Tags: env.TagIds.map(
|
||||||
Group:
|
(tagId) => tagsQuery.data?.find((t) => t.ID === tagId)?.Name || ''
|
||||||
groupsQuery.data?.find((g) => g.Id === env.GroupId)?.Name || '',
|
),
|
||||||
Tags: env.TagIds.map(
|
})),
|
||||||
(tagId) => tagsQuery.data?.find((t) => t.ID === tagId)?.Name || ''
|
[environmentsQuery.environments, groupsQuery.data, tagsQuery.data]
|
||||||
),
|
|
||||||
})),
|
|
||||||
[
|
|
||||||
environmentsQuery.environments,
|
|
||||||
groupsQuery.data,
|
|
||||||
hideEnvironmentIds,
|
|
||||||
tagsQuery.data,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const totalCount = environmentsQuery.totalCount - hideEnvironmentIds.length;
|
const { totalCount } = environmentsQuery;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Datatable<DecoratedEnvironment>
|
<Datatable<DecoratedEnvironment>
|
||||||
|
|
|
@ -32,6 +32,7 @@ export interface BaseEnvironmentsQueryParams {
|
||||||
types?: EnvironmentType[] | readonly EnvironmentType[];
|
types?: EnvironmentType[] | readonly EnvironmentType[];
|
||||||
tagIds?: TagId[];
|
tagIds?: TagId[];
|
||||||
endpointIds?: EnvironmentId[];
|
endpointIds?: EnvironmentId[];
|
||||||
|
excludeIds?: EnvironmentId[];
|
||||||
tagsPartialMatch?: boolean;
|
tagsPartialMatch?: boolean;
|
||||||
groupIds?: EnvironmentGroupId[];
|
groupIds?: EnvironmentGroupId[];
|
||||||
status?: EnvironmentStatus[];
|
status?: EnvironmentStatus[];
|
||||||
|
|
Loading…
Reference in New Issue