diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
index 914d09152..1e40c9382 100644
--- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md
+++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
@@ -181,6 +181,11 @@ than hardcoding panel copy, routes, or range presets in the frontend. The
frontend models may validate and present the catalog, but the canonical panel
title, descriptions, endpoints, filename prefixes, range windows, and column
list belong to the API reporting contract.
+The same reporting catalog ownership now also governs the operator resource-
+selection cap for performance reports. `ReportingPanel.tsx` and
+`ResourcePicker.tsx` may present or enforce that limit, but they must receive
+it from the backend-owned `multiResourceMax` definition rather than hardcoding
+the reporting cap in frontend-local constants.
The shared updates settings owner also defines the user-facing framing for
rc-tagged builds. `frontend-modern/src/components/Settings/updatesSettingsModel.ts`
and `frontend-modern/src/utils/updatesPresentation.ts` must present that
diff --git a/frontend-modern/src/components/Settings/ReportingPanel.tsx b/frontend-modern/src/components/Settings/ReportingPanel.tsx
index a8ff43c67..44cf25ce7 100644
--- a/frontend-modern/src/components/Settings/ReportingPanel.tsx
+++ b/frontend-modern/src/components/Settings/ReportingPanel.tsx
@@ -163,6 +163,7 @@ export function ReportingPanel() {
helpText="Select the resources to include in the report"
>
diff --git a/frontend-modern/src/components/Settings/ResourcePicker.tsx b/frontend-modern/src/components/Settings/ResourcePicker.tsx
index f8a8fea7e..e290fa16e 100644
--- a/frontend-modern/src/components/Settings/ResourcePicker.tsx
+++ b/frontend-modern/src/components/Settings/ResourcePicker.tsx
@@ -21,7 +21,7 @@ import { showWarning } from '@/utils/toast';
import { getResourceTypePresentation } from '@/utils/resourceTypePresentation';
import { getSimpleStatusIndicator } from '@/utils/status';
-const MAX_SELECTION = 50;
+const DEFAULT_MAX_SELECTION = 50;
export interface SelectedResource {
id: string;
@@ -30,6 +30,7 @@ export interface SelectedResource {
}
interface ResourcePickerProps {
+ maxSelection?: number;
selected: Accessor;
onSelectionChange: (items: SelectedResource[]) => void;
}
@@ -39,6 +40,7 @@ export function ResourcePicker(props: ResourcePickerProps) {
const [search, setSearch] = createSignal('');
const [typeFilter, setTypeFilter] = createSignal('all');
const [tagFilter, setTagFilter] = createSignal('');
+ const maxSelection = () => props.maxSelection ?? DEFAULT_MAX_SELECTION;
// Filter to reportable resource types across infrastructure, workloads, storage, and recovery.
const reportableResources = createMemo(() => {
@@ -90,8 +92,8 @@ export function ResourcePicker(props: ResourcePickerProps) {
if (isSelected(resource.id)) {
props.onSelectionChange(current.filter((s) => s.id !== resource.id));
} else {
- if (current.length >= MAX_SELECTION) {
- showWarning(`Maximum ${MAX_SELECTION} resources can be selected`);
+ if (current.length >= maxSelection()) {
+ showWarning(`Maximum ${maxSelection()} resources can be selected`);
return;
}
props.onSelectionChange([
@@ -117,11 +119,14 @@ export function ResourcePicker(props: ResourcePickerProps) {
}));
const newSelection = [...current, ...toAdd];
- if (newSelection.length > MAX_SELECTION) {
+ if (newSelection.length > maxSelection()) {
showWarning(
- `Maximum ${MAX_SELECTION} resources can be selected. Only ${MAX_SELECTION - current.length} more can be added.`,
+ `Maximum ${maxSelection()} resources can be selected. Only ${maxSelection() - current.length} more can be added.`,
);
- props.onSelectionChange([...current, ...toAdd.slice(0, MAX_SELECTION - current.length)]);
+ props.onSelectionChange([
+ ...current,
+ ...toAdd.slice(0, maxSelection() - current.length),
+ ]);
return;
}
props.onSelectionChange(newSelection);
@@ -329,7 +334,7 @@ export function ResourcePicker(props: ResourcePickerProps) {
{props.selected().length} selected
- = MAX_SELECTION}>
+ = maxSelection()}>
(max)
diff --git a/frontend-modern/src/components/Settings/__tests__/ResourcePicker.test.tsx b/frontend-modern/src/components/Settings/__tests__/ResourcePicker.test.tsx
index 856acbf98..575caf48a 100644
--- a/frontend-modern/src/components/Settings/__tests__/ResourcePicker.test.tsx
+++ b/frontend-modern/src/components/Settings/__tests__/ResourcePicker.test.tsx
@@ -39,6 +39,13 @@ const createSelectedResources = (count: number): SelectedResource[] =>
}));
const renderPicker = (initialSelection: SelectedResource[] = []) => {
+ return renderPickerWithOptions(initialSelection, {});
+};
+
+const renderPickerWithOptions = (
+ initialSelection: SelectedResource[] = [],
+ options: { maxSelection?: number } = {},
+) => {
const onSelectionChange = vi.fn();
render(() => {
const [selected, setSelected] = createSignal(initialSelection);
@@ -46,7 +53,13 @@ const renderPicker = (initialSelection: SelectedResource[] = []) => {
setSelected(items);
onSelectionChange(items);
};
- return ;
+ return (
+
+ );
});
return { onSelectionChange };
};
@@ -304,6 +317,24 @@ describe('ResourcePicker', () => {
expect(onSelectionChange).not.toHaveBeenCalled();
});
+ it('obeys a caller-provided max selection limit', async () => {
+ mockResources = [
+ makeResource({ id: 'vm-new', type: 'vm', name: 'Overflow VM', displayName: 'Overflow VM' }),
+ ];
+
+ const { onSelectionChange } = renderPickerWithOptions(createSelectedResources(2), {
+ maxSelection: 2,
+ });
+
+ const resourceButton = (await screen.findByText('Overflow VM')).closest('button');
+ expect(resourceButton).toBeTruthy();
+ fireEvent.click(resourceButton!);
+
+ expect(showWarningMock).toHaveBeenCalledWith('Maximum 2 resources can be selected');
+ expect(screen.getByText('2 selected')).toBeInTheDocument();
+ expect(onSelectionChange).not.toHaveBeenCalled();
+ });
+
it('supports select-all-visible and clear-all', async () => {
mockResources = [
makeResource({