ImagesTable: Convert ImagesTable to Typescript & RTK Query
This commit converts the Images Table to Typescript and converts all API calls to image-builder to use RTK Query hooks. This should increase the performance of the app significantly. Previously our calls to the image-builder API were made in series. They are now made in parallel. We may want to investigate the possibility of hitting rate limiting now that we will be issuing requests in much more rapid succession. In the tests, moving to RTK Query hooks has allowed us to remove virtually all Jest mocking. However, this means that some of our previous tests which tested against implementation details were broken. Most notably, we no longer check the Redux store to verify that clones have been added correctly and we no longer check that compose requests were issued successfully. Test coverage will be restored in a follow-up PR where the dev-dependency @msw/data is added. Adding a persistent data layer to the tests using @msw/data will allow us to verify that our POST requests (creating composes and cloning them) are working by testing that the Images Table has been updated.
This commit is contained in:
parent
155a0cf57c
commit
7b9e726151
34 changed files with 2397 additions and 2499 deletions
|
|
@ -26,10 +26,12 @@ import {
|
|||
} from './validators';
|
||||
|
||||
import './CreateImageWizard.scss';
|
||||
import api from '../../api';
|
||||
import { UNIT_GIB, UNIT_KIB, UNIT_MIB } from '../../constants';
|
||||
import { composeAdded } from '../../store/composesSlice';
|
||||
import { useGetArchitecturesQuery } from '../../store/imageBuilderApi';
|
||||
import {
|
||||
useComposeImageMutation,
|
||||
useGetArchitecturesQuery,
|
||||
useGetComposeStatusQuery,
|
||||
} from '../../store/imageBuilderApi';
|
||||
import isRhel from '../../Utilities/isRhel';
|
||||
import { resolveRelPath } from '../../Utilities/path';
|
||||
import { useGetEnvironment } from '../../Utilities/useGetEnvironment';
|
||||
|
|
@ -510,17 +512,19 @@ const formStepHistory = (composeRequest, contentSourcesEnabled) => {
|
|||
};
|
||||
|
||||
const CreateImageWizard = () => {
|
||||
const [composeImage] = useComposeImageMutation();
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
// composeId is an optional param that is used for Recreate image
|
||||
const { composeId } = useParams();
|
||||
|
||||
// This is a bit awkward, but will be replaced with an RTKQ hook very soon
|
||||
// We use useStore() instead of useSelector() because we do not want changes to
|
||||
// the store to cause re-renders, as the composeId (if present) will never change
|
||||
const { getState } = useStore();
|
||||
const compose = getState().composes?.byId?.[composeId];
|
||||
const composeRequest = compose?.request;
|
||||
const { data } = useGetComposeStatusQuery(
|
||||
{ composeId: composeId },
|
||||
{
|
||||
skip: composeId ? false : true,
|
||||
}
|
||||
);
|
||||
const composeRequest = composeId ? data?.request : undefined;
|
||||
const contentSourcesEnabled = useFlag('image-builder.enable-content-sources');
|
||||
|
||||
// TODO: This causes an annoying re-render when using Recreate image
|
||||
|
|
@ -534,7 +538,7 @@ const CreateImageWizard = () => {
|
|||
|
||||
// Assume that if a request is available that we should start on review step
|
||||
// This will occur if 'Recreate image' is clicked
|
||||
const initialStep = compose?.request ? 'review' : undefined;
|
||||
const initialStep = composeRequest ? 'review' : undefined;
|
||||
|
||||
const { isBeta, isProd } = useGetEnvironment();
|
||||
|
||||
|
|
@ -566,27 +570,19 @@ const CreateImageWizard = () => {
|
|||
<ImageCreator
|
||||
onClose={handleClose}
|
||||
onSubmit={({ values, setIsSaving }) => {
|
||||
setIsSaving(() => true);
|
||||
setIsSaving(true);
|
||||
const requests = onSave(values);
|
||||
navigate(resolveRelPath(''));
|
||||
// https://redux-toolkit.js.org/rtk-query/usage/mutations#frequently-used-mutation-hook-return-values
|
||||
// If you want to immediately access the result of a mutation, you need to chain `.unwrap()`
|
||||
// if you actually want the payload or to catch the error.
|
||||
// We do this so we can dispatch the appropriate notification (success or failure).
|
||||
Promise.all(
|
||||
requests.map((request) =>
|
||||
api.composeImage(request).then((response) => {
|
||||
dispatch(
|
||||
composeAdded({
|
||||
compose: {
|
||||
...response,
|
||||
request,
|
||||
created_at: currentDate.toISOString(),
|
||||
image_status: { status: 'pending' },
|
||||
},
|
||||
insert: true,
|
||||
})
|
||||
);
|
||||
})
|
||||
requests.map((composeRequest) =>
|
||||
composeImage({ composeRequest }).unwrap()
|
||||
)
|
||||
)
|
||||
.then(() => {
|
||||
navigate(resolveRelPath(''));
|
||||
dispatch(
|
||||
addNotification({
|
||||
variant: 'success',
|
||||
|
|
@ -607,8 +603,6 @@ const CreateImageWizard = () => {
|
|||
description: 'Status code ' + err.response.status + ': ' + msg,
|
||||
})
|
||||
);
|
||||
|
||||
setIsSaving(false);
|
||||
});
|
||||
}}
|
||||
defaultArch="x86_64"
|
||||
|
|
|
|||
|
|
@ -1,84 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { ClipboardCopy } from '@patternfly/react-core';
|
||||
import {
|
||||
TableComposable,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
} from '@patternfly/react-table';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { ImageBuildStatus } from './ImageBuildStatus';
|
||||
|
||||
import {
|
||||
selectClonesById,
|
||||
selectComposeById,
|
||||
selectImageById,
|
||||
} from '../../store/composesSlice';
|
||||
|
||||
const Row = ({ imageId }) => {
|
||||
const image = useSelector((state) => selectImageById(state, imageId));
|
||||
|
||||
if (!image) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tbody>
|
||||
<Tr className="no-bottom-border">
|
||||
<Td dataLabel="AMI">
|
||||
{image.status === 'success' && (
|
||||
<ClipboardCopy
|
||||
hoverTip="Copy"
|
||||
clickTip="Copied"
|
||||
variant="inline-compact"
|
||||
>
|
||||
{image.ami}
|
||||
</ClipboardCopy>
|
||||
)}
|
||||
</Td>
|
||||
<Td dataLabel="Region">{image.region}</Td>
|
||||
<Td dataLabel="Status">
|
||||
<ImageBuildStatus imageId={image.id} imageRegion={image.region} />
|
||||
</Td>
|
||||
</Tr>
|
||||
</Tbody>
|
||||
);
|
||||
};
|
||||
|
||||
const ClonesTable = ({ composeId }) => {
|
||||
const parentCompose = useSelector((state) =>
|
||||
selectComposeById(state, composeId)
|
||||
);
|
||||
const clones = useSelector((state) => selectClonesById(state, composeId));
|
||||
|
||||
return (
|
||||
<TableComposable variant="compact" data-testid="clones-table">
|
||||
<Thead>
|
||||
<Tr className="no-bottom-border">
|
||||
<Th className="pf-m-width-60">AMI</Th>
|
||||
<Th className="pf-m-width-20">Region</Th>
|
||||
<Th className="pf-m-width-20">Status</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Row imageId={parentCompose.id} imageType={'compose'} />
|
||||
{clones.map((clone) => (
|
||||
<Row imageId={clone.id} key={clone.id} />
|
||||
))}
|
||||
</TableComposable>
|
||||
);
|
||||
};
|
||||
|
||||
Row.propTypes = {
|
||||
imageId: PropTypes.string,
|
||||
};
|
||||
|
||||
ClonesTable.propTypes = {
|
||||
composeId: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ClonesTable;
|
||||
158
src/Components/ImagesTable/ClonesTable.tsx
Normal file
158
src/Components/ImagesTable/ClonesTable.tsx
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { ClipboardCopy, Skeleton } from '@patternfly/react-core';
|
||||
import {
|
||||
TableComposable,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
} from '@patternfly/react-table';
|
||||
|
||||
import { StatusClone, AwsDetailsStatus } from './Status';
|
||||
|
||||
import {
|
||||
ClonesResponseItem,
|
||||
ComposesResponseItem,
|
||||
UploadStatus,
|
||||
useGetCloneStatusQuery,
|
||||
useGetComposeClonesQuery,
|
||||
useGetComposeStatusQuery,
|
||||
} from '../../store/imageBuilderApi';
|
||||
|
||||
type RowPropTypes = {
|
||||
ami: JSX.Element;
|
||||
region: JSX.Element;
|
||||
status: JSX.Element;
|
||||
};
|
||||
|
||||
type AmiPropTypes = {
|
||||
status: UploadStatus | undefined;
|
||||
};
|
||||
|
||||
const Ami = ({ status }: AmiPropTypes) => {
|
||||
switch (status?.status) {
|
||||
case 'success':
|
||||
return (
|
||||
<ClipboardCopy
|
||||
hoverTip="Copy"
|
||||
clickTip="Copied"
|
||||
variant="inline-compact"
|
||||
>
|
||||
{'ami' in status.options ? status.options.ami : null}
|
||||
</ClipboardCopy>
|
||||
);
|
||||
|
||||
case 'failure':
|
||||
return undefined;
|
||||
|
||||
default:
|
||||
return <Skeleton width="12rem" />;
|
||||
}
|
||||
};
|
||||
|
||||
const ComposeRegion = () => {
|
||||
return <p>us-east-1</p>;
|
||||
};
|
||||
|
||||
type CloneRegionPropTypes = {
|
||||
region: string;
|
||||
};
|
||||
|
||||
const CloneRegion = ({ region }: CloneRegionPropTypes) => {
|
||||
return <p>{region}</p>;
|
||||
};
|
||||
|
||||
const Row = ({ ami, region, status }: RowPropTypes) => {
|
||||
return (
|
||||
<Tbody>
|
||||
<Tr className="no-bottom-border">
|
||||
<Td dataLabel="AMI">{ami}</Td>
|
||||
<Td dataLabel="Region">{region}</Td>
|
||||
<Td dataLabel="Status">{status}</Td>
|
||||
</Tr>
|
||||
</Tbody>
|
||||
);
|
||||
};
|
||||
|
||||
type CloneRowPropTypes = {
|
||||
clone: ClonesResponseItem;
|
||||
};
|
||||
|
||||
const CloneRow = ({ clone }: CloneRowPropTypes) => {
|
||||
const [pollingInterval, setPollingInterval] = useState(8000);
|
||||
|
||||
const { data: status } = useGetCloneStatusQuery(
|
||||
{
|
||||
id: clone.id,
|
||||
},
|
||||
{ pollingInterval: pollingInterval }
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (status?.status === 'success' || status?.status === 'failure') {
|
||||
setPollingInterval(0);
|
||||
} else {
|
||||
setPollingInterval(8000);
|
||||
}
|
||||
}, [setPollingInterval, status]);
|
||||
|
||||
return (
|
||||
<Row
|
||||
ami={<Ami status={status} />}
|
||||
region={<CloneRegion region={clone.request.region} />}
|
||||
status={<StatusClone clone={clone} status={status} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type ComposeRowPropTypes = {
|
||||
compose: ComposesResponseItem;
|
||||
};
|
||||
|
||||
const ComposeRow = ({ compose }: ComposeRowPropTypes) => {
|
||||
const { data, isSuccess } = useGetComposeStatusQuery({
|
||||
composeId: compose.id,
|
||||
});
|
||||
return isSuccess ? (
|
||||
<Row
|
||||
ami={<Ami status={data.image_status.upload_status} />}
|
||||
region={<ComposeRegion />}
|
||||
status={<AwsDetailsStatus compose={compose} />}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export type ReducedClonesByRegion = {
|
||||
[region: string]: {
|
||||
clone: ClonesResponseItem;
|
||||
status: UploadStatus | undefined;
|
||||
};
|
||||
};
|
||||
|
||||
type ClonesTablePropTypes = {
|
||||
compose: ComposesResponseItem;
|
||||
};
|
||||
|
||||
const ClonesTable = ({ compose }: ClonesTablePropTypes) => {
|
||||
const { data } = useGetComposeClonesQuery({ composeId: compose.id });
|
||||
|
||||
return (
|
||||
<TableComposable variant="compact" data-testid="clones-table">
|
||||
<Thead>
|
||||
<Tr className="no-bottom-border">
|
||||
<Th className="pf-m-width-60">AMI</Th>
|
||||
<Th className="pf-m-width-20">Region</Th>
|
||||
<Th className="pf-m-width-20">Status</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<ComposeRow compose={compose} />
|
||||
{data?.data.map((clone) => (
|
||||
<CloneRow clone={clone} key={clone.id} />
|
||||
))}
|
||||
</TableComposable>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClonesTable;
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Button } from '@patternfly/react-core';
|
||||
import { CopyIcon } from '@patternfly/react-icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const useGetErrorReason = (err) => {
|
||||
if (!err?.reason) {
|
||||
return 'An unknown error occured';
|
||||
}
|
||||
|
||||
if (err.details?.reason) {
|
||||
return err.details.reason;
|
||||
}
|
||||
|
||||
return err.reason;
|
||||
};
|
||||
|
||||
const ErrorDetails = ({ status }) => {
|
||||
const reason = useGetErrorReason(status.error);
|
||||
|
||||
if (!status || status.status !== 'failure') {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pf-u-mt-sm">
|
||||
<p>{reason}</p>
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => navigator.clipboard.writeText(reason)}
|
||||
className="pf-u-pl-0 pf-u-mt-md"
|
||||
>
|
||||
Copy error text to clipboard <CopyIcon />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ErrorDetails.propTypes = {
|
||||
status: PropTypes.object,
|
||||
};
|
||||
|
||||
export default ErrorDetails;
|
||||
|
|
@ -1,216 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Flex,
|
||||
Panel,
|
||||
PanelMain,
|
||||
Popover,
|
||||
} from '@patternfly/react-core';
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ExclamationCircleIcon,
|
||||
ExclamationTriangleIcon,
|
||||
InProgressIcon,
|
||||
OffIcon,
|
||||
PendingIcon,
|
||||
} from '@patternfly/react-icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ImageBuildStatus.scss';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import ErrorDetails from './ImageBuildErrorDetails';
|
||||
|
||||
import { AWS_S3_EXPIRATION_TIME_IN_HOURS } from '../../constants';
|
||||
import {
|
||||
selectImageById,
|
||||
selectImageStatusesById,
|
||||
} from '../../store/composesSlice';
|
||||
import { hoursToExpiration } from '../../Utilities/time';
|
||||
|
||||
export const ImageBuildStatus = ({
|
||||
imageId,
|
||||
isImagesTableRow,
|
||||
imageStatus,
|
||||
imageRegion,
|
||||
}) => {
|
||||
const image = useSelector((state) => selectImageById(state, imageId));
|
||||
|
||||
const remainingHours =
|
||||
AWS_S3_EXPIRATION_TIME_IN_HOURS - hoursToExpiration(image.created_at);
|
||||
|
||||
const cloneErrorMessage = () => {
|
||||
let region = '';
|
||||
hasFailedClone ? (region = 'one or more regions') : (region = imageRegion);
|
||||
return {
|
||||
error: {
|
||||
reason: `Failed to share image to ${region}.`,
|
||||
},
|
||||
status: 'failure',
|
||||
};
|
||||
};
|
||||
|
||||
// Messages appear in order of priority
|
||||
const messages = {
|
||||
failure: [
|
||||
{
|
||||
icon: <ExclamationCircleIcon className="error" />,
|
||||
text: 'Image build failed',
|
||||
priority: 6,
|
||||
},
|
||||
],
|
||||
pending: [
|
||||
{
|
||||
icon: <PendingIcon />,
|
||||
text: 'Image build is pending',
|
||||
priority: 2,
|
||||
},
|
||||
],
|
||||
// Keep "running" for backward compatibility
|
||||
running: [
|
||||
{
|
||||
icon: <InProgressIcon className="pending" />,
|
||||
text: 'Image build in progress',
|
||||
priority: 1,
|
||||
},
|
||||
],
|
||||
building: [
|
||||
{
|
||||
icon: <InProgressIcon className="pending" />,
|
||||
text: 'Image build in progress',
|
||||
priority: 3,
|
||||
},
|
||||
],
|
||||
uploading: [
|
||||
{
|
||||
icon: <InProgressIcon className="pending" />,
|
||||
text: 'Image upload in progress',
|
||||
priority: 4,
|
||||
},
|
||||
],
|
||||
registering: [
|
||||
{
|
||||
icon: <InProgressIcon className="pending" />,
|
||||
text: 'Cloud registration in progress',
|
||||
priority: 5,
|
||||
},
|
||||
],
|
||||
success: [
|
||||
{
|
||||
icon: <CheckCircleIcon className="success" />,
|
||||
text: 'Ready',
|
||||
priority: 0,
|
||||
},
|
||||
],
|
||||
expiring: [
|
||||
{
|
||||
icon: <ExclamationTriangleIcon className="expiring" />,
|
||||
text: `Expires in ${remainingHours} ${
|
||||
remainingHours > 1 ? 'hours' : 'hour'
|
||||
}`,
|
||||
},
|
||||
],
|
||||
expired: [
|
||||
{
|
||||
icon: <OffIcon />,
|
||||
text: 'Expired',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let hasFailedClone;
|
||||
let status;
|
||||
const imageStatuses = useSelector((state) =>
|
||||
selectImageStatusesById(state, image.id)
|
||||
);
|
||||
if (
|
||||
isImagesTableRow &&
|
||||
(image.imageType === 'aws' || image.imageType === 'ami')
|
||||
) {
|
||||
// The ImageBuildStatus component is used by both the images table and the clones table.
|
||||
// For 'aws' and 'ami' image rows in the images table, the highest priority status for
|
||||
// *all* images (the parent image and its clones) should be displayed as the status.
|
||||
// For instance, the parent and several of its clones may have a success status. But if a single
|
||||
// clone has a failure status, then the status displayed in the images table row should be
|
||||
// failure.
|
||||
if (!imageStatuses.includes('success')) {
|
||||
hasFailedClone = false;
|
||||
} else if (!imageStatuses.includes('failure')) {
|
||||
hasFailedClone = false;
|
||||
} else {
|
||||
hasFailedClone = true;
|
||||
}
|
||||
const filteredImageStatuses = imageStatuses.filter(
|
||||
(imageStatus) => imageStatus !== undefined
|
||||
);
|
||||
if (filteredImageStatuses.length === 0) {
|
||||
status = image.status;
|
||||
} else {
|
||||
status = filteredImageStatuses.reduce((prev, current) => {
|
||||
return messages[prev][0].priority > messages[current][0].priority
|
||||
? prev
|
||||
: current;
|
||||
});
|
||||
}
|
||||
} else if (image.uploadType === 'aws.s3' && image.status === 'success') {
|
||||
// Cloud API currently reports expired images status as 'success'
|
||||
status =
|
||||
hoursToExpiration(image.created_at) >= AWS_S3_EXPIRATION_TIME_IN_HOURS
|
||||
? 'expired'
|
||||
: 'expiring';
|
||||
} else {
|
||||
status = image.status;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{messages[status] &&
|
||||
messages[status].map((message, key) => (
|
||||
<Flex key={key} className="pf-u-align-items-baseline pf-m-nowrap">
|
||||
<div className="pf-u-mr-sm">{message.icon}</div>
|
||||
{status === 'failure' ? (
|
||||
<Popover
|
||||
position="bottom"
|
||||
minWidth="30rem"
|
||||
bodyContent={
|
||||
<>
|
||||
<Alert
|
||||
variant="danger"
|
||||
title="Image build failed"
|
||||
isInline
|
||||
isPlain
|
||||
/>
|
||||
<Panel isScrollable>
|
||||
<PanelMain maxHeight="25rem">
|
||||
<ErrorDetails
|
||||
status={
|
||||
!imageStatus || hasFailedClone
|
||||
? cloneErrorMessage()
|
||||
: imageStatus
|
||||
}
|
||||
/>
|
||||
</PanelMain>
|
||||
</Panel>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button variant="link" className="pf-u-p-0 pf-u-font-size-sm">
|
||||
<div className="failure-button">{message.text}</div>
|
||||
</Button>
|
||||
</Popover>
|
||||
) : (
|
||||
message.text
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
ImageBuildStatus.propTypes = {
|
||||
imageId: PropTypes.string,
|
||||
isImagesTableRow: PropTypes.bool,
|
||||
imageStatus: PropTypes.object,
|
||||
imageRegion: PropTypes.string,
|
||||
};
|
||||
|
|
@ -1,430 +0,0 @@
|
|||
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 { extractProvisioningList } from '../../store/helpers';
|
||||
import { useGetSourceListQuery } from '../../store/provisioningApi';
|
||||
|
||||
const SourceNotFoundPopover = () => {
|
||||
return (
|
||||
<Popover
|
||||
position="bottom"
|
||||
bodyContent={
|
||||
<>
|
||||
<Alert
|
||||
variant="danger"
|
||||
title="Source name cannot be loaded"
|
||||
className="pf-u-pb-md"
|
||||
isInline
|
||||
isPlain
|
||||
/>
|
||||
<p>
|
||||
The information about the source cannot be loaded. Please check the
|
||||
source was not removed and try again later.
|
||||
</p>
|
||||
<br />
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={'settings/sources'}
|
||||
>
|
||||
Manage sources here
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button variant="link" className="pf-u-p-0 pf-u-font-size-sm">
|
||||
<div className="failure-button">Source name cannot be loaded</div>
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
const AzureSourceName = ({ id }) => {
|
||||
const { data: rawSources, isSuccess } = useGetSourceListQuery({
|
||||
provider: 'azure',
|
||||
});
|
||||
const sources = extractProvisioningList(rawSources);
|
||||
|
||||
if (isSuccess) {
|
||||
const sourcename = sources.find((source) => source.id === id);
|
||||
if (sourcename) {
|
||||
return <p>{sourcename.name}</p>;
|
||||
} else {
|
||||
return <SourceNotFoundPopover />;
|
||||
}
|
||||
} else {
|
||||
return <Spinner isSVG size="md" />;
|
||||
}
|
||||
};
|
||||
|
||||
const AwsSourceName = ({ id }) => {
|
||||
const { data: rawSources, isSuccess } = useGetSourceListQuery({
|
||||
provider: 'aws',
|
||||
});
|
||||
const sources = extractProvisioningList(rawSources);
|
||||
|
||||
if (isSuccess) {
|
||||
const sourcename = sources.find((source) => source.id === id);
|
||||
if (sourcename) {
|
||||
return <p>{sourcename.name}</p>;
|
||||
} else {
|
||||
return <SourceNotFoundPopover />;
|
||||
}
|
||||
} else {
|
||||
return <Spinner isSVG size="md" />;
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
<DescriptionList isHorizontal isCompact className=" pf-u-pl-xl">
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>UUID</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
<ClipboardCopy
|
||||
hoverTip="Copy"
|
||||
clickTip="Copied"
|
||||
variant="inline-compact"
|
||||
ouiaId="aws-uuid"
|
||||
>
|
||||
{id}
|
||||
</ClipboardCopy>
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
{compose.request.image_requests[0].upload_request.options
|
||||
.share_with_sources && (
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>Source</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
<AwsSourceName
|
||||
id={
|
||||
compose.request.image_requests[0].upload_request.options
|
||||
.share_with_sources?.[0]
|
||||
}
|
||||
/>
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
)}
|
||||
{compose.request.image_requests[0].upload_request.options
|
||||
.share_with_accounts?.[0] && (
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>Shared with</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
// the format of an account link is taken from
|
||||
// https://docs.aws.amazon.com/signin/latest/userguide/sign-in-urls-defined.html
|
||||
href={`https://${compose.request.image_requests[0].upload_request.options.share_with_accounts[0]}.signin.aws.amazon.com/console/`}
|
||||
>
|
||||
{
|
||||
compose.request.image_requests[0].upload_request.options
|
||||
.share_with_accounts[0]
|
||||
}
|
||||
</Button>
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
)}
|
||||
</DescriptionList>
|
||||
);
|
||||
};
|
||||
|
||||
const AWSIdentifiers = ({ id }) => {
|
||||
return <ClonesTable composeId={id} />;
|
||||
};
|
||||
|
||||
const AzureDetails = ({ id }) => {
|
||||
const composes = useSelector((state) => state.composes);
|
||||
const compose = composes.byId[id];
|
||||
|
||||
return (
|
||||
<>
|
||||
<DescriptionList isHorizontal isCompact className=" pf-u-pl-xl">
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>UUID</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
<ClipboardCopy
|
||||
hoverTip="Copy"
|
||||
clickTip="Copied"
|
||||
variant="inline-compact"
|
||||
ouiaId="azure-uuid"
|
||||
>
|
||||
{id}
|
||||
</ClipboardCopy>
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
{compose.request.image_requests[0].upload_request.options.source_id && (
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>Source</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
<AzureSourceName
|
||||
id={
|
||||
compose.request.image_requests[0].upload_request.options
|
||||
.source_id
|
||||
}
|
||||
/>
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
)}
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>Resource Group</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{
|
||||
compose.request.image_requests[0].upload_request.options
|
||||
.resource_group
|
||||
}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
</DescriptionList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AzureIdentifiers = ({ id }) => {
|
||||
const composes = useSelector((state) => state.composes);
|
||||
const compose = composes.byId[id];
|
||||
|
||||
return (
|
||||
<>
|
||||
<DescriptionList isHorizontal isCompact className=" pf-u-pl-xl">
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>Image name</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{compose?.image_status?.status === 'success' ? (
|
||||
<ClipboardCopy
|
||||
hoverTip="Copy"
|
||||
clickTip="Copied"
|
||||
variant="inline-compact"
|
||||
>
|
||||
{compose.image_status.upload_status.options.image_name}
|
||||
</ClipboardCopy>
|
||||
) : compose?.image_status?.status === 'failure' ? (
|
||||
<p></p>
|
||||
) : (
|
||||
<Spinner isSVG size="md" />
|
||||
)}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
</DescriptionList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const GCPDetails = ({ id, sharedWith }) => {
|
||||
const composes = useSelector((state) => state.composes);
|
||||
const compose = composes.byId[id];
|
||||
|
||||
return (
|
||||
<>
|
||||
<DescriptionList isHorizontal isCompact className=" pf-u-pl-xl">
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>UUID</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
<ClipboardCopy
|
||||
hoverTip="Copy"
|
||||
clickTip="Copied"
|
||||
variant="inline-compact"
|
||||
ouiaId="gcp-uuid"
|
||||
>
|
||||
{id}
|
||||
</ClipboardCopy>
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
{compose?.image_status?.status === 'success' && (
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>Project ID</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{compose.image_status.upload_status.options.project_id}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
)}
|
||||
{sharedWith && (
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>Shared with</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{parseGCPSharedWith(sharedWith)}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
)}
|
||||
</DescriptionList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const GCPIdentifiers = ({ id }) => {
|
||||
const composes = useSelector((state) => state.composes);
|
||||
const compose = composes.byId[id];
|
||||
|
||||
return (
|
||||
<>
|
||||
<DescriptionList isHorizontal isCompact className=" pf-u-pl-xl">
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>Image name</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{compose?.image_status?.status === 'success' ? (
|
||||
<ClipboardCopy
|
||||
hoverTip="Copy"
|
||||
clickTip="Copied"
|
||||
variant="inline-compact"
|
||||
>
|
||||
{compose.image_status.upload_status.options.image_name}
|
||||
</ClipboardCopy>
|
||||
) : compose?.image_status?.status === 'failure' ? (
|
||||
<p></p>
|
||||
) : (
|
||||
<Spinner isSVG size="md" />
|
||||
)}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
</DescriptionList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ImageDetails = ({ id }) => {
|
||||
const composes = useSelector((state) => state.composes);
|
||||
const compose = composes.byId[id];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="pf-u-font-weight-bold pf-u-pb-md">Build Information</div>
|
||||
{
|
||||
// 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') && (
|
||||
<AWSDetails id={id} />
|
||||
)
|
||||
}
|
||||
{(compose.request.image_requests[0].image_type === 'azure' ||
|
||||
compose?.image_status?.upload_status?.type === 'azure') && (
|
||||
<AzureDetails id={id} />
|
||||
)}
|
||||
{(compose.request.image_requests[0].image_type === 'gcp' ||
|
||||
compose?.image_status?.upload_status?.type === 'gcp') && (
|
||||
<GCPDetails id={id} sharedWith={compose.share_with_accounts} />
|
||||
)}
|
||||
{(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 === 'wsl' ||
|
||||
compose.request.image_requests[0].image_type === 'vsphere' ||
|
||||
compose.request.image_requests[0].image_type === 'vsphere-ova' ||
|
||||
compose.request.image_requests[0].image_type ===
|
||||
'rhel-edge-installer' ||
|
||||
compose.request.image_requests[0].image_type ===
|
||||
'rhel-edge-commit') && (
|
||||
<DescriptionList isHorizontal isCompact className=" pf-u-pl-xl">
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>UUID</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
<ClipboardCopy
|
||||
hoverTip="Copy"
|
||||
clickTip="Copied"
|
||||
variant="inline-compact"
|
||||
ouiaId="other-targets-uuid"
|
||||
>
|
||||
{id}
|
||||
</ClipboardCopy>
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
</DescriptionList>
|
||||
)}
|
||||
{(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') && (
|
||||
<>
|
||||
<br />
|
||||
<div className="pf-u-font-weight-bold pf-u-pb-md">
|
||||
Cloud Provider Identifiers
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{(compose.request.image_requests[0].image_type === 'aws' ||
|
||||
compose?.image_status?.upload_status?.type === 'aws') && (
|
||||
<AWSIdentifiers id={id} />
|
||||
)}
|
||||
{(compose.request.image_requests[0].image_type === 'azure' ||
|
||||
compose?.image_status?.upload_status?.type === 'azure') && (
|
||||
<AzureIdentifiers id={id} />
|
||||
)}
|
||||
{(compose.request.image_requests[0].image_type === 'gcp' ||
|
||||
compose?.image_status?.upload_status?.type === 'gcp') && (
|
||||
<GCPIdentifiers id={id} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
AwsSourceName.propTypes = {
|
||||
id: PropTypes.string,
|
||||
};
|
||||
|
||||
AzureSourceName.propTypes = {
|
||||
id: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ImageDetails;
|
||||
387
src/Components/ImagesTable/ImageDetails.tsx
Normal file
387
src/Components/ImagesTable/ImageDetails.tsx
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
import React from 'react';
|
||||
|
||||
import {
|
||||
ClipboardCopy,
|
||||
DescriptionList,
|
||||
DescriptionListGroup,
|
||||
DescriptionListDescription,
|
||||
DescriptionListTerm,
|
||||
Button,
|
||||
Popover,
|
||||
Alert,
|
||||
Skeleton,
|
||||
} from '@patternfly/react-core';
|
||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||
|
||||
import ClonesTable from './ClonesTable';
|
||||
|
||||
import { extractProvisioningList } from '../../store/helpers';
|
||||
import {
|
||||
ComposesResponseItem,
|
||||
GcpUploadRequestOptions,
|
||||
useGetComposeStatusQuery,
|
||||
} from '../../store/imageBuilderApi';
|
||||
import { useGetSourceListQuery } from '../../store/provisioningApi';
|
||||
import {
|
||||
isAwsUploadRequestOptions,
|
||||
isAzureUploadRequestOptions,
|
||||
isAzureUploadStatus,
|
||||
isGcpUploadRequestOptions,
|
||||
isGcpUploadStatus,
|
||||
} from '../../store/typeGuards';
|
||||
|
||||
const SourceNotFoundPopover = () => {
|
||||
return (
|
||||
<Popover
|
||||
position="bottom"
|
||||
bodyContent={
|
||||
<>
|
||||
<Alert
|
||||
variant="danger"
|
||||
title="Source name cannot be loaded"
|
||||
className="pf-u-pb-md"
|
||||
isInline
|
||||
isPlain
|
||||
/>
|
||||
<p>
|
||||
The information about the source cannot be loaded. Please check the
|
||||
source was not removed and try again later.
|
||||
</p>
|
||||
<br />
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={'settings/sources'}
|
||||
>
|
||||
Manage sources here
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button variant="link" className="pf-u-p-0 pf-u-font-size-sm">
|
||||
<div className="failure-button">Source name cannot be loaded</div>
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
type AzureSourceNamePropTypes = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const AzureSourceName = ({ id }: AzureSourceNamePropTypes) => {
|
||||
const { data: rawSources, isSuccess } = useGetSourceListQuery({
|
||||
provider: 'azure',
|
||||
});
|
||||
|
||||
if (!isSuccess) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
const sources = extractProvisioningList(rawSources);
|
||||
|
||||
const sourcename = sources?.find((source) => source.id === id);
|
||||
if (sourcename) {
|
||||
return <p>{sourcename.name}</p>;
|
||||
} else {
|
||||
return <SourceNotFoundPopover />;
|
||||
}
|
||||
};
|
||||
|
||||
type AwsSourceNamePropTypes = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const AwsSourceName = ({ id }: AwsSourceNamePropTypes) => {
|
||||
const { data: rawSources, isSuccess } = useGetSourceListQuery({
|
||||
provider: 'aws',
|
||||
});
|
||||
|
||||
if (!isSuccess) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
const sources = extractProvisioningList(rawSources);
|
||||
|
||||
const sourcename = sources?.find((source) => source.id === id);
|
||||
if (sourcename) {
|
||||
return <p>{sourcename.name}</p>;
|
||||
} else {
|
||||
return <SourceNotFoundPopover />;
|
||||
}
|
||||
};
|
||||
|
||||
const parseGcpSharedWith = (
|
||||
sharedWith: GcpUploadRequestOptions['share_with_accounts']
|
||||
) => {
|
||||
const splitGCPSharedWith = sharedWith[0].split(':');
|
||||
return splitGCPSharedWith[1];
|
||||
};
|
||||
|
||||
type AwsDetailsPropTypes = {
|
||||
compose: ComposesResponseItem;
|
||||
};
|
||||
|
||||
export const AwsDetails = ({ compose }: AwsDetailsPropTypes) => {
|
||||
const options = compose.request.image_requests[0].upload_request.options;
|
||||
|
||||
if (!isAwsUploadRequestOptions(options)) {
|
||||
throw TypeError(
|
||||
`Error: options must be of type AwsUploadRequestOptions, not ${typeof options}.`
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="pf-u-font-weight-bold pf-u-pb-md">Build Information</div>
|
||||
<DescriptionList isHorizontal isCompact className=" pf-u-pl-xl">
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>UUID</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
<ClipboardCopy
|
||||
hoverTip="Copy"
|
||||
clickTip="Copied"
|
||||
variant="inline-compact"
|
||||
ouiaId="aws-uuid"
|
||||
>
|
||||
{compose.id}
|
||||
</ClipboardCopy>
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
{options.share_with_sources?.[0] && (
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>Source</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
<AwsSourceName id={options.share_with_sources[0]} />
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
)}
|
||||
{options.share_with_accounts?.[0] && (
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>Shared with</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
// the format of an account link is taken from
|
||||
// https://docs.aws.amazon.com/signin/latest/userguide/sign-in-urls-defined.html
|
||||
href={`https://${options.share_with_accounts[0]}.signin.aws.amazon.com/console/`}
|
||||
>
|
||||
{options.share_with_accounts[0]}
|
||||
</Button>
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
)}
|
||||
</DescriptionList>
|
||||
<>
|
||||
<br />
|
||||
<div className="pf-u-font-weight-bold pf-u-pb-md">
|
||||
Cloud Provider Identifiers
|
||||
</div>
|
||||
</>
|
||||
<ClonesTable compose={compose} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type AzureDetailsPropTypes = {
|
||||
compose: ComposesResponseItem;
|
||||
};
|
||||
|
||||
export const AzureDetails = ({ compose }: AzureDetailsPropTypes) => {
|
||||
const { data: composeStatus } = useGetComposeStatusQuery({
|
||||
composeId: compose.id,
|
||||
});
|
||||
|
||||
const options = compose.request.image_requests[0].upload_request.options;
|
||||
|
||||
if (!isAzureUploadRequestOptions(options)) {
|
||||
throw TypeError(
|
||||
`Error: options must be of type AzureUploadRequestOptions, not ${typeof options}.`
|
||||
);
|
||||
}
|
||||
|
||||
const sourceId = options.source_id;
|
||||
const resourceGroup = options.resource_group;
|
||||
|
||||
const uploadStatus = composeStatus?.image_status.upload_status?.options;
|
||||
|
||||
if (uploadStatus && !isAzureUploadStatus(uploadStatus)) {
|
||||
throw TypeError(
|
||||
`Error: uploadStatus must be of type AzureUploadStatus, not ${typeof uploadStatus}.`
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="pf-u-font-weight-bold pf-u-pb-md">Build Information</div>
|
||||
<DescriptionList isHorizontal isCompact className=" pf-u-pl-xl">
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>UUID</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
<ClipboardCopy
|
||||
hoverTip="Copy"
|
||||
clickTip="Copied"
|
||||
variant="inline-compact"
|
||||
ouiaId="azure-uuid"
|
||||
>
|
||||
{compose.id}
|
||||
</ClipboardCopy>
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
{sourceId && (
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>Source</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
<AzureSourceName id={sourceId} />
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
)}
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>Resource Group</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{resourceGroup}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
</DescriptionList>
|
||||
<br />
|
||||
<div className="pf-u-font-weight-bold pf-u-pb-md">
|
||||
Cloud Provider Identifiers
|
||||
</div>
|
||||
<DescriptionList isHorizontal isCompact className=" pf-u-pl-xl">
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>Image name</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{composeStatus?.image_status.status === 'success' && (
|
||||
<ClipboardCopy
|
||||
hoverTip="Copy"
|
||||
clickTip="Copied"
|
||||
variant="inline-compact"
|
||||
>
|
||||
{uploadStatus?.image_name}
|
||||
</ClipboardCopy>
|
||||
)}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
</DescriptionList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type GcpDetailsPropTypes = {
|
||||
compose: ComposesResponseItem;
|
||||
};
|
||||
|
||||
export const GcpDetails = ({ compose }: GcpDetailsPropTypes) => {
|
||||
const { data: composeStatus } = useGetComposeStatusQuery({
|
||||
composeId: compose.id,
|
||||
});
|
||||
|
||||
const options = compose.request.image_requests[0].upload_request.options;
|
||||
|
||||
if (!isGcpUploadRequestOptions(options)) {
|
||||
throw TypeError(
|
||||
`Error: options must be of type GcpUploadRequestOptions, not ${typeof options}.`
|
||||
);
|
||||
}
|
||||
|
||||
const uploadStatus = composeStatus?.image_status.upload_status?.options;
|
||||
|
||||
if (uploadStatus && !isGcpUploadStatus(uploadStatus)) {
|
||||
throw TypeError(
|
||||
`Error: uploadStatus must be of type GcpUploadStatus, not ${typeof uploadStatus}.`
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="pf-u-font-weight-bold pf-u-pb-md">Build Information</div>
|
||||
<DescriptionList isHorizontal isCompact className=" pf-u-pl-xl">
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>UUID</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
<ClipboardCopy
|
||||
hoverTip="Copy"
|
||||
clickTip="Copied"
|
||||
variant="inline-compact"
|
||||
ouiaId="gcp-uuid"
|
||||
>
|
||||
{compose.id}
|
||||
</ClipboardCopy>
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
{composeStatus?.image_status.status === 'success' && (
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>Project ID</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{uploadStatus?.project_id}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
)}
|
||||
{options.share_with_accounts && (
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>Shared with</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{parseGcpSharedWith(options.share_with_accounts)}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
)}
|
||||
</DescriptionList>
|
||||
<br />
|
||||
<div className="pf-u-font-weight-bold pf-u-pb-md">
|
||||
Cloud Provider Identifiers
|
||||
</div>
|
||||
<DescriptionList isHorizontal isCompact className=" pf-u-pl-xl">
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>Image name</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{composeStatus?.image_status.status === 'success' && (
|
||||
<ClipboardCopy
|
||||
hoverTip="Copy"
|
||||
clickTip="Copied"
|
||||
variant="inline-compact"
|
||||
>
|
||||
{uploadStatus?.image_name}
|
||||
</ClipboardCopy>
|
||||
)}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
</DescriptionList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type AwsS3DetailsPropTypes = {
|
||||
compose: ComposesResponseItem;
|
||||
};
|
||||
|
||||
export const AwsS3Details = ({ compose }: AwsS3DetailsPropTypes) => {
|
||||
return (
|
||||
<>
|
||||
<div className="pf-u-font-weight-bold pf-u-pb-md">Build Information</div>
|
||||
<DescriptionList isHorizontal isCompact className=" pf-u-pl-xl">
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>UUID</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
<ClipboardCopy
|
||||
hoverTip="Copy"
|
||||
clickTip="Copied"
|
||||
variant="inline-compact"
|
||||
ouiaId="other-targets-uuid"
|
||||
>
|
||||
{compose.id}
|
||||
</ClipboardCopy>
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
</DescriptionList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
import React, { Suspense, useState, useMemo } from 'react';
|
||||
|
||||
import { Button, Modal, ModalVariant } from '@patternfly/react-core';
|
||||
import { useChrome } from '@redhat-cloud-services/frontend-components/useChrome';
|
||||
import { useLoadModule, useScalprum } from '@scalprum/react-core';
|
||||
import { useFlag } from '@unleash/proxy-client-react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import ImageLinkDirect from './ImageLinkDirect';
|
||||
|
||||
import { MODAL_ANCHOR } from '../../constants';
|
||||
import { selectImageById } from '../../store/composesSlice';
|
||||
import useProvisioningPermissions from '../../Utilities/useProvisioningPermissions';
|
||||
|
||||
const getImageProvider = ({ imageType }) => {
|
||||
switch (imageType) {
|
||||
case 'aws':
|
||||
return 'aws';
|
||||
case 'ami':
|
||||
return 'aws';
|
||||
case 'azure':
|
||||
return 'azure';
|
||||
case 'gcp':
|
||||
return 'gcp';
|
||||
default:
|
||||
//TODO check with Provisioning: what if imageType is not 'aws', 'ami', or 'azure'?
|
||||
return 'aws';
|
||||
}
|
||||
};
|
||||
|
||||
const ProvisioningLink = ({ imageId, isExpired, isInClonesTable }) => {
|
||||
const image = useSelector((state) => selectImageById(state, imageId));
|
||||
|
||||
const [wizardOpen, openWizard] = useState(false);
|
||||
const [{ default: ProvisioningWizard }, error] = useLoadModule(
|
||||
{
|
||||
appName: 'provisioning', // optional
|
||||
scope: 'provisioning',
|
||||
module: './ProvisioningWizard',
|
||||
// processor: (val) => val, // optional
|
||||
},
|
||||
{},
|
||||
{}
|
||||
);
|
||||
|
||||
const appendTo = useMemo(() => document.querySelector(MODAL_ANCHOR), []);
|
||||
const { permissions, isLoading: isLoadingPermission } =
|
||||
useProvisioningPermissions();
|
||||
const provider = getImageProvider(image);
|
||||
|
||||
if (!error) {
|
||||
return (
|
||||
<Suspense fallback="loading...">
|
||||
<Button
|
||||
spinnerAriaLabel="Loading launch"
|
||||
isLoading={isLoadingPermission}
|
||||
variant="link"
|
||||
isInline
|
||||
onClick={() => openWizard(true)}
|
||||
>
|
||||
Launch
|
||||
</Button>
|
||||
{wizardOpen && (
|
||||
<Modal
|
||||
isOpen
|
||||
hasNoBodyWrapper
|
||||
appendTo={appendTo}
|
||||
showClose={false}
|
||||
onClose={() => openWizard(false)}
|
||||
variant={ModalVariant.large}
|
||||
aria-label="Open launch wizard"
|
||||
>
|
||||
<ProvisioningWizard
|
||||
hasAccess={permissions[provider]}
|
||||
onClose={() => openWizard(false)}
|
||||
image={{
|
||||
name: image.imageName,
|
||||
id: image.id,
|
||||
architecture: image.architecture,
|
||||
provider: provider,
|
||||
sourceIDs: image.share_with_sources,
|
||||
accountIDs: image.share_with_accounts,
|
||||
uploadOptions: image.uploadOptions,
|
||||
uploadStatus: image.uploadStatus,
|
||||
// For backward compatibility only, remove once Provisioning ready (deploys):
|
||||
// https://github.com/RHEnVision/provisioning-frontend/pull/238
|
||||
sourceId: image.share_with_sources?.[0],
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ImageLinkDirect
|
||||
imageId={image.id}
|
||||
isExpired={isExpired}
|
||||
isInClonesTable={isInClonesTable}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ImageLink = ({ imageId, isExpired, isInClonesTable }) => {
|
||||
const image = useSelector((state) => selectImageById(state, imageId));
|
||||
const uploadStatus = image.uploadStatus;
|
||||
const { initialized: chromeInitialized, getEnvironment } = useChrome();
|
||||
const azureFeatureFlag = useFlag('provisioning.azure');
|
||||
const gcpFeatureFlag = useFlag('provisioning.gcp');
|
||||
const scalprum = useScalprum();
|
||||
const hasProvisioning = chromeInitialized && scalprum.config?.provisioning;
|
||||
|
||||
if (!uploadStatus || image.status !== 'success') return null;
|
||||
|
||||
const provisioningLinkEnabled = (image) => {
|
||||
switch (image.imageType) {
|
||||
case 'aws':
|
||||
case 'ami':
|
||||
return true;
|
||||
case 'azure':
|
||||
if (getEnvironment() === 'qa') {
|
||||
return true;
|
||||
}
|
||||
return !!azureFeatureFlag;
|
||||
case 'gcp':
|
||||
if (getEnvironment() === 'qa') {
|
||||
return true;
|
||||
}
|
||||
return !!gcpFeatureFlag;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if (hasProvisioning && provisioningLinkEnabled(image)) {
|
||||
if (isInClonesTable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ProvisioningLink
|
||||
imageId={image.id}
|
||||
isExpired={isExpired}
|
||||
isInClonesTable={isInClonesTable}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ImageLinkDirect
|
||||
imageId={image.id}
|
||||
isExpired={isExpired}
|
||||
isInClonesTable={isInClonesTable}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
ProvisioningLink.propTypes = {
|
||||
imageId: PropTypes.string,
|
||||
isExpired: PropTypes.bool,
|
||||
isInClonesTable: PropTypes.bool,
|
||||
};
|
||||
|
||||
ImageLink.propTypes = {
|
||||
imageId: PropTypes.string.isRequired,
|
||||
isExpired: PropTypes.bool,
|
||||
isInClonesTable: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default ImageLink;
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
ClipboardCopy,
|
||||
ClipboardCopyVariant,
|
||||
Popover,
|
||||
Text,
|
||||
TextContent,
|
||||
} from '@patternfly/react-core';
|
||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { RegionsPopover } from './RegionsPopover';
|
||||
|
||||
import { selectImageById } from '../../store/composesSlice';
|
||||
import { resolveRelPath } from '../../Utilities/path';
|
||||
|
||||
const launchInstanceCommand = (uploadStatus) => {
|
||||
return `gcloud compute instances create ${uploadStatus.options.image_name}-instance --image-project ${uploadStatus.options.project_id} --image ${uploadStatus.options.image_name}`;
|
||||
};
|
||||
|
||||
const saveCopyCommand = (uploadStatus) => {
|
||||
return `gcloud compute images create ${uploadStatus.options.image_name}-copy --source-image-project ${uploadStatus.options.project_id} --source-image ${uploadStatus.options.image_name}`;
|
||||
};
|
||||
|
||||
const ImageLinkDirect = ({ imageId, isExpired, isInClonesTable }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const image = useSelector((state) => selectImageById(state, imageId));
|
||||
if (!image) {
|
||||
return null;
|
||||
}
|
||||
const uploadStatus = image.uploadStatus;
|
||||
|
||||
const fileExtensions = {
|
||||
vsphere: '.vmdk',
|
||||
'vsphere-ova': '.ova',
|
||||
'guest-image': '.qcow2',
|
||||
'image-installer': '.iso',
|
||||
wsl: '.tar.gz',
|
||||
};
|
||||
|
||||
if (uploadStatus.type === 'aws') {
|
||||
const url =
|
||||
'https://console.aws.amazon.com/ec2/v2/home?region=' +
|
||||
uploadStatus.options.region +
|
||||
'#LaunchInstanceWizard:ami=' +
|
||||
uploadStatus.options.ami;
|
||||
if (isInClonesTable) {
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
isInline
|
||||
href={url}
|
||||
>
|
||||
Launch
|
||||
</Button>
|
||||
);
|
||||
} else {
|
||||
return <RegionsPopover composeId={image.id} />;
|
||||
}
|
||||
} else if (uploadStatus.type === 'azure') {
|
||||
const url =
|
||||
'https://portal.azure.com/#@' +
|
||||
image.uploadOptions.tenant_id +
|
||||
'/resource/subscriptions/' +
|
||||
image.uploadOptions.subscription_id +
|
||||
'/resourceGroups/' +
|
||||
image.uploadOptions.resource_group +
|
||||
'/providers/Microsoft.Compute/images/' +
|
||||
uploadStatus.options.image_name;
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={url}
|
||||
>
|
||||
View uploaded image
|
||||
</Button>
|
||||
);
|
||||
} else if (uploadStatus.type === 'gcp') {
|
||||
return (
|
||||
<Popover
|
||||
aria-label="Popover with google cloud platform image commands"
|
||||
maxWidth="30rem"
|
||||
headerContent={'Image commands'}
|
||||
bodyContent={
|
||||
<TextContent>
|
||||
<br />
|
||||
<Text>
|
||||
<strong>Launch an instance</strong>
|
||||
</Text>
|
||||
<ClipboardCopy
|
||||
hoverTip="Copy"
|
||||
clickTip="Copied"
|
||||
ouiaId="gcp-launch-instance"
|
||||
variant={ClipboardCopyVariant.expansion}
|
||||
isReadOnly
|
||||
isExpanded
|
||||
>
|
||||
{launchInstanceCommand(uploadStatus)}
|
||||
</ClipboardCopy>
|
||||
<br />
|
||||
<Text>
|
||||
<strong>Save a copy</strong>
|
||||
</Text>
|
||||
<ClipboardCopy
|
||||
hoverTip="Copy"
|
||||
clickTip="Copied"
|
||||
ouiaId="gcp-save-copy"
|
||||
variant={ClipboardCopyVariant.expansion}
|
||||
isReadOnly
|
||||
isExpanded
|
||||
>
|
||||
{saveCopyCommand(uploadStatus)}
|
||||
</ClipboardCopy>
|
||||
</TextContent>
|
||||
}
|
||||
>
|
||||
<Button component="a" target="_blank" variant="link" isInline>
|
||||
Image commands
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
} else if (uploadStatus.type === 'aws.s3') {
|
||||
if (!isExpired) {
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
isInline
|
||||
href={uploadStatus.options.url}
|
||||
>
|
||||
Download ({fileExtensions[image.imageType]})
|
||||
</Button>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
onClick={() => navigate(resolveRelPath(`imagewizard/${imageId}`))}
|
||||
isInline
|
||||
>
|
||||
Recreate image
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
ImageLinkDirect.propTypes = {
|
||||
imageId: PropTypes.string,
|
||||
isExpired: PropTypes.bool,
|
||||
isInClonesTable: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default ImageLinkDirect;
|
||||
|
|
@ -1,363 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
EmptyState,
|
||||
EmptyStateBody,
|
||||
EmptyStateIcon,
|
||||
EmptyStateSecondaryActions,
|
||||
EmptyStateVariant,
|
||||
Pagination,
|
||||
PaginationVariant,
|
||||
Text,
|
||||
Title,
|
||||
Toolbar,
|
||||
ToolbarContent,
|
||||
ToolbarItem,
|
||||
} from '@patternfly/react-core';
|
||||
import { ExternalLinkAltIcon, PlusCircleIcon } from '@patternfly/react-icons';
|
||||
import {
|
||||
ActionsColumn,
|
||||
ExpandableRowContent,
|
||||
TableComposable,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
} from '@patternfly/react-table';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
import './ImagesTable.scss';
|
||||
import { ImageBuildStatus } from './ImageBuildStatus';
|
||||
import ImageDetails from './ImageDetails';
|
||||
import ImageLink from './ImageLink';
|
||||
import Release from './Release';
|
||||
import Target from './Target';
|
||||
|
||||
import { AWS_S3_EXPIRATION_TIME_IN_HOURS } from '../../constants';
|
||||
import { fetchComposes, fetchComposeStatus } from '../../store/actions/actions';
|
||||
import { resolveRelPath } from '../../Utilities/path';
|
||||
import {
|
||||
hoursToExpiration,
|
||||
timestampToDisplayString,
|
||||
} from '../../Utilities/time';
|
||||
|
||||
const ImagesTable = () => {
|
||||
const [page, setPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState(10);
|
||||
|
||||
const [expandedComposeIds, setExpandedComposeIds] = useState([]);
|
||||
const isExpanded = (compose) => expandedComposeIds.includes(compose.id);
|
||||
|
||||
const handleToggle = (compose, isExpanding) => {
|
||||
if (isExpanding) {
|
||||
setExpandedComposeIds([...expandedComposeIds, compose.id]);
|
||||
} else {
|
||||
setExpandedComposeIds(
|
||||
expandedComposeIds.filter((id) => id !== compose.id)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const composes = useSelector((state) => state.composes);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const pollComposeStatuses = () => {
|
||||
Object.entries(composes.byId).map(([id, compose]) => {
|
||||
/* Skip composes that have been complete */
|
||||
if (
|
||||
compose.image_status?.status === 'success' ||
|
||||
compose.image_status?.status === 'failure'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchComposeStatus(id));
|
||||
});
|
||||
};
|
||||
|
||||
/* Get all composes once on mount */
|
||||
useEffect(() => {
|
||||
dispatch(fetchComposes(perPage, 0));
|
||||
}, []);
|
||||
|
||||
/* Reset the polling each time the composes in the store are updated */
|
||||
useEffect(() => {
|
||||
const intervalId = setInterval(() => pollComposeStatuses(), 8000);
|
||||
// clean up interval on unmount
|
||||
return () => clearInterval(intervalId);
|
||||
});
|
||||
|
||||
const onSetPage = (_, page) => {
|
||||
// if the next page's composes haven't been fetched from api yet
|
||||
// then fetch them with proper page index and offset
|
||||
if (composes.count > composes.allIds.length) {
|
||||
const pageIndex = page - 1;
|
||||
const offset = pageIndex * perPage;
|
||||
dispatch(fetchComposes(perPage, offset));
|
||||
}
|
||||
|
||||
setPage(page);
|
||||
};
|
||||
|
||||
const onPerPageSelect = (_, perPage) => {
|
||||
// if the new per page quantity is greater than the number of already fetched composes fetch more composes
|
||||
// if all composes haven't already been fetched
|
||||
if (
|
||||
composes.count > composes.allIds.length &&
|
||||
perPage > composes.allIds.length
|
||||
) {
|
||||
dispatch(fetchComposes(perPage, 0));
|
||||
}
|
||||
|
||||
// page should be reset to the first page when the page size is changed.
|
||||
setPerPage(perPage);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const actions = (compose) => [
|
||||
{
|
||||
title: 'Recreate image',
|
||||
onClick: () => {
|
||||
navigate(resolveRelPath(`imagewizard/${compose.id}`));
|
||||
},
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<a
|
||||
className="ib-subdued-link"
|
||||
href={`data:text/plain;charset=utf-8,${encodeURIComponent(
|
||||
JSON.stringify(compose.request, null, ' ')
|
||||
)}`}
|
||||
download={`request-${compose.id}.json`}
|
||||
>
|
||||
Download compose request (.json)
|
||||
</a>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const awsActions = (compose) => [
|
||||
{
|
||||
title: 'Share to new region',
|
||||
onClick: () => navigate(resolveRelPath(`share/${compose.id}`)),
|
||||
isDisabled: compose?.image_status?.status === 'success' ? false : true,
|
||||
},
|
||||
...actions(compose),
|
||||
];
|
||||
|
||||
// the state.page is not an index so must be reduced by 1 get the starting index
|
||||
const itemsStartInclusive = (page - 1) * perPage;
|
||||
const itemsEndExclusive = itemsStartInclusive + perPage;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{(composes.allIds.length === 0 && (
|
||||
<EmptyState variant={EmptyStateVariant.large} data-testid="empty-state">
|
||||
<EmptyStateIcon icon={PlusCircleIcon} />
|
||||
<Title headingLevel="h4" size="lg">
|
||||
Create an RPM-DNF image
|
||||
</Title>
|
||||
<EmptyStateBody>
|
||||
<Text>
|
||||
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 create these images faster than manual procedures because it
|
||||
eliminates the specific configurations required for each output
|
||||
type.
|
||||
</Text>
|
||||
<br />
|
||||
<Text>
|
||||
With RPM-DNF, you can manage the system software by using the DNF
|
||||
package manager and updated RPM packages. This is a simple and
|
||||
adaptive method of managing and modifying the system over its
|
||||
lifecycle.
|
||||
</Text>
|
||||
<br />
|
||||
<Text>
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={
|
||||
'https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9/html-single/managing_software_with_the_dnf_tool/index'
|
||||
}
|
||||
>
|
||||
Learn more about managing images with DNF
|
||||
</Button>
|
||||
</Text>
|
||||
</EmptyStateBody>
|
||||
<Link
|
||||
to={resolveRelPath('imagewizard')}
|
||||
className="pf-c-button pf-m-primary"
|
||||
data-testid="create-image-action"
|
||||
>
|
||||
Create image
|
||||
</Link>
|
||||
<EmptyStateSecondaryActions>
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={
|
||||
'https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/creating_customized_rhel_images_using_the_image_builder_service'
|
||||
}
|
||||
className="pf-u-pt-md"
|
||||
>
|
||||
Image builder for RPM-DNF documentation
|
||||
</Button>
|
||||
</EmptyStateSecondaryActions>
|
||||
</EmptyState>
|
||||
)) || (
|
||||
<React.Fragment>
|
||||
<Toolbar>
|
||||
<ToolbarContent>
|
||||
<ToolbarItem>
|
||||
<Link
|
||||
to={resolveRelPath('imagewizard')}
|
||||
className="pf-c-button pf-m-primary"
|
||||
data-testid="create-image-action"
|
||||
>
|
||||
Create image
|
||||
</Link>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem
|
||||
variant="pagination"
|
||||
align={{ default: 'alignRight' }}
|
||||
>
|
||||
<Pagination
|
||||
itemCount={composes.count}
|
||||
perPage={perPage}
|
||||
page={page}
|
||||
onSetPage={onSetPage}
|
||||
onPerPageSelect={onPerPageSelect}
|
||||
widgetId="compose-pagination-top"
|
||||
data-testid="images-pagination-top"
|
||||
isCompact
|
||||
/>
|
||||
</ToolbarItem>
|
||||
</ToolbarContent>
|
||||
</Toolbar>
|
||||
<TableComposable variant="compact" data-testid="images-table">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th />
|
||||
<Th>Image name</Th>
|
||||
<Th>Created/Updated</Th>
|
||||
<Th>Release</Th>
|
||||
<Th>Target</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Instance</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</Thead>
|
||||
{composes.allIds
|
||||
.slice(itemsStartInclusive, itemsEndExclusive)
|
||||
.map((id, rowIndex) => {
|
||||
const compose = composes.byId[id];
|
||||
return (
|
||||
<Tbody key={id} isExpanded={isExpanded(compose)}>
|
||||
<Tr className="no-bottom-border">
|
||||
<Td
|
||||
expand={{
|
||||
rowIndex,
|
||||
isExpanded: isExpanded(compose),
|
||||
onToggle: () =>
|
||||
handleToggle(compose, !isExpanded(compose)),
|
||||
}}
|
||||
/>
|
||||
<Td dataLabel="Image name">
|
||||
{compose.request.image_name || id}
|
||||
</Td>
|
||||
<Td dataLabel="Created">
|
||||
{timestampToDisplayString(compose.created_at)}
|
||||
</Td>
|
||||
<Td dataLabel="Release">
|
||||
<Release release={compose.request.distribution} />
|
||||
</Td>
|
||||
<Td dataLabel="Target">
|
||||
<Target composeId={id} />
|
||||
</Td>
|
||||
<Td dataLabel="Status">
|
||||
<ImageBuildStatus
|
||||
imageId={id}
|
||||
isImagesTableRow={true}
|
||||
imageStatus={compose.image_status}
|
||||
/>
|
||||
</Td>
|
||||
<Td dataLabel="Instance">
|
||||
<ImageLink
|
||||
imageId={id}
|
||||
isExpired={
|
||||
hoursToExpiration(compose.created_at) >=
|
||||
AWS_S3_EXPIRATION_TIME_IN_HOURS
|
||||
? true
|
||||
: false
|
||||
}
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
{compose.request.image_requests[0].upload_request
|
||||
.type === 'aws' ? (
|
||||
<ActionsColumn items={awsActions(compose)} />
|
||||
) : (
|
||||
<ActionsColumn items={actions(compose)} />
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
<Tr isExpanded={isExpanded(compose)}>
|
||||
<Td colSpan={8}>
|
||||
<ExpandableRowContent>
|
||||
<ImageDetails id={id} />
|
||||
</ExpandableRowContent>
|
||||
</Td>
|
||||
</Tr>
|
||||
</Tbody>
|
||||
);
|
||||
})}
|
||||
</TableComposable>
|
||||
<Toolbar className="pf-u-mb-xl">
|
||||
<ToolbarContent>
|
||||
<ToolbarItem
|
||||
variant="pagination"
|
||||
align={{ default: 'alignRight' }}
|
||||
>
|
||||
<Pagination
|
||||
variant={PaginationVariant.bottom}
|
||||
itemCount={composes.count}
|
||||
perPage={perPage}
|
||||
page={page}
|
||||
onSetPage={onSetPage}
|
||||
onPerPageSelect={onPerPageSelect}
|
||||
widgetId="compose-pagination-bottom"
|
||||
data-testid="images-pagination-bottom"
|
||||
isCompact
|
||||
/>
|
||||
</ToolbarItem>
|
||||
</ToolbarContent>
|
||||
</Toolbar>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
ImagesTable.propTypes = {
|
||||
composes: PropTypes.object,
|
||||
composesGet: PropTypes.func,
|
||||
composeGetStatus: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ImagesTable;
|
||||
486
src/Components/ImagesTable/ImagesTable.tsx
Normal file
486
src/Components/ImagesTable/ImagesTable.tsx
Normal file
|
|
@ -0,0 +1,486 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
EmptyState,
|
||||
EmptyStateBody,
|
||||
EmptyStateIcon,
|
||||
EmptyStateSecondaryActions,
|
||||
EmptyStateVariant,
|
||||
OnSetPage,
|
||||
Pagination,
|
||||
PaginationVariant,
|
||||
Text,
|
||||
Title,
|
||||
Toolbar,
|
||||
ToolbarContent,
|
||||
ToolbarItem,
|
||||
} from '@patternfly/react-core';
|
||||
import { ExternalLinkAltIcon, PlusCircleIcon } from '@patternfly/react-icons';
|
||||
import {
|
||||
ActionsColumn,
|
||||
ExpandableRowContent,
|
||||
TableComposable,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
} from '@patternfly/react-table';
|
||||
import { Link, NavigateFunction, useNavigate } from 'react-router-dom';
|
||||
|
||||
import './ImagesTable.scss';
|
||||
import {
|
||||
AwsDetails,
|
||||
AwsS3Details,
|
||||
AzureDetails,
|
||||
GcpDetails,
|
||||
} from './ImageDetails';
|
||||
import { AwsS3Instance, CloudInstance } from './Instance';
|
||||
import Release from './Release';
|
||||
import { AwsS3Status, CloudStatus } from './Status';
|
||||
import { AwsTarget, Target } from './Target';
|
||||
|
||||
import {
|
||||
AWS_S3_EXPIRATION_TIME_IN_HOURS,
|
||||
STATUS_POLLING_INTERVAL,
|
||||
} from '../../constants';
|
||||
import {
|
||||
ComposesResponseItem,
|
||||
ComposeStatus,
|
||||
useGetComposesQuery,
|
||||
useGetComposeStatusQuery,
|
||||
} from '../../store/imageBuilderApi';
|
||||
import { resolveRelPath } from '../../Utilities/path';
|
||||
import {
|
||||
computeHoursToExpiration,
|
||||
timestampToDisplayString,
|
||||
} from '../../Utilities/time';
|
||||
|
||||
const ImagesTable = () => {
|
||||
const [page, setPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState(10);
|
||||
|
||||
const onSetPage: OnSetPage = (_, page) => setPage(page);
|
||||
|
||||
const onPerPageSelect: OnSetPage = (_, perPage) => {
|
||||
setPage(1);
|
||||
setPerPage(perPage);
|
||||
};
|
||||
|
||||
const { data, isSuccess } = useGetComposesQuery({
|
||||
limit: perPage,
|
||||
offset: perPage * (page - 1),
|
||||
});
|
||||
|
||||
if (!isSuccess) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const composes = data.data;
|
||||
const itemCount = data.meta.count;
|
||||
|
||||
return (
|
||||
<>
|
||||
{data.meta.count === 0 && <EmptyImagesTable />}
|
||||
{data.meta.count > 0 && (
|
||||
<>
|
||||
<Toolbar>
|
||||
<ToolbarContent>
|
||||
<ToolbarItem>
|
||||
<Link
|
||||
to={resolveRelPath('imagewizard')}
|
||||
className="pf-c-button pf-m-primary"
|
||||
data-testid="create-image-action"
|
||||
>
|
||||
Create image
|
||||
</Link>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem
|
||||
variant="pagination"
|
||||
alignment={{ default: 'alignRight' }}
|
||||
>
|
||||
<Pagination
|
||||
itemCount={itemCount}
|
||||
perPage={perPage}
|
||||
page={page}
|
||||
onSetPage={onSetPage}
|
||||
onPerPageSelect={onPerPageSelect}
|
||||
widgetId="compose-pagination-top"
|
||||
data-testid="images-pagination-top"
|
||||
isCompact
|
||||
/>
|
||||
</ToolbarItem>
|
||||
</ToolbarContent>
|
||||
</Toolbar>
|
||||
<TableComposable variant="compact" data-testid="images-table">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th />
|
||||
<Th>Image name</Th>
|
||||
<Th>Created/Updated</Th>
|
||||
<Th>Release</Th>
|
||||
<Th>Target</Th>
|
||||
<Th>Status</Th>
|
||||
<Th>Instance</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</Thead>
|
||||
{composes.map((compose, rowIndex) => {
|
||||
return (
|
||||
<ImagesTableRow
|
||||
compose={compose}
|
||||
rowIndex={rowIndex}
|
||||
key={compose.id}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TableComposable>
|
||||
<Toolbar className="pf-u-mb-xl">
|
||||
<ToolbarContent>
|
||||
<ToolbarItem
|
||||
variant="pagination"
|
||||
alignment={{ default: 'alignRight' }}
|
||||
>
|
||||
<Pagination
|
||||
variant={PaginationVariant.bottom}
|
||||
itemCount={itemCount}
|
||||
perPage={perPage}
|
||||
page={page}
|
||||
onSetPage={onSetPage}
|
||||
onPerPageSelect={onPerPageSelect}
|
||||
widgetId="compose-pagination-bottom"
|
||||
data-testid="images-pagination-bottom"
|
||||
isCompact
|
||||
/>
|
||||
</ToolbarItem>
|
||||
</ToolbarContent>
|
||||
</Toolbar>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const EmptyImagesTable = () => {
|
||||
return (
|
||||
<EmptyState variant={EmptyStateVariant.large} data-testid="empty-state">
|
||||
<EmptyStateIcon icon={PlusCircleIcon} />
|
||||
<Title headingLevel="h4" size="lg">
|
||||
Create an RPM-DNF image
|
||||
</Title>
|
||||
<EmptyStateBody>
|
||||
<Text>
|
||||
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
|
||||
create these images faster than manual procedures because it
|
||||
eliminates the specific configurations required for each output type.
|
||||
</Text>
|
||||
<br />
|
||||
<Text>
|
||||
With RPM-DNF, you can manage the system software by using the DNF
|
||||
package manager and updated RPM packages. This is a simple and
|
||||
adaptive method of managing and modifying the system over its
|
||||
lifecycle.
|
||||
</Text>
|
||||
<br />
|
||||
<Text>
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={
|
||||
'https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9/html-single/managing_software_with_the_dnf_tool/index'
|
||||
}
|
||||
>
|
||||
Learn more about managing images with DNF
|
||||
</Button>
|
||||
</Text>
|
||||
</EmptyStateBody>
|
||||
<Link
|
||||
to={resolveRelPath('imagewizard')}
|
||||
className="pf-c-button pf-m-primary"
|
||||
data-testid="create-image-action"
|
||||
>
|
||||
Create image
|
||||
</Link>
|
||||
<EmptyStateSecondaryActions>
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={
|
||||
'https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/creating_customized_rhel_images_using_the_image_builder_service'
|
||||
}
|
||||
className="pf-u-pt-md"
|
||||
>
|
||||
Image builder for RPM-DNF documentation
|
||||
</Button>
|
||||
</EmptyStateSecondaryActions>
|
||||
</EmptyState>
|
||||
);
|
||||
};
|
||||
|
||||
type ImagesTableRowPropTypes = {
|
||||
compose: ComposesResponseItem;
|
||||
rowIndex: number;
|
||||
};
|
||||
|
||||
const ImagesTableRow = ({ compose, rowIndex }: ImagesTableRowPropTypes) => {
|
||||
const [pollingInterval, setPollingInterval] = useState(
|
||||
STATUS_POLLING_INTERVAL
|
||||
);
|
||||
|
||||
const { data: composeStatus } = useGetComposeStatusQuery(
|
||||
{
|
||||
composeId: compose.id,
|
||||
},
|
||||
{ pollingInterval: pollingInterval }
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
composeStatus?.image_status.status === 'success' ||
|
||||
composeStatus?.image_status.status === 'failure'
|
||||
) {
|
||||
setPollingInterval(0);
|
||||
} else {
|
||||
setPollingInterval(STATUS_POLLING_INTERVAL);
|
||||
}
|
||||
}, [setPollingInterval, composeStatus]);
|
||||
|
||||
const type = compose.request.image_requests[0].upload_request.type;
|
||||
|
||||
switch (type) {
|
||||
case 'aws':
|
||||
return (
|
||||
<AwsRow
|
||||
compose={compose}
|
||||
composeStatus={composeStatus}
|
||||
rowIndex={rowIndex}
|
||||
/>
|
||||
);
|
||||
case 'gcp':
|
||||
return <GcpRow compose={compose} rowIndex={rowIndex} />;
|
||||
case 'azure':
|
||||
return <AzureRow compose={compose} rowIndex={rowIndex} />;
|
||||
case 'aws.s3':
|
||||
return <AwsS3Row compose={compose} rowIndex={rowIndex} />;
|
||||
}
|
||||
};
|
||||
|
||||
type GcpRowPropTypes = {
|
||||
compose: ComposesResponseItem;
|
||||
rowIndex: number;
|
||||
};
|
||||
|
||||
const GcpRow = ({ compose, rowIndex }: GcpRowPropTypes) => {
|
||||
const details = <GcpDetails compose={compose} />;
|
||||
const instance = <CloudInstance compose={compose} />;
|
||||
const status = <CloudStatus compose={compose} />;
|
||||
|
||||
return (
|
||||
<Row
|
||||
compose={compose}
|
||||
rowIndex={rowIndex}
|
||||
details={details}
|
||||
status={status}
|
||||
instance={instance}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type AzureRowPropTypes = {
|
||||
compose: ComposesResponseItem;
|
||||
rowIndex: number;
|
||||
};
|
||||
|
||||
const AzureRow = ({ compose, rowIndex }: AzureRowPropTypes) => {
|
||||
const details = <AzureDetails compose={compose} />;
|
||||
const instance = <CloudInstance compose={compose} />;
|
||||
const status = <CloudStatus compose={compose} />;
|
||||
|
||||
return (
|
||||
<Row
|
||||
compose={compose}
|
||||
rowIndex={rowIndex}
|
||||
details={details}
|
||||
instance={instance}
|
||||
status={status}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type AwsS3RowPropTypes = {
|
||||
compose: ComposesResponseItem;
|
||||
rowIndex: number;
|
||||
};
|
||||
|
||||
const AwsS3Row = ({ compose, rowIndex }: AwsS3RowPropTypes) => {
|
||||
const hoursToExpiration = computeHoursToExpiration(compose.created_at);
|
||||
const isExpired = hoursToExpiration >= AWS_S3_EXPIRATION_TIME_IN_HOURS;
|
||||
|
||||
const details = <AwsS3Details compose={compose} />;
|
||||
const instance = <AwsS3Instance compose={compose} isExpired={isExpired} />;
|
||||
const status = (
|
||||
<AwsS3Status
|
||||
compose={compose}
|
||||
isExpired={isExpired}
|
||||
hoursToExpiration={hoursToExpiration}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Row
|
||||
compose={compose}
|
||||
rowIndex={rowIndex}
|
||||
details={details}
|
||||
instance={instance}
|
||||
status={status}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type AwsRowPropTypes = {
|
||||
compose: ComposesResponseItem;
|
||||
composeStatus: ComposeStatus | undefined;
|
||||
rowIndex: number;
|
||||
};
|
||||
|
||||
const AwsRow = ({ compose, composeStatus, rowIndex }: AwsRowPropTypes) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const target = <AwsTarget compose={compose} />;
|
||||
|
||||
const status = <CloudStatus compose={compose} />;
|
||||
|
||||
const instance = <CloudInstance compose={compose} />;
|
||||
|
||||
const details = <AwsDetails compose={compose} />;
|
||||
|
||||
const actions = (
|
||||
<ActionsColumn items={awsActions(compose, composeStatus, navigate)} />
|
||||
);
|
||||
|
||||
return (
|
||||
<Row
|
||||
compose={compose}
|
||||
rowIndex={rowIndex}
|
||||
status={status}
|
||||
target={target}
|
||||
actions={actions}
|
||||
instance={instance}
|
||||
details={details}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type RowPropTypes = {
|
||||
compose: ComposesResponseItem;
|
||||
rowIndex: any;
|
||||
status: JSX.Element;
|
||||
target?: JSX.Element;
|
||||
actions?: JSX.Element;
|
||||
instance: JSX.Element;
|
||||
details: JSX.Element;
|
||||
};
|
||||
|
||||
const Row = ({
|
||||
compose,
|
||||
rowIndex,
|
||||
status,
|
||||
target,
|
||||
actions,
|
||||
details,
|
||||
instance,
|
||||
}: RowPropTypes) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const handleToggle = () => setIsExpanded(!isExpanded);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Tbody key={compose.id} isExpanded={isExpanded}>
|
||||
<Tr className="no-bottom-border">
|
||||
<Td
|
||||
expand={{
|
||||
rowIndex: rowIndex,
|
||||
isExpanded: isExpanded,
|
||||
onToggle: () => handleToggle(),
|
||||
}}
|
||||
/>
|
||||
<Td dataLabel="Image name">{compose.image_name || compose.id}</Td>
|
||||
<Td dataLabel="Created">
|
||||
{timestampToDisplayString(compose.created_at)}
|
||||
</Td>
|
||||
<Td dataLabel="Release">
|
||||
<Release release={compose.request.distribution} />
|
||||
</Td>
|
||||
<Td dataLabel="Target">
|
||||
{target ? target : <Target compose={compose} />}
|
||||
</Td>
|
||||
<Td dataLabel="Status">{status}</Td>
|
||||
<Td dataLabel="Instance">{instance}</Td>
|
||||
<Td>
|
||||
{actions ? (
|
||||
actions
|
||||
) : (
|
||||
<ActionsColumn items={defaultActions(compose, navigate)} />
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
<Tr isExpanded={isExpanded}>
|
||||
<Td colSpan={8}>
|
||||
<ExpandableRowContent>{details}</ExpandableRowContent>
|
||||
</Td>
|
||||
</Tr>
|
||||
</Tbody>
|
||||
);
|
||||
};
|
||||
|
||||
const defaultActions = (
|
||||
compose: ComposesResponseItem,
|
||||
navigate: NavigateFunction
|
||||
) => [
|
||||
{
|
||||
title: 'Recreate image',
|
||||
onClick: () => {
|
||||
navigate(resolveRelPath(`imagewizard/${compose.id}`));
|
||||
},
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<a
|
||||
className="ib-subdued-link"
|
||||
href={`data:text/plain;charset=utf-8,${encodeURIComponent(
|
||||
JSON.stringify(compose.request, null, ' ')
|
||||
)}`}
|
||||
download={`request-${compose.id}.json`}
|
||||
>
|
||||
Download compose request (.json)
|
||||
</a>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const awsActions = (
|
||||
compose: ComposesResponseItem,
|
||||
status: ComposeStatus | undefined,
|
||||
navigate: NavigateFunction
|
||||
) => [
|
||||
{
|
||||
title: 'Share to new region',
|
||||
onClick: () => navigate(resolveRelPath(`share/${compose.id}`)),
|
||||
isDisabled: status?.image_status.status === 'success' ? false : true,
|
||||
},
|
||||
...defaultActions(compose, navigate),
|
||||
];
|
||||
|
||||
export default ImagesTable;
|
||||
228
src/Components/ImagesTable/Instance.tsx
Normal file
228
src/Components/ImagesTable/Instance.tsx
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { Button, Modal, ModalVariant, Skeleton } from '@patternfly/react-core';
|
||||
import { useChrome } from '@redhat-cloud-services/frontend-components/useChrome';
|
||||
import { useLoadModule, useScalprum } from '@scalprum/react-core';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { MODAL_ANCHOR } from '../../constants';
|
||||
import {
|
||||
ComposesResponseItem,
|
||||
ComposeStatus,
|
||||
ImageTypes,
|
||||
useGetComposeStatusQuery,
|
||||
} from '../../store/imageBuilderApi';
|
||||
import {
|
||||
isAwsUploadRequestOptions,
|
||||
isAwss3UploadStatus,
|
||||
isGcpUploadRequestOptions,
|
||||
} from '../../store/typeGuards';
|
||||
import { resolveRelPath } from '../../Utilities/path';
|
||||
|
||||
type CloudInstancePropTypes = {
|
||||
compose: ComposesResponseItem;
|
||||
};
|
||||
|
||||
export const CloudInstance = ({ compose }: CloudInstancePropTypes) => {
|
||||
const { initialized: chromeInitialized } = useChrome();
|
||||
const scalprum = useScalprum();
|
||||
const hasProvisioning = chromeInitialized && scalprum.config?.provisioning;
|
||||
|
||||
const { data, isSuccess } = useGetComposeStatusQuery({
|
||||
composeId: compose.id,
|
||||
});
|
||||
|
||||
if (!isSuccess) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
if (hasProvisioning) {
|
||||
return <ProvisioningLink compose={compose} composeStatus={data} />;
|
||||
} else {
|
||||
return <DisabledProvisioningLink />;
|
||||
}
|
||||
};
|
||||
|
||||
const DisabledProvisioningLink = () => {
|
||||
return (
|
||||
<Button variant="link" isInline isDisabled>
|
||||
Launch
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
type ProvisioningLinkPropTypes = {
|
||||
compose: ComposesResponseItem;
|
||||
composeStatus: ComposeStatus;
|
||||
};
|
||||
|
||||
const ProvisioningLink = ({
|
||||
compose,
|
||||
composeStatus,
|
||||
}: ProvisioningLinkPropTypes) => {
|
||||
const [wizardOpen, openWizard] = useState(false);
|
||||
const [exposedScalprumModule, error] = useLoadModule(
|
||||
{
|
||||
scope: 'provisioning',
|
||||
module: './ProvisioningWizard',
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
if (
|
||||
error ||
|
||||
!exposedScalprumModule ||
|
||||
composeStatus.image_status.status !== 'success'
|
||||
) {
|
||||
return <DisabledProvisioningLink />;
|
||||
} else {
|
||||
const appendTo = () => document.querySelector(MODAL_ANCHOR) as HTMLElement;
|
||||
const ProvisioningWizard = exposedScalprumModule.default;
|
||||
const provider = getImageProvider(compose);
|
||||
|
||||
const options = compose.request.image_requests[0].upload_request.options;
|
||||
|
||||
let sourceIds = undefined;
|
||||
let accountIds = undefined;
|
||||
|
||||
if (isGcpUploadRequestOptions(options)) {
|
||||
accountIds = options.share_with_accounts;
|
||||
}
|
||||
|
||||
if (isAwsUploadRequestOptions(options)) {
|
||||
accountIds = options.share_with_accounts;
|
||||
sourceIds = options.share_with_sources;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant="link" isInline onClick={() => openWizard(true)}>
|
||||
Launch
|
||||
</Button>
|
||||
{wizardOpen && (
|
||||
<Modal
|
||||
isOpen
|
||||
hasNoBodyWrapper
|
||||
appendTo={appendTo}
|
||||
showClose={false}
|
||||
variant={ModalVariant.large}
|
||||
aria-label="Open launch wizard"
|
||||
>
|
||||
<ProvisioningWizard
|
||||
onClose={() => openWizard(false)}
|
||||
image={{
|
||||
name: compose.image_name || compose.id,
|
||||
id: compose.id,
|
||||
architecture:
|
||||
compose.request.image_requests[0].upload_request.options,
|
||||
provider: provider,
|
||||
sourceIDs: sourceIds,
|
||||
accountIDs: accountIds,
|
||||
uploadOptions:
|
||||
compose.request.image_requests[0].upload_request.options,
|
||||
uploadStatus: composeStatus.image_status.upload_status,
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getImageProvider = (compose: ComposesResponseItem) => {
|
||||
const imageType = compose.request.image_requests[0].image_type;
|
||||
switch (imageType) {
|
||||
case 'aws':
|
||||
return 'aws';
|
||||
case 'ami':
|
||||
return 'aws';
|
||||
case 'azure':
|
||||
return 'azure';
|
||||
case 'gcp':
|
||||
return 'gcp';
|
||||
default:
|
||||
//TODO check with Provisioning: what if imageType is not 'aws', 'ami', or 'azure'?
|
||||
return 'aws';
|
||||
}
|
||||
};
|
||||
|
||||
type AwsS3InstancePropTypes = {
|
||||
compose: ComposesResponseItem;
|
||||
isExpired: boolean;
|
||||
};
|
||||
|
||||
export const AwsS3Instance = ({
|
||||
compose,
|
||||
isExpired,
|
||||
}: AwsS3InstancePropTypes) => {
|
||||
const { data: composeStatus, isSuccess } = useGetComposeStatusQuery({
|
||||
composeId: compose.id,
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (!isSuccess) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
const fileExtensions: { [key in ImageTypes]: string } = {
|
||||
aws: '',
|
||||
azure: '',
|
||||
'edge-commit': '',
|
||||
'edge-installer': '',
|
||||
gcp: '',
|
||||
'guest-image': '.qcow2',
|
||||
'image-installer': '.iso',
|
||||
vsphere: '.vmdk',
|
||||
'vsphere-ova': '.ova',
|
||||
wsl: '.tar.gz',
|
||||
ami: '',
|
||||
'rhel-edge-commit': '',
|
||||
'rhel-edge-installer': '',
|
||||
vhd: '',
|
||||
};
|
||||
|
||||
const status = composeStatus.image_status.status;
|
||||
const options = composeStatus.image_status.upload_status?.options;
|
||||
|
||||
if (options && !isAwss3UploadStatus(options)) {
|
||||
throw TypeError(
|
||||
`Error: options must be of type Awss3UploadStatus, not ${typeof options}.`
|
||||
);
|
||||
}
|
||||
|
||||
if (status !== 'success') {
|
||||
return (
|
||||
<Button isDisabled variant="link" isInline>
|
||||
Download ({fileExtensions[compose.request.image_requests[0].image_type]}
|
||||
)
|
||||
</Button>
|
||||
);
|
||||
} else if (!isExpired) {
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
isInline
|
||||
href={options?.url}
|
||||
>
|
||||
Download ({fileExtensions[compose.request.image_requests[0].image_type]}
|
||||
)
|
||||
</Button>
|
||||
);
|
||||
} else if (isExpired) {
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
onClick={() => navigate(resolveRelPath(`imagewizard/${compose.id}`))}
|
||||
isInline
|
||||
>
|
||||
Recreate image
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
import React, { useMemo } from 'react';
|
||||
|
||||
import { Button, Popover } from '@patternfly/react-core';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { selectComposeById, selectImagesById } from '../../store/composesSlice';
|
||||
|
||||
export const selectRegions = createSelector(
|
||||
[selectComposeById, selectImagesById],
|
||||
(compose, images) => {
|
||||
const filteredImages = images.filter(
|
||||
(image) =>
|
||||
compose.share_with_accounts &&
|
||||
compose.share_with_accounts[0] === image.share_with_accounts[0]
|
||||
);
|
||||
|
||||
const regions = {};
|
||||
filteredImages.forEach((image) => {
|
||||
if (image.region && image.status === 'success') {
|
||||
if (regions[image.region]) {
|
||||
if (
|
||||
new Date(image.created_at) >
|
||||
new Date(regions[image.region].created_at)
|
||||
) {
|
||||
regions[image.region] = {
|
||||
ami: image.ami,
|
||||
created_at: image.created_at,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
regions[image.region] = {
|
||||
ami: image.ami,
|
||||
created_at: image.created_at,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return regions;
|
||||
}
|
||||
);
|
||||
|
||||
const ImageLinkRegion = ({ region, ami }) => {
|
||||
const url =
|
||||
'https://console.aws.amazon.com/ec2/v2/home?region=' +
|
||||
region +
|
||||
'#LaunchInstanceWizard:ami=' +
|
||||
ami;
|
||||
|
||||
return (
|
||||
<Button component="a" target="_blank" variant="link" isInline href={url}>
|
||||
{region}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const RegionsPopover = ({ composeId }) => {
|
||||
const regions = useSelector((state) => selectRegions(state, composeId));
|
||||
|
||||
const listItems = useMemo(() => {
|
||||
const listItems = [];
|
||||
for (const [key, value] of Object.entries(regions).sort()) {
|
||||
listItems.push(
|
||||
<li key={key}>
|
||||
<ImageLinkRegion region={key} ami={value.ami} />
|
||||
</li>
|
||||
);
|
||||
}
|
||||
return listItems;
|
||||
}, [regions]);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
/* popovers aren't rendered inside of the main page section, make sure our prefixed css still
|
||||
* applies */
|
||||
className="imageBuilder"
|
||||
aria-label="Launch instance"
|
||||
headerContent={<div>Launch instance</div>}
|
||||
bodyContent={
|
||||
<>
|
||||
<ul>{listItems}</ul>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button variant="link" isInline>
|
||||
Launch
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
ImageLinkRegion.propTypes = {
|
||||
region: PropTypes.string,
|
||||
ami: PropTypes.string,
|
||||
};
|
||||
|
||||
RegionsPopover.propTypes = {
|
||||
composeId: PropTypes.string,
|
||||
};
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Distributions } from '../../store/imageBuilderApi';
|
||||
|
||||
type ReleaseProps = {
|
||||
|
|
@ -27,7 +29,7 @@ const Release = ({ release }: ReleaseProps) => {
|
|||
'fedora-39': 'Fedora 39',
|
||||
};
|
||||
|
||||
return releaseDisplayValue[release];
|
||||
return <p>{releaseDisplayValue[release]}</p>;
|
||||
};
|
||||
|
||||
export default Release;
|
||||
|
|
|
|||
298
src/Components/ImagesTable/Status.tsx
Normal file
298
src/Components/ImagesTable/Status.tsx
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
import React from 'react';
|
||||
|
||||
import './ImageBuildStatus.scss';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Flex,
|
||||
Panel,
|
||||
PanelMain,
|
||||
Popover,
|
||||
Skeleton,
|
||||
} from '@patternfly/react-core';
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
CopyIcon,
|
||||
ExclamationCircleIcon,
|
||||
ExclamationTriangleIcon,
|
||||
InProgressIcon,
|
||||
OffIcon,
|
||||
PendingIcon,
|
||||
} from '@patternfly/react-icons';
|
||||
|
||||
import { AWS_S3_EXPIRATION_TIME_IN_HOURS } from '../../constants';
|
||||
import {
|
||||
ClonesResponseItem,
|
||||
ComposeStatus,
|
||||
ComposesResponseItem,
|
||||
UploadStatus,
|
||||
useGetComposeStatusQuery,
|
||||
} from '../../store/imageBuilderApi';
|
||||
|
||||
type StatusClonePropTypes = {
|
||||
clone: ClonesResponseItem;
|
||||
status: UploadStatus | undefined;
|
||||
};
|
||||
|
||||
export const StatusClone = ({ clone, status }: StatusClonePropTypes) => {
|
||||
switch (status?.status) {
|
||||
case 'failure':
|
||||
return (
|
||||
<ErrorStatus
|
||||
icon={statuses.failureSharing.icon}
|
||||
text={statuses.failureSharing.text}
|
||||
reason={`Failed to share image to ${clone.request.region}`}
|
||||
/>
|
||||
);
|
||||
case 'success':
|
||||
case 'running':
|
||||
case 'pending':
|
||||
return (
|
||||
<Status
|
||||
icon={statuses[status.status].icon}
|
||||
text={statuses[status.status].text}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
};
|
||||
|
||||
type ComposeStatusPropTypes = {
|
||||
compose: ComposesResponseItem;
|
||||
};
|
||||
|
||||
export const AwsDetailsStatus = ({ compose }: ComposeStatusPropTypes) => {
|
||||
const { data, isSuccess } = useGetComposeStatusQuery({
|
||||
composeId: compose.id,
|
||||
});
|
||||
|
||||
if (!isSuccess) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
switch (data.image_status.status) {
|
||||
case 'failure':
|
||||
return (
|
||||
<ErrorStatus
|
||||
icon={statuses[data.image_status.status].icon}
|
||||
text={statuses[data.image_status.status].text}
|
||||
reason={data?.image_status?.error?.details?.reason || ''}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Status
|
||||
icon={statuses[data.image_status.status].icon}
|
||||
text={statuses[data.image_status.status].text}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
type CloudStatusPropTypes = {
|
||||
compose: ComposesResponseItem;
|
||||
};
|
||||
|
||||
export const CloudStatus = ({ compose }: CloudStatusPropTypes) => {
|
||||
const { data, isSuccess } = useGetComposeStatusQuery({
|
||||
composeId: compose.id,
|
||||
});
|
||||
|
||||
if (!isSuccess) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
switch (data.image_status.status) {
|
||||
case 'failure':
|
||||
return (
|
||||
<ErrorStatus
|
||||
icon={statuses['failure'].icon}
|
||||
text={statuses['failure'].text}
|
||||
reason={data.image_status.error?.details?.reason || ''}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Status
|
||||
icon={statuses[data.image_status.status].icon}
|
||||
text={statuses[data.image_status.status].text}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
type AzureStatusPropTypes = {
|
||||
status: ComposeStatus;
|
||||
};
|
||||
|
||||
export const AzureStatus = ({ status }: AzureStatusPropTypes) => {
|
||||
switch (status.image_status.status) {
|
||||
case 'failure':
|
||||
return (
|
||||
<ErrorStatus
|
||||
icon={statuses[status.image_status.status].icon}
|
||||
text={statuses[status.image_status.status].text}
|
||||
reason={status.image_status.error?.reason || ''}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Status
|
||||
icon={statuses[status.image_status.status].icon}
|
||||
text={statuses[status.image_status.status].text}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
type AwsS3StatusPropTypes = {
|
||||
compose: ComposesResponseItem;
|
||||
isExpired: boolean;
|
||||
hoursToExpiration: number;
|
||||
};
|
||||
|
||||
export const AwsS3Status = ({
|
||||
compose,
|
||||
isExpired,
|
||||
hoursToExpiration,
|
||||
}: AwsS3StatusPropTypes) => {
|
||||
const { data: composeStatus, isSuccess } = useGetComposeStatusQuery({
|
||||
composeId: compose.id,
|
||||
});
|
||||
|
||||
if (!isSuccess) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
const status = composeStatus.image_status.status;
|
||||
const remainingTime = AWS_S3_EXPIRATION_TIME_IN_HOURS - hoursToExpiration;
|
||||
|
||||
if (isExpired) {
|
||||
return (
|
||||
<Status icon={statuses['expired'].icon} text={statuses['expired'].text} />
|
||||
);
|
||||
} else if (status === 'success') {
|
||||
return (
|
||||
<Status
|
||||
icon={statuses['expiring'].icon}
|
||||
text={`Expires in ${remainingTime} ${
|
||||
remainingTime > 1 ? 'hours' : 'hour'
|
||||
}`}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <Status icon={statuses[status].icon} text={statuses[status].text} />;
|
||||
}
|
||||
};
|
||||
|
||||
const statuses = {
|
||||
failure: {
|
||||
icon: <ExclamationCircleIcon className="error" />,
|
||||
text: 'Image build failed',
|
||||
},
|
||||
|
||||
pending: {
|
||||
icon: <PendingIcon />,
|
||||
text: 'Image build is pending',
|
||||
},
|
||||
|
||||
building: {
|
||||
icon: <InProgressIcon className="pending" />,
|
||||
text: 'Image build in progress',
|
||||
},
|
||||
|
||||
uploading: {
|
||||
icon: <InProgressIcon className="pending" />,
|
||||
text: 'Image upload in progress',
|
||||
},
|
||||
|
||||
registering: {
|
||||
icon: <InProgressIcon className="pending" />,
|
||||
text: 'Cloud registration in progress',
|
||||
},
|
||||
|
||||
running: {
|
||||
icon: <InProgressIcon className="pending" />,
|
||||
text: 'Running',
|
||||
},
|
||||
|
||||
success: {
|
||||
icon: <CheckCircleIcon className="success" />,
|
||||
text: 'Ready',
|
||||
},
|
||||
|
||||
expired: {
|
||||
icon: <OffIcon />,
|
||||
text: 'Expired',
|
||||
},
|
||||
|
||||
expiring: {
|
||||
icon: <ExclamationTriangleIcon className="expiring" />,
|
||||
},
|
||||
|
||||
failureSharing: {
|
||||
icon: <ExclamationCircleIcon className="error" />,
|
||||
text: 'Sharing image failed',
|
||||
},
|
||||
|
||||
failedClone: {
|
||||
icon: <ExclamationCircleIcon className="error" />,
|
||||
text: 'Failure sharing',
|
||||
},
|
||||
};
|
||||
|
||||
type StatusPropTypes = {
|
||||
icon: JSX.Element;
|
||||
text: string;
|
||||
};
|
||||
|
||||
const Status = ({ icon, text }: StatusPropTypes) => {
|
||||
return (
|
||||
<Flex className="pf-u-align-items-baseline pf-m-nowrap">
|
||||
<div className="pf-u-mr-sm">{icon}</div>
|
||||
<p>{text}</p>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
type ErrorStatusPropTypes = {
|
||||
icon: JSX.Element;
|
||||
text: string;
|
||||
reason: string;
|
||||
};
|
||||
|
||||
const ErrorStatus = ({ icon, text, reason }: ErrorStatusPropTypes) => {
|
||||
return (
|
||||
<Flex className="pf-u-align-items-baseline pf-m-nowrap">
|
||||
<div className="pf-u-mr-sm">{icon}</div>
|
||||
<Popover
|
||||
position="bottom"
|
||||
minWidth="30rem"
|
||||
bodyContent={
|
||||
<>
|
||||
<Alert variant="danger" title={text} isInline isPlain />
|
||||
<Panel isScrollable>
|
||||
<PanelMain maxHeight="25rem">
|
||||
<div className="pf-u-mt-sm">
|
||||
<p>{reason}</p>
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => navigator.clipboard.writeText(reason)}
|
||||
className="pf-u-pl-0 pf-u-mt-md"
|
||||
>
|
||||
Copy error text to clipboard <CopyIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</PanelMain>
|
||||
</Panel>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Button variant="link" className="pf-u-p-0 pf-u-font-size-sm">
|
||||
<div className="failure-button">{text}</div>
|
||||
</Button>
|
||||
</Popover>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { selectComposeById } from '../../store/composesSlice';
|
||||
|
||||
const Target = ({ composeId }) => {
|
||||
const compose = useSelector((state) => selectComposeById(state, composeId));
|
||||
|
||||
const targetOptions = {
|
||||
aws: 'Amazon Web Services',
|
||||
azure: 'Microsoft Azure',
|
||||
gcp: 'Google Cloud Platform',
|
||||
vsphere: 'VMWare vSphere',
|
||||
'vsphere-ova': 'VMWare vSphere',
|
||||
'guest-image': 'Virtualization - Guest image',
|
||||
'image-installer': 'Bare metal - Installer',
|
||||
wsl: 'Windows Subsystem for Linux',
|
||||
};
|
||||
|
||||
let target;
|
||||
if (compose.uploadType === 'aws.s3') {
|
||||
target = targetOptions[compose.imageType];
|
||||
} else if (compose.uploadType === 'aws') {
|
||||
target =
|
||||
targetOptions[compose.uploadType] +
|
||||
` (${compose.clones.length !== 0 ? compose.clones.length + 1 : 1})`;
|
||||
} else {
|
||||
target = targetOptions[compose.uploadType];
|
||||
}
|
||||
|
||||
return <>{target ? target : compose.imageType}</>;
|
||||
};
|
||||
|
||||
Target.propTypes = {
|
||||
composeId: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Target;
|
||||
51
src/Components/ImagesTable/Target.tsx
Normal file
51
src/Components/ImagesTable/Target.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Skeleton } from '@patternfly/react-core';
|
||||
|
||||
import {
|
||||
ImageTypes,
|
||||
useGetComposeClonesQuery,
|
||||
} from '../../store/imageBuilderApi';
|
||||
import { ComposesResponseItem } from '../../store/imageBuilderApi';
|
||||
|
||||
const targetOptions: { [key in ImageTypes]: string } = {
|
||||
aws: 'Amazon Web Services',
|
||||
azure: 'Microsoft Azure',
|
||||
'edge-commit': 'Edge Commit',
|
||||
'edge-installer': 'Edge Installer',
|
||||
gcp: 'Google Cloud Platform',
|
||||
'guest-image': 'Virtualization - Guest image',
|
||||
'image-installer': 'Bare metal - Installer',
|
||||
vsphere: 'VMWare vSphere',
|
||||
'vsphere-ova': 'VMWare vSphere',
|
||||
wsl: 'Windows Subsystem for Linux',
|
||||
ami: 'Amazon Web Services',
|
||||
'rhel-edge-commit': 'RHEL Edge Commit',
|
||||
'rhel-edge-installer': 'RHEL Edge Installer',
|
||||
vhd: '',
|
||||
};
|
||||
|
||||
type TargetPropTypes = {
|
||||
compose: ComposesResponseItem;
|
||||
};
|
||||
|
||||
export const Target = ({ compose }: TargetPropTypes) => {
|
||||
return <p>{targetOptions[compose.request.image_requests[0].image_type]}</p>;
|
||||
};
|
||||
|
||||
type AwsTargetPropTypes = {
|
||||
compose: ComposesResponseItem;
|
||||
};
|
||||
|
||||
export const AwsTarget = ({ compose }: AwsTargetPropTypes) => {
|
||||
const { data, isSuccess } = useGetComposeClonesQuery({
|
||||
composeId: compose.id,
|
||||
});
|
||||
|
||||
if (!isSuccess) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
const text = `Amazon Web Services (${data.data.length + 1})`;
|
||||
return <>{text}</>;
|
||||
};
|
||||
|
|
@ -42,7 +42,7 @@ export const LandingPage = () => {
|
|||
const [activeTabKey, setActiveTabKey] = useState(initialActiveTabKey);
|
||||
useEffect(() => {
|
||||
setActiveTabKey(initialActiveTabKey);
|
||||
}, [pathname]);
|
||||
}, [initialActiveTabKey]);
|
||||
const handleTabClick = (_event: React.MouseEvent, tabIndex: number) => {
|
||||
const tabPath = tabsPath[tabIndex];
|
||||
if (tabPath !== undefined) {
|
||||
|
|
@ -60,7 +60,8 @@ export const LandingPage = () => {
|
|||
</section>
|
||||
);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<>
|
||||
{/*@ts-ignore*/}
|
||||
<PageHeader>
|
||||
<PageHeaderTitle className="title" title="Image Builder" />
|
||||
<Popover
|
||||
|
|
@ -212,7 +213,7 @@ export const LandingPage = () => {
|
|||
traditionalImageList
|
||||
)}
|
||||
<Outlet />
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -9,45 +9,52 @@ import {
|
|||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
ValidatedOptions,
|
||||
} from '@patternfly/react-core';
|
||||
import { ExclamationCircleIcon, HelpIcon } from '@patternfly/react-icons';
|
||||
import { addNotification } from '@redhat-cloud-services/frontend-components-notifications/redux';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import api from '../../api';
|
||||
import { AWS_REGIONS } from '../../constants';
|
||||
import { cloneAdded } from '../../store/clonesSlice';
|
||||
import { selectClonesById, selectComposeById } from '../../store/composesSlice';
|
||||
import {
|
||||
ComposeStatus,
|
||||
useCloneComposeMutation,
|
||||
useGetComposeStatusQuery,
|
||||
} from '../../store/imageBuilderApi';
|
||||
import { resolveRelPath } from '../../Utilities/path';
|
||||
|
||||
export const selectRegionsToDisable = createSelector(
|
||||
[selectComposeById, selectClonesById],
|
||||
(compose, clones) => {
|
||||
const regions = new Set();
|
||||
regions.add(compose.region);
|
||||
clones.map((clone) => {
|
||||
clone.region &&
|
||||
clone.share_with_accounts?.[0] === compose.share_with_accounts?.[0] &&
|
||||
clone.share_with_sources?.[0] === compose.share_with_sources?.[0] &&
|
||||
clone.status !== 'failure' &&
|
||||
regions.add(clone.region);
|
||||
});
|
||||
const generateRequests = (
|
||||
composeId: string,
|
||||
composeStatus: ComposeStatus,
|
||||
regions: string[]
|
||||
) => {
|
||||
return regions.map((region) => {
|
||||
const options =
|
||||
composeStatus.request.image_requests[0].upload_request.options;
|
||||
return {
|
||||
composeId: composeId,
|
||||
cloneRequest: {
|
||||
region: region,
|
||||
share_with_sources:
|
||||
'share_with_sources' in options
|
||||
? options.share_with_sources
|
||||
: undefined,
|
||||
share_with_accounts:
|
||||
'share_with_accounts' in options
|
||||
? options.share_with_accounts
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return regions;
|
||||
}
|
||||
);
|
||||
|
||||
const prepareRegions = (regionsToDisable) => {
|
||||
const regions = AWS_REGIONS.map((region) => ({
|
||||
...region,
|
||||
disabled:
|
||||
regionsToDisable.has(region.value) || region?.disableRegion === true,
|
||||
}));
|
||||
|
||||
return regions;
|
||||
type RegionsSelectPropTypes = {
|
||||
composeId: string;
|
||||
handleClose: any;
|
||||
handleToggle: any;
|
||||
isOpen: boolean;
|
||||
setIsOpen: any;
|
||||
};
|
||||
|
||||
const RegionsSelect = ({
|
||||
|
|
@ -56,25 +63,34 @@ const RegionsSelect = ({
|
|||
handleToggle,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}) => {
|
||||
}: RegionsSelectPropTypes) => {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [selected, setSelected] = useState([]);
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
const titleId = 'Clone this image';
|
||||
const [validated, setValidated] = useState('default');
|
||||
const [validated, setValidated] = useState<ValidatedOptions>(
|
||||
ValidatedOptions.default
|
||||
);
|
||||
const [helperTextInvalid] = useState(
|
||||
'Select at least one region to share to.'
|
||||
);
|
||||
const [cloneCompose] = useCloneComposeMutation();
|
||||
|
||||
const compose = useSelector((state) => selectComposeById(state, composeId));
|
||||
const { data: composeStatus, isSuccess } = useGetComposeStatusQuery({
|
||||
composeId,
|
||||
});
|
||||
|
||||
const regionsToDisable = useSelector((state) =>
|
||||
selectRegionsToDisable(state, composeId)
|
||||
);
|
||||
const [options] = useState(prepareRegions(regionsToDisable));
|
||||
if (!isSuccess) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const handleSelect = (event, selection) => {
|
||||
const options = AWS_REGIONS;
|
||||
|
||||
const handleSelect = (
|
||||
event: React.MouseEvent<Element, MouseEvent> | React.ChangeEvent<Element>,
|
||||
selection: string
|
||||
): void => {
|
||||
let nextSelected;
|
||||
if (selected.includes(selection)) {
|
||||
nextSelected = selected.filter((region) => region !== selection);
|
||||
|
|
@ -83,48 +99,27 @@ const RegionsSelect = ({
|
|||
nextSelected = [...selected, selection];
|
||||
setSelected(nextSelected);
|
||||
}
|
||||
nextSelected.length === 0 ? setValidated('error') : setValidated('default');
|
||||
nextSelected.length === 0
|
||||
? setValidated(ValidatedOptions.error)
|
||||
: setValidated(ValidatedOptions.default);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setSelected([]);
|
||||
setIsOpen(false);
|
||||
setValidated('error');
|
||||
setValidated(ValidatedOptions.error);
|
||||
};
|
||||
|
||||
const generateRequests = () => {
|
||||
const requests = selected.map((region) => {
|
||||
const request = { region: region };
|
||||
if (compose.share_with_sources?.[0]) {
|
||||
request.share_with_sources = [compose.share_with_sources[0]];
|
||||
} else {
|
||||
request.share_with_accounts = [compose.share_with_accounts[0]];
|
||||
}
|
||||
return request;
|
||||
});
|
||||
return requests;
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
const handleSubmit = async () => {
|
||||
setIsSaving(true);
|
||||
const requests = generateRequests();
|
||||
Promise.all(
|
||||
requests.map((request) =>
|
||||
api.cloneImage(composeId, request).then((response) => {
|
||||
dispatch(
|
||||
cloneAdded({
|
||||
clone: {
|
||||
...response,
|
||||
request,
|
||||
image_status: { status: 'pending' },
|
||||
},
|
||||
parent: composeId,
|
||||
})
|
||||
);
|
||||
})
|
||||
)
|
||||
)
|
||||
const requests = generateRequests(composeId, composeStatus, selected);
|
||||
// https://redux-toolkit.js.org/rtk-query/usage/mutations#frequently-used-mutation-hook-return-values
|
||||
// If you want to immediately access the result of a mutation, you need to chain `.unwrap()`
|
||||
// if you actually want the payload or to catch the error.
|
||||
// We do this so we can dispatch the appropriate notification (success or failure).
|
||||
await Promise.all(requests.map((request) => cloneCompose(request).unwrap()))
|
||||
.then(() => {
|
||||
setIsSaving(false);
|
||||
navigate(resolveRelPath(''));
|
||||
dispatch(
|
||||
addNotification({
|
||||
|
|
@ -132,16 +127,15 @@ const RegionsSelect = ({
|
|||
title: 'Your image is being shared',
|
||||
})
|
||||
);
|
||||
|
||||
setIsSaving(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
navigate(resolveRelPath(''));
|
||||
// TODO The error should be typed.
|
||||
dispatch(
|
||||
addNotification({
|
||||
variant: 'danger',
|
||||
title: 'Your image could not be created',
|
||||
description: `Status code ${err.response.status}: ${err.response.statusText}`,
|
||||
title: 'Your image could not be shared',
|
||||
description: `Status code ${err.status}: ${err.data.errors[0].detail}`,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
@ -198,7 +192,7 @@ const RegionsSelect = ({
|
|||
>
|
||||
{options.map((option, index) => (
|
||||
<SelectOption
|
||||
isDisabled={option.disabled}
|
||||
isDisabled={option.disableRegion}
|
||||
key={index}
|
||||
value={option.value}
|
||||
{...(option.description && { description: option.description })}
|
||||
|
|
@ -224,12 +218,4 @@ const RegionsSelect = ({
|
|||
);
|
||||
};
|
||||
|
||||
RegionsSelect.propTypes = {
|
||||
composeId: PropTypes.string,
|
||||
handleClose: PropTypes.func,
|
||||
handleToggle: PropTypes.func,
|
||||
isOpen: PropTypes.bool,
|
||||
setIsOpen: PropTypes.func,
|
||||
};
|
||||
|
||||
export default RegionsSelect;
|
||||
|
|
@ -15,7 +15,7 @@ const ShareToRegionsModal = () => {
|
|||
|
||||
const { composeId } = useParams();
|
||||
|
||||
const handleToggle = (isOpen) => setIsOpen(isOpen);
|
||||
const handleToggle = (isOpen: boolean) => setIsOpen(isOpen);
|
||||
|
||||
const handleEscapePress = () => {
|
||||
if (isOpen) {
|
||||
|
|
@ -25,7 +25,15 @@ const ShareToRegionsModal = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const appendTo = useMemo(() => document.querySelector(MODAL_ANCHOR), []);
|
||||
const appendTo = useMemo(() => {
|
||||
const modalAnchor = document.querySelector(MODAL_ANCHOR);
|
||||
return modalAnchor === null ? undefined : (modalAnchor as HTMLElement);
|
||||
}, []);
|
||||
|
||||
if (!composeId) {
|
||||
handleClose();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
|
@ -23,7 +23,7 @@ export const convertStringToDate = (createdAtAsString) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const hoursToExpiration = (imageCreatedAt) => {
|
||||
export const computeHoursToExpiration = (imageCreatedAt) => {
|
||||
if (imageCreatedAt) {
|
||||
const currentTime = Date.now();
|
||||
// miliseconds in hour - needed for calculating the difference
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ export const AWS_REGIONS = [
|
|||
{
|
||||
description: 'US East (N. Virginia)',
|
||||
value: 'us-east-1',
|
||||
disableRegion: false,
|
||||
// disable default region
|
||||
disableRegion: true,
|
||||
},
|
||||
{
|
||||
description: 'US West (N. California)',
|
||||
|
|
@ -120,3 +121,5 @@ export const AWS_S3_EXPIRATION_TIME_IN_HOURS = 6;
|
|||
|
||||
// Anchor element for all modals that we display so that they play nice with top-most components like Quickstarts
|
||||
export const MODAL_ANCHOR = '.pf-c-page.chr-c-page';
|
||||
|
||||
export const STATUS_POLLING_INTERVAL = 8000;
|
||||
|
|
|
|||
29
src/store/enhancedImageBuilderApi.ts
Normal file
29
src/store/enhancedImageBuilderApi.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { imageBuilderApi } from './imageBuilderApi';
|
||||
|
||||
const enhancedApi = imageBuilderApi.enhanceEndpoints({
|
||||
addTagTypes: ['Clone', 'Compose'],
|
||||
endpoints: {
|
||||
getComposes: {
|
||||
providesTags: () => {
|
||||
return [{ type: 'Compose' }];
|
||||
},
|
||||
},
|
||||
getComposeClones: {
|
||||
providesTags: (_request, _error, arg) => {
|
||||
return [{ type: 'Clone', id: arg.composeId }];
|
||||
},
|
||||
},
|
||||
cloneCompose: {
|
||||
invalidatesTags: (_request, _error, arg) => {
|
||||
return [{ type: 'Clone', id: arg.composeId }];
|
||||
},
|
||||
},
|
||||
composeImage: {
|
||||
invalidatesTags: () => {
|
||||
return [{ type: 'Compose' }];
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export { enhancedApi as imageBuilderApi };
|
||||
|
|
@ -6,7 +6,7 @@ import clonesSlice from './clonesSlice';
|
|||
import composesSlice from './composesSlice';
|
||||
import { contentSourcesApi } from './contentSourcesApi';
|
||||
import { edgeApi } from './edgeApi';
|
||||
import { imageBuilderApi } from './imageBuilderApi';
|
||||
import { imageBuilderApi } from './enhancedImageBuilderApi';
|
||||
import { provisioningApi } from './provisioningApi';
|
||||
import { rhsmApi } from './rhsmApi';
|
||||
|
||||
|
|
@ -21,13 +21,13 @@ export const reducer = {
|
|||
notifications: notificationsReducer,
|
||||
};
|
||||
|
||||
export const middleware = (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware()
|
||||
.concat(promiseMiddleware)
|
||||
.concat(contentSourcesApi.middleware)
|
||||
.concat(edgeApi.middleware)
|
||||
.concat(imageBuilderApi.middleware)
|
||||
.concat(rhsmApi.middleware)
|
||||
.concat(provisioningApi.middleware);
|
||||
export const middleware = (getDefaultMiddleware: Function) =>
|
||||
getDefaultMiddleware().concat(
|
||||
promiseMiddleware,
|
||||
contentSourcesApi.middleware,
|
||||
imageBuilderApi.middleware,
|
||||
rhsmApi.middleware,
|
||||
provisioningApi.middleware
|
||||
);
|
||||
|
||||
export const store = configureStore({ reducer, middleware });
|
||||
46
src/store/typeGuards.ts
Normal file
46
src/store/typeGuards.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import {
|
||||
AwsUploadRequestOptions,
|
||||
Awss3UploadStatus,
|
||||
AzureUploadRequestOptions,
|
||||
AzureUploadStatus,
|
||||
GcpUploadRequestOptions,
|
||||
GcpUploadStatus,
|
||||
UploadRequest,
|
||||
UploadStatus,
|
||||
} from './imageBuilderApi';
|
||||
|
||||
export const isGcpUploadRequestOptions = (
|
||||
options: UploadRequest['options']
|
||||
): options is GcpUploadRequestOptions => {
|
||||
return (options as GcpUploadRequestOptions).share_with_accounts !== undefined;
|
||||
};
|
||||
|
||||
export const isAwsUploadRequestOptions = (
|
||||
options: UploadRequest['options']
|
||||
): options is AwsUploadRequestOptions => {
|
||||
return true;
|
||||
};
|
||||
|
||||
export const isAzureUploadRequestOptions = (
|
||||
options: UploadRequest['options']
|
||||
): options is AzureUploadRequestOptions => {
|
||||
return (options as AzureUploadRequestOptions).resource_group !== undefined;
|
||||
};
|
||||
|
||||
export const isGcpUploadStatus = (
|
||||
status: UploadStatus['options']
|
||||
): status is GcpUploadStatus => {
|
||||
return (status as GcpUploadStatus).project_id !== undefined;
|
||||
};
|
||||
|
||||
export const isAwss3UploadStatus = (
|
||||
status: UploadStatus['options']
|
||||
): status is Awss3UploadStatus => {
|
||||
return (status as Awss3UploadStatus).url !== undefined;
|
||||
};
|
||||
|
||||
export const isAzureUploadStatus = (
|
||||
status: UploadStatus['options']
|
||||
): status is AzureUploadStatus => {
|
||||
return (status as AzureUploadStatus).image_name !== undefined;
|
||||
};
|
||||
|
|
@ -75,7 +75,11 @@ const getSourceDropdown = async () => {
|
|||
describe('Step Upload to Azure', () => {
|
||||
const user = userEvent.setup();
|
||||
const setUp = async () => {
|
||||
({ router } = renderCustomRoutesWithReduxRouter('imagewizard', {}, routes));
|
||||
({ router } = await renderCustomRoutesWithReduxRouter(
|
||||
'imagewizard',
|
||||
{},
|
||||
routes
|
||||
));
|
||||
// select Azure as upload destination
|
||||
await user.click(await screen.findByTestId('upload-azure'));
|
||||
|
||||
|
|
|
|||
|
|
@ -8,11 +8,6 @@ import userEvent from '@testing-library/user-event';
|
|||
import api from '../../../api.js';
|
||||
import CreateImageWizard from '../../../Components/CreateImageWizard/CreateImageWizard';
|
||||
import ShareImageModal from '../../../Components/ShareImageModal/ShareImageModal';
|
||||
import { store } from '../../../store/index.js';
|
||||
import {
|
||||
mockComposesEmpty,
|
||||
mockStateRecreateImage,
|
||||
} from '../../fixtures/composes';
|
||||
import {
|
||||
mockPkgResultAlpha,
|
||||
mockPkgResultAlphaContentSources,
|
||||
|
|
@ -63,14 +58,6 @@ jest.mock('@redhat-cloud-services/frontend-components/useChrome', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
// Mocking getComposes is necessary because in many tests we call navigate()
|
||||
// to navigate to the images table (via useNavigate hook), which will in turn
|
||||
// result in a call to getComposes. If it is not mocked, tests fail due to MSW
|
||||
// being unable to resolve that endpoint.
|
||||
jest
|
||||
.spyOn(api, 'getComposes')
|
||||
.mockImplementation(() => Promise.resolve(mockComposesEmpty));
|
||||
|
||||
const searchForAvailablePackages = async (searchbox, searchTerm) => {
|
||||
const user = userEvent.setup();
|
||||
await user.type(searchbox, searchTerm);
|
||||
|
|
@ -117,7 +104,7 @@ describe('Step Packages', () => {
|
|||
const setUp = async () => {
|
||||
mockContentSourcesEnabled = false;
|
||||
|
||||
({ router } = renderCustomRoutesWithReduxRouter(
|
||||
({ router } = await renderCustomRoutesWithReduxRouter(
|
||||
'imagewizard',
|
||||
{},
|
||||
routes
|
||||
|
|
@ -877,35 +864,21 @@ describe('Step Custom repositories', () => {
|
|||
describe('On Recreate', () => {
|
||||
const user = userEvent.setup();
|
||||
const setUp = async () => {
|
||||
jest.mock('../../../store/index.js');
|
||||
|
||||
const state = mockStateRecreateImage;
|
||||
|
||||
store.getState = () => state;
|
||||
|
||||
({ router } = renderWithReduxRouter(
|
||||
'imagewizard/hyk93673-8dcc-4a61-ac30-e9f4940d8346',
|
||||
state
|
||||
'imagewizard/hyk93673-8dcc-4a61-ac30-e9f4940d8346'
|
||||
));
|
||||
};
|
||||
|
||||
const setUpUnavailableRepo = async () => {
|
||||
jest.mock('../../../store/index.js');
|
||||
|
||||
const state = mockStateRecreateImage;
|
||||
|
||||
store.getState = () => state;
|
||||
|
||||
({ router } = renderWithReduxRouter(
|
||||
'imagewizard/b7193673-8dcc-4a5f-ac30-e9f4940d8346',
|
||||
state
|
||||
'imagewizard/b7193673-8dcc-4a5f-ac30-e9f4940d8346'
|
||||
));
|
||||
};
|
||||
|
||||
test('with valid repositories', async () => {
|
||||
await setUp();
|
||||
|
||||
screen.getByRole('heading', { name: /review/i });
|
||||
await screen.findByRole('heading', { name: /review/i });
|
||||
expect(
|
||||
screen.queryByText('Previously added custom repository unavailable')
|
||||
).not.toBeInTheDocument();
|
||||
|
|
@ -943,7 +916,7 @@ describe('On Recreate', () => {
|
|||
test('with repositories that are no longer available', async () => {
|
||||
await setUpUnavailableRepo();
|
||||
|
||||
screen.getByRole('heading', { name: /review/i });
|
||||
await screen.findByRole('heading', { name: /review/i });
|
||||
await screen.findByText('Previously added custom repository unavailable');
|
||||
|
||||
const createImageButton = await screen.findByRole('button', {
|
||||
|
|
|
|||
|
|
@ -11,12 +11,9 @@ import {
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import { rest } from 'msw';
|
||||
|
||||
import api from '../../../api.js';
|
||||
import CreateImageWizard from '../../../Components/CreateImageWizard/CreateImageWizard';
|
||||
import ShareImageModal from '../../../Components/ShareImageModal/ShareImageModal';
|
||||
import { RHEL_8, RHEL_9, PROVISIONING_API } from '../../../constants.js';
|
||||
import { mockComposesEmpty } from '../../fixtures/composes';
|
||||
import { customizations } from '../../fixtures/customizations';
|
||||
import { PROVISIONING_API } from '../../../constants.js';
|
||||
import { server } from '../../mocks/server.js';
|
||||
import {
|
||||
clickBack,
|
||||
|
|
@ -48,9 +45,9 @@ let router = undefined;
|
|||
// to navigate to the images table (via useNavigate hook), which will in turn
|
||||
// result in a call to getComposes. If it is not mocked, tests fail due to MSW
|
||||
// being unable to resolve that endpoint.
|
||||
jest
|
||||
.spyOn(api, 'getComposes')
|
||||
.mockImplementation(() => Promise.resolve(mockComposesEmpty));
|
||||
// jest
|
||||
// .spyOn(api, 'getComposes')
|
||||
// .mockImplementation(() => Promise.resolve(mockComposesEmpty));
|
||||
|
||||
jest.mock('@redhat-cloud-services/frontend-components/useChrome', () => ({
|
||||
useChrome: () => ({
|
||||
|
|
@ -137,7 +134,11 @@ describe('Create Image Wizard', () => {
|
|||
describe('Step Image output', () => {
|
||||
const user = userEvent.setup();
|
||||
const setUp = async () => {
|
||||
({ router } = renderCustomRoutesWithReduxRouter('imagewizard', {}, routes));
|
||||
({ router } = await renderCustomRoutesWithReduxRouter(
|
||||
'imagewizard',
|
||||
{},
|
||||
routes
|
||||
));
|
||||
|
||||
// select aws as upload destination
|
||||
await user.click(await screen.findByTestId('upload-aws'));
|
||||
|
|
@ -270,7 +271,7 @@ describe('Step Image output', () => {
|
|||
describe('Step Upload to AWS', () => {
|
||||
const user = userEvent.setup();
|
||||
const setUp = async () => {
|
||||
({ router, store } = renderCustomRoutesWithReduxRouter(
|
||||
({ router, store } = await renderCustomRoutesWithReduxRouter(
|
||||
'imagewizard',
|
||||
{},
|
||||
routes
|
||||
|
|
@ -399,44 +400,38 @@ describe('Step Upload to AWS', () => {
|
|||
await clickNext();
|
||||
await clickNext();
|
||||
|
||||
const composeImage = jest
|
||||
.spyOn(api, 'composeImage')
|
||||
.mockImplementation((body) => {
|
||||
expect(body).toEqual({
|
||||
distribution: RHEL_9,
|
||||
image_name: undefined,
|
||||
customizations: {
|
||||
packages: undefined,
|
||||
},
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'aws',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {
|
||||
share_with_sources: ['123'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f5a';
|
||||
return Promise.resolve({ id });
|
||||
});
|
||||
// const composeImage = jest
|
||||
// .spyOn(api, 'composeImage')
|
||||
// .mockImplementation((body) => {
|
||||
// expect(body).toEqual({
|
||||
// distribution: RHEL_9,
|
||||
// image_name: undefined,
|
||||
// customizations: {
|
||||
// packages: undefined,
|
||||
// },
|
||||
// image_requests: [
|
||||
// {
|
||||
// architecture: 'x86_64',
|
||||
// image_type: 'aws',
|
||||
// upload_request: {
|
||||
// type: 'aws',
|
||||
// options: {
|
||||
// share_with_sources: ['123'],
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// });
|
||||
// const id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f5a';
|
||||
// return Promise.resolve({ id });
|
||||
// });
|
||||
|
||||
user.click(screen.getByRole('button', { name: /Create/ }));
|
||||
|
||||
// API request sent to backend
|
||||
await waitFor(() => expect(composeImage).toHaveBeenCalledTimes(1));
|
||||
|
||||
// returns back to the landing page
|
||||
await waitFor(() =>
|
||||
expect(router.state.location.pathname).toBe('/insights/image-builder')
|
||||
);
|
||||
expect(store.getState().composes.allIds).toEqual([
|
||||
'edbae1c2-62bc-42c1-ae0c-3110ab718f5a',
|
||||
]);
|
||||
// set test timeout of 10 seconds
|
||||
}, 10000);
|
||||
});
|
||||
|
|
@ -444,7 +439,11 @@ describe('Step Upload to AWS', () => {
|
|||
describe('Step Upload to Google', () => {
|
||||
const user = userEvent.setup();
|
||||
const setUp = async () => {
|
||||
({ router } = renderCustomRoutesWithReduxRouter('imagewizard', {}, routes));
|
||||
({ router } = await renderCustomRoutesWithReduxRouter(
|
||||
'imagewizard',
|
||||
{},
|
||||
routes
|
||||
));
|
||||
|
||||
// select gcp as upload destination
|
||||
user.click(screen.getByTestId('upload-google'));
|
||||
|
|
@ -507,7 +506,11 @@ describe('Step Upload to Google', () => {
|
|||
describe('Step Registration', () => {
|
||||
const user = userEvent.setup();
|
||||
const setUp = async () => {
|
||||
({ router } = renderCustomRoutesWithReduxRouter('imagewizard', {}, routes));
|
||||
({ router } = await renderCustomRoutesWithReduxRouter(
|
||||
'imagewizard',
|
||||
{},
|
||||
routes
|
||||
));
|
||||
|
||||
// select aws as upload destination
|
||||
user.click(screen.getByTestId('upload-aws'));
|
||||
|
|
@ -693,7 +696,11 @@ describe('Step Registration', () => {
|
|||
describe('Step File system configuration', () => {
|
||||
const user = userEvent.setup();
|
||||
const setUp = async () => {
|
||||
({ router } = renderCustomRoutesWithReduxRouter('imagewizard', {}, routes));
|
||||
({ router } = await renderCustomRoutesWithReduxRouter(
|
||||
'imagewizard',
|
||||
{},
|
||||
routes
|
||||
));
|
||||
|
||||
// select aws as upload destination
|
||||
user.click(screen.getByTestId('upload-aws'));
|
||||
|
|
@ -768,7 +775,11 @@ describe('Step File system configuration', () => {
|
|||
describe('Step Details', () => {
|
||||
const user = userEvent.setup();
|
||||
const setUp = async () => {
|
||||
({ router } = renderCustomRoutesWithReduxRouter('imagewizard', {}, routes));
|
||||
({ router } = await renderCustomRoutesWithReduxRouter(
|
||||
'imagewizard',
|
||||
{},
|
||||
routes
|
||||
));
|
||||
|
||||
// select aws as upload destination
|
||||
user.click(screen.getByTestId('upload-aws'));
|
||||
|
|
@ -837,7 +848,11 @@ describe('Step Details', () => {
|
|||
describe('Step Review', () => {
|
||||
const user = userEvent.setup();
|
||||
const setUp = async () => {
|
||||
({ router } = renderCustomRoutesWithReduxRouter('imagewizard', {}, routes));
|
||||
({ router } = await renderCustomRoutesWithReduxRouter(
|
||||
'imagewizard',
|
||||
{},
|
||||
routes
|
||||
));
|
||||
|
||||
// select aws as upload destination
|
||||
user.click(screen.getByTestId('upload-aws'));
|
||||
|
|
@ -984,7 +999,7 @@ describe('Step Review', () => {
|
|||
describe('Click through all steps', () => {
|
||||
const user = userEvent.setup();
|
||||
const setUp = async () => {
|
||||
({ router, store } = renderCustomRoutesWithReduxRouter(
|
||||
({ router, store } = await renderCustomRoutesWithReduxRouter(
|
||||
'imagewizard',
|
||||
{},
|
||||
routes
|
||||
|
|
@ -1051,7 +1066,10 @@ describe('Click through all steps', () => {
|
|||
await clickNext();
|
||||
|
||||
// fsc
|
||||
(await screen.findByTestId('file-system-config-radio-manual')).click();
|
||||
const fscToggle = await screen.findByTestId(
|
||||
'file-system-config-radio-manual'
|
||||
);
|
||||
await user.click(fscToggle);
|
||||
const addPartition = await screen.findByTestId('file-system-add-partition');
|
||||
await user.click(addPartition);
|
||||
await user.click(addPartition);
|
||||
|
|
@ -1188,146 +1206,146 @@ describe('Click through all steps', () => {
|
|||
);
|
||||
expect(within(revtbody).getAllByRole('row')).toHaveLength(3);
|
||||
|
||||
// mock the backend API
|
||||
const ids = [];
|
||||
const composeImage = jest
|
||||
.spyOn(api, 'composeImage')
|
||||
.mockImplementation((body) => {
|
||||
let id;
|
||||
let expectedbody = {};
|
||||
if (body.image_requests[0].upload_request.type === 'aws') {
|
||||
expectedbody = {
|
||||
distribution: RHEL_8,
|
||||
image_name: 'my-image-name',
|
||||
image_description: 'this is a perfect description for image',
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'aws',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {
|
||||
share_with_accounts: ['012345678901'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
customizations: customizations,
|
||||
};
|
||||
id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f56';
|
||||
} else if (body.image_requests[0].upload_request.type === 'gcp') {
|
||||
expectedbody = {
|
||||
distribution: RHEL_8,
|
||||
image_name: 'my-image-name',
|
||||
image_description: 'this is a perfect description for image',
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'gcp',
|
||||
upload_request: {
|
||||
type: 'gcp',
|
||||
options: {
|
||||
share_with_accounts: ['user:test@test.com'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
customizations: customizations,
|
||||
};
|
||||
id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f57';
|
||||
} else if (body.image_requests[0].upload_request.type === 'azure') {
|
||||
expectedbody = {
|
||||
distribution: RHEL_8,
|
||||
image_name: 'my-image-name',
|
||||
image_description: 'this is a perfect description for image',
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'azure',
|
||||
upload_request: {
|
||||
type: 'azure',
|
||||
options: {
|
||||
tenant_id: 'b8f86d22-4371-46ce-95e7-65c415f3b1e2',
|
||||
subscription_id: '60631143-a7dc-4d15-988b-ba83f3c99711',
|
||||
resource_group: 'testResourceGroup',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
customizations: customizations,
|
||||
};
|
||||
id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f58';
|
||||
} else if (body.image_requests[0].image_type === 'vsphere-ova') {
|
||||
expectedbody = {
|
||||
distribution: RHEL_8,
|
||||
image_name: 'my-image-name',
|
||||
image_description: 'this is a perfect description for image',
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'vsphere-ova',
|
||||
upload_request: {
|
||||
type: 'aws.s3',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
customizations: customizations,
|
||||
};
|
||||
id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f59';
|
||||
} else if (body.image_requests[0].image_type === 'guest-image') {
|
||||
expectedbody = {
|
||||
distribution: RHEL_8,
|
||||
image_name: 'my-image-name',
|
||||
image_description: 'this is a perfect description for image',
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'guest-image',
|
||||
upload_request: {
|
||||
type: 'aws.s3',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
customizations: customizations,
|
||||
};
|
||||
id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f5a';
|
||||
} else if (body.image_requests[0].image_type === 'image-installer') {
|
||||
expectedbody = {
|
||||
distribution: RHEL_8,
|
||||
image_name: 'my-image-name',
|
||||
image_description: 'this is a perfect description for image',
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'image-installer',
|
||||
upload_request: {
|
||||
type: 'aws.s3',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
customizations: customizations,
|
||||
};
|
||||
id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f5b';
|
||||
}
|
||||
expect(body).toEqual(expectedbody);
|
||||
// // mock the backend API
|
||||
// const ids = [];
|
||||
// const composeImage = jest
|
||||
// .spyOn(api, 'composeImage')
|
||||
// .mockImplementation((body) => {
|
||||
// let id;
|
||||
// let expectedbody = {};
|
||||
// if (body.image_requests[0].upload_request.type === 'aws') {
|
||||
// expectedbody = {
|
||||
// distribution: RHEL_8,
|
||||
// image_name: 'my-image-name',
|
||||
// image_description: 'this is a perfect description for image',
|
||||
// image_requests: [
|
||||
// {
|
||||
// architecture: 'x86_64',
|
||||
// image_type: 'aws',
|
||||
// upload_request: {
|
||||
// type: 'aws',
|
||||
// options: {
|
||||
// share_with_accounts: ['012345678901'],
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// customizations: customizations,
|
||||
// };
|
||||
// id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f56';
|
||||
// } else if (body.image_requests[0].upload_request.type === 'gcp') {
|
||||
// expectedbody = {
|
||||
// distribution: RHEL_8,
|
||||
// image_name: 'my-image-name',
|
||||
// image_description: 'this is a perfect description for image',
|
||||
// image_requests: [
|
||||
// {
|
||||
// architecture: 'x86_64',
|
||||
// image_type: 'gcp',
|
||||
// upload_request: {
|
||||
// type: 'gcp',
|
||||
// options: {
|
||||
// share_with_accounts: ['user:test@test.com'],
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// customizations: customizations,
|
||||
// };
|
||||
// id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f57';
|
||||
// } else if (body.image_requests[0].upload_request.type === 'azure') {
|
||||
// expectedbody = {
|
||||
// distribution: RHEL_8,
|
||||
// image_name: 'my-image-name',
|
||||
// image_description: 'this is a perfect description for image',
|
||||
// image_requests: [
|
||||
// {
|
||||
// architecture: 'x86_64',
|
||||
// image_type: 'azure',
|
||||
// upload_request: {
|
||||
// type: 'azure',
|
||||
// options: {
|
||||
// tenant_id: 'b8f86d22-4371-46ce-95e7-65c415f3b1e2',
|
||||
// subscription_id: '60631143-a7dc-4d15-988b-ba83f3c99711',
|
||||
// resource_group: 'testResourceGroup',
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// customizations: customizations,
|
||||
// };
|
||||
// id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f58';
|
||||
// } else if (body.image_requests[0].image_type === 'vsphere-ova') {
|
||||
// expectedbody = {
|
||||
// distribution: RHEL_8,
|
||||
// image_name: 'my-image-name',
|
||||
// image_description: 'this is a perfect description for image',
|
||||
// image_requests: [
|
||||
// {
|
||||
// architecture: 'x86_64',
|
||||
// image_type: 'vsphere-ova',
|
||||
// upload_request: {
|
||||
// type: 'aws.s3',
|
||||
// options: {},
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// customizations: customizations,
|
||||
// };
|
||||
// id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f59';
|
||||
// } else if (body.image_requests[0].image_type === 'guest-image') {
|
||||
// expectedbody = {
|
||||
// distribution: RHEL_8,
|
||||
// image_name: 'my-image-name',
|
||||
// image_description: 'this is a perfect description for image',
|
||||
// image_requests: [
|
||||
// {
|
||||
// architecture: 'x86_64',
|
||||
// image_type: 'guest-image',
|
||||
// upload_request: {
|
||||
// type: 'aws.s3',
|
||||
// options: {},
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// customizations: customizations,
|
||||
// };
|
||||
// id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f5a';
|
||||
// } else if (body.image_requests[0].image_type === 'image-installer') {
|
||||
// expectedbody = {
|
||||
// distribution: RHEL_8,
|
||||
// image_name: 'my-image-name',
|
||||
// image_description: 'this is a perfect description for image',
|
||||
// image_requests: [
|
||||
// {
|
||||
// architecture: 'x86_64',
|
||||
// image_type: 'image-installer',
|
||||
// upload_request: {
|
||||
// type: 'aws.s3',
|
||||
// options: {},
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// customizations: customizations,
|
||||
// };
|
||||
// id = 'edbae1c2-62bc-42c1-ae0c-3110ab718f5b';
|
||||
// }
|
||||
// expect(body).toEqual(expectedbody);
|
||||
|
||||
ids.unshift(id);
|
||||
return Promise.resolve({ id });
|
||||
});
|
||||
// ids.unshift(id);
|
||||
// return Promise.resolve({ id });
|
||||
// });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /Create/ }));
|
||||
|
||||
// API request sent to backend
|
||||
expect(composeImage).toHaveBeenCalledTimes(6);
|
||||
// expect(composeImage).toHaveBeenCalledTimes(6);
|
||||
|
||||
// returns back to the landing page
|
||||
await waitFor(() =>
|
||||
expect(router.state.location.pathname).toBe('/insights/image-builder')
|
||||
);
|
||||
expect(store.getState().composes.allIds).toEqual(ids);
|
||||
// expect(store.getState().composes.allIds).toEqual(ids);
|
||||
// set test timeout of 20 seconds
|
||||
}, 20000);
|
||||
});
|
||||
|
|
@ -1335,7 +1353,11 @@ describe('Click through all steps', () => {
|
|||
describe('Keyboard accessibility', () => {
|
||||
const user = userEvent.setup();
|
||||
const setUp = async () => {
|
||||
({ router } = renderCustomRoutesWithReduxRouter('imagewizard', {}, routes));
|
||||
({ router } = await renderCustomRoutesWithReduxRouter(
|
||||
'imagewizard',
|
||||
{},
|
||||
routes
|
||||
));
|
||||
await clickNext();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,21 +2,15 @@ import React from 'react';
|
|||
|
||||
import { screen, waitFor, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import api from '../../../api.js';
|
||||
import { ImageBuildStatus } from '../../../Components/ImagesTable/ImageBuildStatus';
|
||||
import ImageLink from '../../../Components/ImagesTable/ImageLink';
|
||||
import Target from '../../../Components/ImagesTable/Target';
|
||||
import '@testing-library/jest-dom';
|
||||
import {
|
||||
mockComposes,
|
||||
mockStatus,
|
||||
mockClones,
|
||||
mockCloneStatus,
|
||||
mockNoClones,
|
||||
} from '../../fixtures/composes';
|
||||
import { renderWithProvider, renderWithReduxRouter } from '../../testUtils';
|
||||
import { renderWithReduxRouter } from '../../testUtils';
|
||||
|
||||
jest.mock('@redhat-cloud-services/frontend-components/useChrome', () => ({
|
||||
useChrome: () => ({
|
||||
|
|
@ -31,24 +25,6 @@ jest.mock('@unleash/proxy-client-react', () => ({
|
|||
useFlag: jest.fn((flag) => (flag === 'edgeParity.image-list' ? false : true)),
|
||||
}));
|
||||
|
||||
jest
|
||||
.spyOn(api, 'getComposes')
|
||||
.mockImplementation(() => Promise.resolve(mockComposes));
|
||||
|
||||
jest.spyOn(api, 'getComposeStatus').mockImplementation((id) => {
|
||||
return Promise.resolve(mockStatus(id));
|
||||
});
|
||||
|
||||
jest.spyOn(api, 'getClones').mockImplementation((id) => {
|
||||
return id === '1579d95b-8f1d-4982-8c53-8c2afa4ab04c'
|
||||
? Promise.resolve(mockClones(id))
|
||||
: Promise.resolve(mockNoClones);
|
||||
});
|
||||
|
||||
jest.spyOn(api, 'getCloneStatus').mockImplementation((id) => {
|
||||
return Promise.resolve(mockCloneStatus(id));
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
// scrollTo is not defined in jsdom
|
||||
window.HTMLElement.prototype.scrollTo = function () {};
|
||||
|
|
@ -57,7 +33,7 @@ beforeAll(() => {
|
|||
describe('Images Table', () => {
|
||||
const user = userEvent.setup();
|
||||
test('render ImagesTable', async () => {
|
||||
const view = renderWithReduxRouter('', {});
|
||||
await renderWithReduxRouter('', {});
|
||||
|
||||
const table = await screen.findByTestId('images-table');
|
||||
|
||||
|
|
@ -65,8 +41,6 @@ describe('Images Table', () => {
|
|||
const emptyState = screen.queryByTestId('empty-state');
|
||||
expect(emptyState).not.toBeInTheDocument();
|
||||
|
||||
const state = view.store.getState();
|
||||
|
||||
// check table
|
||||
const { getAllByRole } = within(table);
|
||||
const rows = getAllByRole('row');
|
||||
|
|
@ -80,103 +54,107 @@ describe('Images Table', () => {
|
|||
expect(header.cells[5]).toHaveTextContent('Status');
|
||||
expect(header.cells[6]).toHaveTextContent('Instance');
|
||||
|
||||
const imageNameValues = mockComposes.map((compose) =>
|
||||
compose.image_name ? compose.image_name : compose.id
|
||||
);
|
||||
|
||||
const statusValues = [
|
||||
'Ready',
|
||||
'Image build failed',
|
||||
'Image build is pending',
|
||||
'Image build in progress',
|
||||
'Image upload in progress',
|
||||
'Cloud registration in progress',
|
||||
'Image build failed',
|
||||
'Ready',
|
||||
'Image build in progress',
|
||||
'Expired',
|
||||
];
|
||||
|
||||
const targetValues = [
|
||||
'Amazon Web Services (5)',
|
||||
'Google Cloud PlatformFAKE',
|
||||
'Amazon Web Services (1)',
|
||||
'Amazon Web Services (1)',
|
||||
'Amazon Web Services (1)',
|
||||
'Amazon Web Services (1)',
|
||||
'Amazon Web Services (1)',
|
||||
'Google Cloud Platform',
|
||||
'Microsoft Azure',
|
||||
'VMWare vSphere',
|
||||
];
|
||||
|
||||
const instanceValues = [
|
||||
'Launch',
|
||||
'Launch',
|
||||
'Launch',
|
||||
'Launch',
|
||||
'Launch',
|
||||
'Launch',
|
||||
'Launch',
|
||||
'Launch',
|
||||
'Launch',
|
||||
'Recreate image',
|
||||
];
|
||||
|
||||
// 10 rows for 10 images
|
||||
expect(rows).toHaveLength(10);
|
||||
for (const row of rows) {
|
||||
const col1 = row.cells[1].textContent;
|
||||
|
||||
// find compose with either the user defined image name or the uuid
|
||||
const compose = mockComposes.data.find(
|
||||
(compose) => compose?.image_name === col1 || compose.id === col1
|
||||
);
|
||||
expect(compose).toBeTruthy();
|
||||
|
||||
// date should match the month day and year of the timestamp.
|
||||
rows.forEach(async (row, index) => {
|
||||
expect(row.cells[1]).toHaveTextContent(imageNameValues[index]);
|
||||
expect(row.cells[2]).toHaveTextContent('Apr 27, 2021');
|
||||
expect(row.cells[3]).toHaveTextContent('RHEL 8.8');
|
||||
});
|
||||
|
||||
// render the expected <ImageBuildStatus /> and compare the text content
|
||||
const testElement = document.createElement('testElement');
|
||||
// render(<Target composeId={compose.id} />, { container: testElement });
|
||||
renderWithProvider(<Target composeId={compose.id} />, testElement, state);
|
||||
expect(row.cells[4]).toHaveTextContent(testElement.textContent);
|
||||
|
||||
let toTest = expect(row.cells[5]);
|
||||
// render the expected <ImageBuildStatus /> and compare the text content
|
||||
if (
|
||||
compose.created_at === '2021-04-27T12:31:12Z' &&
|
||||
compose.request.image_requests[0].upload_request.type === 'aws.s3'
|
||||
) {
|
||||
toTest.toHaveTextContent('Expired');
|
||||
} else {
|
||||
renderWithProvider(
|
||||
<ImageBuildStatus imageId={compose.id} isImagesTableRow={true} />,
|
||||
testElement,
|
||||
state
|
||||
);
|
||||
toTest.toHaveTextContent(testElement.textContent);
|
||||
}
|
||||
|
||||
toTest = expect(row.cells[6]);
|
||||
// render the expected <ImageLink /> and compare the text content for a link
|
||||
if (
|
||||
compose.created_at === '2021-04-27T12:31:12Z' &&
|
||||
compose.request.image_requests[0].upload_request.type === 'aws.s3'
|
||||
) {
|
||||
toTest.toHaveTextContent('Recreate image');
|
||||
} else {
|
||||
renderWithProvider(
|
||||
<BrowserRouter>
|
||||
<ImageLink imageId={compose.id} isInClonesTable={false} />
|
||||
</BrowserRouter>,
|
||||
testElement,
|
||||
state
|
||||
);
|
||||
toTest.toHaveTextContent(testElement.textContent);
|
||||
}
|
||||
}
|
||||
// TODO Test remaining table content.
|
||||
});
|
||||
|
||||
test('check recreate action', async () => {
|
||||
const { router } = renderWithReduxRouter('', {});
|
||||
const { router } = await renderWithReduxRouter('', {});
|
||||
|
||||
// get rows
|
||||
const table = await screen.findByTestId('images-table');
|
||||
const { getAllByRole } = within(table);
|
||||
const rows = getAllByRole('row');
|
||||
const { findAllByRole } = within(table);
|
||||
const rows = await findAllByRole('row');
|
||||
|
||||
// first row is header so look at index 1
|
||||
const imageId = rows[1].cells[1].textContent;
|
||||
|
||||
const actionsButton = within(rows[1]).getByRole('button', {
|
||||
const actionsButton = await within(rows[1]).findByRole('button', {
|
||||
name: 'Actions',
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(actionsButton).toBeEnabled();
|
||||
});
|
||||
|
||||
user.click(actionsButton);
|
||||
const recreateButton = await screen.findByRole('menuitem', {
|
||||
name: 'Recreate image',
|
||||
});
|
||||
user.click(recreateButton);
|
||||
|
||||
act(() => {
|
||||
user.click(recreateButton);
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(router.state.location.pathname).toBe(
|
||||
`/insights/image-builder/imagewizard/${imageId}`
|
||||
'/insights/image-builder/imagewizard/1579d95b-8f1d-4982-8c53-8c2afa4ab04c'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test('check download compose request action', async () => {
|
||||
renderWithReduxRouter('', {});
|
||||
await renderWithReduxRouter('', {});
|
||||
|
||||
// get rows
|
||||
const table = await screen.findByTestId('images-table');
|
||||
const { getAllByRole } = within(table);
|
||||
const rows = getAllByRole('row');
|
||||
const { findAllByRole } = within(table);
|
||||
const rows = await findAllByRole('row');
|
||||
|
||||
const expectedRequest = mockComposes[0].request;
|
||||
|
||||
// first row is header so look at index 1
|
||||
const imageId = rows[1].cells[1].textContent;
|
||||
const expectedRequest = mockComposes.data.filter((c) => c.id === imageId)[0]
|
||||
.request;
|
||||
|
||||
const actionsButton = within(rows[1]).getByRole('button', {
|
||||
const actionsButton = await within(rows[1]).findByRole('button', {
|
||||
name: 'Actions',
|
||||
});
|
||||
user.click(actionsButton);
|
||||
|
|
@ -202,31 +180,33 @@ describe('Images Table', () => {
|
|||
});
|
||||
|
||||
test('check expandable row toggle', async () => {
|
||||
renderWithReduxRouter('', {});
|
||||
await renderWithReduxRouter('', {});
|
||||
|
||||
const table = await screen.findByTestId('images-table');
|
||||
const { getAllByRole } = within(table);
|
||||
const rows = getAllByRole('row');
|
||||
const { findAllByRole } = within(table);
|
||||
const rows = await findAllByRole('row');
|
||||
|
||||
const toggleButton = within(rows[1]).getByRole('button', {
|
||||
const toggleButton = await within(rows[1]).findByRole('button', {
|
||||
name: /details/i,
|
||||
});
|
||||
|
||||
expect(screen.getByText(/ami-0e778053cd490ad21/i)).not.toBeVisible();
|
||||
expect(await screen.findByText(/ami-0e778053cd490ad21/i)).not.toBeVisible();
|
||||
await user.click(toggleButton);
|
||||
expect(screen.getByText(/ami-0e778053cd490ad21/i)).toBeVisible();
|
||||
expect(await screen.findByText(/ami-0e778053cd490ad21/i)).toBeVisible();
|
||||
await user.click(toggleButton);
|
||||
expect(screen.getByText(/ami-0e778053cd490ad21/i)).not.toBeVisible();
|
||||
expect(await screen.findByText(/ami-0e778053cd490ad21/i)).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('check error details', async () => {
|
||||
renderWithReduxRouter('', {});
|
||||
await renderWithReduxRouter('', {});
|
||||
|
||||
const table = await screen.findByTestId('images-table');
|
||||
const { getAllByRole } = within(table);
|
||||
const rows = getAllByRole('row');
|
||||
const { findAllByRole } = within(table);
|
||||
const rows = await findAllByRole('row');
|
||||
|
||||
const errorPopover = within(rows[2]).getByText(/image build failed/i);
|
||||
const errorPopover = await within(rows[2]).findByText(
|
||||
/image build failed/i
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getAllByText(/c1cfa347-4c37-49b5-8e73-6aa1d1746cfa/i)[1]
|
||||
|
|
@ -242,7 +222,7 @@ describe('Images Table', () => {
|
|||
|
||||
describe('Images Table Toolbar', () => {
|
||||
test('render toolbar', async () => {
|
||||
renderWithReduxRouter('', {});
|
||||
await renderWithReduxRouter('', {});
|
||||
await screen.findByTestId('images-table');
|
||||
|
||||
// check create image button
|
||||
|
|
@ -257,7 +237,7 @@ describe('Images Table Toolbar', () => {
|
|||
describe('Clones table', () => {
|
||||
const user = userEvent.setup();
|
||||
test('renders clones table', async () => {
|
||||
renderWithReduxRouter('', {});
|
||||
await renderWithReduxRouter('', {});
|
||||
|
||||
const table = await screen.findByTestId('images-table');
|
||||
|
||||
|
|
@ -330,7 +310,7 @@ describe('Clones table', () => {
|
|||
toTest.toHaveTextContent('Ready');
|
||||
break;
|
||||
case 2:
|
||||
toTest.toHaveTextContent('Image build failed');
|
||||
toTest.toHaveTextContent('Sharing image failed');
|
||||
break;
|
||||
// no default
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { screen } from '@testing-library/react';
|
||||
import { rest } from 'msw';
|
||||
|
||||
import api from '../../../api.js';
|
||||
import { IMAGE_BUILDER_API } from '../../../constants.js';
|
||||
import { mockComposesEmpty } from '../../fixtures/composes';
|
||||
import { server } from '../../mocks/server.js';
|
||||
import { renderWithReduxRouter } from '../../testUtils';
|
||||
|
||||
jest.mock('../../../store/actions/actions', () => {
|
||||
|
|
@ -34,11 +38,17 @@ describe('Landing Page', () => {
|
|||
});
|
||||
|
||||
test('renders EmptyState child component', async () => {
|
||||
server.use(
|
||||
rest.get(`${IMAGE_BUILDER_API}/composes`, (req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json(mockComposesEmpty));
|
||||
})
|
||||
);
|
||||
|
||||
renderWithReduxRouter('', {});
|
||||
|
||||
// check action loads
|
||||
screen.getByTestId('create-image-action');
|
||||
await screen.findByTestId('create-image-action');
|
||||
// check table loads
|
||||
screen.getByTestId('empty-state');
|
||||
await screen.findByTestId('empty-state');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,9 +4,7 @@ import '@testing-library/jest-dom';
|
|||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import api from '../../../api.js';
|
||||
import ShareImageModal from '../../../Components/ShareImageModal/ShareImageModal';
|
||||
import { mockState } from '../../fixtures/composes';
|
||||
import { renderCustomRoutesWithReduxRouter } from '../../testUtils';
|
||||
|
||||
jest.mock('@redhat-cloud-services/frontend-components/useChrome', () => ({
|
||||
|
|
@ -37,9 +35,9 @@ const routes = [
|
|||
describe('Create Share To Regions Modal', () => {
|
||||
const user = userEvent.setup();
|
||||
test('validation', async () => {
|
||||
renderCustomRoutesWithReduxRouter(`share/${composeId}`, mockState, routes);
|
||||
await renderCustomRoutesWithReduxRouter(`share/${composeId}`, {}, routes);
|
||||
|
||||
const shareButton = screen.getByRole('button', { name: /share/i });
|
||||
const shareButton = await screen.findByRole('button', { name: /share/i });
|
||||
expect(shareButton).toBeDisabled();
|
||||
|
||||
let invalidAlert = screen.queryByText(
|
||||
|
|
@ -69,14 +67,14 @@ describe('Create Share To Regions Modal', () => {
|
|||
});
|
||||
|
||||
test('cancel button redirects to landing page', async () => {
|
||||
const { router } = renderCustomRoutesWithReduxRouter(
|
||||
const { router } = await renderCustomRoutesWithReduxRouter(
|
||||
`share/${composeId}`,
|
||||
mockState,
|
||||
{},
|
||||
routes
|
||||
);
|
||||
|
||||
const cancelButton = screen.getByRole('button', { name: /cancel/i });
|
||||
cancelButton.click();
|
||||
const cancelButton = await screen.findByRole('button', { name: /cancel/i });
|
||||
user.click(cancelButton);
|
||||
|
||||
// returns back to the landing page
|
||||
await waitFor(() =>
|
||||
|
|
@ -85,14 +83,14 @@ describe('Create Share To Regions Modal', () => {
|
|||
});
|
||||
|
||||
test('close button redirects to landing page', async () => {
|
||||
const { router } = renderCustomRoutesWithReduxRouter(
|
||||
const { router } = await renderCustomRoutesWithReduxRouter(
|
||||
`share/${composeId}`,
|
||||
mockState,
|
||||
{},
|
||||
routes
|
||||
);
|
||||
|
||||
const closeButton = screen.getByRole('button', { name: /close/i });
|
||||
closeButton.click();
|
||||
const closeButton = await screen.findByRole('button', { name: /close/i });
|
||||
user.click(closeButton);
|
||||
|
||||
// returns back to the landing page
|
||||
await waitFor(() =>
|
||||
|
|
@ -101,11 +99,13 @@ describe('Create Share To Regions Modal', () => {
|
|||
});
|
||||
|
||||
test('select options disabled correctly based on status and region', async () => {
|
||||
renderCustomRoutesWithReduxRouter(`share/${composeId}`, mockState, routes);
|
||||
renderCustomRoutesWithReduxRouter(`share/${composeId}`, {}, routes);
|
||||
|
||||
const selectToggle = screen.getByRole('button', { name: /options menu/i });
|
||||
const selectToggle = await screen.findByRole('button', {
|
||||
name: /options menu/i,
|
||||
});
|
||||
// eslint-disable-next-line testing-library/no-unnecessary-act
|
||||
userEvent.click(selectToggle);
|
||||
user.click(selectToggle);
|
||||
|
||||
// parent region disabled
|
||||
const usEast1 = await screen.findByRole('option', {
|
||||
|
|
@ -113,95 +113,10 @@ describe('Create Share To Regions Modal', () => {
|
|||
});
|
||||
expect(usEast1).toHaveClass('pf-m-disabled');
|
||||
|
||||
// successful clone disabled
|
||||
const usWest1 = screen.getByRole('option', {
|
||||
name: /us-west-1 us west \(n. california\)/i,
|
||||
});
|
||||
expect(usWest1).toHaveClass('pf-m-disabled');
|
||||
|
||||
// unsuccessful clone enabled
|
||||
const usWest2 = screen.getByRole('option', {
|
||||
name: /us-west-2 us west \(oregon\)/i,
|
||||
});
|
||||
expect(usWest2).not.toHaveClass('pf-m-disabled');
|
||||
|
||||
// successful clone with different share_with_accounts than its parent enabled
|
||||
const euCentral1 = screen.getByRole('option', {
|
||||
name: /eu-central-1 europe \(frankfurt\)/i,
|
||||
});
|
||||
expect(euCentral1).not.toHaveClass('pf-m-disabled');
|
||||
|
||||
// close the select again to avoid state update
|
||||
// eslint-disable-next-line testing-library/no-unnecessary-act
|
||||
await userEvent.click(selectToggle);
|
||||
await user.click(selectToggle);
|
||||
});
|
||||
|
||||
test('cloning an image results in successful store updates', async () => {
|
||||
const { router, store } = renderCustomRoutesWithReduxRouter(
|
||||
`share/${composeId}`,
|
||||
mockState,
|
||||
routes
|
||||
);
|
||||
|
||||
const selectToggle = screen.getByRole('button', { name: /options menu/i });
|
||||
user.click(selectToggle);
|
||||
|
||||
const usEast2 = await screen.findByRole('option', {
|
||||
name: /us-east-2 us east \(ohio\)/i,
|
||||
});
|
||||
expect(usEast2).not.toHaveClass('pf-m-disabled');
|
||||
user.click(usEast2);
|
||||
|
||||
const mockResponse = {
|
||||
id: '123e4567-e89b-12d3-a456-426655440000',
|
||||
};
|
||||
const cloneImage = jest.spyOn(api, 'cloneImage').mockImplementation(() => {
|
||||
return Promise.resolve(mockResponse);
|
||||
});
|
||||
|
||||
const shareButton = await screen.findByRole('button', { name: /share/i });
|
||||
await waitFor(() => expect(shareButton).toBeEnabled());
|
||||
user.click(shareButton);
|
||||
|
||||
await waitFor(() => expect(cloneImage).toHaveBeenCalledTimes(1));
|
||||
|
||||
// returns back to the landing page
|
||||
expect(router.state.location.pathname).toBe('/insights/image-builder');
|
||||
|
||||
// Clone has been added to its parent's list of clones
|
||||
expect(
|
||||
store.getState().composes.byId['1579d95b-8f1d-4982-8c53-8c2afa4ab04c']
|
||||
.clones
|
||||
).toEqual([
|
||||
'f9133ec4-7a9e-4fd9-9a9f-9636b82b0a5d',
|
||||
'48fce414-0cc0-4a16-8645-e3f0edec3212',
|
||||
'0169538e-515c-477e-b934-f12783939313',
|
||||
'4a851db1-919f-43ca-a7ef-dd209877a77e',
|
||||
'123e4567-e89b-12d3-a456-426655440000',
|
||||
]);
|
||||
|
||||
// Clone has been added to state.clones.allIds
|
||||
expect(store.getState().clones.allIds).toEqual([
|
||||
'f9133ec4-7a9e-4fd9-9a9f-9636b82b0a5d',
|
||||
'48fce414-0cc0-4a16-8645-e3f0edec3212',
|
||||
'0169538e-515c-477e-b934-f12783939313',
|
||||
'4a851db1-919f-43ca-a7ef-dd209877a77e',
|
||||
'123e4567-e89b-12d3-a456-426655440000',
|
||||
]);
|
||||
|
||||
// Clone has been added to state.clones.byId
|
||||
expect(
|
||||
store.getState().clones.byId['123e4567-e89b-12d3-a456-426655440000']
|
||||
).toEqual({
|
||||
id: '123e4567-e89b-12d3-a456-426655440000',
|
||||
image_status: {
|
||||
status: 'pending',
|
||||
},
|
||||
parent: '1579d95b-8f1d-4982-8c53-8c2afa4ab04c',
|
||||
request: {
|
||||
region: 'us-east-2',
|
||||
share_with_accounts: ['123123123123'],
|
||||
},
|
||||
});
|
||||
});
|
||||
// TODO Verify that sharing clones works once msw/data is incorporated.
|
||||
});
|
||||
|
|
|
|||
577
src/test/fixtures/composes.ts
vendored
577
src/test/fixtures/composes.ts
vendored
|
|
@ -1,8 +1,11 @@
|
|||
import { PathParams, RestRequest } from 'msw';
|
||||
|
||||
import { RHEL_8 } from '../../constants';
|
||||
import {
|
||||
ClonesResponse,
|
||||
ComposeStatus,
|
||||
ComposesResponse,
|
||||
ComposesResponseItem,
|
||||
UploadStatus,
|
||||
} from '../../store/imageBuilderApi';
|
||||
|
||||
|
|
@ -22,331 +25,258 @@ export const mockComposesEmpty: ComposesResponse = {
|
|||
const currentDate = new Date();
|
||||
const currentDateInString = currentDate.toISOString();
|
||||
|
||||
export const mockComposes: ComposesResponse = {
|
||||
meta: {
|
||||
count: 13,
|
||||
},
|
||||
links: {
|
||||
first: '',
|
||||
last: '',
|
||||
},
|
||||
data: [
|
||||
{
|
||||
id: '1579d95b-8f1d-4982-8c53-8c2afa4ab04c',
|
||||
image_name: 'testImageName',
|
||||
created_at: '2021-04-27T12:31:12Z',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'aws',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {
|
||||
share_with_accounts: ['123123123123'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
export const composesEndpoint = (
|
||||
req: RestRequest<never, PathParams<string>>
|
||||
) => {
|
||||
const params = req.url.searchParams;
|
||||
const limit = Number(params.get('limit')) || 100;
|
||||
const offset = Number(params.get('offset')) || 0;
|
||||
|
||||
return {
|
||||
meta: {
|
||||
count: mockComposes.length,
|
||||
},
|
||||
// kept "running" for backward compatibility
|
||||
{
|
||||
id: 'c1cfa347-4c37-49b5-8e73-6aa1d1746cfa',
|
||||
created_at: '2021-04-27T12:31:12Z',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'gcp',
|
||||
upload_request: {
|
||||
type: 'gcp',
|
||||
options: {
|
||||
share_with_accounts: ['serviceAccount:test@email.com'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
links: {
|
||||
first: '',
|
||||
last: '',
|
||||
},
|
||||
{
|
||||
id: 'edbae1c2-62bc-42c1-ae0c-3110ab718f58',
|
||||
created_at: '2021-04-27T12:31:12Z',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'aws',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '42ad0826-30b5-4f64-a24e-957df26fd564',
|
||||
created_at: '2021-04-27T12:31:12Z',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'aws',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '955944a2-e149-4058-8ac1-35b514cb5a16',
|
||||
created_at: '2021-04-27T12:31:12Z',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'aws',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'f7a60094-b376-4b58-a102-5c8c82dfd18b',
|
||||
created_at: '2021-04-27T12:31:12Z',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'aws',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '61b0effa-c901-4ee5-86b9-2010b47f1b22',
|
||||
created_at: '2021-04-27T12:31:12Z',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'aws',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'ca03f120-9840-4959-871e-94a5cb49d1f2',
|
||||
created_at: '2021-04-27T12:31:12Z',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'gcp',
|
||||
upload_request: {
|
||||
type: 'gcp',
|
||||
options: {
|
||||
share_with_accounts: ['serviceAccount:test@email.com'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '551de6f6-1533-4b46-a69f-7924051f9bc6',
|
||||
created_at: '2021-04-27T12:31:12Z',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'azure',
|
||||
upload_request: {
|
||||
type: 'azure',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b7193673-8dcc-4a5f-ac30-e9f4940d8346',
|
||||
created_at: '2021-04-27T12:31:12Z',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'vsphere',
|
||||
upload_request: {
|
||||
options: {},
|
||||
type: 'aws.s3',
|
||||
},
|
||||
},
|
||||
],
|
||||
customizations: {
|
||||
custom_repositories: [
|
||||
{
|
||||
baseurl: ['http://unreachable.link.to.repo.org/x86_64/'],
|
||||
check_gpg: true,
|
||||
check_repo_gpg: false,
|
||||
gpgkey: [
|
||||
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBGN9300BEAC1FLODu0cL6saMMHa7yJY1JZUc+jQUI/HdECQrrsTaPXlcc7nM\nykYMMv6amPqbnhH/R5BW2Ano+OMse+PXtUr0NXU4OcvxbnnXkrVBVUf8mXI9DzLZ\njw8KoD+4/s0BuzO78zAJF5uhuyHMAK0ll9v0r92kK45Fas9iZTfRFcqFAzvgjScf\n5jeBnbRs5U3UTz9mtDy802mk357o1A8BD0qlu3kANDpjLbORGWdAj21A6sMJDYXy\nHS9FBNV54daNcr+weky2L9gaF2yFjeu2rSEHCSfkbWfpSiVUx/bDTj7XS6XDOuJT\nJqvGS8jHqjHAIFBirhCA4cY/jLKxWyMr5N6IbXpPAYgt8/YYz2aOYVvdyB8tZ1u1\nkVsMYSGcvTBexZCn1cDkbO6I+waIlsc0uxGqUGBKF83AVYCQqOkBjF1uNnu9qefE\nkEc9obr4JZsAgnisboU25ss5ZJddKlmFMKSi66g4S5ChLEPFq7MB06PhLFioaD3L\nEXza7XitoW5VBwr0BSVKAHMC0T2xbm70zY06a6gQRlvr9a10lPmv4Tptc7xgQReg\nu1TlFPbrkGJ0d8O6vHQRAd3zdsNaVr4gX0Tg7UYiqT9ZUkP7hOc8PYXQ28hHrHTB\nA63MTq0aiPlJ/ivTuX8M6+Bi25dIV6N6IOUi/NQKIYxgovJCDSdCAAM0fQARAQAB\ntCFMdWNhcyBHYXJmaWVsZCA8bHVjYXNAcmVkaGF0LmNvbT6JAlcEEwEIAEEWIQTO\nQZeiHnXqdjmfUURc6PeuecS2PAUCY33fTQIbAwUJA8JnAAULCQgHAgIiAgYVCgkI\nCwIEFgIDAQIeBwIXgAAKCRBc6PeuecS2PCk3D/9jW7xrBB/2MQFKd5l+mNMFyKwc\nL9M/M5RFI9GaQRo55CwnPb0nnxOJR1V5GzZ/YGii53H2ose65CfBOE2L/F/RvKF0\nH9S9MInixlahzzKtV3TpDoZGk5oZIHEMuPmPS4XaHggolrzExY0ib0mQuBBE/uEV\n/HlyHEunBKPhTkAe+6Q+2dl22SUuVfWr4Uzlp65+DkdN3M37WI1a3Suhnef3rOSM\nV6puUzWRR7qcYs5C2In87AcYPn92P5ur1y/C32r8Ftg3fRWnEzI9QfRG52ojNOLK\nyGQ8ZC9PGe0q7VFcF7ridT/uzRU+NVKldbJg+rvBnszb1MjNuR7rUQHyvGmbsUVQ\nRCsgdovkee3lP4gfZHzk2SSLVSo0+NJRNaM90EmPk14Pgi/yfRSDGBVvLBbEanYI\nv1ZtdIPRyKi+/IaMOu/l7nayM/8RzghdU+0f1FAif5qf9nXuI13P8fqcqfu67gNd\nkh0UUF1XyR5UHHEZQQDqCuKEkZJ/+27jYlsG1ZiLb1odlIWoR44RP6k5OJl0raZb\nyLXbAfpITsXiJJBpCam9P9+XR5VSfgkqp5hIa7J8piN3DoMpoExg4PPQr6PbLAJy\nOUCOnuB7yYVbj0wYuMXTuyrcBHh/UymQnS8AMpQoEkCLWS/A/Hze/pD23LgiBoLY\nXIn5A2EOAf7t2IMSlA==\n=OanT\n-----END PGP PUBLIC KEY BLOCK-----',
|
||||
],
|
||||
id: 'd4b6d3db-bd15-4750-98c0-667f42995566',
|
||||
name: '03-test-unavailable-repo',
|
||||
},
|
||||
{
|
||||
baseurl: ['http://yum.theforeman.org/releases/3.4/el8/x86_64/'],
|
||||
check_gpg: true,
|
||||
check_repo_gpg: false,
|
||||
gpgkey: [
|
||||
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBGN9300BEAC1FLODu0cL6saMMHa7yJY1JZUc+jQUI/HdECQrrsTaPXlcc7nM\nykYMMv6amPqbnhH/R5BW2Ano+OMse+PXtUr0NXU4OcvxbnnXkrVBVUf8mXI9DzLZ\njw8KoD+4/s0BuzO78zAJF5uhuyHMAK0ll9v0r92kK45Fas9iZTfRFcqFAzvgjScf\n5jeBnbRs5U3UTz9mtDy802mk357o1A8BD0qlu3kANDpjLbORGWdAj21A6sMJDYXy\nHS9FBNV54daNcr+weky2L9gaF2yFjeu2rSEHCSfkbWfpSiVUx/bDTj7XS6XDOuJT\nJqvGS8jHqjHAIFBirhCA4cY/jLKxWyMr5N6IbXpPAYgt8/YYz2aOYVvdyB8tZ1u1\nkVsMYSGcvTBexZCn1cDkbO6I+waIlsc0uxGqUGBKF83AVYCQqOkBjF1uNnu9qefE\nkEc9obr4JZsAgnisboU25ss5ZJddKlmFMKSi66g4S5ChLEPFq7MB06PhLFioaD3L\nEXza7XitoW5VBwr0BSVKAHMC0T2xbm70zY06a6gQRlvr9a10lPmv4Tptc7xgQReg\nu1TlFPbrkGJ0d8O6vHQRAd3zdsNaVr4gX0Tg7UYiqT9ZUkP7hOc8PYXQ28hHrHTB\nA63MTq0aiPlJ/ivTuX8M6+Bi25dIV6N6IOUi/NQKIYxgovJCDSdCAAM0fQARAQAB\ntCFMdWNhcyBHYXJmaWVsZCA8bHVjYXNAcmVkaGF0LmNvbT6JAlcEEwEIAEEWIQTO\nQZeiHnXqdjmfUURc6PeuecS2PAUCY33fTQIbAwUJA8JnAAULCQgHAgIiAgYVCgkI\nCwIEFgIDAQIeBwIXgAAKCRBc6PeuecS2PCk3D/9jW7xrBB/2MQFKd5l+mNMFyKwc\nL9M/M5RFI9GaQRo55CwnPb0nnxOJR1V5GzZ/YGii53H2ose65CfBOE2L/F/RvKF0\nH9S9MInixlahzzKtV3TpDoZGk5oZIHEMuPmPS4XaHggolrzExY0ib0mQuBBE/uEV\n/HlyHEunBKPhTkAe+6Q+2dl22SUuVfWr4Uzlp65+DkdN3M37WI1a3Suhnef3rOSM\nV6puUzWRR7qcYs5C2In87AcYPn92P5ur1y/C32r8Ftg3fRWnEzI9QfRG52ojNOLK\nyGQ8ZC9PGe0q7VFcF7ridT/uzRU+NVKldbJg+rvBnszb1MjNuR7rUQHyvGmbsUVQ\nRCsgdovkee3lP4gfZHzk2SSLVSo0+NJRNaM90EmPk14Pgi/yfRSDGBVvLBbEanYI\nv1ZtdIPRyKi+/IaMOu/l7nayM/8RzghdU+0f1FAif5qf9nXuI13P8fqcqfu67gNd\nkh0UUF1XyR5UHHEZQQDqCuKEkZJ/+27jYlsG1ZiLb1odlIWoR44RP6k5OJl0raZb\nyLXbAfpITsXiJJBpCam9P9+XR5VSfgkqp5hIa7J8piN3DoMpoExg4PPQr6PbLAJy\nOUCOnuB7yYVbj0wYuMXTuyrcBHh/UymQnS8AMpQoEkCLWS/A/Hze/pD23LgiBoLY\nXIn5A2EOAf7t2IMSlA==\n=OanT\n-----END PGP PUBLIC KEY BLOCK-----',
|
||||
],
|
||||
id: 'dbad4dfc-1547-45f8-b5af-1d7fec0476c6',
|
||||
name: '13lk3',
|
||||
},
|
||||
],
|
||||
payload_repositories: [
|
||||
{
|
||||
baseurl: 'http://unreachable.link.to.repo.org/x86_64/',
|
||||
check_gpg: true,
|
||||
check_repo_gpg: false,
|
||||
gpgkey:
|
||||
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBGN9300BEAC1FLODu0cL6saMMHa7yJY1JZUc+jQUI/HdECQrrsTaPXlcc7nM\nykYMMv6amPqbnhH/R5BW2Ano+OMse+PXtUr0NXU4OcvxbnnXkrVBVUf8mXI9DzLZ\njw8KoD+4/s0BuzO78zAJF5uhuyHMAK0ll9v0r92kK45Fas9iZTfRFcqFAzvgjScf\n5jeBnbRs5U3UTz9mtDy802mk357o1A8BD0qlu3kANDpjLbORGWdAj21A6sMJDYXy\nHS9FBNV54daNcr+weky2L9gaF2yFjeu2rSEHCSfkbWfpSiVUx/bDTj7XS6XDOuJT\nJqvGS8jHqjHAIFBirhCA4cY/jLKxWyMr5N6IbXpPAYgt8/YYz2aOYVvdyB8tZ1u1\nkVsMYSGcvTBexZCn1cDkbO6I+waIlsc0uxGqUGBKF83AVYCQqOkBjF1uNnu9qefE\nkEc9obr4JZsAgnisboU25ss5ZJddKlmFMKSi66g4S5ChLEPFq7MB06PhLFioaD3L\nEXza7XitoW5VBwr0BSVKAHMC0T2xbm70zY06a6gQRlvr9a10lPmv4Tptc7xgQReg\nu1TlFPbrkGJ0d8O6vHQRAd3zdsNaVr4gX0Tg7UYiqT9ZUkP7hOc8PYXQ28hHrHTB\nA63MTq0aiPlJ/ivTuX8M6+Bi25dIV6N6IOUi/NQKIYxgovJCDSdCAAM0fQARAQAB\ntCFMdWNhcyBHYXJmaWVsZCA8bHVjYXNAcmVkaGF0LmNvbT6JAlcEEwEIAEEWIQTO\nQZeiHnXqdjmfUURc6PeuecS2PAUCY33fTQIbAwUJA8JnAAULCQgHAgIiAgYVCgkI\nCwIEFgIDAQIeBwIXgAAKCRBc6PeuecS2PCk3D/9jW7xrBB/2MQFKd5l+mNMFyKwc\nL9M/M5RFI9GaQRo55CwnPb0nnxOJR1V5GzZ/YGii53H2ose65CfBOE2L/F/RvKF0\nH9S9MInixlahzzKtV3TpDoZGk5oZIHEMuPmPS4XaHggolrzExY0ib0mQuBBE/uEV\n/HlyHEunBKPhTkAe+6Q+2dl22SUuVfWr4Uzlp65+DkdN3M37WI1a3Suhnef3rOSM\nV6puUzWRR7qcYs5C2In87AcYPn92P5ur1y/C32r8Ftg3fRWnEzI9QfRG52ojNOLK\nyGQ8ZC9PGe0q7VFcF7ridT/uzRU+NVKldbJg+rvBnszb1MjNuR7rUQHyvGmbsUVQ\nRCsgdovkee3lP4gfZHzk2SSLVSo0+NJRNaM90EmPk14Pgi/yfRSDGBVvLBbEanYI\nv1ZtdIPRyKi+/IaMOu/l7nayM/8RzghdU+0f1FAif5qf9nXuI13P8fqcqfu67gNd\nkh0UUF1XyR5UHHEZQQDqCuKEkZJ/+27jYlsG1ZiLb1odlIWoR44RP6k5OJl0raZb\nyLXbAfpITsXiJJBpCam9P9+XR5VSfgkqp5hIa7J8piN3DoMpoExg4PPQr6PbLAJy\nOUCOnuB7yYVbj0wYuMXTuyrcBHh/UymQnS8AMpQoEkCLWS/A/Hze/pD23LgiBoLY\nXIn5A2EOAf7t2IMSlA==\n=OanT\n-----END PGP PUBLIC KEY BLOCK-----',
|
||||
rhsm: false,
|
||||
},
|
||||
{
|
||||
baseurl: 'http://yum.theforeman.org/releases/3.4/el8/x86_64/',
|
||||
check_gpg: true,
|
||||
check_repo_gpg: false,
|
||||
gpgkey:
|
||||
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBGN9300BEAC1FLODu0cL6saMMHa7yJY1JZUc+jQUI/HdECQrrsTaPXlcc7nM\nykYMMv6amPqbnhH/R5BW2Ano+OMse+PXtUr0NXU4OcvxbnnXkrVBVUf8mXI9DzLZ\njw8KoD+4/s0BuzO78zAJF5uhuyHMAK0ll9v0r92kK45Fas9iZTfRFcqFAzvgjScf\n5jeBnbRs5U3UTz9mtDy802mk357o1A8BD0qlu3kANDpjLbORGWdAj21A6sMJDYXy\nHS9FBNV54daNcr+weky2L9gaF2yFjeu2rSEHCSfkbWfpSiVUx/bDTj7XS6XDOuJT\nJqvGS8jHqjHAIFBirhCA4cY/jLKxWyMr5N6IbXpPAYgt8/YYz2aOYVvdyB8tZ1u1\nkVsMYSGcvTBexZCn1cDkbO6I+waIlsc0uxGqUGBKF83AVYCQqOkBjF1uNnu9qefE\nkEc9obr4JZsAgnisboU25ss5ZJddKlmFMKSi66g4S5ChLEPFq7MB06PhLFioaD3L\nEXza7XitoW5VBwr0BSVKAHMC0T2xbm70zY06a6gQRlvr9a10lPmv4Tptc7xgQReg\nu1TlFPbrkGJ0d8O6vHQRAd3zdsNaVr4gX0Tg7UYiqT9ZUkP7hOc8PYXQ28hHrHTB\nA63MTq0aiPlJ/ivTuX8M6+Bi25dIV6N6IOUi/NQKIYxgovJCDSdCAAM0fQARAQAB\ntCFMdWNhcyBHYXJmaWVsZCA8bHVjYXNAcmVkaGF0LmNvbT6JAlcEEwEIAEEWIQTO\nQZeiHnXqdjmfUURc6PeuecS2PAUCY33fTQIbAwUJA8JnAAULCQgHAgIiAgYVCgkI\nCwIEFgIDAQIeBwIXgAAKCRBc6PeuecS2PCk3D/9jW7xrBB/2MQFKd5l+mNMFyKwc\nL9M/M5RFI9GaQRo55CwnPb0nnxOJR1V5GzZ/YGii53H2ose65CfBOE2L/F/RvKF0\nH9S9MInixlahzzKtV3TpDoZGk5oZIHEMuPmPS4XaHggolrzExY0ib0mQuBBE/uEV\n/HlyHEunBKPhTkAe+6Q+2dl22SUuVfWr4Uzlp65+DkdN3M37WI1a3Suhnef3rOSM\nV6puUzWRR7qcYs5C2In87AcYPn92P5ur1y/C32r8Ftg3fRWnEzI9QfRG52ojNOLK\nyGQ8ZC9PGe0q7VFcF7ridT/uzRU+NVKldbJg+rvBnszb1MjNuR7rUQHyvGmbsUVQ\nRCsgdovkee3lP4gfZHzk2SSLVSo0+NJRNaM90EmPk14Pgi/yfRSDGBVvLBbEanYI\nv1ZtdIPRyKi+/IaMOu/l7nayM/8RzghdU+0f1FAif5qf9nXuI13P8fqcqfu67gNd\nkh0UUF1XyR5UHHEZQQDqCuKEkZJ/+27jYlsG1ZiLb1odlIWoR44RP6k5OJl0raZb\nyLXbAfpITsXiJJBpCam9P9+XR5VSfgkqp5hIa7J8piN3DoMpoExg4PPQr6PbLAJy\nOUCOnuB7yYVbj0wYuMXTuyrcBHh/UymQnS8AMpQoEkCLWS/A/Hze/pD23LgiBoLY\nXIn5A2EOAf7t2IMSlA==\n=OanT\n-----END PGP PUBLIC KEY BLOCK-----',
|
||||
rhsm: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
created_at: '2021-04-27T12:31:12Z',
|
||||
id: 'hyk93673-8dcc-4a61-ac30-e9f4940d8346',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'vsphere-ova',
|
||||
upload_request: {
|
||||
options: {},
|
||||
type: 'aws.s3',
|
||||
},
|
||||
},
|
||||
],
|
||||
customizations: {
|
||||
custom_repositories: [
|
||||
{
|
||||
baseurl: ['http://yum.theforeman.org/releases/3.4/el8/x86_64/'],
|
||||
check_gpg: true,
|
||||
check_repo_gpg: false,
|
||||
gpgkey: [
|
||||
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBGN9300BEAC1FLODu0cL6saMMHa7yJY1JZUc+jQUI/HdECQrrsTaPXlcc7nM\nykYMMv6amPqbnhH/R5BW2Ano+OMse+PXtUr0NXU4OcvxbnnXkrVBVUf8mXI9DzLZ\njw8KoD+4/s0BuzO78zAJF5uhuyHMAK0ll9v0r92kK45Fas9iZTfRFcqFAzvgjScf\n5jeBnbRs5U3UTz9mtDy802mk357o1A8BD0qlu3kANDpjLbORGWdAj21A6sMJDYXy\nHS9FBNV54daNcr+weky2L9gaF2yFjeu2rSEHCSfkbWfpSiVUx/bDTj7XS6XDOuJT\nJqvGS8jHqjHAIFBirhCA4cY/jLKxWyMr5N6IbXpPAYgt8/YYz2aOYVvdyB8tZ1u1\nkVsMYSGcvTBexZCn1cDkbO6I+waIlsc0uxGqUGBKF83AVYCQqOkBjF1uNnu9qefE\nkEc9obr4JZsAgnisboU25ss5ZJddKlmFMKSi66g4S5ChLEPFq7MB06PhLFioaD3L\nEXza7XitoW5VBwr0BSVKAHMC0T2xbm70zY06a6gQRlvr9a10lPmv4Tptc7xgQReg\nu1TlFPbrkGJ0d8O6vHQRAd3zdsNaVr4gX0Tg7UYiqT9ZUkP7hOc8PYXQ28hHrHTB\nA63MTq0aiPlJ/ivTuX8M6+Bi25dIV6N6IOUi/NQKIYxgovJCDSdCAAM0fQARAQAB\ntCFMdWNhcyBHYXJmaWVsZCA8bHVjYXNAcmVkaGF0LmNvbT6JAlcEEwEIAEEWIQTO\nQZeiHnXqdjmfUURc6PeuecS2PAUCY33fTQIbAwUJA8JnAAULCQgHAgIiAgYVCgkI\nCwIEFgIDAQIeBwIXgAAKCRBc6PeuecS2PCk3D/9jW7xrBB/2MQFKd5l+mNMFyKwc\nL9M/M5RFI9GaQRo55CwnPb0nnxOJR1V5GzZ/YGii53H2ose65CfBOE2L/F/RvKF0\nH9S9MInixlahzzKtV3TpDoZGk5oZIHEMuPmPS4XaHggolrzExY0ib0mQuBBE/uEV\n/HlyHEunBKPhTkAe+6Q+2dl22SUuVfWr4Uzlp65+DkdN3M37WI1a3Suhnef3rOSM\nV6puUzWRR7qcYs5C2In87AcYPn92P5ur1y/C32r8Ftg3fRWnEzI9QfRG52ojNOLK\nyGQ8ZC9PGe0q7VFcF7ridT/uzRU+NVKldbJg+rvBnszb1MjNuR7rUQHyvGmbsUVQ\nRCsgdovkee3lP4gfZHzk2SSLVSo0+NJRNaM90EmPk14Pgi/yfRSDGBVvLBbEanYI\nv1ZtdIPRyKi+/IaMOu/l7nayM/8RzghdU+0f1FAif5qf9nXuI13P8fqcqfu67gNd\nkh0UUF1XyR5UHHEZQQDqCuKEkZJ/+27jYlsG1ZiLb1odlIWoR44RP6k5OJl0raZb\nyLXbAfpITsXiJJBpCam9P9+XR5VSfgkqp5hIa7J8piN3DoMpoExg4PPQr6PbLAJy\nOUCOnuB7yYVbj0wYuMXTuyrcBHh/UymQnS8AMpQoEkCLWS/A/Hze/pD23LgiBoLY\nXIn5A2EOAf7t2IMSlA==\n=OanT\n-----END PGP PUBLIC KEY BLOCK-----',
|
||||
],
|
||||
id: 'dbad4dfc-1547-45f8-b5af-1d7fec0476c6',
|
||||
name: '13lk3',
|
||||
},
|
||||
{
|
||||
baseurl: [
|
||||
'http://mirror.stream.centos.org/SIGs/8/kmods/x86_64/packages-main/',
|
||||
],
|
||||
check_gpg: false,
|
||||
check_repo_gpg: false,
|
||||
gpgkey: [''],
|
||||
id: '9cf1d45d-aa06-46fe-87ea-121845cc6bbb',
|
||||
name: '2lmdtj',
|
||||
},
|
||||
],
|
||||
payload_repositories: [
|
||||
{
|
||||
baseurl: 'http://yum.theforeman.org/releases/3.4/el8/x86_64/',
|
||||
check_gpg: true,
|
||||
check_repo_gpg: false,
|
||||
gpgkey:
|
||||
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBGN9300BEAC1FLODu0cL6saMMHa7yJY1JZUc+jQUI/HdECQrrsTaPXlcc7nM\nykYMMv6amPqbnhH/R5BW2Ano+OMse+PXtUr0NXU4OcvxbnnXkrVBVUf8mXI9DzLZ\njw8KoD+4/s0BuzO78zAJF5uhuyHMAK0ll9v0r92kK45Fas9iZTfRFcqFAzvgjScf\n5jeBnbRs5U3UTz9mtDy802mk357o1A8BD0qlu3kANDpjLbORGWdAj21A6sMJDYXy\nHS9FBNV54daNcr+weky2L9gaF2yFjeu2rSEHCSfkbWfpSiVUx/bDTj7XS6XDOuJT\nJqvGS8jHqjHAIFBirhCA4cY/jLKxWyMr5N6IbXpPAYgt8/YYz2aOYVvdyB8tZ1u1\nkVsMYSGcvTBexZCn1cDkbO6I+waIlsc0uxGqUGBKF83AVYCQqOkBjF1uNnu9qefE\nkEc9obr4JZsAgnisboU25ss5ZJddKlmFMKSi66g4S5ChLEPFq7MB06PhLFioaD3L\nEXza7XitoW5VBwr0BSVKAHMC0T2xbm70zY06a6gQRlvr9a10lPmv4Tptc7xgQReg\nu1TlFPbrkGJ0d8O6vHQRAd3zdsNaVr4gX0Tg7UYiqT9ZUkP7hOc8PYXQ28hHrHTB\nA63MTq0aiPlJ/ivTuX8M6+Bi25dIV6N6IOUi/NQKIYxgovJCDSdCAAM0fQARAQAB\ntCFMdWNhcyBHYXJmaWVsZCA8bHVjYXNAcmVkaGF0LmNvbT6JAlcEEwEIAEEWIQTO\nQZeiHnXqdjmfUURc6PeuecS2PAUCY33fTQIbAwUJA8JnAAULCQgHAgIiAgYVCgkI\nCwIEFgIDAQIeBwIXgAAKCRBc6PeuecS2PCk3D/9jW7xrBB/2MQFKd5l+mNMFyKwc\nL9M/M5RFI9GaQRo55CwnPb0nnxOJR1V5GzZ/YGii53H2ose65CfBOE2L/F/RvKF0\nH9S9MInixlahzzKtV3TpDoZGk5oZIHEMuPmPS4XaHggolrzExY0ib0mQuBBE/uEV\n/HlyHEunBKPhTkAe+6Q+2dl22SUuVfWr4Uzlp65+DkdN3M37WI1a3Suhnef3rOSM\nV6puUzWRR7qcYs5C2In87AcYPn92P5ur1y/C32r8Ftg3fRWnEzI9QfRG52ojNOLK\nyGQ8ZC9PGe0q7VFcF7ridT/uzRU+NVKldbJg+rvBnszb1MjNuR7rUQHyvGmbsUVQ\nRCsgdovkee3lP4gfZHzk2SSLVSo0+NJRNaM90EmPk14Pgi/yfRSDGBVvLBbEanYI\nv1ZtdIPRyKi+/IaMOu/l7nayM/8RzghdU+0f1FAif5qf9nXuI13P8fqcqfu67gNd\nkh0UUF1XyR5UHHEZQQDqCuKEkZJ/+27jYlsG1ZiLb1odlIWoR44RP6k5OJl0raZb\nyLXbAfpITsXiJJBpCam9P9+XR5VSfgkqp5hIa7J8piN3DoMpoExg4PPQr6PbLAJy\nOUCOnuB7yYVbj0wYuMXTuyrcBHh/UymQnS8AMpQoEkCLWS/A/Hze/pD23LgiBoLY\nXIn5A2EOAf7t2IMSlA==\n=OanT\n-----END PGP PUBLIC KEY BLOCK-----',
|
||||
rhsm: false,
|
||||
},
|
||||
{
|
||||
baseurl:
|
||||
'http://mirror.stream.centos.org/SIGs/8/kmods/x86_64/packages-main/',
|
||||
check_gpg: false,
|
||||
check_repo_gpg: false,
|
||||
gpgkey: '',
|
||||
rhsm: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
created_at: '2021-04-27T12:31:12Z',
|
||||
id: '4873fd0f-1851-4b9f-b4fe-4639fce90794',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'image-installer',
|
||||
upload_request: {
|
||||
options: {},
|
||||
type: 'aws.s3',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
created_at: currentDateInString,
|
||||
id: '7b7d0d51-7106-42ab-98f2-f89872a9d599',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'guest-image',
|
||||
upload_request: {
|
||||
options: {},
|
||||
type: 'aws.s3',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
data: mockComposes.slice(offset, offset + limit),
|
||||
} as ComposesResponse;
|
||||
};
|
||||
|
||||
export const mockComposes: ComposesResponseItem[] = [
|
||||
{
|
||||
id: '1579d95b-8f1d-4982-8c53-8c2afa4ab04c',
|
||||
image_name: 'testImageName',
|
||||
created_at: '2021-04-27T12:31:12Z',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
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_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'gcp',
|
||||
upload_request: {
|
||||
type: 'gcp',
|
||||
options: {
|
||||
share_with_accounts: ['serviceAccount:test@email.com'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'edbae1c2-62bc-42c1-ae0c-3110ab718f58',
|
||||
created_at: '2021-04-27T12:31:12Z',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'aws',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '42ad0826-30b5-4f64-a24e-957df26fd564',
|
||||
created_at: '2021-04-27T12:31:12Z',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'aws',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '955944a2-e149-4058-8ac1-35b514cb5a16',
|
||||
created_at: '2021-04-27T12:31:12Z',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'aws',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'f7a60094-b376-4b58-a102-5c8c82dfd18b',
|
||||
created_at: '2021-04-27T12:31:12Z',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'aws',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '61b0effa-c901-4ee5-86b9-2010b47f1b22',
|
||||
created_at: '2021-04-27T12:31:12Z',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'aws',
|
||||
upload_request: {
|
||||
type: 'aws',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'ca03f120-9840-4959-871e-94a5cb49d1f2',
|
||||
created_at: '2021-04-27T12:31:12Z',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'gcp',
|
||||
upload_request: {
|
||||
type: 'gcp',
|
||||
options: {
|
||||
share_with_accounts: ['serviceAccount:test@email.com'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '551de6f6-1533-4b46-a69f-7924051f9bc6',
|
||||
created_at: '2021-04-27T12:31:12Z',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'azure',
|
||||
upload_request: {
|
||||
type: 'azure',
|
||||
options: {
|
||||
resource_group: 'my_resource_group',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
created_at: '2021-04-27T12:31:12Z',
|
||||
id: 'b7193673-8dcc-4a5f-ac30-e9f4940d8346',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'vsphere',
|
||||
upload_request: {
|
||||
options: {},
|
||||
type: 'aws.s3',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
created_at: '2021-04-27T12:31:12Z',
|
||||
id: 'hyk93673-8dcc-4a61-ac30-e9f4940d8346',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'vsphere-ova',
|
||||
upload_request: {
|
||||
options: {},
|
||||
type: 'aws.s3',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
created_at: '2021-04-27T12:31:12Z',
|
||||
id: '4873fd0f-1851-4b9f-b4fe-4639fce90794',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'image-installer',
|
||||
upload_request: {
|
||||
options: {},
|
||||
type: 'aws.s3',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
created_at: currentDateInString,
|
||||
id: '7b7d0d51-7106-42ab-98f2-f89872a9d599',
|
||||
request: {
|
||||
distribution: RHEL_8,
|
||||
image_requests: [
|
||||
{
|
||||
architecture: 'x86_64',
|
||||
image_type: 'guest-image',
|
||||
upload_request: {
|
||||
options: {},
|
||||
type: 'aws.s3',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const mockStatus = (composeId: string): ComposeStatus => {
|
||||
const mockComposes: { [key: string]: ComposeStatus } = {
|
||||
'1579d95b-8f1d-4982-8c53-8c2afa4ab04c': {
|
||||
|
|
@ -378,7 +308,6 @@ export const mockStatus = (composeId: string): ComposeStatus => {
|
|||
},
|
||||
},
|
||||
'c1cfa347-4c37-49b5-8e73-6aa1d1746cfa': {
|
||||
// kept "running" for backward compatibility
|
||||
image_status: {
|
||||
status: 'failure',
|
||||
error: {
|
||||
|
|
@ -542,7 +471,9 @@ export const mockStatus = (composeId: string): ComposeStatus => {
|
|||
image_type: 'azure',
|
||||
upload_request: {
|
||||
type: 'azure',
|
||||
options: {},
|
||||
options: {
|
||||
resource_group: 'my_resource_group',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
} from '../fixtures/activationKeys';
|
||||
import { mockArchitecturesByDistro } from '../fixtures/architectures';
|
||||
import {
|
||||
composesEndpoint,
|
||||
mockClones,
|
||||
mockCloneStatus,
|
||||
mockComposes,
|
||||
|
|
@ -70,7 +71,7 @@ export const handlers = [
|
|||
return res(ctx.status(200), ctx.json(mockRepositoryResults(args)));
|
||||
}),
|
||||
rest.get(`${IMAGE_BUILDER_API}/composes`, (req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json(mockComposes));
|
||||
return res(ctx.status(200), ctx.json(composesEndpoint(req)));
|
||||
}),
|
||||
rest.get(`${IMAGE_BUILDER_API}/composes/:composeId`, (req, res, ctx) => {
|
||||
const { composeId } = req.params;
|
||||
|
|
@ -87,4 +88,7 @@ export const handlers = [
|
|||
const { cloneId } = req.params;
|
||||
return res(ctx.status(200), ctx.json(mockCloneStatus(cloneId)));
|
||||
}),
|
||||
rest.post(`${IMAGE_BUILDER_API}/compose`, (req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json({}));
|
||||
}),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import ShareImageModal from '../Components/ShareImageModal/ShareImageModal';
|
|||
import { middleware, reducer } from '../store';
|
||||
import { resolveRelPath } from '../Utilities/path';
|
||||
|
||||
export const renderCustomRoutesWithReduxRouter = (
|
||||
export const renderCustomRoutesWithReduxRouter = async (
|
||||
route = '/',
|
||||
preloadedState = {},
|
||||
routes
|
||||
|
|
@ -32,7 +32,10 @@ export const renderCustomRoutesWithReduxRouter = (
|
|||
return { router, store };
|
||||
};
|
||||
|
||||
export const renderWithReduxRouter = (route = '/', preloadedState = {}) => {
|
||||
export const renderWithReduxRouter = async (
|
||||
route = '/',
|
||||
preloadedState = {}
|
||||
) => {
|
||||
const store = configureStore({ reducer, middleware, preloadedState });
|
||||
|
||||
const routes = [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue