fix(policy) fix policy group pagination issues [R8S-855] (#1898)

This commit is contained in:
RHCowan
2026-02-19 13:29:01 +13:00
committed by GitHub
parent 27531a802b
commit e8b49f53e1
6 changed files with 60 additions and 37 deletions

View File

@@ -1,2 +1,3 @@
dist
api/datastore/test_data
api/datastore/test_data
coverage

View File

@@ -23,7 +23,7 @@ export function EditGroupView() {
// Fetch associated environments for this group (not for unassigned group)
const isUnassignedGroup = groupId === 1;
const environmentsQuery = useEnvironmentList(
{ groupIds: [groupId] },
{ groupIds: [groupId], pageLimit: 0 },
{ enabled: !!groupId && !isUnassignedGroup }
);

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useMemo, useState } from 'react';
import { useEnvironmentList } from '@/react/portainer/environments/queries';
@@ -11,6 +11,8 @@ import { AssociatedEnvironmentsTable } from './AssociatedEnvironmentsTable';
import { AvailableEnvironmentsTable } from './AvailableEnvironmentsTable';
interface Props {
/** Group ID when editing an existing group */
groupId?: number;
/** IDs of currently associated environments */
associatedEnvironmentIds: Array<EnvironmentId>;
/** IDs of initially associated environments for tracking unsaved changes */
@@ -20,6 +22,7 @@ interface Props {
}
export function AssociatedEnvironmentsSelector({
groupId,
associatedEnvironmentIds,
initialAssociatedEnvironmentIds,
onChange,
@@ -31,24 +34,37 @@ export function AssociatedEnvironmentsSelector({
// Fetch initially associated environments to populate the cache
const initialEnvsQuery = useEnvironmentList(
groupId
? {
groupIds: [groupId],
pageLimit: 0,
}
: {
endpointIds: initialAssociatedEnvironmentIds,
},
{
endpointIds: initialAssociatedEnvironmentIds,
},
{
enabled: initialAssociatedEnvironmentIds.length > 0,
enabled: groupId
? groupId !== 1
: initialAssociatedEnvironmentIds.length > 0,
}
);
const environmentMap = buildEnvironmentMap(
environmentCache,
initialEnvsQuery.environments
);
const addedIds = associatedEnvironmentIds.filter(
(id) => !initialAssociatedEnvironmentIds.includes(id)
const environmentMap = useMemo(
() => buildEnvironmentMap(environmentCache, initialEnvsQuery.environments),
[environmentCache, initialEnvsQuery.environments]
);
const associatedSet = new Set(associatedEnvironmentIds);
const initialSet = new Set(initialAssociatedEnvironmentIds);
const addedIds = associatedEnvironmentIds.filter((id) => !initialSet.has(id));
const removedIds = initialAssociatedEnvironmentIds.filter(
(id) => !associatedEnvironmentIds.includes(id)
(id) => !associatedSet.has(id)
);
const excludeIdsForAvailableEnvironments = groupId
? addedIds
: associatedEnvironmentIds;
const associatedEnvironments = associatedEnvironmentIds
.map((id) => environmentMap.get(id))
.filter((env): env is Environment => env !== undefined);
@@ -65,8 +81,9 @@ export function AssociatedEnvironmentsSelector({
<div className="w-1/2 flex flex-col">
<AvailableEnvironmentsTable
title="Available environments"
excludeIds={associatedEnvironmentIds}
excludeIds={excludeIdsForAvailableEnvironments}
includeIds={removedIds}
highlightIds={removedIds}
onClickRow={handleAddEnvironment}
data-cy="group-availableEndpoints"
/>

View File

@@ -28,6 +28,8 @@ interface Props extends AutomationTestingProps {
excludeIds: Array<EnvironmentId>;
/** IDs to include in the query (e.g., recently removed from associated - will be highlighted) */
includeIds?: Array<EnvironmentId>;
/** IDs to highlight (unsaved badge) */
highlightIds?: Array<EnvironmentId>;
onClickRow?: (env: EnvironmentTableData) => void;
}
@@ -35,12 +37,16 @@ export function AvailableEnvironmentsTable({
title,
excludeIds,
includeIds = [],
highlightIds = [],
onClickRow,
'data-cy': dataCy,
}: Props) {
const tableState = useTableState(settingsStore, tableKey);
const [page, setPage] = useState(0);
const columns = useMemo(() => buildColumns(includeIds), [includeIds]);
const columns = useMemo(
() => buildColumns(new Set(highlightIds)),
[highlightIds]
);
// Query unassigned environments (group 1)
const unassignedQuery = useEnvironmentList({
@@ -63,14 +69,13 @@ export function AvailableEnvironmentsTable({
);
// Merge results: removed environments + unassigned environments (deduped)
// Only use removedQuery data when includeIds is non-empty to avoid stale cache
const environments = useMemo(() => {
const { environments, uniqueRemovedCount } = useMemo(() => {
const unassigned = unassignedQuery.environments || [];
const removed =
includeIds.length > 0 ? removedQuery.environments || [] : [];
if (removed.length === 0) {
return unassigned;
return { environments: unassigned, uniqueRemovedCount: 0 };
}
const unassignedIds = new Set(unassigned.map((e) => e.Id));
@@ -82,12 +87,18 @@ export function AvailableEnvironmentsTable({
// useTypeGuard on tableState.sortBy.id to use as a key for sorting
const sortKey = getSortKey(tableState.sortBy?.id);
if (sortKey) {
return combined.sort((a, b) => {
const cmp = semverCompare(a[sortKey].toString(), b[sortKey].toString());
return isDesc ? -cmp : cmp;
});
return {
environments: combined.sort((a, b) => {
const cmp = semverCompare(
a[sortKey].toString(),
b[sortKey].toString()
);
return isDesc ? -cmp : cmp;
}),
uniqueRemovedCount: uniqueRemoved.length,
};
}
return combined;
return { environments: combined, uniqueRemovedCount: uniqueRemoved.length };
}, [
unassignedQuery.environments,
removedQuery.environments,
@@ -96,9 +107,7 @@ export function AvailableEnvironmentsTable({
tableState.sortBy?.id,
]);
const totalCount =
unassignedQuery.totalCount +
(includeIds.length > 0 ? removedQuery.environments?.length || 0 : 0);
const totalCount = unassignedQuery.totalCount + uniqueRemovedCount;
return (
<Widget className="flex-1 flex flex-col">
@@ -134,7 +143,7 @@ export function AvailableEnvironmentsTable({
);
}
function buildColumns(highlightIds: Array<EnvironmentId>) {
function buildColumns(highlightIds: Set<EnvironmentId>) {
return [
columnHelper.accessor('Name', {
header: 'Name',
@@ -142,7 +151,7 @@ function buildColumns(highlightIds: Array<EnvironmentId>) {
cell: ({ getValue, row }) => (
<span className="flex items-center gap-2">
<span title={getValue()}>{truncate(getValue(), { length: 64 })}</span>
{highlightIds.includes(row.original.Id) && (
{highlightIds.has(row.original.Id) && (
<Badge type="muted" data-cy="unsaved-badge">
Unsaved
</Badge>

View File

@@ -150,6 +150,7 @@ function InnerForm({
</FormSection>
) : (
<AssociatedEnvironmentsSelector
groupId={groupId}
associatedEnvironmentIds={values.associatedEnvironments}
initialAssociatedEnvironmentIds={initialValues.associatedEnvironments}
onChange={(ids) => setFieldValue('associatedEnvironments', ids)}

View File

@@ -33,6 +33,7 @@ export function getSortType(value?: string): SortType | undefined {
export type Query = EnvironmentsQueryParams & {
page?: number;
/** Use 0 to fetch all environments without pagination (backend supports limit=0). */
pageLimit?: number;
sort?: SortType;
order?: 'asc' | 'desc';
@@ -74,16 +75,10 @@ export function useEnvironmentList(
const { isLoading, data } = useQuery(
[
...environmentQueryKeys.base(),
{
page,
pageLimit,
sort,
order,
...query,
},
{ page, pageLimit, sort, order, ...query },
],
async () => {
const start = (page - 1) * pageLimit + 1;
const start = pageLimit === 0 ? 0 : (page - 1) * pageLimit + 1;
return getEnvironments({
start,