feat(HMS-3515): Change blueprint cards to be clickable
This commit is contained in:
parent
d8c657da6c
commit
45194fa225
6 changed files with 136 additions and 100 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Badge,
|
||||
|
|
@ -7,16 +7,8 @@ import {
|
|||
CardTitle,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
Dropdown,
|
||||
DropdownList,
|
||||
MenuToggle,
|
||||
MenuToggleElement,
|
||||
DropdownItem,
|
||||
Spinner,
|
||||
} from '@patternfly/react-core';
|
||||
import { EllipsisVIcon } from '@patternfly/react-icons';
|
||||
|
||||
import { DeleteBlueprintModal } from './DeleteBlueprintModal';
|
||||
|
||||
import {
|
||||
BlueprintItem,
|
||||
|
|
@ -36,84 +28,26 @@ const BlueprintCard = ({
|
|||
selectedBlueprint,
|
||||
setSelectedBlueprint,
|
||||
}: blueprintProps) => {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const isChecked = blueprint.id === selectedBlueprint;
|
||||
const onSelect = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
const [showDeleteModal, setShowDeleteModal] = React.useState(false);
|
||||
const [deleteBlueprint, { isLoading }] = useDeleteBlueprintMutation();
|
||||
const handleDelete = async () => {
|
||||
setShowDeleteModal(false);
|
||||
await deleteBlueprint({ id: blueprint.id });
|
||||
};
|
||||
const onDeleteClose = () => {
|
||||
setShowDeleteModal(false);
|
||||
};
|
||||
|
||||
const onClickHandler = ({
|
||||
currentTarget: { id: blueprintID },
|
||||
}: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSelectedBlueprint(blueprintID);
|
||||
};
|
||||
|
||||
const headerActions = (
|
||||
<>
|
||||
<Dropdown
|
||||
ouiaId={`blueprint-card-${blueprint.id}-dropdown`}
|
||||
onSelect={onSelect}
|
||||
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
|
||||
<MenuToggle
|
||||
ref={toggleRef}
|
||||
isExpanded={isOpen}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
variant="plain"
|
||||
aria-label="blueprint menu toggle"
|
||||
>
|
||||
<EllipsisVIcon aria-hidden="true" />
|
||||
</MenuToggle>
|
||||
)}
|
||||
isOpen={isOpen}
|
||||
onOpenChange={(isOpen: boolean) => setIsOpen(isOpen)}
|
||||
>
|
||||
<DropdownList>
|
||||
<DropdownItem>Edit details</DropdownItem>
|
||||
<DropdownItem onClick={() => setShowDeleteModal(true)}>
|
||||
Delete blueprint
|
||||
</DropdownItem>
|
||||
</DropdownList>
|
||||
</Dropdown>
|
||||
</>
|
||||
);
|
||||
|
||||
const [, { isLoading }] = useDeleteBlueprintMutation({
|
||||
fixedCacheKey: 'delete-blueprint',
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<DeleteBlueprintModal
|
||||
onDelete={handleDelete}
|
||||
blueprintName={blueprint?.name}
|
||||
isOpen={showDeleteModal}
|
||||
onClose={onDeleteClose}
|
||||
/>
|
||||
<Card
|
||||
ouiaId={`blueprint-card-${blueprint.id}`}
|
||||
isCompact
|
||||
isClickable
|
||||
isSelectable
|
||||
isSelected={isChecked}
|
||||
>
|
||||
<Card ouiaId={`blueprint-card-${blueprint.id}`} isCompact isClickable>
|
||||
<CardHeader
|
||||
data-testid={blueprint.id}
|
||||
selectableActions={{
|
||||
selectableActionId: blueprint.id,
|
||||
name: blueprint.name,
|
||||
variant: 'single',
|
||||
isChecked: isChecked,
|
||||
onChange: onClickHandler,
|
||||
name: 'blueprints',
|
||||
onClickAction: () => setSelectedBlueprint(blueprint.id),
|
||||
}}
|
||||
actions={{ actions: headerActions }}
|
||||
>
|
||||
<CardTitle>
|
||||
{blueprint.name} {isLoading && <Spinner size="md" />}
|
||||
{isLoading && blueprint.id === selectedBlueprint && (
|
||||
<Spinner size="md" />
|
||||
)}
|
||||
|
||||
{blueprint.name}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardBody>{blueprint.description}</CardBody>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ import React, { useState, useCallback } from 'react';
|
|||
import {
|
||||
Bullseye,
|
||||
Button,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
EmptyState,
|
||||
EmptyStateActions,
|
||||
EmptyStateBody,
|
||||
|
|
@ -103,14 +106,23 @@ const BlueprintsSidebar = ({
|
|||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<Button
|
||||
isBlock
|
||||
onClick={() => setSelectedBlueprint(undefined)}
|
||||
variant="link"
|
||||
<Card
|
||||
ouiaId={`blueprint-card-all`}
|
||||
isCompact
|
||||
isClickable
|
||||
isDisabled={!selectedBlueprint}
|
||||
>
|
||||
Show all images
|
||||
</Button>
|
||||
<CardHeader
|
||||
selectableActions={{
|
||||
selectableActionId: 'show-all-card',
|
||||
name: 'blueprints',
|
||||
variant: 'single',
|
||||
onClickAction: () => setSelectedBlueprint(undefined),
|
||||
}}
|
||||
>
|
||||
<CardTitle component="a">Clear selection</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</StackItem>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,11 @@ import {
|
|||
ModalVariant,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import { useGetBlueprintsQuery } from '../../store/imageBuilderApi';
|
||||
|
||||
interface DeleteBlueprintModalProps {
|
||||
onDelete: () => Promise<void>;
|
||||
blueprintName: string;
|
||||
selectedBlueprint: string | undefined;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
|
@ -18,10 +20,21 @@ export const DeleteBlueprintModal: React.FunctionComponent<
|
|||
DeleteBlueprintModalProps
|
||||
> = ({
|
||||
onDelete,
|
||||
blueprintName,
|
||||
selectedBlueprint,
|
||||
isOpen,
|
||||
onClose,
|
||||
}: DeleteBlueprintModalProps) => {
|
||||
const { blueprintName } = useGetBlueprintsQuery(
|
||||
{ search: undefined },
|
||||
{
|
||||
selectFromResult: ({ data }) => ({
|
||||
blueprintName: data?.data?.find(
|
||||
(blueprint: { id: string | undefined }) =>
|
||||
blueprint.id === selectedBlueprint
|
||||
)?.name,
|
||||
}),
|
||||
}
|
||||
);
|
||||
return (
|
||||
<Modal
|
||||
variant={ModalVariant.small}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
|
|
@ -7,6 +7,11 @@ import {
|
|||
TextContent,
|
||||
Flex,
|
||||
FlexItem,
|
||||
Dropdown,
|
||||
DropdownList,
|
||||
MenuToggle,
|
||||
MenuToggleElement,
|
||||
DropdownItem,
|
||||
} from '@patternfly/react-core';
|
||||
import { ExternalLinkAltIcon, HelpIcon } from '@patternfly/react-icons';
|
||||
// eslint-disable-next-line rulesdir/disallow-fec-relative-imports
|
||||
|
|
@ -17,9 +22,13 @@ import {
|
|||
} from '@redhat-cloud-services/frontend-components';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useComposeBlueprintMutation } from '../../store/imageBuilderApi';
|
||||
import {
|
||||
useComposeBlueprintMutation,
|
||||
useDeleteBlueprintMutation,
|
||||
} from '../../store/imageBuilderApi';
|
||||
import { resolveRelPath } from '../../Utilities/path';
|
||||
import './ImageBuilderHeader.scss';
|
||||
import { DeleteBlueprintModal } from '../Blueprints/DeleteBlueprintModal';
|
||||
|
||||
type ImageBuilderHeaderPropTypes = {
|
||||
experimentalFlag?: string | true | undefined;
|
||||
|
|
@ -36,10 +45,34 @@ export const ImageBuilderHeader = ({
|
|||
const onBuildHandler = async () => {
|
||||
selectedBlueprint && (await buildBlueprint({ id: selectedBlueprint }));
|
||||
};
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const onSelect = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
const [showDeleteModal, setShowDeleteModal] = React.useState(false);
|
||||
const [deleteBlueprint] = useDeleteBlueprintMutation({
|
||||
fixedCacheKey: 'delete-blueprint',
|
||||
});
|
||||
const handleDelete = async () => {
|
||||
if (selectedBlueprint) {
|
||||
setShowDeleteModal(false);
|
||||
await deleteBlueprint({ id: selectedBlueprint });
|
||||
}
|
||||
};
|
||||
const onDeleteClose = () => {
|
||||
setShowDeleteModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/*@ts-ignore*/}
|
||||
<DeleteBlueprintModal
|
||||
onDelete={handleDelete}
|
||||
selectedBlueprint={selectedBlueprint}
|
||||
isOpen={showDeleteModal}
|
||||
onClose={onDeleteClose}
|
||||
/>
|
||||
<PageHeader>
|
||||
<Flex>
|
||||
<FlexItem>
|
||||
|
|
@ -120,6 +153,34 @@ export const ImageBuilderHeader = ({
|
|||
>
|
||||
Build images
|
||||
</Button>
|
||||
</FlexItem>
|
||||
<FlexItem>
|
||||
<Dropdown
|
||||
ouiaId={`blueprints-dropdown`}
|
||||
isOpen={isOpen}
|
||||
onSelect={onSelect}
|
||||
onOpenChange={(isOpen: boolean) => setIsOpen(isOpen)}
|
||||
shouldFocusToggleOnSelect
|
||||
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
|
||||
<MenuToggle
|
||||
ref={toggleRef}
|
||||
isExpanded={isOpen}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
variant="secondary"
|
||||
aria-label="blueprint menu toggle"
|
||||
isDisabled={selectedBlueprint === undefined}
|
||||
>
|
||||
Blueprint actions
|
||||
</MenuToggle>
|
||||
)}
|
||||
>
|
||||
<DropdownList>
|
||||
<DropdownItem>Edit details</DropdownItem>
|
||||
<DropdownItem onClick={() => setShowDeleteModal(true)}>
|
||||
Delete blueprint
|
||||
</DropdownItem>
|
||||
</DropdownList>
|
||||
</Dropdown>
|
||||
</FlexItem>{' '}
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ jest.mock('@unleash/proxy-client-react', () => ({
|
|||
describe('Blueprints', () => {
|
||||
const user = userEvent.setup();
|
||||
const blueprintNameWithComposes = 'Dark Chocolate';
|
||||
const blueprintIdWithComposes = '677b010b-e95e-4694-9813-d11d847f1bfc';
|
||||
const blueprintNameEmptyComposes = 'Milk Chocolate';
|
||||
const blueprintIdEmptyComposes = '193482e4-4bd0-4898-a8bc-dc8c33ed669f';
|
||||
|
||||
test('renders blueprints page', async () => {
|
||||
renderWithReduxRouter('', {});
|
||||
|
|
@ -50,37 +52,51 @@ describe('Blueprints', () => {
|
|||
});
|
||||
test('renders blueprint composes', async () => {
|
||||
renderWithReduxRouter('', {});
|
||||
const idMatcher = blueprintIdWithComposes;
|
||||
const nameMatcher = (_, element) =>
|
||||
element.getAttribute('name') === blueprintNameWithComposes;
|
||||
element.getAttribute('name') === 'blueprints';
|
||||
|
||||
const blueprintRadioBtn = await screen.findByRole('radio', {
|
||||
const radioButtons = await screen.findAllByRole('radio', {
|
||||
name: nameMatcher,
|
||||
});
|
||||
await user.click(blueprintRadioBtn);
|
||||
const elementById = radioButtons.find(
|
||||
(button) => button.getAttribute('id') === idMatcher
|
||||
);
|
||||
|
||||
await user.click(elementById);
|
||||
const table = await screen.findByTestId('images-table');
|
||||
const { findByText } = within(table);
|
||||
await findByText(blueprintNameWithComposes);
|
||||
});
|
||||
test('renders blueprint composes empty state', async () => {
|
||||
renderWithReduxRouter('', {});
|
||||
const idMatcher = blueprintIdEmptyComposes;
|
||||
const nameMatcher = (_, element) =>
|
||||
element.getAttribute('name') === blueprintNameEmptyComposes;
|
||||
element.getAttribute('name') === 'blueprints';
|
||||
|
||||
const blueprintRadioBtn = await screen.findByRole('radio', {
|
||||
const radioButtons = await screen.findAllByRole('radio', {
|
||||
name: nameMatcher,
|
||||
});
|
||||
await user.click(blueprintRadioBtn);
|
||||
const elementById = radioButtons.find(
|
||||
(button) => button.getAttribute('id') === idMatcher
|
||||
);
|
||||
await user.click(elementById);
|
||||
expect(screen.queryByTestId('images-table')).not.toBeInTheDocument();
|
||||
});
|
||||
test('click build image button', async () => {
|
||||
renderWithReduxRouter('', {});
|
||||
const idMatcher = blueprintIdWithComposes;
|
||||
const nameMatcher = (_, element) =>
|
||||
element.getAttribute('name') === blueprintNameWithComposes;
|
||||
element.getAttribute('name') === 'blueprints';
|
||||
|
||||
const blueprintRadioBtn = await screen.findByRole('radio', {
|
||||
const radioButtons = await screen.findAllByRole('radio', {
|
||||
name: nameMatcher,
|
||||
});
|
||||
await user.click(blueprintRadioBtn);
|
||||
const elementById = radioButtons.find(
|
||||
(button) => button.getAttribute('id') === idMatcher
|
||||
);
|
||||
|
||||
await user.click(elementById);
|
||||
const buildImageBtn = await screen.findByRole('button', {
|
||||
name: /Build image/i,
|
||||
});
|
||||
|
|
@ -99,7 +115,7 @@ describe('Blueprints', () => {
|
|||
|
||||
// wait for debounce
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByRole('radio')).toHaveLength(1);
|
||||
expect(screen.getAllByRole('radio')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
2
src/test/fixtures/blueprints.ts
vendored
2
src/test/fixtures/blueprints.ts
vendored
|
|
@ -23,7 +23,7 @@ export const mockGetBlueprints: GetBlueprintsApiResponse = {
|
|||
last_modified_at: '2021-09-09T14:38:00.000Z',
|
||||
},
|
||||
{
|
||||
id: '677b0101-e952-4694-9813-d11d847f1bfc',
|
||||
id: '193482e4-4bd0-4898-a8bc-dc8c33ed669f',
|
||||
name: 'Milk Chocolate',
|
||||
description: '40% Milk Chocolate with salted caramel',
|
||||
version: 1,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue