HMS-3796: Add snapshot date selection to wizard

This commit is contained in:
Andrew Dewar 2024-04-17 15:32:29 -06:00 committed by Lucas Garfield
parent a97b4d082d
commit 3231b324f0
42 changed files with 1958 additions and 257 deletions

View file

@ -5,9 +5,11 @@ extends: [
rules:
"@typescript-eslint/ban-ts-comment":
- error
- ts-expect-error: 'allow-with-description'
ts-ignore: 'allow-with-description'
- ts-expect-error: "allow-with-description"
ts-ignore: "allow-with-description"
ts-nocheck: true
ts-check: true
minimumDescriptionLength: 5
"@typescript-eslint/ban-types": off
"@typescript-eslint/no-unused-vars":
- warn

View file

@ -36,7 +36,7 @@ rules:
prefer-const:
- error
- destructuring: any
no-console: 2
no-console: 1
eqeqeq: error
array-callback-return: warn
# Temporarily disabled

9
.prettierrc Normal file
View file

@ -0,0 +1,9 @@
{
"semi": true,
"tabWidth": 2,
"singleQuote": false,
"jsxSingleQuote": false,
"bracketSpacing": true,
"tsxSingleQuote": true,
"tsSingleQuote": true
}

View file

@ -12,6 +12,8 @@ const config: ConfigFile = {
'listRepositories',
'listRepositoriesRpms',
'searchRpm',
'listFeatures',
'listSnapshotsByDate',
],
};

File diff suppressed because it is too large Load diff

View file

