feat(HMS-3391): add blueprint onclick handler
This commit is contained in:
parent
dca9225df5
commit
017f5bfb51
7 changed files with 185 additions and 28 deletions
|
|
@ -19,8 +19,10 @@ import { BlueprintItem } from '../../store/imageBuilderApi';
|
|||
|
||||
type blueprintProps = {
|
||||
blueprint: BlueprintItem;
|
||||
selectedBlueprint: string;
|
||||
setSelectedBlueprint: React.Dispatch<React.SetStateAction<string>>;
|
||||
selectedBlueprint: string | undefined;
|
||||
setSelectedBlueprint: React.Dispatch<
|
||||
React.SetStateAction<string | undefined>
|
||||
>;
|
||||
};
|
||||
|
||||
const BlueprintCard = ({
|
||||
|
|
@ -34,8 +36,10 @@ const BlueprintCard = ({
|
|||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
const onClickHandler = () => {
|
||||
setSelectedBlueprint(blueprint.id);
|
||||
const onClickHandler = ({
|
||||
currentTarget: { id: blueprintID },
|
||||
}: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSelectedBlueprint(blueprintID);
|
||||
};
|
||||
|
||||
const headerActions = (
|
||||
|
|
@ -76,7 +80,6 @@ const BlueprintCard = ({
|
|||
<CardHeader
|
||||
selectableActions={{
|
||||
selectableActionId: blueprint.id,
|
||||
selectableActionAriaLabelledby: 'blueprint radio select',
|
||||
name: blueprint.name,
|
||||
variant: 'single',
|
||||
isChecked: isChecked,
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ import { BlueprintItem } from '../../store/imageBuilderApi';
|
|||
|
||||
type blueprintProps = {
|
||||
blueprints: BlueprintItem[] | undefined;
|
||||
selectedBlueprint: string;
|
||||
setSelectedBlueprint: Dispatch<SetStateAction<string>>;
|
||||
selectedBlueprint: string | undefined;
|
||||
setSelectedBlueprint: Dispatch<SetStateAction<string | undefined>>;
|
||||
};
|
||||
|
||||
const BlueprintsSidebar = ({
|
||||
|
|
@ -66,6 +66,16 @@ const BlueprintsSidebar = ({
|
|||
onClear={() => onChange('')}
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<Button
|
||||
isBlock
|
||||
onClick={() => setSelectedBlueprint(undefined)}
|
||||
variant="link"
|
||||
isDisabled={!selectedBlueprint}
|
||||
>
|
||||
Show all images
|
||||
</Button>
|
||||
</StackItem>
|
||||
{blueprints.map((blueprint: BlueprintItem) => (
|
||||
<StackItem key={blueprint.id}>
|
||||
<BlueprintCard
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ import {
|
|||
import {
|
||||
ComposesResponseItem,
|
||||
ComposeStatus,
|
||||
useGetBlueprintComposesQuery,
|
||||
useGetComposesQuery,
|
||||
useGetComposeStatusQuery,
|
||||
} from '../../store/imageBuilderApi';
|
||||
|
|
@ -63,7 +64,11 @@ import {
|
|||
timestampToDisplayString,
|
||||
} from '../../Utilities/time';
|
||||
|
||||
const ImagesTable = () => {
|
||||
type ImageTableProps = {
|
||||
selectedBlueprint?: string | undefined;
|
||||
};
|
||||
|
||||
const ImagesTable = ({ selectedBlueprint }: ImageTableProps) => {
|
||||
const [page, setPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState(10);
|
||||
|
||||
|
|
@ -74,16 +79,53 @@ const ImagesTable = () => {
|
|||
setPerPage(perPage);
|
||||
};
|
||||
|
||||
const { data, isError, isSuccess } = useGetComposesQuery({
|
||||
limit: perPage,
|
||||
offset: perPage * (page - 1),
|
||||
ignoreImageTypes: [
|
||||
'rhel-edge-commit',
|
||||
'rhel-edge-installer',
|
||||
'edge-commit',
|
||||
'edge-installer',
|
||||
],
|
||||
});
|
||||
const {
|
||||
data: blueprintsComposes,
|
||||
isSuccess: isBlueprintsSuccess,
|
||||
isFetching: isFetchingBlueprintsCompose,
|
||||
isError: isBlueprintsError,
|
||||
} = useGetBlueprintComposesQuery(
|
||||
{
|
||||
id: selectedBlueprint as string,
|
||||
limit: perPage,
|
||||
offset: perPage * (page - 1),
|
||||
},
|
||||
{ skip: !selectedBlueprint }
|
||||
);
|
||||
|
||||
const {
|
||||
data: composesData,
|
||||
isSuccess: isComposesSuccess,
|
||||
isError: isComposesError,
|
||||
isFetching: isFetchingComposes,
|
||||
} = useGetComposesQuery(
|
||||
{
|
||||
limit: perPage,
|
||||
offset: perPage * (page - 1),
|
||||
ignoreImageTypes: [
|
||||
'rhel-edge-commit',
|
||||
'rhel-edge-installer',
|
||||
'edge-commit',
|
||||
'edge-installer',
|
||||
],
|
||||
},
|
||||
{ skip: !!selectedBlueprint }
|
||||
);
|
||||
|
||||
const data = selectedBlueprint ? blueprintsComposes : composesData;
|
||||
const isSuccess = selectedBlueprint ? isBlueprintsSuccess : isComposesSuccess;
|
||||
const isError = selectedBlueprint ? isBlueprintsError : isComposesError;
|
||||
const isFetching = selectedBlueprint
|
||||
? isFetchingBlueprintsCompose
|
||||
: isFetchingComposes;
|
||||
|
||||
if (isFetching) {
|
||||
return (
|
||||
<Bullseye>
|
||||
<Spinner />
|
||||
</Bullseye>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isSuccess) {
|
||||
if (isError) {
|
||||
|
|
@ -103,13 +145,13 @@ const ImagesTable = () => {
|
|||
);
|
||||
}
|
||||
|
||||
const composes = data.data;
|
||||
const itemCount = data.meta.count;
|
||||
const composes = data?.data;
|
||||
const itemCount = data?.meta.count || 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
{data.meta.count === 0 && <EmptyImagesTable />}
|
||||
{data.meta.count > 0 && (
|
||||
{itemCount === 0 && <EmptyImagesTable />}
|
||||
{itemCount > 0 && (
|
||||
<>
|
||||
<Toolbar>
|
||||
<ToolbarContent>
|
||||
|
|
@ -152,7 +194,7 @@ const ImagesTable = () => {
|
|||
<Th />
|
||||
</Tr>
|
||||
</Thead>
|
||||
{composes.map((compose, rowIndex) => {
|
||||
{composes?.map((compose, rowIndex) => {
|
||||
return (
|
||||
<ImagesTableRow
|
||||
compose={compose}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,9 @@ export const LandingPage = () => {
|
|||
}
|
||||
setActiveTabKey(tabIndex);
|
||||
};
|
||||
const [selectedBlueprint, setSelectedBlueprint] = useState<string>('');
|
||||
const [selectedBlueprint, setSelectedBlueprint] = useState<
|
||||
string | undefined
|
||||
>();
|
||||
const { data: blueprints, isLoading } = useGetBlueprintsQuery({});
|
||||
|
||||
const edgeParityFlag = useFlag('edgeParity.image-list');
|
||||
|
|
@ -86,7 +88,7 @@ export const LandingPage = () => {
|
|||
/>
|
||||
</SidebarPanel>
|
||||
<SidebarContent>
|
||||
<ImagesTable />
|
||||
<ImagesTable selectedBlueprint={selectedBlueprint} />
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
</PageSection>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { screen } from '@testing-library/react';
|
||||
import { screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { rest } from 'msw';
|
||||
|
||||
import { IMAGE_BUILDER_API } from '../../../constants';
|
||||
|
|
@ -6,6 +7,8 @@ import { emptyGetBlueprints } from '../../fixtures/blueprints';
|
|||
import { server } from '../../mocks/server';
|
||||
import { renderWithReduxRouter } from '../../testUtils';
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
jest.mock('@redhat-cloud-services/frontend-components/useChrome', () => ({
|
||||
useChrome: () => ({
|
||||
isBeta: () => false,
|
||||
|
|
@ -22,9 +25,14 @@ jest.mock('@unleash/proxy-client-react', () => ({
|
|||
}));
|
||||
|
||||
describe('Blueprints', () => {
|
||||
const user = userEvent.setup();
|
||||
const blueprintNameWithComposes = 'Dark Chocolate';
|
||||
const blueprintNameEmptyComposes = 'Milk Chocolate';
|
||||
|
||||
test('renders blueprints page', async () => {
|
||||
renderWithReduxRouter('', {});
|
||||
await screen.findByText('Dark Chocolate');
|
||||
await screen.findByText(blueprintNameWithComposes);
|
||||
await screen.findByText(blueprintNameEmptyComposes);
|
||||
});
|
||||
test('renders blueprint empty state', async () => {
|
||||
server.use(
|
||||
|
|
@ -39,4 +47,28 @@ describe('Blueprints', () => {
|
|||
renderWithReduxRouter('', {});
|
||||
await screen.findByText('No blueprints yet');
|
||||
});
|
||||
test('renders blueprint composes', async () => {
|
||||
renderWithReduxRouter('', {});
|
||||
const nameMatcher = (_, element) =>
|
||||
element.getAttribute('name') === blueprintNameWithComposes;
|
||||
|
||||
const blueprintRadioBtn = await screen.findByRole('radio', {
|
||||
name: nameMatcher,
|
||||
});
|
||||
await user.click(blueprintRadioBtn);
|
||||
const table = await screen.findByTestId('images-table');
|
||||
const { findByText } = within(table);
|
||||
await findByText(blueprintNameWithComposes);
|
||||
});
|
||||
test('renders blueprint composes empty state', async () => {
|
||||
renderWithReduxRouter('', {});
|
||||
const nameMatcher = (_, element) =>
|
||||
element.getAttribute('name') === blueprintNameEmptyComposes;
|
||||
|
||||
const blueprintRadioBtn = await screen.findByRole('radio', {
|
||||
name: nameMatcher,
|
||||
});
|
||||
await user.click(blueprintRadioBtn);
|
||||
expect(screen.queryByTestId('images-table')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
54
src/test/fixtures/blueprints.ts
vendored
54
src/test/fixtures/blueprints.ts
vendored
|
|
@ -1,6 +1,8 @@
|
|||
import { RHEL_9 } from '../../constants';
|
||||
import {
|
||||
GetBlueprintsApiResponse,
|
||||
CreateBlueprintResponse,
|
||||
GetBlueprintComposesApiResponse,
|
||||
} from '../../store/imageBuilderApi';
|
||||
|
||||
export const mockBlueprintsCreation: CreateBlueprintResponse[] = [
|
||||
|
|
@ -35,3 +37,55 @@ export const emptyGetBlueprints: GetBlueprintsApiResponse = {
|
|||
meta: { count: 0 },
|
||||
data: [],
|
||||
};
|
||||
|
||||
export const mockBlueprintComposes: GetBlueprintComposesApiResponse = {
|
||||
meta: { count: 2 },
|
||||
data: [
|
||||
{
|
||||
id: '1579d95b-8f1d-4982-8c53-8c2afa4ab04c',
|
||||
image_name: 'Dark Chocolate',
|
||||
created_at: '2021-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'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'c1cfa347-4c37-49b5-8e73-6aa1d1746cfa',
|
||||
created_at: '2021-04-27T12:31:12Z',
|
||||
request: {
|
||||
distribution: RHEL_9,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'gcp',
|
||||
upload_request: {
|
||||
type: 'gcp',
|
||||
options: {
|
||||
share_with_accounts: ['serviceAccount:test@email.com'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
links: { first: 'first', last: 'last' },
|
||||
};
|
||||
|
||||
export const mockEmptyBlueprintsComposes: GetBlueprintComposesApiResponse = {
|
||||
meta: { count: 0 },
|
||||
data: [],
|
||||
links: { first: 'first', last: 'last' },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ import {
|
|||
mockActivationKeysResults,
|
||||
} from '../fixtures/activationKeys';
|
||||
import { mockArchitecturesByDistro } from '../fixtures/architectures';
|
||||
import { mockGetBlueprints } from '../fixtures/blueprints';
|
||||
import {
|
||||
mockBlueprintComposes,
|
||||
mockEmptyBlueprintsComposes,
|
||||
mockGetBlueprints,
|
||||
} from '../fixtures/blueprints';
|
||||
import {
|
||||
composesEndpoint,
|
||||
mockClones,
|
||||
|
|
@ -111,4 +115,14 @@ export const handlers = [
|
|||
rest.get(`${IMAGE_BUILDER_API}/experimental/blueprints`, (req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json(mockGetBlueprints));
|
||||
}),
|
||||
rest.get(
|
||||
`${IMAGE_BUILDER_API}/experimental/blueprints/:id/composes`,
|
||||
(req, res, ctx) => {
|
||||
const MilkChocolateBlueprint = mockGetBlueprints.data[1].id;
|
||||
if (req.params.id === MilkChocolateBlueprint) {
|
||||
return res(ctx.status(200), ctx.json(mockEmptyBlueprintsComposes));
|
||||
}
|
||||
return res(ctx.status(200), ctx.json(mockBlueprintComposes));
|
||||
}
|
||||
),
|
||||
];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue