Blueprints: filter composes by blueprint version
Refs: HMS-3412
This commit is contained in:
parent
6af38141be
commit
9b5f3631d1
7 changed files with 230 additions and 12 deletions
72
src/Components/Blueprints/BlueprintVersionFilter.tsx
Normal file
72
src/Components/Blueprints/BlueprintVersionFilter.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import React from 'react';
|
||||
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownList,
|
||||
MenuToggle,
|
||||
MenuToggleElement,
|
||||
} from '@patternfly/react-core';
|
||||
import { FilterIcon } from '@patternfly/react-icons';
|
||||
|
||||
import {
|
||||
versionFilterType,
|
||||
selectBlueprintVersionFilter,
|
||||
setBlueprintVersionFilter,
|
||||
} from '../../store/BlueprintSlice';
|
||||
import { useAppDispatch, useAppSelector } from '../../store/hooks';
|
||||
|
||||
interface blueprintVersionFilterProps {
|
||||
onFilterChange?: () => void;
|
||||
}
|
||||
|
||||
const BlueprintVersionFilter: React.FC<blueprintVersionFilterProps> = ({
|
||||
onFilterChange,
|
||||
}: blueprintVersionFilterProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const blueprintVersionFilter = useAppSelector(selectBlueprintVersionFilter);
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
|
||||
const onToggleClick = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
const onSelect = (
|
||||
_event: React.MouseEvent<Element, MouseEvent> | undefined,
|
||||
value: versionFilterType
|
||||
) => {
|
||||
dispatch(setBlueprintVersionFilter(value));
|
||||
if (onFilterChange) onFilterChange();
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
isOpen={isOpen}
|
||||
onSelect={onSelect}
|
||||
onOpenChange={(isOpen: boolean) => setIsOpen(isOpen)}
|
||||
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
|
||||
<MenuToggle
|
||||
ref={toggleRef}
|
||||
onClick={onToggleClick}
|
||||
isExpanded={isOpen}
|
||||
icon={<FilterIcon />}
|
||||
>
|
||||
{blueprintVersionFilter === 'latest' ? 'Newest' : 'All versions'}
|
||||
</MenuToggle>
|
||||
)}
|
||||
shouldFocusToggleOnSelect
|
||||
>
|
||||
<DropdownList>
|
||||
<DropdownItem value={'all'} key="all">
|
||||
All versions
|
||||
</DropdownItem>
|
||||
<DropdownItem value={'latest'} key="newest">
|
||||
Newest
|
||||
</DropdownItem>
|
||||
</DropdownList>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlueprintVersionFilter;
|
||||
|
|
@ -46,6 +46,8 @@ import {
|
|||
} from '../../constants';
|
||||
import {
|
||||
selectBlueprintSearchInput,
|
||||
selectBlueprintVersionFilter,
|
||||
selectBlueprintVersionFilterAPI,
|
||||
selectSelectedBlueprintId,
|
||||
} from '../../store/BlueprintSlice';
|
||||
import { useAppSelector } from '../../store/hooks';
|
||||
|
|
@ -70,6 +72,7 @@ const ImagesTable = () => {
|
|||
const [perPage, setPerPage] = useState(10);
|
||||
const selectedBlueprintId = useAppSelector(selectSelectedBlueprintId);
|
||||
const blueprintSearchInput = useAppSelector(selectBlueprintSearchInput);
|
||||
const blueprintVersionFilter = useAppSelector(selectBlueprintVersionFilter);
|
||||
|
||||
const { selectedBlueprintVersion } = useGetBlueprintsQuery(
|
||||
{ search: blueprintSearchInput },
|
||||
|
|
@ -100,6 +103,7 @@ const ImagesTable = () => {
|
|||
id: selectedBlueprintId as string,
|
||||
limit: perPage,
|
||||
offset: perPage * (page - 1),
|
||||
blueprintVersion: useAppSelector(selectBlueprintVersionFilterAPI),
|
||||
},
|
||||
{ skip: !selectedBlueprintId }
|
||||
);
|
||||
|
|
@ -163,7 +167,12 @@ const ImagesTable = () => {
|
|||
);
|
||||
}
|
||||
|
||||
const composes = data?.data;
|
||||
let composes = data?.data;
|
||||
if (selectedBlueprintId && blueprintVersionFilter === 'latest') {
|
||||
composes = composes?.filter((compose) => {
|
||||
return compose.blueprint_version === selectedBlueprintVersion;
|
||||
});
|
||||
}
|
||||
const itemCount = data?.meta.count || 0;
|
||||
|
||||
return (
|
||||
|
|
@ -173,7 +182,7 @@ const ImagesTable = () => {
|
|||
itemCount={itemCount}
|
||||
perPage={perPage}
|
||||
page={page}
|
||||
onSetPage={onSetPage}
|
||||
setPage={setPage}
|
||||
onPerPageSelect={onPerPageSelect}
|
||||
/>
|
||||
<Table variant="compact" data-testid="images-table">
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
import { resolveRelPath } from '../../Utilities/path';
|
||||
import { useExperimentalFlag } from '../../Utilities/useExperimentalFlag';
|
||||
import { BlueprintActionsMenu } from '../Blueprints/BlueprintActionsMenu';
|
||||
import BlueprintVersionFilter from '../Blueprints/BlueprintVersionFilter';
|
||||
import { BuildImagesButton } from '../Blueprints/BuildImagesButton';
|
||||
import { DeleteBlueprintModal } from '../Blueprints/DeleteBlueprintModal';
|
||||
|
||||
|
|
@ -28,7 +29,7 @@ interface imagesTableToolbarProps {
|
|||
itemCount: number;
|
||||
perPage: number;
|
||||
page: number;
|
||||
onSetPage: (event: React.MouseEvent, page: number) => void;
|
||||
setPage: (page: number) => void;
|
||||
onPerPageSelect: (event: React.MouseEvent, perPage: number) => void;
|
||||
}
|
||||
|
||||
|
|
@ -36,7 +37,7 @@ const ImagesTableToolbar: React.FC<imagesTableToolbarProps> = ({
|
|||
itemCount,
|
||||
perPage,
|
||||
page,
|
||||
onSetPage,
|
||||
setPage,
|
||||
onPerPageSelect,
|
||||
}: imagesTableToolbarProps) => {
|
||||
const experimentalFlag = useExperimentalFlag();
|
||||
|
|
@ -60,7 +61,7 @@ const ImagesTableToolbar: React.FC<imagesTableToolbarProps> = ({
|
|||
itemCount={itemCount}
|
||||
perPage={perPage}
|
||||
page={page}
|
||||
onSetPage={onSetPage}
|
||||
onSetPage={(_, page) => setPage(page)}
|
||||
onPerPageSelect={onPerPageSelect}
|
||||
widgetId="compose-pagination-top"
|
||||
data-testid="images-pagination-top"
|
||||
|
|
@ -110,6 +111,11 @@ const ImagesTableToolbar: React.FC<imagesTableToolbarProps> = ({
|
|||
<ToolbarItem>
|
||||
<BlueprintActionsMenu setShowDeleteModal={setShowDeleteModal} />
|
||||
</ToolbarItem>
|
||||
{selectedBlueprintId && (
|
||||
<ToolbarItem>
|
||||
<BlueprintVersionFilter onFilterChange={() => setPage(1)} />
|
||||
</ToolbarItem>
|
||||
)}
|
||||
<ToolbarItem variant="pagination" align={{ default: 'alignRight' }}>
|
||||
{pagination}
|
||||
</ToolbarItem>
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
|||
|
||||
import { RootState } from '.';
|
||||
|
||||
export type versionFilterType = 'latest' | 'all';
|
||||
|
||||
type blueprintsState = {
|
||||
selectedBlueprintId: string | undefined;
|
||||
searchInput?: string;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
versionFilter?: versionFilterType;
|
||||
};
|
||||
|
||||
const initialState: blueprintsState = {
|
||||
|
|
@ -14,6 +17,7 @@ const initialState: blueprintsState = {
|
|||
searchInput: undefined,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
versionFilter: 'all',
|
||||
};
|
||||
|
||||
export const selectSelectedBlueprintId = (state: RootState) =>
|
||||
|
|
@ -22,6 +26,18 @@ export const selectBlueprintSearchInput = (state: RootState) =>
|
|||
state.blueprints.searchInput;
|
||||
export const selectOffset = (state: RootState) => state.blueprints.offset;
|
||||
export const selectLimit = (state: RootState) => state.blueprints.limit;
|
||||
export const selectBlueprintVersionFilter = (state: RootState) =>
|
||||
state.blueprints.versionFilter;
|
||||
export const selectBlueprintVersionFilterAPI = (
|
||||
state: RootState
|
||||
): number | undefined => {
|
||||
const blueprintVersionFilter = state.blueprints.versionFilter;
|
||||
// We allow only 'latest' filtering, everything else is understood as 'all'
|
||||
if (blueprintVersionFilter === 'latest') {
|
||||
return -1;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const blueprintsSlice = createSlice({
|
||||
name: 'blueprints',
|
||||
|
|
@ -42,6 +58,12 @@ export const blueprintsSlice = createSlice({
|
|||
setBlueprintLimit: (state, action: PayloadAction<number>) => {
|
||||
state.limit = action.payload;
|
||||
},
|
||||
setBlueprintVersionFilter: (
|
||||
state,
|
||||
action: PayloadAction<versionFilterType>
|
||||
) => {
|
||||
state.versionFilter = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -50,4 +72,5 @@ export const {
|
|||
setBlueprintSearchInput,
|
||||
setBlueprintsOffset,
|
||||
setBlueprintLimit,
|
||||
setBlueprintVersionFilter,
|
||||
} = blueprintsSlice.actions;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,20 @@ jest.mock('@unleash/proxy-client-react', () => ({
|
|||
),
|
||||
}));
|
||||
|
||||
const selectBlueprintById = async (user, bpId) => {
|
||||
const nameMatcher = (_, element) =>
|
||||
element.getAttribute('name') === 'blueprints';
|
||||
|
||||
const radioButtons = await screen.findAllByRole('radio', {
|
||||
name: nameMatcher,
|
||||
});
|
||||
const elementById = radioButtons.find(
|
||||
(button) => button.getAttribute('id') === bpId
|
||||
);
|
||||
await user.click(elementById);
|
||||
return elementById;
|
||||
};
|
||||
|
||||
describe('Blueprints', () => {
|
||||
const user = userEvent.setup();
|
||||
const blueprintNameWithComposes = 'Dark Chocolate';
|
||||
|
|
@ -74,7 +88,7 @@ describe('Blueprints', () => {
|
|||
await user.click(elementById);
|
||||
const table = await screen.findByTestId('images-table');
|
||||
const { findAllByText } = within(table);
|
||||
const images = await findAllByText(blueprintNameWithComposes);
|
||||
const images = await findAllByText('dark-chocolate-aws');
|
||||
expect(images).toHaveLength(2);
|
||||
});
|
||||
test('renders blueprint composes empty state', async () => {
|
||||
|
|
@ -94,6 +108,7 @@ describe('Blueprints', () => {
|
|||
const { findByText } = within(table);
|
||||
await findByText('No images');
|
||||
});
|
||||
|
||||
test('click build image button', async () => {
|
||||
renderWithReduxRouter('', {});
|
||||
const idMatcher = blueprintIdWithComposes;
|
||||
|
|
@ -199,4 +214,28 @@ describe('Blueprints', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('composes filtering', () => {
|
||||
test('filter composes by blueprint version', async () => {
|
||||
renderWithReduxRouter('', {});
|
||||
|
||||
await selectBlueprintById(user, blueprintIdWithComposes);
|
||||
|
||||
// Wait for the filter appear (right now it's hidden unless a blueprint is selected)
|
||||
const composesVersionFilter = await screen.findByRole('button', {
|
||||
name: /All Versions/i,
|
||||
});
|
||||
|
||||
expect(
|
||||
within(screen.getByTestId('images-table')).getAllByRole('row')
|
||||
).toHaveLength(4);
|
||||
|
||||
await user.click(composesVersionFilter);
|
||||
const option = await screen.findByRole('menuitem', { name: 'Newest' });
|
||||
await user.click(option);
|
||||
expect(
|
||||
within(screen.getByTestId('images-table')).getAllByRole('row')
|
||||
).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
31
src/test/fixtures/blueprints.ts
vendored
31
src/test/fixtures/blueprints.ts
vendored
|
|
@ -1,4 +1,4 @@
|
|||
import { RHEL_9 } from '../../constants';
|
||||
import { RHEL_8, RHEL_9 } from '../../constants';
|
||||
import {
|
||||
GetBlueprintsApiResponse,
|
||||
CreateBlueprintResponse,
|
||||
|
|
@ -20,7 +20,7 @@ export const mockGetBlueprints: GetBlueprintsApiResponse = {
|
|||
id: '677b010b-e95e-4694-9813-d11d847f1bfc',
|
||||
name: 'Dark Chocolate',
|
||||
description: '70% Dark Chocolate with crunchy cocoa nibs',
|
||||
version: 1,
|
||||
version: 2,
|
||||
last_modified_at: '2021-09-09T14:38:00.000Z',
|
||||
},
|
||||
{
|
||||
|
|
@ -75,15 +75,36 @@ export const mockBlueprintComposesOutOfSync: GetBlueprintComposesApiResponse = {
|
|||
};
|
||||
|
||||
export const mockBlueprintComposes: GetBlueprintComposesApiResponse = {
|
||||
meta: { count: 2 },
|
||||
meta: { count: 3 },
|
||||
data: [
|
||||
{
|
||||
id: '63e42aaf-b543-41c6-899f-3de1e61838dc',
|
||||
image_name: 'dark-chocolate-aws',
|
||||
created_at: '2023-09-08T14:38:00.000Z',
|
||||
blueprint_version: 2,
|
||||
request: {
|
||||
distribution: RHEL_9,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'aws',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {
|
||||
share_with_accounts: ['123123123123'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '1579d95b-8f1d-4982-8c53-8c2afa4ab04c',
|
||||
image_name: 'Dark Chocolate',
|
||||
image_name: 'dark-chocolate-aws',
|
||||
created_at: '2021-09-08T14:38:00.000Z',
|
||||
blueprint_version: 1,
|
||||
request: {
|
||||
distribution: RHEL_9,
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
|
|
|
|||
50
src/test/fixtures/composes.ts
vendored
50
src/test/fixtures/composes.ts
vendored
|
|
@ -1,6 +1,6 @@
|
|||
import { PathParams, RestRequest } from 'msw';
|
||||
|
||||
import { RHEL_8 } from '../../constants';
|
||||
import { RHEL_8, RHEL_9 } from '../../constants';
|
||||
import {
|
||||
ClonesResponse,
|
||||
ComposeStatus,
|
||||
|
|
@ -401,6 +401,26 @@ export const mockComposes: ComposesResponseItem[] = [
|
|||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '63e42aaf-b543-41c6-899f-3de1e61838dc',
|
||||
image_name: 'dark-chocolate-v2',
|
||||
created_at: '2023-09-08T14:38:00.000Z',
|
||||
request: {
|
||||
distribution: RHEL_9,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'aws',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {
|
||||
share_with_accounts: ['123123123123'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -437,6 +457,34 @@ export const mockStatus = (composeId: string): ComposeStatus => {
|
|||
],
|
||||
},
|
||||
},
|
||||
'63e42aaf-b543-41c6-899f-3de1e61838dc': {
|
||||
image_status: {
|
||||
status: 'success',
|
||||
upload_status: {
|
||||
options: {
|
||||
ami: 'ami-0217b81d9be50e44c',
|
||||
region: 'us-east-1',
|
||||
},
|
||||
status: 'success',
|
||||
type: 'aws',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
distribution: RHEL_9,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'aws',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {
|
||||
share_with_accounts: ['123123123123'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
'c1cfa347-4c37-49b5-8e73-6aa1d1746cfa': {
|
||||
image_status: {
|
||||
status: 'failure',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue