diff --git a/src/Components/ImagesTable/ClonesTable.js b/src/Components/ImagesTable/ClonesTable.js index b6db495b..0cb6b091 100644 --- a/src/Components/ImagesTable/ClonesTable.js +++ b/src/Components/ImagesTable/ClonesTable.js @@ -1,5 +1,6 @@ import React from 'react'; +import { ClipboardCopy } from '@patternfly/react-core'; import { TableComposable, Tbody, @@ -13,7 +14,6 @@ import { useSelector } from 'react-redux'; import { ImageBuildStatus } from './ImageBuildStatus'; -import { useGetAWSSourcesQuery } from '../../store/apiSlice'; import { selectClonesById, selectComposeById, @@ -22,26 +22,21 @@ import { const Row = ({ imageId }) => { const image = useSelector((state) => selectImageById(state, imageId)); - const { data: awsSources, isSuccess } = useGetAWSSourcesQuery(); - - const getAccount = (image) => { - if (image.share_with_sources?.[0]) { - if (isSuccess) { - const accountId = awsSources.find( - (source) => source.id === image.share_with_sources[0] - )?.account_id; - return accountId; - } - return null; - } - return image.share_with_accounts?.[0]; - }; return ( - - {image.id} - {getAccount(image)} + + + {image.status === 'success' && ( + + {image.ami} + + )} + {image.region} @@ -58,15 +53,10 @@ const ClonesTable = ({ composeId }) => { const clones = useSelector((state) => selectClonesById(state, composeId)); return ( - + - UUID - Account + AMI Region Status diff --git a/src/Components/ImagesTable/ImageDetails.js b/src/Components/ImagesTable/ImageDetails.js new file mode 100644 index 00000000..e359b14d --- /dev/null +++ b/src/Components/ImagesTable/ImageDetails.js @@ -0,0 +1,410 @@ +import React from 'react'; + +import { + ClipboardCopy, + DescriptionList, + DescriptionListGroup, + DescriptionListDescription, + DescriptionListTerm, + Button, + Spinner, + Popover, + Alert, +} from '@patternfly/react-core'; +import { ExternalLinkAltIcon } from '@patternfly/react-icons'; +import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; + +import ClonesTable from './ClonesTable'; + +import { + useGetAWSSourcesQuery, + useGetAzureSourcesQuery, +} from '../../store/apiSlice'; + +const sourceNotFoundPopover = () => { + return ( + + +

+ The information about the source cannot be loaded. Please check the + source was not removed and try again later. +

+
+ + + } + > + +
+ ); +}; + +const getAzureSourceName = (id) => { + const { data: sources, isSuccess } = useGetAzureSourcesQuery(); + + if (isSuccess) { + const sourcename = sources.find((source) => source.id === id); + if (sourcename) { + return sourcename.name; + } else { + return sourceNotFoundPopover(); + } + } else { + return ; + } +}; + +const getAWSSourceName = (id) => { + const { data: sources, isSuccess } = useGetAWSSourcesQuery(); + + if (isSuccess) { + const sourcename = sources.find((source) => source.id === id); + if (sourcename) { + return sourcename.name; + } else { + return sourceNotFoundPopover(); + } + } else { + return ; + } +}; + +const parseGCPSharedWith = (sharedWith) => { + const splitGCPSharedWith = sharedWith[0].split(':'); + return splitGCPSharedWith[1]; +}; + +const AWSDetails = ({ id }) => { + const composes = useSelector((state) => state.composes); + const compose = composes.byId[id]; + + return ( + + + UUID + + + {id} + + + + {compose.request.image_requests[0].upload_request.options + .share_with_sources && ( + + Source + + {getAWSSourceName( + compose.request.image_requests[0].upload_request.options + .share_with_sources?.[0] + )} + + + )} + {compose.request.image_requests[0].upload_request.options + .share_with_accounts?.[0] && ( + + Shared with + + + + + )} + + ); +}; + +const AWSIdentifiers = ({ id }) => { + return ; +}; + +const AzureDetails = ({ id }) => { + const composes = useSelector((state) => state.composes); + const compose = composes.byId[id]; + + return ( + <> + + + UUID + + + {id} + + + + {compose.request.image_requests[0].upload_request.options.source_id && ( + + Source + + {getAzureSourceName( + compose.request.image_requests[0].upload_request.options + .source_id + )} + + + )} + + Resource Group + + { + compose.request.image_requests[0].upload_request.options + .resource_group + } + + + + + ); +}; + +const AzureIdentifiers = ({ id }) => { + const composes = useSelector((state) => state.composes); + const compose = composes.byId[id]; + + return ( + <> + + + Image name + + {compose?.image_status?.status === 'success' ? ( + + {compose.image_status.upload_status.options.image_name} + + ) : ( + + )} + + + + + ); +}; + +const GCPDetails = ({ id, sharedWith }) => { + const composes = useSelector((state) => state.composes); + const compose = composes.byId[id]; + + return ( + <> + + + UUID + + + {id} + + + + {compose?.image_status?.status === 'success' && ( + + Project ID + + {compose.image_status.upload_status.options.project_id} + + + )} + {sharedWith && ( + + Shared with + + {parseGCPSharedWith(sharedWith)} + + + )} + + + ); +}; + +const GCPIdentifiers = ({ id }) => { + const composes = useSelector((state) => state.composes); + const compose = composes.byId[id]; + + return ( + <> + + + Image name + + {compose?.image_status?.status === 'success' ? ( + + {compose.image_status.upload_status.options.image_name} + + ) : compose?.image_status?.status === 'failure' ? ( +

+ ) : ( + + )} +
+
+
+ + ); +}; + +const ImageDetails = ({ id }) => { + const composes = useSelector((state) => state.composes); + const compose = composes.byId[id]; + + return ( + <> +
Build Information
+ { + // the information about the image's target differs between images + // built by api and images built by the service + (compose.request.image_requests[0].image_type === 'aws' || + compose?.image_status?.upload_status?.type === 'aws') && ( + + ) + } + {(compose.request.image_requests[0].image_type === 'azure' || + compose?.image_status?.upload_status?.type === 'azure') && ( + + )} + {(compose.request.image_requests[0].image_type === 'gcp' || + compose?.image_status?.upload_status?.type === 'gcp') && ( + + )} + {(compose.request.image_requests[0].image_type === 'guest-image' || + compose.request.image_requests[0].image_type === 'image-installer' || + compose.request.image_requests[0].image_type === 'vsphere' || + compose.request.image_requests[0].image_type === + 'rhel-edge-installer' || + compose.request.image_requests[0].image_type === + 'rhel-edge-commit') && ( + + + UUID + + + {id} + + + + + )} + {(compose.request.image_requests[0].image_type === 'aws' || + compose?.image_status?.upload_status?.type === 'aws' || + compose.request.image_requests[0].image_type === 'gcp' || + compose?.image_status?.upload_status?.type === 'gcp' || + compose.request.image_requests[0].image_type === 'azure' || + compose?.image_status?.upload_status?.type === 'azure') && ( + <> +
+
+ Cloud Provider Identifiers +
+ + )} + {(compose.request.image_requests[0].image_type === 'aws' || + compose?.image_status?.upload_status?.type === 'aws') && ( + + )} + {(compose.request.image_requests[0].image_type === 'azure' || + compose?.image_status?.upload_status?.type === 'azure') && ( + + )} + {(compose.request.image_requests[0].image_type === 'gcp' || + compose?.image_status?.upload_status?.type === 'gcp') && ( + + )} + + ); +}; + +AWSDetails.propTypes = { + id: PropTypes.string, +}; + +AWSIdentifiers.propTypes = { + id: PropTypes.string, +}; + +AzureDetails.propTypes = { + id: PropTypes.string, +}; + +AzureIdentifiers.propTypes = { + id: PropTypes.string, +}; + +GCPDetails.propTypes = { + id: PropTypes.string, + sharedWith: PropTypes.arrayOf(PropTypes.string), +}; + +GCPIdentifiers.propTypes = { + id: PropTypes.string, +}; + +ImageDetails.propTypes = { + id: PropTypes.string, +}; + +export default ImageDetails; diff --git a/src/Components/ImagesTable/ImagesTable.js b/src/Components/ImagesTable/ImagesTable.js index 631ed7cd..78879054 100644 --- a/src/Components/ImagesTable/ImagesTable.js +++ b/src/Components/ImagesTable/ImagesTable.js @@ -29,8 +29,8 @@ import { useDispatch, useSelector } from 'react-redux'; import { Link, useNavigate } from 'react-router-dom'; import './ImagesTable.scss'; -import ClonesTable from './ClonesTable'; import { ImageBuildStatus } from './ImageBuildStatus'; +import ImageDetails from './ImageDetails'; import ImageLink from './ImageLink'; import Release from './Release'; import Target from './Target'; @@ -277,15 +277,9 @@ const ImagesTable = () => { - {compose.request.image_requests[0].upload_request - .type === 'aws' ? ( - - ) : ( - - UUID -
{id}
-
- )} + + + diff --git a/src/test/Components/ImagesTable/ImagesTable.test.js b/src/test/Components/ImagesTable/ImagesTable.test.js index 4af568b8..7d0ff927 100644 --- a/src/test/Components/ImagesTable/ImagesTable.test.js +++ b/src/test/Components/ImagesTable/ImagesTable.test.js @@ -198,17 +198,11 @@ describe('Images Table', () => { name: /details/i, }); - expect( - screen.getAllByText(/1579d95b-8f1d-4982-8c53-8c2afa4ab04c/i)[1] - ).not.toBeVisible(); + expect(screen.getByText(/ami-0e778053cd490ad21/i)).not.toBeVisible(); await user.click(toggleButton); - expect( - screen.getAllByText(/1579d95b-8f1d-4982-8c53-8c2afa4ab04c/i)[1] - ).toBeVisible(); + expect(screen.getByText(/ami-0e778053cd490ad21/i)).toBeVisible(); await user.click(toggleButton); - expect( - screen.getAllByText(/1579d95b-8f1d-4982-8c53-8c2afa4ab04c/i)[1] - ).not.toBeVisible(); + expect(screen.getByText(/ami-0e778053cd490ad21/i)).not.toBeVisible(); }); test('check error details', async () => { @@ -245,7 +239,7 @@ describe('Images Table Toolbar', () => { describe('Clones table', () => { test('renders clones table', async () => { - const view = renderWithReduxRouter('', {}); + renderWithReduxRouter('', {}); const table = await screen.findByTestId('images-table'); @@ -253,8 +247,6 @@ describe('Clones table', () => { const emptyState = screen.queryByTestId('empty-state'); expect(emptyState).not.toBeInTheDocument(); - const state = view.store.getState(); - // get rows const { getAllByRole } = within(table); const rows = getAllByRole('row'); @@ -272,29 +264,25 @@ describe('Clones table', () => { // remove first row from list since it is just header labels const header = cloneRows.shift(); // test the header has correct labels - expect(header.cells[0]).toHaveTextContent('UUID'); - expect(header.cells[1]).toHaveTextContent('Account'); - expect(header.cells[2]).toHaveTextContent('Region'); - expect(header.cells[3]).toHaveTextContent('Status'); + expect(header.cells[0]).toHaveTextContent('AMI'); + expect(header.cells[1]).toHaveTextContent('Region'); + expect(header.cells[2]).toHaveTextContent('Status'); - expect(cloneRows).toHaveLength(5); + // shift by a parent compose as the row has a different format + cloneRows.shift(); + + expect(cloneRows).toHaveLength(4); // prepend parent data const composeId = '1579d95b-8f1d-4982-8c53-8c2afa4ab04c'; const clonesTableData = { - uuid: [composeId, ...mockClones(composeId).data.map((clone) => clone.id)], - created: [ - '2021-04-27 12:31:12.794809 +0000 UTC', - ...mockClones(composeId).data.map((clone) => clone.created_at), - ], - account: [ - '123123123123', + ami: [ ...mockClones(composeId).data.map( - (clone) => clone.request.share_with_accounts[0] + (clone) => mockCloneStatus[clone.id].options.ami ), ], + created: [...mockClones(composeId).data.map((clone) => clone.created_at)], region: [ - 'us-east-1', ...mockClones(composeId).data.map( (clone) => mockCloneStatus[clone.id].options.region ), @@ -302,25 +290,28 @@ describe('Clones table', () => { }; for (const [index, row] of cloneRows.entries()) { - // render UUIDs in correct order - expect(row.cells[0]).toHaveTextContent(clonesTableData.uuid[index]); - - // account cell - expect(row.cells[1]).toHaveTextContent(clonesTableData.account[index]); + // render AMIs in correct order + switch (index) { + case (0, 1, 3): + expect(row.cells[0]).toHaveTextContent(clonesTableData.ami[index]); + break; + case 2: + expect(row.cells[0]).toHaveTextContent(''); + break; + } // region cell - expect(row.cells[2]).toHaveTextContent(clonesTableData.region[index]); - - const testElement = document.createElement('testElement'); - const imageId = clonesTableData.uuid[index]; + expect(row.cells[1]).toHaveTextContent(clonesTableData.region[index]); // status cell - renderWithProvider( - , - testElement, - state - ); - expect(row.cells[3]).toHaveTextContent(testElement.textContent); + switch (index) { + case (0, 1, 3): + expect(row.cells[2]).toHaveTextContent('Ready'); + break; + case 2: + expect(row.cells[2]).toHaveTextContent('Image build failed'); + break; + } } }); });