mirror of
https://github.com/portainer/portainer.git
synced 2025-07-19 05:19:39 +02:00
fix(edge groups): make large edge groups editable [BE-11720] (#558)
This commit is contained in:
parent
7c01f84a5c
commit
1d12011eb5
14 changed files with 373 additions and 46 deletions
|
@ -38,6 +38,7 @@ const (
|
||||||
// @param tagIds query []int false "search environments(endpoints) with these tags (depends on tagsPartialMatch)"
|
// @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 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 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 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 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)"
|
// @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 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 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 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"
|
// @success 200 {array} portainer.Endpoint "Endpoints"
|
||||||
// @failure 500 "Server error"
|
// @failure 500 "Server error"
|
||||||
// @router /endpoints [get]
|
// @router /endpoints [get]
|
||||||
|
|
|
@ -37,6 +37,8 @@ type EnvironmentsQuery struct {
|
||||||
edgeStackId portainer.EdgeStackID
|
edgeStackId portainer.EdgeStackID
|
||||||
edgeStackStatus *portainer.EdgeStackStatusType
|
edgeStackStatus *portainer.EdgeStackStatusType
|
||||||
excludeIds []portainer.EndpointID
|
excludeIds []portainer.EndpointID
|
||||||
|
edgeGroupIds []portainer.EdgeGroupID
|
||||||
|
excludeEdgeGroupIds []portainer.EdgeGroupID
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
|
func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
|
||||||
|
@ -77,6 +79,16 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
|
||||||
return EnvironmentsQuery{}, err
|
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")
|
agentVersions := getArrayQueryParameter(r, "agentVersions")
|
||||||
|
|
||||||
name, _ := request.RetrieveQueryParameter(r, "name", true)
|
name, _ := request.RetrieveQueryParameter(r, "name", true)
|
||||||
|
@ -117,6 +129,8 @@ func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
|
||||||
edgeCheckInPassedSeconds: edgeCheckInPassedSeconds,
|
edgeCheckInPassedSeconds: edgeCheckInPassedSeconds,
|
||||||
edgeStackId: portainer.EdgeStackID(edgeStackId),
|
edgeStackId: portainer.EdgeStackID(edgeStackId),
|
||||||
edgeStackStatus: edgeStackStatus,
|
edgeStackStatus: edgeStackStatus,
|
||||||
|
edgeGroupIds: edgeGroupIDs,
|
||||||
|
excludeEdgeGroupIds: excludeEdgeGroupIds,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,6 +157,14 @@ func (handler *Handler) filterEndpointsByQuery(
|
||||||
filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, query.groupIds)
|
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 != "" {
|
if query.name != "" {
|
||||||
filteredEndpoints = filterEndpointsByName(filteredEndpoints, query.name)
|
filteredEndpoints = filterEndpointsByName(filteredEndpoints, query.name)
|
||||||
}
|
}
|
||||||
|
@ -295,6 +317,70 @@ func filterEndpointsByGroupIDs(endpoints []portainer.Endpoint, endpointGroupIDs
|
||||||
return endpoints[:n]
|
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(
|
func filterEndpointsBySearchCriteria(
|
||||||
endpoints []portainer.Endpoint,
|
endpoints []portainer.Endpoint,
|
||||||
endpointGroups []portainer.EndpointGroup,
|
endpointGroups []portainer.EndpointGroup,
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { EdgeAsyncIntervalsForm } from '@/react/edge/components/EdgeAsyncInterva
|
||||||
import { EdgeCheckinIntervalField } from '@/react/edge/components/EdgeCheckInIntervalField';
|
import { EdgeCheckinIntervalField } from '@/react/edge/components/EdgeCheckInIntervalField';
|
||||||
import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm';
|
import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm';
|
||||||
import { EdgeGroupsSelector } from '@/react/edge/edge-stacks/components/EdgeGroupsSelector';
|
import { EdgeGroupsSelector } from '@/react/edge/edge-stacks/components/EdgeGroupsSelector';
|
||||||
|
import { AssociatedEdgeGroupEnvironmentsSelector } from '@/react/edge/components/AssociatedEdgeGroupEnvironmentsSelector';
|
||||||
|
|
||||||
const ngModule = angular
|
const ngModule = angular
|
||||||
.module('portainer.edge.react.components', [])
|
.module('portainer.edge.react.components', [])
|
||||||
|
@ -61,6 +62,15 @@ const ngModule = angular
|
||||||
'value',
|
'value',
|
||||||
'error',
|
'error',
|
||||||
])
|
])
|
||||||
|
)
|
||||||
|
.component(
|
||||||
|
'associatedEdgeGroupEnvironmentsSelector',
|
||||||
|
r2a(withReactQuery(AssociatedEdgeGroupEnvironmentsSelector), [
|
||||||
|
'onChange',
|
||||||
|
'value',
|
||||||
|
'error',
|
||||||
|
'edgeGroupId',
|
||||||
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
export const componentsModule = ngModule.name;
|
export const componentsModule = ngModule.name;
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { EdgeTypes, EnvironmentId } from '@/react/portainer/environments/types';
|
import { EdgeTypes, EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
import { EdgeEnvironmentsAssociationTable } from '@/react/edge/components/EdgeEnvironmentsAssociationTable';
|
||||||
|
|
||||||
import { FormError } from '@@/form-components/FormError';
|
import { FormError } from '@@/form-components/FormError';
|
||||||
import { ArrayError } from '@@/form-components/InputList/InputList';
|
import { ArrayError } from '@@/form-components/InputList/InputList';
|
||||||
|
|
||||||
import { EdgeGroupAssociationTable } from './EdgeGroupAssociationTable';
|
|
||||||
|
|
||||||
export function AssociatedEdgeEnvironmentsSelector({
|
export function AssociatedEdgeEnvironmentsSelector({
|
||||||
onChange,
|
onChange,
|
||||||
value,
|
value,
|
||||||
|
@ -20,9 +19,9 @@ export function AssociatedEdgeEnvironmentsSelector({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="col-sm-12 small text-muted">
|
<div className="col-sm-12 small text-muted">
|
||||||
You can select which environment should be part of this group by moving
|
You can also select environments individually by moving them to the
|
||||||
them to the associated environments table. Simply click on any
|
associated environments table. Simply click on any environment entry to
|
||||||
environment entry to move it from one table to the other.
|
move it from one table to the other.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
@ -36,7 +35,7 @@ export function AssociatedEdgeEnvironmentsSelector({
|
||||||
<div className="col-sm-12 mt-4">
|
<div className="col-sm-12 mt-4">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="w-1/2">
|
<div className="w-1/2">
|
||||||
<EdgeGroupAssociationTable
|
<EdgeEnvironmentsAssociationTable
|
||||||
title="Available environments"
|
title="Available environments"
|
||||||
query={{
|
query={{
|
||||||
types: EdgeTypes,
|
types: EdgeTypes,
|
||||||
|
@ -51,7 +50,7 @@ export function AssociatedEdgeEnvironmentsSelector({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-1/2">
|
<div className="w-1/2">
|
||||||
<EdgeGroupAssociationTable
|
<EdgeEnvironmentsAssociationTable
|
||||||
title="Associated environments"
|
title="Associated environments"
|
||||||
query={{
|
query={{
|
||||||
types: EdgeTypes,
|
types: EdgeTypes,
|
||||||
|
|
|
@ -0,0 +1,116 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
EdgeGroupId,
|
||||||
|
Environment,
|
||||||
|
EnvironmentId,
|
||||||
|
} from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { FormError } from '@@/form-components/FormError';
|
||||||
|
import { ArrayError } from '@@/form-components/InputList/InputList';
|
||||||
|
|
||||||
|
import { EdgeGroupAssociationTable } from './EdgeGroupAssociationTable';
|
||||||
|
|
||||||
|
export function AssociatedEdgeGroupEnvironmentsSelector({
|
||||||
|
onChange,
|
||||||
|
value,
|
||||||
|
error,
|
||||||
|
edgeGroupId,
|
||||||
|
}: {
|
||||||
|
onChange: (
|
||||||
|
value: EnvironmentId[],
|
||||||
|
meta: { type: 'add' | 'remove'; value: EnvironmentId }
|
||||||
|
) => void;
|
||||||
|
value: EnvironmentId[];
|
||||||
|
error?: ArrayError<Array<EnvironmentId>>;
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<div className="col-sm-12 small text-muted">
|
||||||
|
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.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="col-sm-12">
|
||||||
|
<FormError>
|
||||||
|
{typeof error === 'string' ? error : error.join(', ')}
|
||||||
|
</FormError>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="col-sm-12 mt-4">
|
||||||
|
<div className="flex">
|
||||||
|
<div className="w-1/2">
|
||||||
|
<EdgeGroupAssociationTable
|
||||||
|
title="Available environments"
|
||||||
|
query={{
|
||||||
|
excludeEdgeGroupIds: edgeGroupId ? [edgeGroupId] : [],
|
||||||
|
}}
|
||||||
|
addEnvironments={dissociatedEnvironments}
|
||||||
|
excludeEnvironments={associatedEnvironments}
|
||||||
|
onClickRow={(env) => {
|
||||||
|
if (!value.includes(env.Id)) {
|
||||||
|
updateEditedEnvironments(env);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
data-cy="edgeGroupCreate-availableEndpoints"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-1/2">
|
||||||
|
<EdgeGroupAssociationTable
|
||||||
|
title="Associated environments"
|
||||||
|
query={{
|
||||||
|
edgeGroupIds: edgeGroupId ? [edgeGroupId] : [],
|
||||||
|
endpointIds: edgeGroupId ? undefined : [], // workaround to avoid showing all environments for new edge group
|
||||||
|
}}
|
||||||
|
addEnvironments={associatedEnvironments}
|
||||||
|
excludeEnvironments={dissociatedEnvironments}
|
||||||
|
onClickRow={(env) => {
|
||||||
|
if (value.includes(env.Id)) {
|
||||||
|
updateEditedEnvironments(env);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
data-cy="edgeGroupCreate-associatedEndpointsTable"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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<DecoratedEnvironment> = 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 (
|
||||||
|
<Datatable<DecoratedEnvironment>
|
||||||
|
title={title}
|
||||||
|
columns={columns}
|
||||||
|
settingsManager={tableState}
|
||||||
|
dataset={memoizedEnvironments}
|
||||||
|
isServerSidePagination
|
||||||
|
page={page}
|
||||||
|
onPageChange={setPage}
|
||||||
|
totalCount={totalCount}
|
||||||
|
renderRow={(row) => (
|
||||||
|
<TableRow<DecoratedEnvironment>
|
||||||
|
cells={row.getVisibleCells()}
|
||||||
|
onClick={() => onClickRow(row.original)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
data-cy={dataCy}
|
||||||
|
disableSelect
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,52 +1,32 @@
|
||||||
import { createColumnHelper } from '@tanstack/react-table';
|
|
||||||
import { truncate } from 'lodash';
|
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { useTags } from '@/portainer/tags/queries';
|
import { useTags } from '@/portainer/tags/queries';
|
||||||
import { useGroups } from '@/react/portainer/environments/environment-groups/queries';
|
import { useGroups } from '@/react/portainer/environments/environment-groups/queries';
|
||||||
import { EnvironmentsQueryParams } from '@/react/portainer/environments/environment.service';
|
import { EnvironmentsQueryParams } from '@/react/portainer/environments/environment.service';
|
||||||
import { useEnvironmentList } from '@/react/portainer/environments/queries';
|
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 { AutomationTestingProps } from '@/types';
|
||||||
|
import {
|
||||||
|
columns,
|
||||||
|
DecoratedEnvironment,
|
||||||
|
} from '@/react/edge/components/associationTableColumnHelper';
|
||||||
|
|
||||||
import { Datatable, TableRow } from '@@/datatables';
|
import { Datatable, TableRow } from '@@/datatables';
|
||||||
import { useTableStateWithoutStorage } from '@@/datatables/useTableState';
|
import { useTableStateWithoutStorage } from '@@/datatables/useTableState';
|
||||||
|
|
||||||
type DecoratedEnvironment = Environment & {
|
|
||||||
Tags: string[];
|
|
||||||
Group: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const columHelper = createColumnHelper<DecoratedEnvironment>();
|
|
||||||
|
|
||||||
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({
|
export function EdgeGroupAssociationTable({
|
||||||
title,
|
title,
|
||||||
query,
|
query,
|
||||||
onClickRow = () => {},
|
onClickRow = () => {},
|
||||||
|
addEnvironments = [],
|
||||||
|
excludeEnvironments = [],
|
||||||
'data-cy': dataCy,
|
'data-cy': dataCy,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
query: EnvironmentsQueryParams;
|
query: EnvironmentsQueryParams;
|
||||||
onClickRow?: (env: Environment) => void;
|
onClickRow?: (env: Environment) => void;
|
||||||
|
addEnvironments?: Environment[];
|
||||||
|
excludeEnvironments?: Environment[];
|
||||||
} & AutomationTestingProps) {
|
} & AutomationTestingProps) {
|
||||||
const tableState = useTableStateWithoutStorage('Name');
|
const tableState = useTableStateWithoutStorage('Name');
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
|
@ -56,8 +36,11 @@ export function EdgeGroupAssociationTable({
|
||||||
search: tableState.search,
|
search: tableState.search,
|
||||||
sort: tableState.sortBy?.id as 'Group' | 'Name',
|
sort: tableState.sortBy?.id as 'Group' | 'Name',
|
||||||
order: tableState.sortBy?.desc ? 'desc' : 'asc',
|
order: tableState.sortBy?.desc ? 'desc' : 'asc',
|
||||||
|
types: EdgeTypes,
|
||||||
|
excludeIds: excludeEnvironments?.map((env) => env.Id),
|
||||||
...query,
|
...query,
|
||||||
});
|
});
|
||||||
|
|
||||||
const groupsQuery = useGroups({
|
const groupsQuery = useGroups({
|
||||||
enabled: environmentsQuery.environments.length > 0,
|
enabled: environmentsQuery.environments.length > 0,
|
||||||
});
|
});
|
||||||
|
@ -65,7 +48,7 @@ export function EdgeGroupAssociationTable({
|
||||||
enabled: environmentsQuery.environments.length > 0,
|
enabled: environmentsQuery.environments.length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const environments: Array<DecoratedEnvironment> = useMemo(
|
const memoizedEnvironments: Array<DecoratedEnvironment> = useMemo(
|
||||||
() =>
|
() =>
|
||||||
environmentsQuery.environments.map((env) => ({
|
environmentsQuery.environments.map((env) => ({
|
||||||
...env,
|
...env,
|
||||||
|
@ -79,12 +62,29 @@ export function EdgeGroupAssociationTable({
|
||||||
|
|
||||||
const { totalCount } = environmentsQuery;
|
const { totalCount } = environmentsQuery;
|
||||||
|
|
||||||
|
const memoizedAddEnvironments: Array<DecoratedEnvironment> = 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 (
|
return (
|
||||||
<Datatable<DecoratedEnvironment>
|
<Datatable<DecoratedEnvironment>
|
||||||
title={title}
|
title={title}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
settingsManager={tableState}
|
settingsManager={tableState}
|
||||||
dataset={environments}
|
dataset={memoizedEnvironments.concat(filteredAddEnvironments)}
|
||||||
isServerSidePagination
|
isServerSidePagination
|
||||||
page={page}
|
page={page}
|
||||||
onPageChange={setPage}
|
onPageChange={setPage}
|
||||||
|
|
30
app/react/edge/components/associationTableColumnHelper.ts
Normal file
30
app/react/edge/components/associationTableColumnHelper.ts
Normal file
|
@ -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<DecoratedEnvironment>();
|
||||||
|
|
||||||
|
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 }),
|
||||||
|
}),
|
||||||
|
];
|
|
@ -35,6 +35,7 @@ export function EdgeGroupForm({
|
||||||
name: group.Name,
|
name: group.Name,
|
||||||
partialMatch: group.PartialMatch,
|
partialMatch: group.PartialMatch,
|
||||||
tagIds: group.TagIds,
|
tagIds: group.TagIds,
|
||||||
|
edgeGroupId: group.Id,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
name: '',
|
name: '',
|
||||||
|
@ -42,6 +43,7 @@ export function EdgeGroupForm({
|
||||||
environmentIds: [],
|
environmentIds: [],
|
||||||
partialMatch: false,
|
partialMatch: false,
|
||||||
tagIds: [],
|
tagIds: [],
|
||||||
|
edgeGroupId: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useFormikContext } from 'formik';
|
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 { FormSection } from '@@/form-components/FormSection';
|
||||||
import { confirmDestructive } from '@@/modals/confirm';
|
import { confirmDestructive } from '@@/modals/confirm';
|
||||||
|
@ -14,7 +14,7 @@ export function StaticGroupFieldset({ isEdit }: { isEdit?: boolean }) {
|
||||||
return (
|
return (
|
||||||
<FormSection title="Associated environments">
|
<FormSection title="Associated environments">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<AssociatedEdgeEnvironmentsSelector
|
<AssociatedEdgeGroupEnvironmentsSelector
|
||||||
value={values.environmentIds}
|
value={values.environmentIds}
|
||||||
error={errors.environmentIds}
|
error={errors.environmentIds}
|
||||||
onChange={async (environmentIds, meta) => {
|
onChange={async (environmentIds, meta) => {
|
||||||
|
@ -33,6 +33,7 @@ export function StaticGroupFieldset({ isEdit }: { isEdit?: boolean }) {
|
||||||
|
|
||||||
setFieldValue('environmentIds', environmentIds);
|
setFieldValue('environmentIds', environmentIds);
|
||||||
}}
|
}}
|
||||||
|
edgeGroupId={values.edgeGroupId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
|
@ -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';
|
import { TagId } from '@/portainer/tags/types';
|
||||||
|
|
||||||
export interface FormValues {
|
export interface FormValues {
|
||||||
|
edgeGroupId: EdgeGroupId;
|
||||||
name: string;
|
name: string;
|
||||||
dynamic: boolean;
|
dynamic: boolean;
|
||||||
environmentIds: EnvironmentId[];
|
environmentIds: EnvironmentId[];
|
||||||
|
|
|
@ -21,6 +21,7 @@ export function useValidation({
|
||||||
is: true,
|
is: true,
|
||||||
then: (schema) => schema.min(1, 'Tags are required'),
|
then: (schema) => schema.min(1, 'Tags are required'),
|
||||||
}),
|
}),
|
||||||
|
edgeGroupId: number().default(0).notRequired(),
|
||||||
}),
|
}),
|
||||||
[nameValidation]
|
[nameValidation]
|
||||||
);
|
);
|
||||||
|
|
|
@ -9,13 +9,10 @@ export function getInternalNodeIpAddress(node?: Node) {
|
||||||
const controlPlaneLabels = [
|
const controlPlaneLabels = [
|
||||||
'node-role.kubernetes.io/control-plane',
|
'node-role.kubernetes.io/control-plane',
|
||||||
'node-role.kubernetes.io/master',
|
'node-role.kubernetes.io/master',
|
||||||
'node.kubernetes.io/microk8s-controlplane'
|
'node.kubernetes.io/microk8s-controlplane',
|
||||||
];
|
];
|
||||||
|
|
||||||
const roleLabels = [
|
const roleLabels = ['kubernetes.io/role', 'node.kubernetes.io/role'];
|
||||||
'kubernetes.io/role',
|
|
||||||
'node.kubernetes.io/role'
|
|
||||||
];
|
|
||||||
|
|
||||||
export function getRole(node: Node): 'Control plane' | 'Worker' {
|
export function getRole(node: Node): 'Control plane' | 'Worker' {
|
||||||
const hasControlPlaneLabel = controlPlaneLabels.some(
|
const hasControlPlaneLabel = controlPlaneLabels.some(
|
||||||
|
|
|
@ -50,6 +50,7 @@ export interface BaseEnvironmentsQueryParams {
|
||||||
edgeCheckInPassedSeconds?: number;
|
edgeCheckInPassedSeconds?: number;
|
||||||
platformTypes?: PlatformType[];
|
platformTypes?: PlatformType[];
|
||||||
edgeGroupIds?: EdgeGroupId[];
|
edgeGroupIds?: EdgeGroupId[];
|
||||||
|
excludeEdgeGroupIds?: EdgeGroupId[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EnvironmentsQueryParams = BaseEnvironmentsQueryParams &
|
export type EnvironmentsQueryParams = BaseEnvironmentsQueryParams &
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue