mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-09 19:32:24 +00:00
Drive reporting selection limits from catalog
This commit is contained in:
parent
7728b352c0
commit
cf80556a6d
4 changed files with 50 additions and 8 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -163,6 +163,7 @@ export function ReportingPanel() {
|
|||
helpText="Select the resources to include in the report"
|
||||
>
|
||||
<ResourcePicker
|
||||
maxSelection={performanceReport()?.multiResourceMax}
|
||||
selected={selectedResources}
|
||||
onSelectionChange={setSelectedResources}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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<SelectedResource[]>;
|
||||
onSelectionChange: (items: SelectedResource[]) => void;
|
||||
}
|
||||
|
|
@ -39,6 +40,7 @@ export function ResourcePicker(props: ResourcePickerProps) {
|
|||
const [search, setSearch] = createSignal('');
|
||||
const [typeFilter, setTypeFilter] = createSignal<TypeFilter>('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) {
|
|||
</div>
|
||||
<span class="text-xs sm:text-sm text-slate-500">
|
||||
{props.selected().length} selected
|
||||
<Show when={props.selected().length >= MAX_SELECTION}>
|
||||
<Show when={props.selected().length >= maxSelection()}>
|
||||
<span class="text-amber-400 ml-1">(max)</span>
|
||||
</Show>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -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<SelectedResource[]>(initialSelection);
|
||||
|
|
@ -46,7 +53,13 @@ const renderPicker = (initialSelection: SelectedResource[] = []) => {
|
|||
setSelected(items);
|
||||
onSelectionChange(items);
|
||||
};
|
||||
return <ResourcePicker selected={selected} onSelectionChange={handleSelectionChange} />;
|
||||
return (
|
||||
<ResourcePicker
|
||||
maxSelection={options.maxSelection}
|
||||
selected={selected}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
/>
|
||||
);
|
||||
});
|
||||
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({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue