diff --git a/src/Components/Blueprints/BlueprintCard.tsx b/src/Components/Blueprints/BlueprintCard.tsx new file mode 100644 index 00000000..2d46ce8f --- /dev/null +++ b/src/Components/Blueprints/BlueprintCard.tsx @@ -0,0 +1,97 @@ +import React, { useState } from 'react'; + +import { + Badge, + Card, + CardHeader, + CardTitle, + CardBody, + CardFooter, + Dropdown, + DropdownList, + MenuToggle, + MenuToggleElement, + DropdownItem, +} from '@patternfly/react-core'; +import { EllipsisVIcon } from '@patternfly/react-icons'; + +import { BlueprintItem } from '../../store/imageBuilderApi'; + +type blueprintProps = { + blueprint: BlueprintItem; + selectedBlueprint: string; + setSelectedBlueprint: React.Dispatch>; +}; + +const BlueprintCard = ({ + blueprint, + selectedBlueprint, + setSelectedBlueprint, +}: blueprintProps) => { + const [isOpen, setIsOpen] = useState(false); + const isChecked = blueprint.id === selectedBlueprint; + const onSelect = () => { + setIsOpen(!isOpen); + }; + + const onClickHandler = () => { + setSelectedBlueprint(blueprint.id); + }; + + const headerActions = ( + <> + ) => ( + setIsOpen(!isOpen)} + variant="plain" + aria-label="blueprint menu toggle" + > + + )} + isOpen={isOpen} + onOpenChange={(isOpen: boolean) => setIsOpen(isOpen)} + > + + Edit details + Delete blueprint + + + + ); + + return ( + + + {blueprint.name} + + {blueprint.description} + + Version {blueprint.version} + + + ); +}; + +export default BlueprintCard; diff --git a/src/Components/Blueprints/BlueprintsSideBar.tsx b/src/Components/Blueprints/BlueprintsSideBar.tsx new file mode 100644 index 00000000..f5dd74b4 --- /dev/null +++ b/src/Components/Blueprints/BlueprintsSideBar.tsx @@ -0,0 +1,83 @@ +import React, { useState, Dispatch, SetStateAction } from 'react'; + +import { + Button, + EmptyState, + EmptyStateActions, + EmptyStateBody, + EmptyStateFooter, + EmptyStateHeader, + EmptyStateIcon, + SearchInput, + Stack, + StackItem, +} from '@patternfly/react-core'; +import { PlusCircleIcon } from '@patternfly/react-icons'; + +import BlueprintCard from './BlueprintCard'; + +import { BlueprintItem } from '../../store/imageBuilderApi'; + +type blueprintProps = { + blueprints: BlueprintItem[] | undefined; + selectedBlueprint: string; + setSelectedBlueprint: Dispatch>; +}; + +const BlueprintsSidebar = ({ + blueprints, + selectedBlueprint, + setSelectedBlueprint, +}: blueprintProps) => { + const [blueprintFilter, setBlueprintFilter] = useState(''); + + const onChange = (value: string) => { + setBlueprintFilter(value); + }; + + const emptyBlueprints = ( + + } + /> + To get started, create a blueprint. + + + + + + + ); + + if (blueprints === undefined || blueprints?.length === 0) { + return emptyBlueprints; + } + + return ( + <> + + + onChange(value)} + onClear={() => onChange('')} + /> + + {blueprints.map((blueprint: BlueprintItem) => ( + + + + ))} + + + ); +}; + +export default BlueprintsSidebar; diff --git a/src/Components/LandingPage/LandingPage.tsx b/src/Components/LandingPage/LandingPage.tsx index 04baf1e9..a4fc5deb 100644 --- a/src/Components/LandingPage/LandingPage.tsx +++ b/src/Components/LandingPage/LandingPage.tsx @@ -10,6 +10,12 @@ import { Text, TextContent, TabAction, + PageSection, + Spinner, + Sidebar, + SidebarContent, + SidebarPanel, + Bullseye, } from '@patternfly/react-core'; import { ExternalLinkAltIcon, HelpIcon } from '@patternfly/react-icons'; import { useFlag } from '@unleash/proxy-client-react'; @@ -19,8 +25,10 @@ import './LandingPage.scss'; import Quickstarts from './Quickstarts'; +import { useGetBlueprintsQuery } from '../../store/imageBuilderApi'; import { manageEdgeImagesUrlName } from '../../Utilities/edge'; import { resolveRelPath } from '../../Utilities/path'; +import BlueprintsSidebar from '../Blueprints/BlueprintsSideBar'; import EdgeImagesTable from '../edge/ImagesTable'; import ImagesTable from '../ImagesTable/ImagesTable'; import { ImageBuilderHeader } from '../sharedComponents/ImageBuilderHeader'; @@ -45,15 +53,58 @@ export const LandingPage = () => { } setActiveTabKey(tabIndex); }; + const [selectedBlueprint, setSelectedBlueprint] = useState(''); + const { data: blueprints, isLoading } = useGetBlueprintsQuery({}); const edgeParityFlag = useFlag('edgeParity.image-list'); - const traditionalImageList = ( -
- + const experimentalFlag = + useFlag('image-builder.new-wizard.enabled') || process.env.EXPERIMENTAL; - -
+ const traditionalImageList = ( + <> + + + + + + + ); + + const experimentalImageList = ( + <> + + + + + + + + + + + + + + + ); + + const imageList = experimentalFlag + ? experimentalImageList + : traditionalImageList; + + if (isLoading) { + return ( + + + + ); + } + return ( <> @@ -99,7 +150,7 @@ export const LandingPage = () => { /> } > - {traditionalImageList} + {imageList} { ) : ( - traditionalImageList + imageList )} diff --git a/src/Components/sharedComponents/ImageBuilderHeader.tsx b/src/Components/sharedComponents/ImageBuilderHeader.tsx index 889b0432..446ccaa6 100644 --- a/src/Components/sharedComponents/ImageBuilderHeader.tsx +++ b/src/Components/sharedComponents/ImageBuilderHeader.tsx @@ -1,6 +1,13 @@ import React from 'react'; -import { Button, Popover, Text, TextContent } from '@patternfly/react-core'; +import { + Button, + Popover, + Text, + TextContent, + Flex, + FlexItem, +} from '@patternfly/react-core'; import { ExternalLinkAltIcon, HelpIcon } from '@patternfly/react-icons'; // eslint-disable-next-line rulesdir/disallow-fec-relative-imports import { @@ -16,62 +23,72 @@ export const ImageBuilderHeader = () => { <> {/*@ts-ignore*/} - - - - Image builder is a tool for creating deployment-ready customized - system images: installation disks, virtual machines, cloud - vendor-specific images, and others. By using image builder, you - can make these images faster than manual procedures because it - eliminates the specific configurations required for each output - type. - - - - - - - - - } - > - - - + + + + + + Image builder is a tool for creating deployment-ready + customized system images: installation disks, virtual + machines, cloud vendor-specific images, and others. By using + image builder, you can make these images faster than manual + procedures because it eliminates the specific configurations + required for each output type. + + + + + + + + + } + > + + + + + + + + + + + ); diff --git a/src/test/Components/Blueprints/Blueprints.test.js b/src/test/Components/Blueprints/Blueprints.test.js new file mode 100644 index 00000000..12b4e984 --- /dev/null +++ b/src/test/Components/Blueprints/Blueprints.test.js @@ -0,0 +1,42 @@ +import { screen } from '@testing-library/react'; +import { rest } from 'msw'; + +import { IMAGE_BUILDER_API } from '../../../constants'; +import { emptyGetBlueprints } from '../../fixtures/blueprints'; +import { server } from '../../mocks/server'; +import { renderWithReduxRouter } from '../../testUtils'; + +jest.mock('@redhat-cloud-services/frontend-components/useChrome', () => ({ + useChrome: () => ({ + isBeta: () => false, + isProd: () => true, + getEnvironment: () => 'prod', + }), +})); + +jest.mock('@unleash/proxy-client-react', () => ({ + useUnleashContext: () => jest.fn(), + useFlag: jest.fn((flag) => + flag === 'image-builder.new-wizard.enabled' ? true : false + ), +})); + +describe('Blueprints', () => { + test('renders blueprints page', async () => { + renderWithReduxRouter('', {}); + await screen.findByText('Dark Chocolate'); + }); + test('renders blueprint empty state', async () => { + server.use( + rest.get( + `${IMAGE_BUILDER_API}/experimental/blueprints`, + (req, res, ctx) => { + return res(ctx.status(200), ctx.json(emptyGetBlueprints)); + } + ) + ); + + renderWithReduxRouter('', {}); + await screen.findByText('No blueprints yet'); + }); +}); diff --git a/src/test/Components/CreateImageWizard/CreateImageWizard.test.js b/src/test/Components/CreateImageWizard/CreateImageWizard.test.js index 274d5b89..1dc618af 100644 --- a/src/test/Components/CreateImageWizard/CreateImageWizard.test.js +++ b/src/test/Components/CreateImageWizard/CreateImageWizard.test.js @@ -114,7 +114,7 @@ describe('Create Image Wizard', () => { test('renders component', async () => { renderCustomRoutesWithReduxRouter('imagewizard', {}, routes); // check heading - await screen.findByRole('heading', { name: /Image Builder/ }); + await screen.findByRole('heading', { name: /Images/ }); await screen.findByRole('button', { name: 'Image output' }); await screen.findByRole('button', { name: 'Register' }); diff --git a/src/test/Components/CreateImageWizardV2/CreateImageWizard.test.tsx b/src/test/Components/CreateImageWizardV2/CreateImageWizard.test.tsx index 9ea3f11f..4165c845 100644 --- a/src/test/Components/CreateImageWizardV2/CreateImageWizard.test.tsx +++ b/src/test/Components/CreateImageWizardV2/CreateImageWizard.test.tsx @@ -115,7 +115,7 @@ describe('Create Image Wizard', () => { test('renders component', async () => { renderCustomRoutesWithReduxRouter('imagewizard', {}, routes); // check heading - await screen.findByRole('heading', { name: /Image Builder/ }); + await screen.findByRole('heading', { name: /Images/ }); await screen.findByRole('button', { name: 'Image output' }); await screen.findByRole('button', { name: 'Register' }); diff --git a/src/test/Components/LandingPage/LandingPage.test.js b/src/test/Components/LandingPage/LandingPage.test.js index d7ac4a76..0c471814 100644 --- a/src/test/Components/LandingPage/LandingPage.test.js +++ b/src/test/Components/LandingPage/LandingPage.test.js @@ -24,7 +24,7 @@ describe('Landing Page', () => { renderWithReduxRouter('', {}); // check heading - await screen.findByRole('heading', { name: /Image Builder/i }); + await screen.findByRole('heading', { name: /Images/i }); }); test('renders EmptyState child component', async () => { diff --git a/src/test/fixtures/blueprints.ts b/src/test/fixtures/blueprints.ts new file mode 100644 index 00000000..20750dfd --- /dev/null +++ b/src/test/fixtures/blueprints.ts @@ -0,0 +1,37 @@ +import { + GetBlueprintsApiResponse, + CreateBlueprintResponse, +} from '../../store/imageBuilderApi'; + +export const mockBlueprintsCreation: CreateBlueprintResponse[] = [ + { + id: '677b010b-e95e-4694-9813-d11d847f1bfc', + }, +]; + +export const mockGetBlueprints: GetBlueprintsApiResponse = { + links: { first: 'first', last: 'last' }, + meta: { count: 2 }, + data: [ + { + id: '677b010b-e95e-4694-9813-d11d847f1bfc', + name: 'Dark Chocolate', + description: '70% Dark Chocolate with crunchy cocoa nibs', + version: 1, + last_modified_at: '2021-09-09T14:38:00.000Z', + }, + { + id: '677b0101-e952-4694-9813-d11d847f1bfc', + name: 'Milk Chocolate', + description: '40% Milk Chocolate with salted caramel', + version: 1, + last_modified_at: '2021-09-08T14:38:00.000Z', + }, + ], +}; + +export const emptyGetBlueprints: GetBlueprintsApiResponse = { + links: { first: 'first', last: 'last' }, + meta: { count: 0 }, + data: [], +}; diff --git a/src/test/mocks/handlers.js b/src/test/mocks/handlers.js index 193f2cfe..7d609204 100644 --- a/src/test/mocks/handlers.js +++ b/src/test/mocks/handlers.js @@ -11,6 +11,7 @@ import { mockActivationKeysResults, } from '../fixtures/activationKeys'; import { mockArchitecturesByDistro } from '../fixtures/architectures'; +import { mockGetBlueprints } from '../fixtures/blueprints'; import { composesEndpoint, mockClones, @@ -107,4 +108,7 @@ export const handlers = [ return res(ctx.status(200), ctx.json(oscapCustomizations(profile))); } ), + rest.get(`${IMAGE_BUILDER_API}/experimental/blueprints`, (req, res, ctx) => { + return res(ctx.status(200), ctx.json(mockGetBlueprints)); + }), ];