@ -72,3 +72,13 @@ ul.pf-m-plain {
.panel-border {
--pf-v5-c-panel--before--BorderColor: #BEE1F4;
}
// Targets the alert within the Reviewsteps > content dropdown
// Removes excess top margin padding
div.pf-v5-c-alert.pf-m-inline.pf-m-plain.pf-m-warning {
margin-top: 18px;
h4 {
margin-block-start: 0;
}
}

View file

@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import {
Button,
@ -20,6 +20,7 @@ import RegistrationStep from './steps/Registration';
import RepositoriesStep from './steps/Repositories';
import ReviewStep from './steps/Review';
import ReviewWizardFooter from './steps/Review/Footer/Footer';
import SnapshotStep from './steps/Snapshot';
import Aws from './steps/TargetEnvironment/Aws';
import Azure from './steps/TargetEnvironment/Azure';
import Gcp from './steps/TargetEnvironment/Gcp';
@ -32,6 +33,7 @@ import {
} from './validators';
import { RHEL_8, AARCH64 } from '../../constants';
import { useListFeaturesQuery } from '../../store/contentSourcesApi';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import './CreateImageWizard.scss';
import {
@ -53,6 +55,8 @@ import {
selectRegistrationType,
selectStepValidation,
addImageType,
selectSnapshotDate,
selectUseLatest,
} from '../../store/wizardSlice';
import { resolveRelPath } from '../../Utilities/path';
import { ImageBuilderHeader } from '../sharedComponents/ImageBuilderHeader';
@ -93,14 +97,33 @@ export const CustomWizardFooter = ({
};
type CreateImageWizardProps = {
startStepIndex?: number;
isEdit?: boolean;
};
const CreateImageWizard = ({ startStepIndex = 1 }: CreateImageWizardProps) => {
const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
const navigate = useNavigate();
const dispatch = useAppDispatch();
const [searchParams] = useSearchParams();
// Remove this and all fallthrough logic when snapshotting is enabled in Prod-stable
// =========================TO REMOVE=======================
const { data, isSuccess, isFetching, isError } =
useListFeaturesQuery(undefined);
const snapshottingEnabled = useMemo(
() =>
!(
!isError &&
!isFetching &&
isSuccess &&
data?.snapshots?.accessible === false &&
data?.snapshots?.enabled === false
),
[data, isSuccess, isFetching, isError]
);
// =========================TO REMOVE=======================
// IMPORTANT: Ensure the wizard starts with a fresh initial state
useEffect(() => {
dispatch(initializeWizard());
@ -139,6 +162,11 @@ const CreateImageWizard = ({ startStepIndex = 1 }: CreateImageWizardProps) => {
const registrationType = useAppSelector(selectRegistrationType);
const activationKey = useAppSelector(selectActivationKey);
const snapshotDate = useAppSelector(selectSnapshotDate);
const useLatest = useAppSelector(selectUseLatest);
const snapshotStepRequiresChoice = !useLatest && !snapshotDate;
const [currentStep, setCurrentStep] = React.useState<WizardStepType>();
const onStepChange = (
_event: React.MouseEvent<HTMLButtonElement>,
@ -152,7 +180,7 @@ const CreateImageWizard = ({ startStepIndex = 1 }: CreateImageWizardProps) => {
<ImageBuilderHeader />
<section className="pf-l-page__main-section pf-c-page__main-section">
<Wizard
startIndex={startStepIndex}
startIndex={isEdit ? (snapshottingEnabled ? 14 : 13) : 1}
onClose={() => navigate(resolveRelPath(''))}
onStepChange={onStepChange}
isVisitRequired
@ -268,10 +296,27 @@ const CreateImageWizard = ({ startStepIndex = 1 }: CreateImageWizardProps) => {
name="Content"
id="step-content"
steps={[
...(snapshottingEnabled
? [
<WizardStep
name="Repository snapshot"
id="wizard-repository-snapshot"
key="wizard-repository-snapshot"
footer={
<CustomWizardFooter
disableNext={snapshotStepRequiresChoice}
/>
}
>
<SnapshotStep />
</WizardStep>,
]
: []),
<WizardStep
name="Custom repositories"
id="wizard-custom-repositories"
key="wizard-custom-repositories"
isDisabled={snapshotStepRequiresChoice}
footer={<CustomWizardFooter disableNext={false} />}
>
<RepositoriesStep />
@ -280,12 +325,13 @@ const CreateImageWizard = ({ startStepIndex = 1 }: CreateImageWizardProps) => {
name="Additional packages"
id="wizard-additional-packages"
key="wizard-additional-packages"
isDisabled={snapshotStepRequiresChoice}
footer={<CustomWizardFooter disableNext={false} />}
>
<PackagesStep />
</WizardStep>,
]}
></WizardStep>
/>
<WizardStep
name="Details"
id="step-details"
@ -295,6 +341,7 @@ const CreateImageWizard = ({ startStepIndex = 1 }: CreateImageWizardProps) => {
? 'error'
: 'default'
}
isDisabled={snapshotStepRequiresChoice}
footer={
<CustomWizardFooter
disableNext={detailsValidation !== 'success'}
@ -306,9 +353,11 @@ const CreateImageWizard = ({ startStepIndex = 1 }: CreateImageWizardProps) => {
<WizardStep
name="Review"
id="step-review"
isDisabled={snapshotStepRequiresChoice}
footer={<ReviewWizardFooter />}
>
<ReviewStep />
{/* Intentional prop drilling for simplicity - To be removed */}
<ReviewStep snapshottingEnabled={snapshottingEnabled} />
</WizardStep>
</Wizard>
</section>

View file

@ -36,7 +36,7 @@ const EditImageWizard = ({ blueprintId }: EditImageWizardProps) => {
navigate(resolveRelPath(''));
}
}, [error, navigate]);
return <CreateImageWizard startStepIndex={13} />;
return <CreateImageWizard isEdit />;
};
export default EditImageWizard;

View file

@ -351,7 +351,11 @@ const Repositories = () => {
const handleSelectAll = () => {
if (data) {
updateSelected(data.data?.map((repo) => repo.url) || []);
updateSelected(
data.data
?.filter(({ status }) => status === 'Valid')
.map((repo) => repo.url) || []
);
}
};

View file

@ -33,7 +33,7 @@ import {
selectRegistrationType,
} from '../../../../store/wizardSlice';
const Review = () => {
const Review = ({ snapshottingEnabled }: { snapshottingEnabled: boolean }) => {
const blueprintName = useAppSelector(selectBlueprintName);
const blueprintDescription = useAppSelector(selectBlueprintDescription);
const distribution = useAppSelector(selectDistribution);
@ -172,7 +172,8 @@ const Review = () => {
isIndented
data-testid="content-expandable"
>
<ContentList />
{/* Intentional prop drilling for simplicity - To be removed */}
<ContentList snapshottingEnabled={snapshottingEnabled} />
</ExpandableSection>
{(blueprintName || blueprintDescription) && (
<ExpandableSection

View file

@ -1,9 +1,20 @@
import React from 'react';
import { Alert, Panel, PanelMain, Spinner } from '@patternfly/react-core';
import {
Alert,
EmptyState,
EmptyStateHeader,
EmptyStateIcon,
Panel,
PanelMain,
Spinner,
} from '@patternfly/react-core';
import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
import { useListRepositoriesQuery } from '../../../../store/contentSourcesApi';
import {
ApiSnapshotForDate,
useListRepositoriesQuery,
} from '../../../../store/contentSourcesApi';
import { useAppSelector } from '../../../../store/hooks';
import {
selectCustomRepositories,
@ -93,6 +104,95 @@ export const FSReviewTable = () => {
);
};
const Error = () => {
return (
<Alert title="Repositories unavailable" variant="danger" isPlain isInline>
Repositories cannot be reached, try again later.
</Alert>
);
};
const Loading = () => {
return (
<EmptyState>
<EmptyStateHeader
titleText="Loading"
icon={<EmptyStateIcon icon={Spinner} />}
headingLevel="h4"
/>
</EmptyState>
);
};
export const SnapshotTable = ({
snapshotForDate,
}: {
snapshotForDate: ApiSnapshotForDate[];
}) => {
const { data, isSuccess, isLoading, isError } = useListRepositoriesQuery({
uuid: snapshotForDate.map(({ repository_uuid }) => repository_uuid).join(),
origin: 'red_hat,external', // Make sure to show both redhat and custom
});
const isAfterSet = new Set(
snapshotForDate
.filter(({ is_after }) => is_after)
.map(({ repository_uuid }) => repository_uuid)
);
const stringToDateToMMDDYYYY = (strDate: string) => {
const date = new Date(strDate);
return `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date
.getDate()
.toString()
.padStart(2, '0')}/${date.getFullYear()}`;
};
return (
(isError && <Error />) ||
(isLoading && <Loading />) ||
(isSuccess && (
<Panel isScrollable>
<PanelMain maxHeight="30ch">
<Table aria-label="Packages table" variant="compact">
<Thead>
<Tr>
<Th>Name</Th>
<Th>Last snapshot date</Th>
</Tr>
</Thead>
<Tbody data-testid="packages-tbody-review">
{data?.data?.map(({ uuid, name, last_snapshot }, pkgIndex) => (
<Tr key={pkgIndex}>
<Td>{name}</Td>
<Td>
{uuid && isAfterSet.has(uuid) ? (
<Alert
title={
last_snapshot?.created_at
? stringToDateToMMDDYYYY(last_snapshot.created_at)
: 'N/A'
}
variant="warning"
isPlain
isInline
/>
) : last_snapshot?.created_at ? (
stringToDateToMMDDYYYY(last_snapshot.created_at)
) : (
'N/A'
)}
</Td>
</Tr>
))}
</Tbody>
</Table>
</PanelMain>
</Panel>
))
);
};
export const PackagesTable = () => {
const packages = useAppSelector(selectPackages);
return (

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useMemo } from 'react';
import {
Alert,
@ -16,7 +16,11 @@ import {
import { ExclamationTriangleIcon } from '@patternfly/react-icons';
import ActivationKeyInformation from './../Registration/ActivationKeyInformation';
import { PackagesTable, RepositoriesTable } from './ReviewStepTables';
import {
PackagesTable,
RepositoriesTable,
SnapshotTable,
} from './ReviewStepTables';
import { FSReviewTable } from './ReviewStepTables';
import {
@ -26,6 +30,7 @@ import {
RHEL_8_MAINTENANCE_SUPPORT,
RHEL_9,
} from '../../../../constants';
import { useListSnapshotsByDateMutation } from '../../../../store/contentSourcesApi';
import { useAppSelector } from '../../../../store/hooks';
import { useGetSourceListQuery } from '../../../../store/provisioningApi';
import { useShowActivationKeyQuery } from '../../../../store/rhsmApi';
@ -51,8 +56,14 @@ import {
selectRegistrationType,
selectFileSystemPartitionMode,
selectRecommendedRepositories,
selectSnapshotDate,
selectUseLatest,
} from '../../../../store/wizardSlice';
import { toMonthAndYear } from '../../../../Utilities/time';
import {
convertMMDDYYYYToYYYYMMDD,
toMonthAndYear,
yyyyMMddFormat,
} from '../../../../Utilities/time';
import { MinimumSizePopover } from '../FileSystem/FileSystemTable';
import { MajorReleasesLifecyclesChart } from '../ImageOutput/ReleaseLifecycle';
import OscapProfileInformation from '../Oscap/OscapProfileInformation';
@ -385,17 +396,126 @@ export const TargetEnvOtherList = () => {
);
};
export const ContentList = () => {
export const ContentList = ({
snapshottingEnabled,
}: {
snapshottingEnabled: boolean;
}) => {
const customRepositories = useAppSelector(selectCustomRepositories);
const packages = useAppSelector(selectPackages);
const recommendedRepositories = useAppSelector(selectRecommendedRepositories);
const snapshotDate = useAppSelector(selectSnapshotDate);
const useLatest = useAppSelector(selectUseLatest);
const customAndRecommendedRepositoryUUIDS = useMemo(
() =>
[
...customRepositories.map(({ id }) => id),
...recommendedRepositories.map(({ uuid }) => uuid),
] as string[],
[customRepositories, recommendedRepositories]
);
const [listSnapshotsByDate, { data, isSuccess, isLoading, isError }] =
useListSnapshotsByDateMutation();
useEffect(() => {
listSnapshotsByDate({
apiListSnapshotByDateRequest: {
repository_uuids: customAndRecommendedRepositoryUUIDS,
date: useLatest
? yyyyMMddFormat(new Date())
: convertMMDDYYYYToYYYYMMDD(snapshotDate),
},
});
}, [
customAndRecommendedRepositoryUUIDS,
listSnapshotsByDate,
snapshotDate,
useLatest,
]);
const duplicatePackages = packages.filter(
(item, index) => packages.indexOf(item) !== index
);
const noRepositoriesSelected =
customAndRecommendedRepositoryUUIDS.length === 0;
const hasSnapshotDateAfter = data?.data?.some(({ is_after }) => is_after);
const snapshottingText = useMemo(() => {
switch (true) {
case noRepositoriesSelected:
return 'No repositories selected';
case isLoading:
return '';
case useLatest:
return 'Use latest';
case !!snapshotDate:
return `State as of ${snapshotDate}`;
default:
return '';
}
}, [noRepositoriesSelected, isLoading, useLatest, snapshotDate]);
return (
<>
<TextContent>
<TextList component={TextListVariants.dl}>
{snapshottingEnabled ? (
<>
<TextListItem
component={TextListItemVariants.dt}
className="pf-u-min-width"
>
Repository Snapshot
</TextListItem>
<TextListItem
component={TextListItemVariants.dd}
data-testid="snapshot-method"
>
<Popover
position="bottom"
headerContent={
useLatest
? 'Repositories as of today'
: `Repositories as of ${snapshotDate}`
}
hasAutoWidth
minWidth="60rem"
bodyContent={
<SnapshotTable snapshotForDate={data?.data || []} />
}
>
<Button
variant="link"
aria-label="Snapshot method"
className="pf-u-p-0"
isDisabled={noRepositoriesSelected || isLoading || isError}
isLoading={isLoading}
>
{snapshottingText}
</Button>
</Popover>
{!useLatest &&
!isLoading &&
isSuccess &&
hasSnapshotDateAfter ? (
<Alert
variant="warning"
isInline
isPlain
title="A snapshot for this date is not available for some repositories."
/>
) : (
''
)}
</TextListItem>
</>
) : (
''
)}
<TextListItem component={TextListItemVariants.dt}>
Custom repositories
</TextListItem>

View file

@ -4,13 +4,18 @@ import { Form, Title } from '@patternfly/react-core';
import Review from './ReviewStep';
const ReviewStep = () => {
const ReviewStep = ({
snapshottingEnabled,
}: {
snapshottingEnabled: boolean;
}) => {
return (
<Form>
<Title headingLevel="h1" size="xl">
Review
</Title>
<Review />
{/* Intentional prop drilling for simplicity - To be removed */}
<Review snapshottingEnabled={snapshottingEnabled} />
</Form>
);
};

View file

@ -0,0 +1,117 @@
import React from 'react';
import {
Alert,
Button,
DatePicker,
Flex,
FormGroup,
Grid,
Radio,
Text,
Title,
} from '@patternfly/react-core';
import ConditionalTooltip from './components/ConditionalTooltip';
import { useListFeaturesQuery } from '../../../../store/contentSourcesApi';
import { useAppDispatch, useAppSelector } from '../../../../store/hooks';
import {
selectSnapshotDate,
selectUseLatest,
changeUseLatest,
changeSnapshotDate,
} from '../../../../store/wizardSlice';
import {
dateToMMDDYYYY,
parseMMDDYYYYtoDate,
} from '../../../../Utilities/time';
const dateValidators = [
(date: Date) => {
if (date.getTime() > Date.now()) {
return 'Cannot set a date in the future';
}
return '';
},
];
export default function Snapshot() {
const dispatch = useAppDispatch();
const snapshotDate = useAppSelector(selectSnapshotDate);
const useLatest = useAppSelector(selectUseLatest);
return (
<>
<FormGroup>
<Radio
id="use latest snapshot radio"
ouiaId="use-latest-snapshot-radio"
name="use-latest-snapshot"
label="Use latest content"
description="Use the newest repository state available when building this image."
isChecked={useLatest}
onChange={() => !useLatest && dispatch(changeUseLatest(true))}
/>
<Radio
id="use snapshot date radio"
ouiaId="use-snapshot-date-radio"
name="use-snapshot-date"
label="Use a snapshot"
description="Target a date and build images with repository information from this date."
isChecked={!useLatest}
onChange={() => useLatest && dispatch(changeUseLatest(false))}
/>
</FormGroup>
{useLatest ? (
<>
<Title headingLevel="h1" size="xl">
Use latest content
</Title>
<Grid>
<Text>
Image Builder will automatically use the newest state of
repositories when building this image.
</Text>
</Grid>
</>
) : (
<>
<Title headingLevel="h1" size="xl">
Use a snapshot
</Title>
<FormGroup label="Select snapshot date" isRequired>
<Flex
direction={{ default: 'row' }}
alignContent={{ default: 'alignContentCenter' }}
>
<DatePicker
id="pick snapshot date radio"
name="pick-snapshot-date"
value={snapshotDate}
required
requiredDateOptions={{ isRequired: true }}
placeholder="MM/DD/YYYY"
dateParse={parseMMDDYYYYtoDate}
dateFormat={dateToMMDDYYYY}
validators={dateValidators}
onChange={(_, val) => dispatch(changeSnapshotDate(val))}
/>
<Button
variant="link"
onClick={() => dispatch(changeSnapshotDate(''))}
>
Reset
</Button>
</Flex>
</FormGroup>
<Grid>
<Text>
Image Builder will reflect the state of repositories based on the
selected date when building this image.
</Text>
</Grid>
</>
)}
</>
);
}

View file

@ -0,0 +1,25 @@
import React, { cloneElement } from 'react';
import { Tooltip, TooltipProps } from '@patternfly/react-core';
interface Props extends TooltipProps {
show: boolean;
setDisabled?: boolean;
}
const ConditionalTooltip = ({ show, children, setDisabled, ...rest }: Props) =>
show ? (
<Tooltip {...rest}>
<div>
{children &&
cloneElement(
children,
setDisabled ? { isDisabled: setDisabled } : undefined
)}
</div>
</Tooltip>
) : (
<div>{children}</div>
);
export default ConditionalTooltip;

View file

@ -0,0 +1,37 @@
import React from 'react';
import { Button, Form, Grid, Text, Title } from '@patternfly/react-core';
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
import { useHref } from 'react-router-dom';
import Snapshot from './Snapshot';
export default function SnapshotStep() {
const path = useHref('image-builder');
const pathname = path.split('image-builder')[0] + 'content';
return (
<Form>
<Title headingLevel="h1" size="xl">
Repository snapshot
</Title>
<Grid>
<Text>
Control the consistency of the packages in the repository used to
build the image.
</Text>
<Button
component="a"
target="_blank"
variant="link"
iconPosition="right"
isInline
icon={<ExternalLinkAltIcon />}
href={pathname + '/repositories'}
>
Create and manage repositories here
</Button>
</Grid>
<Snapshot />
</Form>
);
}

View file

@ -46,7 +46,13 @@ import {
wizardState,
selectFileSystemPartitionMode,
selectPartitions,
selectSnapshotDate,
selectUseLatest,
} from '../../../store/wizardSlice';
import {
convertMMDDYYYYToYYYYMMDD,
convertYYYYMMDDTOMMDDYYYY,
} from '../../../Utilities/time';
import {
convertSchemaToIBCustomRepo,
convertSchemaToIBPayloadRepo,
@ -102,6 +108,11 @@ export const mapRequestToState = (request: BlueprintResponse): wizardState => {
(image) => image.image_type === 'azure'
);
const snapshot_date = convertYYYYMMDDTOMMDDYYYY(
request.image_requests.find((image) => !!image.snapshot_date)
?.snapshot_date || ''
);
const awsUploadOptions = aws?.upload_request
.options as AwsUploadRequestOptions;
const gcpUploadOptions = gcp?.upload_request
@ -154,6 +165,10 @@ export const mapRequestToState = (request: BlueprintResponse): wizardState => {
source: { id: awsUploadOptions?.share_with_sources?.[0] },
sourceId: awsUploadOptions?.share_with_sources?.[0],
},
snapshotting: {
useLatest: !snapshot_date,
snapshotDate: snapshot_date,
},
repositories: {
customRepositories: request.customizations.custom_repositories || [],
payloadRepositories: request.customizations.payload_repositories || [],
@ -179,6 +194,8 @@ export const mapRequestToState = (request: BlueprintResponse): wizardState => {
const getImageRequests = (state: RootState): ImageRequest[] => {
const imageTypes = selectImageTypes(state);
const snapshotDate = convertMMDDYYYYToYYYYMMDD(selectSnapshotDate(state));
const useLatest = selectUseLatest(state);
return imageTypes.map((type) => ({
architecture: selectArchitecture(state),
image_type: type,
@ -186,6 +203,7 @@ const getImageRequests = (state: RootState): ImageRequest[] => {
type: uploadTypeByTargetEnv(type),
options: getImageOptions(type, state),
},
snapshot_date: useLatest ? '' : snapshotDate,
}));
};

View file

@ -1,49 +0,0 @@
export const timestampToDisplayString = (ts) => {
// timestamp has format 2021-04-27T12:31:12Z
// must be converted to ms timestamp and then reformatted to Apr 27, 2021
if (!ts) {
return '';
}
// get YYYY-MM-DD format
const ms = Date.parse(ts);
const options = { month: 'short', day: 'numeric', year: 'numeric' };
const tsDisplay = new Intl.DateTimeFormat('en-US', options).format(ms);
return tsDisplay;
};
export const convertStringToDate = (createdAtAsString) => {
if (isNaN(Date.parse(createdAtAsString))) {
// converts property created_at of the image object from string to UTC
const [dateValues, timeValues] = createdAtAsString.split(' ');
const datetimeString = `${dateValues}T${timeValues}Z`;
return Date.parse(datetimeString);
} else {
return Date.parse(createdAtAsString);
}
};
export const computeHoursToExpiration = (imageCreatedAt) => {
if (imageCreatedAt) {
const currentTime = Date.now();
// miliseconds in hour - needed for calculating the difference
// between current date and the date of the image creation
const msInHour = 1000 * 60 * 60;
const timeUntilExpiration = Math.floor(
(currentTime - convertStringToDate(imageCreatedAt)) / msInHour
);
return timeUntilExpiration;
} else {
// when creating a new image, the compose.created_at can be undefined when first queued
return 0;
}
};
export const toMonthAndYear = (dateString) => {
const options = {
year: 'numeric',
month: 'long',
};
const date = new Date(dateString);
return date.toLocaleDateString('en-US', options);
};

85
src/Utilities/time.ts Normal file
View file

@ -0,0 +1,85 @@
export const parseMMDDYYYYtoDate = (val: string) => {
const [mm, dd, yyyy] = val.split('/');
const newVal = `${yyyy}-${mm}-${dd}`;
return mm && dd && yyyy ? new Date(`${newVal}T00:00:00`) : new Date('');
};
export const parseYYYYMMDDToDate = (val: string) =>
val ? new Date(`${val}T00:00:00`) : new Date('');
export const yyyyMMddFormat = (date: Date) =>
`${date.getFullYear()}-${(date.getMonth() + 1)
.toString()
.padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
export const convertMMDDYYYYToYYYYMMDD = (dateStr: string) => {
if (!dateStr) return '';
const date = parseMMDDYYYYtoDate(dateStr);
return yyyyMMddFormat(date);
};
export const dateToMMDDYYYY = (date: Date) =>
`${(date.getMonth() + 1).toString().padStart(2, '0')}/${date
.getDate()
.toString()
.padStart(2, '0')}/${date.getFullYear()}`;
export const convertYYYYMMDDTOMMDDYYYY = (dateStr: string) => {
if (!dateStr) return '';
const date = parseYYYYMMDDToDate(dateStr);
return dateToMMDDYYYY(date);
};
export const toMonthAndYear = (dateString: string) => {
const options: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'long',
};
const date = new Date(dateString);
return date.toLocaleDateString('en-US', options);
};
export const timestampToDisplayString = (ts?: string) => {
// timestamp has format 2021-04-27T12:31:12Z
// must be converted to ms timestamp and then reformatted to Apr 27, 2021
if (!ts) {
return '';
}
// get YYYY-MM-DD format
const ms = Date.parse(ts);
const options: Intl.DateTimeFormatOptions = {
month: 'short',
day: 'numeric',
year: 'numeric',
};
const tsDisplay = new Intl.DateTimeFormat('en-US', options).format(ms);
return tsDisplay;
};
export const convertStringToDate = (createdAtAsString: string = '') => {
if (isNaN(Date.parse(createdAtAsString))) {
// converts property created_at of the image object from string to UTC
const [dateValues, timeValues] = createdAtAsString.split(' ');
const datetimeString = `${dateValues}T${timeValues}Z`;
return Date.parse(datetimeString);
} else {
return Date.parse(createdAtAsString);
}
};
export const computeHoursToExpiration = (imageCreatedAt: string) => {
if (imageCreatedAt) {
const currentTime = Date.now();
// miliseconds in hour - needed for calculating the difference
// between current date and the date of the image creation
const msInHour = 1000 * 60 * 60;
const timeUntilExpiration = Math.floor(
(currentTime - convertStringToDate(imageCreatedAt)) / msInHour
);
return timeUntilExpiration;
} else {
// when creating a new image, the compose.created_at can be undefined when first queued
return 0;
}
};

View file

@ -1,6 +1,9 @@
import { emptyContentSourcesApi as api } from "./emptyContentSourcesApi";
const injectedRtkApi = api.injectEndpoints({
endpoints: (build) => ({
listFeatures: build.query<ListFeaturesApiResponse, ListFeaturesApiArg>({
query: () => ({ url: `/features/` }),
}),
listRepositories: build.query<
ListRepositoriesApiResponse,
ListRepositoriesApiArg
@ -56,10 +59,22 @@ const injectedRtkApi = api.injectEndpoints({
body: queryArg.apiContentUnitSearchRequest,
}),
}),
listSnapshotsByDate: build.mutation<
ListSnapshotsByDateApiResponse,
ListSnapshotsByDateApiArg
>({
query: (queryArg) => ({
url: `/snapshots/for_date/`,
method: "POST",
body: queryArg.apiListSnapshotByDateRequest,
}),
}),
}),
overrideExisting: false,
});
export { injectedRtkApi as contentSourcesApi };
export type ListFeaturesApiResponse = /** status 200 OK */ ApiFeatureSet;
export type ListFeaturesApiArg = void;
export type ListRepositoriesApiResponse =
/** status 200 OK */ ApiRepositoryCollectionResponseRead;
export type ListRepositoriesApiArg = {
@ -81,11 +96,11 @@ export type ListRepositoriesApiArg = {
name?: string;
/** A comma separated list of URLs to control api response. */
url?: string;
/** A comma separated list of uuids to control api response. */
/** A comma separated list of UUIDs to control api response. */
uuid?: string;
/** Sort the response data based on specific repository parameters. Sort criteria can include `name`, `url`, `status`, and `package_count`. */
sortBy?: string;
/** A comma separated list of statuses to control api response. Statuses can include `pending`, `valid`, `invalid`. */
/** A comma separated list of statuses to control api response. Statuses can include `pending`, `valid`, `invalid`, `unavailable`. */
status?: string;
/** A comma separated list of origins to filter api response. Origins can include `red_hat` and `external`. */
origin?: string;
@ -117,6 +132,21 @@ export type SearchRpmApiArg = {
/** request body */
apiContentUnitSearchRequest: ApiContentUnitSearchRequest;
};
export type ListSnapshotsByDateApiResponse =
/** status 200 OK */ ApiListSnapshotByDateResponse;
export type ListSnapshotsByDateApiArg = {
/** request body */
apiListSnapshotByDateRequest: ApiListSnapshotByDateRequest;
};
export type ApiFeature = {
/** Whether the current user can access the feature */
accessible?: boolean;
/** Whether the feature is enabled on the running server */
enabled?: boolean;
};
export type ApiFeatureSet = {
[key: string]: ApiFeature;
};
export type ApiSnapshotResponse = {
/** Count of each content type */
added_counts?: {
@ -138,6 +168,26 @@ export type ApiSnapshotResponse = {
url?: string;
uuid?: string;
};
export type ApiTaskInfoResponse = {
/** Timestamp of task creation */
created_at?: string;
/** Timestamp task ended running at */
ended_at?: string;
/** Error thrown while running task */
error?: string;
/** Organization ID of the owner */
org_id?: string;
/** Name of the associated repository */
repository_name?: string;
/** UUID of the associated repository */
repository_uuid?: string;
/** Status of task (running, failed, completed, canceled, pending) */
status?: string;
/** Type of task */
type?: string;
/** UUID of the object */
uuid?: string;
};
export type ApiRepositoryResponse = {
/** Content Type (rpm) of the repository */
content_type?: string;
@ -149,11 +199,16 @@ export type ApiRepositoryResponse = {
failed_introspections_count?: number;
/** GPG key for repository */
gpg_key?: string;
/** Label used to configure the yum repository on clients */
label?: string;
/** Error of last attempted introspection */
last_introspection_error?: string;
/** Status of last introspection */
last_introspection_status?: string;
/** Timestamp of last attempted introspection */
last_introspection_time?: string;
last_snapshot?: ApiSnapshotResponse;
last_snapshot_task?: ApiTaskInfoResponse;
/** UUID of the last snapshot task */
last_snapshot_task_uuid?: string;
/** UUID of the last dao.Snapshot */
@ -174,7 +229,7 @@ export type ApiRepositoryResponse = {
package_count?: number;
/** Enable snapshotting and hosting of this repository */
snapshot?: boolean;
/** Status of repository introspection (Valid, Invalid, Unavailable, Pending) */
/** Combined status of last introspection and snapshot of repository (Valid, Invalid, Unavailable, Pending) */
status?: string;
/** URL of the remote yum repository */
url?: string;
@ -192,11 +247,16 @@ export type ApiRepositoryResponseRead = {
failed_introspections_count?: number;
/** GPG key for repository */
gpg_key?: string;
/** Label used to configure the yum repository on clients */
label?: string;
/** Error of last attempted introspection */
last_introspection_error?: string;
/** Status of last introspection */
last_introspection_status?: string;
/** Timestamp of last attempted introspection */
last_introspection_time?: string;
last_snapshot?: ApiSnapshotResponse;
last_snapshot_task?: ApiTaskInfoResponse;
/** UUID of the last snapshot task */
last_snapshot_task_uuid?: string;
/** UUID of the last dao.Snapshot */
@ -219,7 +279,7 @@ export type ApiRepositoryResponseRead = {
package_count?: number;
/** Enable snapshotting and hosting of this repository */
snapshot?: boolean;
/** Status of repository introspection (Valid, Invalid, Unavailable, Pending) */
/** Combined status of last introspection and snapshot of repository (Valid, Invalid, Unavailable, Pending) */
status?: string;
/** URL of the remote yum repository */
url?: string;
@ -286,7 +346,7 @@ export type ApiRepositoryRequest = {
url?: string;
};
export type ApiRepositoryRpm = {
/** The Architecture of the rpm */
/** The architecture of the rpm */
arch?: string;
/** The checksum of the rpm */
checksum?: string;
@ -322,12 +382,31 @@ export type ApiContentUnitSearchRequest = {
search?: string;
/** URLs of repositories to search */
urls?: string[];
/** List of RepositoryConfig UUIDs to search */
/** List of repository UUIDs to search */
uuids?: string[];
};
export type ApiSnapshotForDate = {
/** Is the snapshot after the specified date */
is_after?: boolean;
match?: ApiSnapshotResponse;
/** Repository uuid for associated snapshot */
repository_uuid?: string;
};
export type ApiListSnapshotByDateResponse = {
/** Requested Data */
data?: ApiSnapshotForDate[];
};
export type ApiListSnapshotByDateRequest = {
/** Exact date to search by. */
date?: string;
/** Repository UUIDs to find snapshots for */
repository_uuids?: string[];
};
export const {
useListFeaturesQuery,
useListRepositoriesQuery,
useCreateRepositoryMutation,
useListRepositoriesRpmsQuery,
useSearchRpmMutation,
useListSnapshotsByDateMutation,
} = injectedRtkApi;

View file

@ -77,6 +77,10 @@ export type wizardState = {
partitions: Partition[];
isNextButtonTouched: boolean;
};
snapshotting: {
useLatest: boolean;
snapshotDate: string;
};
repositories: {
customRepositories: CustomRepository[];
payloadRepositories: Repository[];
@ -136,6 +140,10 @@ const initialState: wizardState = {
partitions: [],
isNextButtonTouched: true,
},
snapshotting: {
useLatest: true,
snapshotDate: '',
},
repositories: {
customRepositories: [],
payloadRepositories: [],
@ -241,6 +249,13 @@ export const selectPartitions = (state: RootState) => {
return state.wizard.fileSystem.partitions;
};
export const selectUseLatest = (state: RootState) => {
return state.wizard.snapshotting.useLatest;
};
export const selectSnapshotDate = (state: RootState) => {
return state.wizard.snapshotting.snapshotDate;
};
export const selectCustomRepositories = (state: RootState) => {
return state.wizard.repositories.customRepositories;
};
@ -505,6 +520,12 @@ export const wizardSlice = createSlice({
state.fileSystem.partitions[partitionIndex].min_size = min_size;
}
},
changeUseLatest: (state, action: PayloadAction<boolean>) => {
state.snapshotting.useLatest = action.payload;
},
changeSnapshotDate: (state, action: PayloadAction<string>) => {
state.snapshotting.snapshotDate = action.payload;
},
changeCustomRepositories: (
state,
action: PayloadAction<CustomRepository[]>
@ -624,6 +645,8 @@ export const {
changePartitionUnit,
changePartitionMinSize,
changePartitionOrder,
changeUseLatest,
changeSnapshotDate,
changeCustomRepositories,
changePayloadRepositories,
addRecommendedRepository,

View file

@ -106,6 +106,7 @@ describe('Step Compliance', () => {
})
).not.toBeInTheDocument();
await clickNext(); // skip RepositorySnapshot
await clickNext(); // skip Repositories
// check that there are no Packages contained when selecting the "None" profile option
@ -172,6 +173,7 @@ describe('Step Compliance', () => {
await screen.findByRole('heading', { name: /File system configuration/i });
await screen.findByText(/tmp/i);
await clickNext(); // skip RepositorySnapshots
await clickNext(); // skip Repositories
// check that the Packages contains correct packages

View file

@ -155,6 +155,8 @@ describe('Step Packages', () => {
await clickNext();
// skip OpenSCAP
await clickNext();
// skip snapshots
await clickNext();
// skip Repositories
await clickNext();
// skip fsc
@ -431,6 +433,8 @@ describe('Step Custom repositories', () => {
await clickNext();
// skip fsc
await clickNext();
// skip snapshots
await clickNext();
};
test('selected repositories stored in and retrieved from form state', async () => {

View file

@ -410,6 +410,7 @@ describe('Step Upload to AWS', () => {
await clickNext();
await clickNext();
await clickNext();
await clickNext();
await enterBlueprintName();
await clickNext();
@ -602,6 +603,7 @@ describe('Step Registration', () => {
await clickNext();
await clickNext();
await clickNext();
await clickNext();
await enterBlueprintName();
await clickNext();
const review = await screen.findByTestId('review-registration');
@ -648,6 +650,7 @@ describe('Step Registration', () => {
await clickNext();
await clickNext();
await clickNext();
await clickNext();
await enterBlueprintName();
await clickNext();
const review = await screen.findByTestId('review-registration');
@ -695,6 +698,7 @@ describe('Step Registration', () => {
await clickNext();
await clickNext();
await clickNext();
await clickNext();
await enterBlueprintName();
await clickNext();
const review = await screen.findByTestId('review-registration');
@ -725,6 +729,7 @@ describe('Step Registration', () => {
await clickNext();
await clickNext();
await clickNext();
await clickNext();
await enterBlueprintName();
await clickNext();
await screen.findByText('Register the system later');
@ -858,6 +863,8 @@ describe('Step Details', () => {
await clickNext();
// skip fsc
await clickNext();
// skip snapshots
await clickNext();
};
test('image name invalid for more than 63 chars', async () => {
@ -932,6 +939,8 @@ describe('Step Review', () => {
await clickNext();
// skip OpenScap
await clickNext();
// skip snpashotstep
await clickNext();
// skip repositories
await clickNext();
// skip packages
@ -993,7 +1002,8 @@ describe('Step Review', () => {
// skip Oscap
await clickNext();
// skip snpashotstep
await clickNext();
// skip packages
await clickNext();
// skip repositories

View file

@ -38,6 +38,7 @@ const goToDetailsStep = async () => {
await clickNext();
await clickNext();
await clickNext();
await clickNext();
};
const enterBlueprintDescription = async () => {

View file

@ -97,6 +97,7 @@ const goToReviewStep = async () => {
await clickNext();
await clickNext();
await clickNext();
await clickNext();
await enterBlueprintName();
await clickNext();
};

View file

@ -57,6 +57,7 @@ const clickToReview = async () => {
await clickNext(); // skip Registration
await clickNext(); // skip OSCAP
await clickNext(); // skip FSC
await clickNext(); // skip SnapshotRepositories
await clickNext(); // skip Repositories
await clickNext(); // skip Packages
const nameInput = await screen.findByRole('textbox', {

View file

@ -81,6 +81,7 @@ const selectNone = async () => {
const goToReviewStep = async () => {
await clickNext(); // File system configuration
await clickNext(); // Snapshot repositories
await clickNext(); // Custom repositories
await clickNext(); // Additional packages
await clickNext(); // Details

View file

@ -47,6 +47,7 @@ const goToPackagesStep = async () => {
await clickRegisterLater();
await clickNext(); // OpenSCAP
await clickNext(); // File System
await clickNext(); // Snapshots
await clickNext(); // Custom repositories
await clickNext(); // Additional packages
};

View file

@ -70,6 +70,7 @@ const goToReviewStep = async () => {
await clickNext();
await clickNext();
await clickNext();
await clickNext();
await enterBlueprintName();
await clickNext();
};
@ -78,6 +79,7 @@ describe('registration request generated correctly', () => {
const imageRequest: ImageRequest = {
architecture: 'x86_64',
image_type: 'image-installer',
snapshot_date: '',
upload_request: {
options: {},
type: 'aws.s3',

View file

@ -44,6 +44,7 @@ const goToRepositoriesStep = async () => {
await clickRegisterLater();
await clickNext(); // OpenSCAP
await clickNext(); // File System
await clickNext(); // Snapshot
await clickNext(); // Custom repositories
};

View file

@ -0,0 +1,160 @@
import { screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { CREATE_BLUEPRINT } from '../../../../../constants';
import {
CreateBlueprintRequest,
CustomRepository,
Repository,
} from '../../../../../store/imageBuilderApi';
import { clickNext } from '../../../../testUtils';
import {
blueprintRequest,
clickRegisterLater,
enterBlueprintName,
interceptBlueprintRequest,
render,
} from '../../wizardTestUtils';
jest.mock('@redhat-cloud-services/frontend-components/useChrome', () => ({
useChrome: () => ({
auth: {
getUser: () => {
return {
identity: {
internal: {
org_id: 5,
},
},
};
},
},
isBeta: () => true,
isProd: () => true,
getEnvironment: () => 'prod',
}),
}));
const goToSnapshotStep = async () => {
const bareMetalCheckBox = await screen.findByRole('checkbox', {
name: /bare metal installer checkbox/i,
});
await userEvent.click(bareMetalCheckBox);
await clickNext(); // Registration
await clickRegisterLater();
await clickNext(); // OpenSCAP
await clickNext(); // File System
await clickNext();
};
const goToReviewStep = async () => {
await clickNext(); // Repositories step
await clickNext(); // Additional packages
await clickNext();
await clickNext(); // Details
await enterBlueprintName();
await clickNext();
};
const selectFirstRepository = async () => {
await userEvent.click(
await screen.findByRole('checkbox', { name: /select row 0/i })
);
};
const selectUseSnapshot = async () => {
await userEvent.click(
await screen.findByRole('radio', { name: /Use a snapshot/i })
);
};
const updateDatePickerWithValue = async (date: string) => {
await userEvent.type(
await screen.findByRole('textbox', { name: /Date picker/i }),
date
);
};
const clickContentDropdown = async () => {
await userEvent.click(
(
await screen.findAllByRole('button', { name: /Content/i })
)[1]
);
};
const getSnapshotMethodElement = async () =>
await screen.findByRole('button', { name: /Snapshot method/i });
describe('repository snapshot tab - ', () => {
const expectedPayloadRepositories: Repository[] = [
{
baseurl: 'http://valid.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,
},
];
const expectedCustomRepositories: CustomRepository[] = [
{
baseurl: ['http://valid.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: 'ae39f556-6986-478a-95d1-f9c7e33d066c',
name: '01-test-valid-repo',
},
];
test('select use a snapshot with 1 repo selected', async () => {
await render();
await goToSnapshotStep();
await selectUseSnapshot();
await updateDatePickerWithValue('04/22/2024');
await clickNext(); // To repositories step
await selectFirstRepository();
await goToReviewStep();
await clickContentDropdown();
const snapshotMethodElement = await getSnapshotMethodElement();
// Check date was recorded correctly
expect(snapshotMethodElement).toHaveTextContent('State as of 04/22/2024');
// Check that the button is clickable (has 1 repo selected)
expect(snapshotMethodElement).toHaveAttribute('aria-disabled', 'false');
// Check the date was passed correctly to the blueprint
const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);
blueprintRequest.image_requests[0].snapshot_date = '2024-04-22';
const expectedRequest: CreateBlueprintRequest = {
...blueprintRequest,
customizations: {
custom_repositories: expectedCustomRepositories,
payload_repositories: expectedPayloadRepositories,
},
};
expect(receivedRequest).toEqual(expectedRequest);
});
test('select use a snapshot with no repos selected', async () => {
await render();
await goToSnapshotStep();
await selectUseSnapshot();
await updateDatePickerWithValue('04/22/2024');
await clickNext(); // To repositories step
await goToReviewStep();
await clickContentDropdown();
const snapshotMethodElement = await getSnapshotMethodElement();
// Check date was recorded correctly
expect(snapshotMethodElement).toHaveTextContent('No repositories selected');
// Check that the button is clickable (has 1 repo selected)
expect(snapshotMethodElement).toHaveAttribute('aria-disabled', 'true');
});
});

View file

@ -43,6 +43,7 @@ const goToReview = async () => {
await clickRegisterLater();
await clickNext(); // OpenSCAP
await clickNext(); // File system customization
await clickNext(); // Snapshot repositories
await clickNext(); // Custom repositories
await clickNext(); // Additional packages
await clickNext(); // Details
@ -93,6 +94,7 @@ describe('aws image type request generated correctly', () => {
const expectedImageRequest: ImageRequest = {
architecture: 'x86_64',
image_type: 'aws',
snapshot_date: '',
upload_request: {
options: {
share_with_sources: ['123'],
@ -119,6 +121,7 @@ describe('aws image type request generated correctly', () => {
const expectedImageRequest: ImageRequest = {
architecture: 'x86_64',
image_type: 'aws',
snapshot_date: '',
upload_request: {
options: {
share_with_accounts: ['123123123123'],

View file

@ -43,6 +43,7 @@ const goToReview = async () => {
await clickRegisterLater();
await clickNext(); // OpenSCAP
await clickNext(); // File system customization
await clickNext(); // Snapshot repositories
await clickNext(); // Custom repositories
await clickNext(); // Additional packages
await clickNext(); // Details
@ -120,6 +121,7 @@ describe('azure image type request generated correctly', () => {
const expectedImageRequest: ImageRequest = {
architecture: 'x86_64',
image_type: 'azure',
snapshot_date: '',
upload_request: {
options: {
source_id: '666',
@ -150,6 +152,7 @@ describe('azure image type request generated correctly', () => {
const expectedImageRequest: ImageRequest = {
architecture: 'x86_64',
image_type: 'azure',
snapshot_date: '',
upload_request: {
type: 'azure',
options: {

View file

@ -44,6 +44,7 @@ const goToReview = async () => {
await clickRegisterLater();
await clickNext(); // OpenSCAP
await clickNext(); // File system customization
await clickNext(); // Snapshot repositories
await clickNext(); // Custom repositories
await clickNext(); // Additional packages
await clickNext(); // Details

View file

@ -120,6 +120,7 @@ const goToReviewStep = async () => {
await clickRegisterLater();
await clickNext(); // OpenSCAP
await clickNext(); // File system customization
await clickNext(); // Snapshots
await clickNext(); // Custom repositories
await clickNext(); // Additional packages
await clickNext(); // Details

View file

@ -41,6 +41,7 @@ const routes = [
export const imageRequest: ImageRequest = {
architecture: 'x86_64',
image_type: 'image-installer',
snapshot_date: '',
upload_request: {
options: {},
type: 'aws.s3',

View file

@ -144,6 +144,7 @@ export const mockBlueprintComposes: GetBlueprintComposesApiResponse = {
{
architecture: 'x86_64',
image_type: 'aws',
snapshot_date: '',
upload_request: {
type: 'aws',
options: {
@ -165,6 +166,7 @@ export const mockBlueprintComposes: GetBlueprintComposesApiResponse = {
{
architecture: 'x86_64',
image_type: 'aws',
snapshot_date: '',
upload_request: {
type: 'aws',
options: {
@ -186,6 +188,7 @@ export const mockBlueprintComposes: GetBlueprintComposesApiResponse = {
{
architecture: 'x86_64',
image_type: 'gcp',
snapshot_date: '',
upload_request: {
type: 'gcp',
options: {

16
src/test/fixtures/features.ts vendored Normal file
View file

@ -0,0 +1,16 @@
import { ListFeaturesApiResponse } from '../../store/contentSourcesApi';
export const mockedFeatureResponse: ListFeaturesApiResponse = {
admintasks: {
enabled: true,
accessible: false,
},
newrepositoryfiltering: {
enabled: false,
accessible: false,
},
snapshots: {
enabled: true,
accessible: true,
},
};

74
src/test/fixtures/snapshots.ts vendored Normal file
View file

@ -0,0 +1,74 @@
export const mockSourcesPackagesResults = {
data: [
{
repository_uuid: '893dbe59-c473-4933-b23c-be6ae806e48d',
is_after: false,
match: {
uuid: '8e1165f2-95f6-4046-a593-6858b34306a3',
created_at: '2024-04-10T15:43:29.95512Z',
repository_path:
'00db8641/893dbe59-c473-4933-b23c-be6ae806e48d/d22d7840-ef14-4420-bccf-408de7949ff1',
content_counts: {
'rpm.advisory': 2,
'rpm.package': 8,
'rpm.packagecategory': 1,
'rpm.packagegroup': 2,
},
added_counts: {
'rpm.advisory': 2,
'rpm.package': 8,
'rpm.packagecategory': 1,
'rpm.packagegroup': 2,
},
removed_counts: {},
url: '',
},
},
{
repository_uuid: '50467940-7d8e-4eee-a3bd-f8a2556c406c',
is_after: false,
match: {
uuid: 'cc0e882b-e988-4b9a-844a-54d47cb5a9a9',
created_at: '2024-04-10T15:50:59.368233Z',
repository_path:
'00db8641/50467940-7d8e-4eee-a3bd-f8a2556c406c/a1f85ae7-22ac-4e28-8ae3-f8cb249fa5b7',
content_counts: {
'rpm.advisory': 2,
'rpm.package': 8,
},
added_counts: {
'rpm.advisory': 2,
'rpm.package': 8,
},
removed_counts: {},
url: '',
},
},
{
repository_uuid: 'b805848d-1918-4861-9f63-c72f83cbd018',
is_after: false,
match: {
uuid: 'e65b258d-d571-4411-8955-0214fd396aa0',
created_at: '2024-04-17T00:42:07.045744Z',
repository_path:
'00db8641/b805848d-1918-4861-9f63-c72f83cbd018/78335df8-d5e6-4d65-a32f-80a09e6fc17d',
content_counts: {
'rpm.advisory': 4565,
'rpm.package': 20851,
'rpm.packagecategory': 1,
'rpm.packageenvironment': 1,
'rpm.packagegroup': 21,
},
added_counts: {
'rpm.advisory': 4565,
'rpm.package': 20851,
'rpm.packagecategory': 1,
'rpm.packageenvironment': 1,
'rpm.packagegroup': 21,
},
removed_counts: {},
url: '',
},
},
],
};

View file

@ -1,4 +1,6 @@
import 'whatwg-fetch';
//Needed for correct jest extends types
import '@testing-library/jest-dom';
import failOnConsole from 'jest-fail-on-console';
import { server } from './mocks/server';

View file

@ -25,6 +25,7 @@ import {
mockCloneStatus,
mockStatus,
} from '../fixtures/composes';
import { mockedFeatureResponse } from '../fixtures/features';
import {
distributionOscapProfiles,
oscapCustomizations,
@ -60,6 +61,15 @@ export const handlers = [
const { search } = await req.json();
return res(ctx.status(200), ctx.json(mockSourcesPackagesResults(search)));
}),
rest.get(`${CONTENT_SOURCES_API}/features/`, async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(mockedFeatureResponse));
}),
rest.post(
`${CONTENT_SOURCES_API}/snapshots/for_date/`,
async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(mockSourcesPackagesResults));
}
),
rest.get(`${IMAGE_BUILDER_API}/packages`, (req, res, ctx) => {
const search = req.url.searchParams.get('search');
return res(ctx.status(200), ctx.json(mockPackagesResults(search)));