diff --git a/api/http/handler/endpoints/endpoint_list.go b/api/http/handler/endpoints/endpoint_list.go
index 6be1c7ff2..86f1b1d3c 100644
--- a/api/http/handler/endpoints/endpoint_list.go
+++ b/api/http/handler/endpoints/endpoint_list.go
@@ -38,6 +38,7 @@ const (
// @param tagIds query []int false "search environments(endpoints) with these tags (depends on tagsPartialMatch)"
// @param tagsPartialMatch query bool false "If true, will return environment(endpoint) which has one of tagIds, if false (or missing) will return only environments(endpoints) that has all the tags"
// @param endpointIds query []int false "will return only these environments(endpoints)"
+// @param excludeIds query []int false "will exclude 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 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)"
@@ -48,6 +49,8 @@ const (
// @param name query string false "will return only environments(endpoints) with this name"
// @param edgeStackId query portainer.EdgeStackID false "will return the environements of the specified edge stack"
// @param edgeStackStatus query string false "only applied when edgeStackId exists. Filter the returned environments based on their deployment status in the stack (not the environment status!)" Enum("Pending", "Ok", "Error", "Acknowledged", "Remove", "RemoteUpdateSuccess", "ImagesPulled")
+// @param edgeGroupIds query []int false "List environments(endpoints) of these edge groups"
+// @param excludeEdgeGroupIds query []int false "Exclude environments(endpoints) of these edge groups"
// @success 200 {array} portainer.Endpoint "Endpoints"
// @failure 500 "Server error"
// @router /endpoints [get]
diff --git a/api/http/handler/endpoints/filter.go b/api/http/handler/endpoints/filter.go
index df230f99e..6dc41b0bd 100644
--- a/api/http/handler/endpoints/filter.go
+++ b/api/http/handler/endpoints/filter.go
@@ -37,6 +37,8 @@ type EnvironmentsQuery struct {
edgeStackId portainer.EdgeStackID
edgeStackStatus *portainer.EdgeStackStatusType
excludeIds []portainer.EndpointID
+ edgeGroupIds []portainer.EdgeGroupID
+ excludeEdgeGroupIds []portainer.EdgeGroupID
}
func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
@@ -77,6 +79,16 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
return EnvironmentsQuery{}, err
}
+ edgeGroupIDs, err := getNumberArrayQueryParameter[portainer.EdgeGroupID](r, "edgeGroupIds")
+ if err != nil {
+ return EnvironmentsQuery{}, err
+ }
+
+ excludeEdgeGroupIds, err := getNumberArrayQueryParameter[portainer.EdgeGroupID](r, "excludeEdgeGroupIds")
+ if err != nil {
+ return EnvironmentsQuery{}, err
+ }
+
agentVersions := getArrayQueryParameter(r, "agentVersions")
name, _ := request.RetrieveQueryParameter(r, "name", true)
@@ -117,6 +129,8 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
edgeCheckInPassedSeconds: edgeCheckInPassedSeconds,
edgeStackId: portainer.EdgeStackID(edgeStackId),
edgeStackStatus: edgeStackStatus,
+ edgeGroupIds: edgeGroupIDs,
+ excludeEdgeGroupIds: excludeEdgeGroupIds,
}, nil
}
@@ -143,6 +157,14 @@ func (handler *Handler) filterEndpointsByQuery(
filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, query.groupIds)
}
+ if len(query.edgeGroupIds) > 0 {
+ filteredEndpoints, edgeGroups = filterEndpointsByEdgeGroupIDs(filteredEndpoints, edgeGroups, query.edgeGroupIds)
+ }
+
+ if len(query.excludeEdgeGroupIds) > 0 {
+ filteredEndpoints, edgeGroups = filterEndpointsByExcludeEdgeGroupIDs(filteredEndpoints, edgeGroups, query.excludeEdgeGroupIds)
+ }
+
if query.name != "" {
filteredEndpoints = filterEndpointsByName(filteredEndpoints, query.name)
}
@@ -295,6 +317,70 @@ func filterEndpointsByGroupIDs(endpoints []portainer.Endpoint, endpointGroupIDs
return endpoints[:n]
}
+func filterEndpointsByEdgeGroupIDs(endpoints []portainer.Endpoint, edgeGroups []portainer.EdgeGroup, edgeGroupIDs []portainer.EdgeGroupID) ([]portainer.Endpoint, []portainer.EdgeGroup) {
+ edgeGroupIDFilterSet := make(map[portainer.EdgeGroupID]struct{}, len(edgeGroupIDs))
+ for _, id := range edgeGroupIDs {
+ edgeGroupIDFilterSet[id] = struct{}{}
+ }
+
+ n := 0
+ for _, edgeGroup := range edgeGroups {
+ if _, exists := edgeGroupIDFilterSet[edgeGroup.ID]; exists {
+ edgeGroups[n] = edgeGroup
+ n++
+ }
+ }
+ edgeGroups = edgeGroups[:n]
+
+ endpointIDSet := make(map[portainer.EndpointID]struct{})
+ for _, edgeGroup := range edgeGroups {
+ for _, endpointID := range edgeGroup.Endpoints {
+ endpointIDSet[endpointID] = struct{}{}
+ }
+ }
+
+ n = 0
+ for _, endpoint := range endpoints {
+ if _, exists := endpointIDSet[endpoint.ID]; exists {
+ endpoints[n] = endpoint
+ n++
+ }
+ }
+
+ return endpoints[:n], edgeGroups
+}
+
+func filterEndpointsByExcludeEdgeGroupIDs(endpoints []portainer.Endpoint, edgeGroups []portainer.EdgeGroup, excludeEdgeGroupIds []portainer.EdgeGroupID) ([]portainer.Endpoint, []portainer.EdgeGroup) {
+ excludeEdgeGroupIDSet := make(map[portainer.EdgeGroupID]struct{}, len(excludeEdgeGroupIds))
+ for _, id := range excludeEdgeGroupIds {
+ excludeEdgeGroupIDSet[id] = struct{}{}
+ }
+
+ n := 0
+ excludeEndpointIDSet := make(map[portainer.EndpointID]struct{})
+ for _, edgeGroup := range edgeGroups {
+ if _, ok := excludeEdgeGroupIDSet[edgeGroup.ID]; ok {
+ for _, endpointID := range edgeGroup.Endpoints {
+ excludeEndpointIDSet[endpointID] = struct{}{}
+ }
+ } else {
+ edgeGroups[n] = edgeGroup
+ n++
+ }
+ }
+ edgeGroups = edgeGroups[:n]
+
+ n = 0
+ for _, endpoint := range endpoints {
+ if _, ok := excludeEndpointIDSet[endpoint.ID]; !ok {
+ endpoints[n] = endpoint
+ n++
+ }
+ }
+
+ return endpoints[:n], edgeGroups
+}
+
func filterEndpointsBySearchCriteria(
endpoints []portainer.Endpoint,
endpointGroups []portainer.EndpointGroup,
diff --git a/app/edge/react/components/index.ts b/app/edge/react/components/index.ts
index b4913e51c..1902af60a 100644
--- a/app/edge/react/components/index.ts
+++ b/app/edge/react/components/index.ts
@@ -8,6 +8,7 @@ import { EdgeAsyncIntervalsForm } from '@/react/edge/components/EdgeAsyncInterva
import { EdgeCheckinIntervalField } from '@/react/edge/components/EdgeCheckInIntervalField';
import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm';
import { EdgeGroupsSelector } from '@/react/edge/edge-stacks/components/EdgeGroupsSelector';
+import { AssociatedEdgeGroupEnvironmentsSelector } from '@/react/edge/components/AssociatedEdgeGroupEnvironmentsSelector';
const ngModule = angular
.module('portainer.edge.react.components', [])
@@ -61,6 +62,15 @@ const ngModule = angular
'value',
'error',
])
+ )
+ .component(
+ 'associatedEdgeGroupEnvironmentsSelector',
+ r2a(withReactQuery(AssociatedEdgeGroupEnvironmentsSelector), [
+ 'onChange',
+ 'value',
+ 'error',
+ 'edgeGroupId',
+ ])
);
export const componentsModule = ngModule.name;
diff --git a/app/react/edge/components/AssociatedEdgeEnvironmentsSelector.tsx b/app/react/edge/components/AssociatedEdgeEnvironmentsSelector.tsx
index 7c8bce1d5..1fdd0e30c 100644
--- a/app/react/edge/components/AssociatedEdgeEnvironmentsSelector.tsx
+++ b/app/react/edge/components/AssociatedEdgeEnvironmentsSelector.tsx
@@ -1,10 +1,9 @@
import { EdgeTypes, EnvironmentId } from '@/react/portainer/environments/types';
+import { EdgeEnvironmentsAssociationTable } from '@/react/edge/components/EdgeEnvironmentsAssociationTable';
import { FormError } from '@@/form-components/FormError';
import { ArrayError } from '@@/form-components/InputList/InputList';
-import { EdgeGroupAssociationTable } from './EdgeGroupAssociationTable';
-
export function AssociatedEdgeEnvironmentsSelector({
onChange,
value,
@@ -20,9 +19,9 @@ export function AssociatedEdgeEnvironmentsSelector({
return (
<>
- You can select which environment should be part of this group by moving
- them to the associated environments table. Simply click on any
- environment entry to move it from one table to the other.
+ You can also select environments individually by moving them to the
+ associated environments table. Simply click on any environment entry to
+ move it from one table to the other.
{error && (
@@ -36,7 +35,7 @@ export function AssociatedEdgeEnvironmentsSelector({
-
-
void;
+ value: EnvironmentId[];
+ error?: ArrayError>;
+ edgeGroupId?: EdgeGroupId;
+}) {
+ const [associatedEnvironments, setAssociatedEnvironments] = useState<
+ Environment[]
+ >([]);
+ const [dissociatedEnvironments, setDissociatedEnvironments] = useState<
+ Environment[]
+ >([]);
+
+ function updateEditedEnvironments(env: Environment) {
+ // If the env is associated, this update is a dissociation
+ const isAssociated = value.includes(env.Id);
+
+ setAssociatedEnvironments((prev) =>
+ isAssociated
+ ? prev.filter((prevEnv) => prevEnv.Id !== env.Id)
+ : [...prev, env]
+ );
+
+ setDissociatedEnvironments((prev) =>
+ isAssociated
+ ? [...prev, env]
+ : prev.filter((prevEnv) => prevEnv.Id !== env.Id)
+ );
+
+ const updatedValue = isAssociated
+ ? value.filter((id) => id !== env.Id)
+ : [...value, env.Id];
+
+ onChange(updatedValue, {
+ type: isAssociated ? 'remove' : 'add',
+ value: env.Id,
+ });
+ }
+
+ return (
+ <>
+
+ You can select which environment should be part of this group by moving
+ them to the associated environments table. Simply click on any
+ environment entry to move it from one table to the other.
+
+
+ {error && (
+
+
+ {typeof error === 'string' ? error : error.join(', ')}
+
+
+ )}
+
+
+
+
+ {
+ if (!value.includes(env.Id)) {
+ updateEditedEnvironments(env);
+ }
+ }}
+ data-cy="edgeGroupCreate-availableEndpoints"
+ />
+
+
+ {
+ if (value.includes(env.Id)) {
+ updateEditedEnvironments(env);
+ }
+ }}
+ data-cy="edgeGroupCreate-associatedEndpointsTable"
+ />
+
+
+
+ >
+ );
+}
diff --git a/app/react/edge/components/EdgeEnvironmentsAssociationTable.tsx b/app/react/edge/components/EdgeEnvironmentsAssociationTable.tsx
new file mode 100644
index 000000000..d7d87ec51
--- /dev/null
+++ b/app/react/edge/components/EdgeEnvironmentsAssociationTable.tsx
@@ -0,0 +1,77 @@
+import { useMemo, useState } from 'react';
+
+import { useEnvironmentList } from '@/react/portainer/environments/queries';
+import { EdgeTypes, Environment } from '@/react/portainer/environments/types';
+import { useGroups } from '@/react/portainer/environments/environment-groups/queries';
+import { useTags } from '@/portainer/tags/queries';
+import { EnvironmentsQueryParams } from '@/react/portainer/environments/environment.service';
+import { AutomationTestingProps } from '@/types';
+
+import { useTableStateWithoutStorage } from '@@/datatables/useTableState';
+import { Datatable, TableRow } from '@@/datatables';
+
+import { columns, DecoratedEnvironment } from './associationTableColumnHelper';
+
+export function EdgeEnvironmentsAssociationTable({
+ title,
+ query,
+ onClickRow = () => {},
+ 'data-cy': dataCy,
+}: {
+ title: string;
+ query: EnvironmentsQueryParams;
+ onClickRow?: (env: Environment) => void;
+} & AutomationTestingProps) {
+ const tableState = useTableStateWithoutStorage('Name');
+ const [page, setPage] = useState(0);
+ const environmentsQuery = useEnvironmentList({
+ pageLimit: tableState.pageSize,
+ page: page + 1,
+ search: tableState.search,
+ sort: tableState.sortBy?.id as 'Group' | 'Name',
+ order: tableState.sortBy?.desc ? 'desc' : 'asc',
+ types: EdgeTypes,
+ ...query,
+ });
+ const groupsQuery = useGroups({
+ enabled: environmentsQuery.environments.length > 0,
+ });
+ const tagsQuery = useTags({
+ enabled: environmentsQuery.environments.length > 0,
+ });
+
+ const memoizedEnvironments: Array = useMemo(
+ () =>
+ environmentsQuery.environments.map((env) => ({
+ ...env,
+ Group: 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]
+ );
+
+ const { totalCount } = environmentsQuery;
+
+ return (
+
+ title={title}
+ columns={columns}
+ settingsManager={tableState}
+ dataset={memoizedEnvironments}
+ isServerSidePagination
+ page={page}
+ onPageChange={setPage}
+ totalCount={totalCount}
+ renderRow={(row) => (
+
+ cells={row.getVisibleCells()}
+ onClick={() => onClickRow(row.original)}
+ />
+ )}
+ data-cy={dataCy}
+ disableSelect
+ />
+ );
+}
diff --git a/app/react/edge/components/EdgeGroupAssociationTable.tsx b/app/react/edge/components/EdgeGroupAssociationTable.tsx
index 70ff66a08..1f58950ca 100644
--- a/app/react/edge/components/EdgeGroupAssociationTable.tsx
+++ b/app/react/edge/components/EdgeGroupAssociationTable.tsx
@@ -1,52 +1,32 @@
-import { createColumnHelper } from '@tanstack/react-table';
-import { truncate } from 'lodash';
import { useMemo, useState } from 'react';
import { useTags } from '@/portainer/tags/queries';
import { useGroups } from '@/react/portainer/environments/environment-groups/queries';
import { EnvironmentsQueryParams } from '@/react/portainer/environments/environment.service';
import { useEnvironmentList } from '@/react/portainer/environments/queries';
-import { Environment } from '@/react/portainer/environments/types';
+import { EdgeTypes, Environment } from '@/react/portainer/environments/types';
import { AutomationTestingProps } from '@/types';
+import {
+ columns,
+ DecoratedEnvironment,
+} from '@/react/edge/components/associationTableColumnHelper';
import { Datatable, TableRow } from '@@/datatables';
import { useTableStateWithoutStorage } from '@@/datatables/useTableState';
-type DecoratedEnvironment = Environment & {
- Tags: string[];
- Group: string;
-};
-
-const columHelper = createColumnHelper();
-
-const columns = [
- columHelper.accessor('Name', {
- header: 'Name',
- id: 'Name',
- cell: ({ getValue }) => truncate(getValue(), { length: 64 }),
- }),
- columHelper.accessor('Group', {
- header: 'Group',
- id: 'Group',
- cell: ({ getValue }) => truncate(getValue(), { length: 64 }),
- }),
- columHelper.accessor((row) => row.Tags.join(','), {
- header: 'Tags',
- id: 'tags',
- enableSorting: false,
- cell: ({ getValue }) => truncate(getValue(), { length: 64 }),
- }),
-];
-
export function EdgeGroupAssociationTable({
title,
query,
onClickRow = () => {},
+ addEnvironments = [],
+ excludeEnvironments = [],
'data-cy': dataCy,
}: {
title: string;
query: EnvironmentsQueryParams;
onClickRow?: (env: Environment) => void;
+ addEnvironments?: Environment[];
+ excludeEnvironments?: Environment[];
} & AutomationTestingProps) {
const tableState = useTableStateWithoutStorage('Name');
const [page, setPage] = useState(0);
@@ -56,8 +36,11 @@ export function EdgeGroupAssociationTable({
search: tableState.search,
sort: tableState.sortBy?.id as 'Group' | 'Name',
order: tableState.sortBy?.desc ? 'desc' : 'asc',
+ types: EdgeTypes,
+ excludeIds: excludeEnvironments?.map((env) => env.Id),
...query,
});
+
const groupsQuery = useGroups({
enabled: environmentsQuery.environments.length > 0,
});
@@ -65,7 +48,7 @@ export function EdgeGroupAssociationTable({
enabled: environmentsQuery.environments.length > 0,
});
- const environments: Array = useMemo(
+ const memoizedEnvironments: Array = useMemo(
() =>
environmentsQuery.environments.map((env) => ({
...env,
@@ -79,12 +62,29 @@ export function EdgeGroupAssociationTable({
const { totalCount } = environmentsQuery;
+ const memoizedAddEnvironments: Array = useMemo(
+ () =>
+ addEnvironments.map((env) => ({
+ ...env,
+ Group: groupsQuery.data?.find((g) => g.Id === env.GroupId)?.Name || '',
+ Tags: env.TagIds.map(
+ (tagId) => tagsQuery.data?.find((t) => t.ID === tagId)?.Name || ''
+ ),
+ })),
+ [addEnvironments, groupsQuery.data, tagsQuery.data]
+ );
+
+ // Filter out environments that are already in the table, this is to prevent duplicates, which can happen when an environment is associated and then disassociated
+ const filteredAddEnvironments = memoizedAddEnvironments.filter(
+ (env) => !memoizedEnvironments.some((e) => e.Id === env.Id)
+ );
+
return (
title={title}
columns={columns}
settingsManager={tableState}
- dataset={environments}
+ dataset={memoizedEnvironments.concat(filteredAddEnvironments)}
isServerSidePagination
page={page}
onPageChange={setPage}
diff --git a/app/react/edge/components/associationTableColumnHelper.ts b/app/react/edge/components/associationTableColumnHelper.ts
new file mode 100644
index 000000000..6c83be77f
--- /dev/null
+++ b/app/react/edge/components/associationTableColumnHelper.ts
@@ -0,0 +1,30 @@
+import { createColumnHelper } from '@tanstack/react-table';
+import { truncate } from 'lodash';
+
+import { Environment } from '@/react/portainer/environments/types';
+
+export type DecoratedEnvironment = Environment & {
+ Tags: string[];
+ Group: string;
+};
+
+const columHelper = createColumnHelper();
+
+export const columns = [
+ columHelper.accessor('Name', {
+ header: 'Name',
+ id: 'Name',
+ cell: ({ getValue }) => truncate(getValue(), { length: 64 }),
+ }),
+ columHelper.accessor('Group', {
+ header: 'Group',
+ id: 'Group',
+ cell: ({ getValue }) => truncate(getValue(), { length: 64 }),
+ }),
+ columHelper.accessor((row) => row.Tags.join(','), {
+ header: 'Tags',
+ id: 'tags',
+ enableSorting: false,
+ cell: ({ getValue }) => truncate(getValue(), { length: 64 }),
+ }),
+];
diff --git a/app/react/edge/edge-groups/components/EdgeGroupForm/EdgeGroupForm.tsx b/app/react/edge/edge-groups/components/EdgeGroupForm/EdgeGroupForm.tsx
index 1e401dfa7..7287875d8 100644
--- a/app/react/edge/edge-groups/components/EdgeGroupForm/EdgeGroupForm.tsx
+++ b/app/react/edge/edge-groups/components/EdgeGroupForm/EdgeGroupForm.tsx
@@ -35,6 +35,7 @@ export function EdgeGroupForm({
name: group.Name,
partialMatch: group.PartialMatch,
tagIds: group.TagIds,
+ edgeGroupId: group.Id,
}
: {
name: '',
@@ -42,6 +43,7 @@ export function EdgeGroupForm({
environmentIds: [],
partialMatch: false,
tagIds: [],
+ edgeGroupId: 0,
}
}
onSubmit={onSubmit}
diff --git a/app/react/edge/edge-groups/components/EdgeGroupForm/StaticGroupFieldset.tsx b/app/react/edge/edge-groups/components/EdgeGroupForm/StaticGroupFieldset.tsx
index bb333af7b..aecec7fc9 100644
--- a/app/react/edge/edge-groups/components/EdgeGroupForm/StaticGroupFieldset.tsx
+++ b/app/react/edge/edge-groups/components/EdgeGroupForm/StaticGroupFieldset.tsx
@@ -1,6 +1,6 @@
import { useFormikContext } from 'formik';
-import { AssociatedEdgeEnvironmentsSelector } from '@/react/edge/components/AssociatedEdgeEnvironmentsSelector';
+import { AssociatedEdgeGroupEnvironmentsSelector } from '@/react/edge/components/AssociatedEdgeGroupEnvironmentsSelector';
import { FormSection } from '@@/form-components/FormSection';
import { confirmDestructive } from '@@/modals/confirm';
@@ -14,7 +14,7 @@ export function StaticGroupFieldset({ isEdit }: { isEdit?: boolean }) {
return (
-
{
@@ -33,6 +33,7 @@ export function StaticGroupFieldset({ isEdit }: { isEdit?: boolean }) {
setFieldValue('environmentIds', environmentIds);
}}
+ edgeGroupId={values.edgeGroupId}
/>
diff --git a/app/react/edge/edge-groups/components/EdgeGroupForm/types.tsx b/app/react/edge/edge-groups/components/EdgeGroupForm/types.tsx
index 2c4e0d6bf..1d67d9322 100644
--- a/app/react/edge/edge-groups/components/EdgeGroupForm/types.tsx
+++ b/app/react/edge/edge-groups/components/EdgeGroupForm/types.tsx
@@ -1,7 +1,11 @@
-import { EnvironmentId } from '@/react/portainer/environments/types';
+import {
+ EdgeGroupId,
+ EnvironmentId,
+} from '@/react/portainer/environments/types';
import { TagId } from '@/portainer/tags/types';
export interface FormValues {
+ edgeGroupId: EdgeGroupId;
name: string;
dynamic: boolean;
environmentIds: EnvironmentId[];
diff --git a/app/react/edge/edge-groups/components/EdgeGroupForm/useValidation.tsx b/app/react/edge/edge-groups/components/EdgeGroupForm/useValidation.tsx
index 7f4bc8284..436cd6888 100644
--- a/app/react/edge/edge-groups/components/EdgeGroupForm/useValidation.tsx
+++ b/app/react/edge/edge-groups/components/EdgeGroupForm/useValidation.tsx
@@ -21,6 +21,7 @@ export function useValidation({
is: true,
then: (schema) => schema.min(1, 'Tags are required'),
}),
+ edgeGroupId: number().default(0).notRequired(),
}),
[nameValidation]
);
diff --git a/app/react/kubernetes/cluster/HomeView/NodesDatatable/utils.ts b/app/react/kubernetes/cluster/HomeView/NodesDatatable/utils.ts
index 05bac7ce0..d1450f404 100644
--- a/app/react/kubernetes/cluster/HomeView/NodesDatatable/utils.ts
+++ b/app/react/kubernetes/cluster/HomeView/NodesDatatable/utils.ts
@@ -9,13 +9,10 @@ export function getInternalNodeIpAddress(node?: Node) {
const controlPlaneLabels = [
'node-role.kubernetes.io/control-plane',
'node-role.kubernetes.io/master',
- 'node.kubernetes.io/microk8s-controlplane'
+ 'node.kubernetes.io/microk8s-controlplane',
];
-const roleLabels = [
- 'kubernetes.io/role',
- 'node.kubernetes.io/role'
-];
+const roleLabels = ['kubernetes.io/role', 'node.kubernetes.io/role'];
export function getRole(node: Node): 'Control plane' | 'Worker' {
const hasControlPlaneLabel = controlPlaneLabels.some(
diff --git a/app/react/portainer/environments/environment.service/index.ts b/app/react/portainer/environments/environment.service/index.ts
index c6d770e74..6d4e22d63 100644
--- a/app/react/portainer/environments/environment.service/index.ts
+++ b/app/react/portainer/environments/environment.service/index.ts
@@ -50,6 +50,7 @@ export interface BaseEnvironmentsQueryParams {
edgeCheckInPassedSeconds?: number;
platformTypes?: PlatformType[];
edgeGroupIds?: EdgeGroupId[];
+ excludeEdgeGroupIds?: EdgeGroupId[];
}
export type EnvironmentsQueryParams = BaseEnvironmentsQueryParams &