V2Wizard: Implement <Aws> subset of Image Output step
DDF has been dropped, RTK being used for state management now.
This commit is contained in:
parent
e5c55828e3
commit
091c34431e
8 changed files with 430 additions and 106 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
|
|
@ -10,10 +10,17 @@ import {
|
|||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import ImageOutputStep from './steps/ImageOutput';
|
||||
import Aws, { AwsShareMethod } from './steps/TargetEnvironment/Aws';
|
||||
import { isAwsAccountIdValid } from './validators';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '../../store/hooks';
|
||||
import './CreateImageWizard.scss';
|
||||
import { initializeWizard, selectImageTypes } from '../../store/wizardSlice';
|
||||
import {
|
||||
initializeWizard,
|
||||
selectAwsAccount,
|
||||
selectAwsSource,
|
||||
selectImageTypes,
|
||||
} from '../../store/wizardSlice';
|
||||
import { resolveRelPath } from '../../Utilities/path';
|
||||
import { ImageBuilderHeader } from '../sharedComponents/ImageBuilderHeader';
|
||||
|
||||
|
|
@ -49,6 +56,11 @@ const CreateImageWizard = () => {
|
|||
|
||||
const targetEnvironments = useAppSelector((state) => selectImageTypes(state));
|
||||
|
||||
const [awsShareMethod, setAwsShareMethod] =
|
||||
useState<AwsShareMethod>('sources');
|
||||
const awsAccountId = useAppSelector((state) => selectAwsAccount(state));
|
||||
const awsSourceId = useAppSelector((state) => selectAwsSource(state));
|
||||
|
||||
return (
|
||||
<>
|
||||
<ImageBuilderHeader />
|
||||
|
|
@ -68,15 +80,32 @@ const CreateImageWizard = () => {
|
|||
<WizardStep
|
||||
name="Target Environment"
|
||||
id="step-target-environment"
|
||||
isHidden={
|
||||
!targetEnvironments.find(
|
||||
(target) =>
|
||||
target === 'aws' || target === 'gcp' || target === 'azure'
|
||||
)
|
||||
}
|
||||
steps={[
|
||||
<WizardStep
|
||||
name="Amazon Web Services"
|
||||
id="wizard-target-aws"
|
||||
key="wizard-target-aws"
|
||||
footer={<CustomWizardFooter disableNext={true} />}
|
||||
footer={
|
||||
<CustomWizardFooter
|
||||
disableNext={
|
||||
awsShareMethod === 'manual'
|
||||
? !isAwsAccountIdValid(awsAccountId)
|
||||
: awsSourceId === undefined
|
||||
}
|
||||
/>
|
||||
}
|
||||
isHidden={!targetEnvironments.includes('aws')}
|
||||
>
|
||||
{/* <Aws /> */}
|
||||
<Aws
|
||||
shareMethod={awsShareMethod}
|
||||
setShareMethod={setAwsShareMethod}
|
||||
/>
|
||||
</WizardStep>,
|
||||
]}
|
||||
/>
|
||||
|
|
|
|||
57
src/Components/CreateImageWizardV2/ValidatedTextInput.tsx
Normal file
57
src/Components/CreateImageWizardV2/ValidatedTextInput.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
HelperText,
|
||||
HelperTextItem,
|
||||
TextInput,
|
||||
TextInputProps,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
interface ValidatedTextInputPropTypes extends TextInputProps {
|
||||
ariaLabel: string | undefined;
|
||||
helperText: string | undefined;
|
||||
validator: (value: string | undefined) => Boolean;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const ValidatedTextInput = ({
|
||||
ariaLabel,
|
||||
helperText,
|
||||
validator,
|
||||
value,
|
||||
onChange,
|
||||
}: ValidatedTextInputPropTypes) => {
|
||||
const [isPristine, setIsPristine] = useState(!value ? true : false);
|
||||
|
||||
const handleBlur = () => {
|
||||
setIsPristine(false);
|
||||
};
|
||||
|
||||
const handleValidation = () => {
|
||||
// Prevent premature validation during user's first entry
|
||||
if (isPristine) {
|
||||
return 'default';
|
||||
}
|
||||
return validator(value) ? 'success' : 'error';
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextInput
|
||||
value={value}
|
||||
type="text"
|
||||
onChange={onChange}
|
||||
validated={handleValidation()}
|
||||
aria-label={ariaLabel}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
{!isPristine && !validator(value) && (
|
||||
<HelperText>
|
||||
<HelperTextItem variant="error" hasIcon>
|
||||
{helperText}
|
||||
</HelperTextItem>
|
||||
</HelperText>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import React from 'react';
|
||||
|
||||
import {
|
||||
Alert,
|
||||
HelperText,
|
||||
HelperTextItem,
|
||||
TextInput,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import { useGetSourceUploadInfoQuery } from '../../../../../store/provisioningApi';
|
||||
|
||||
import { V1ListSourceResponseItem } from '.';
|
||||
|
||||
type AwsAccountIdProps = {
|
||||
source: V1ListSourceResponseItem | undefined;
|
||||
};
|
||||
|
||||
export const AwsAccountId = ({ source }: AwsAccountIdProps) => {
|
||||
const { data, isError } = useGetSourceUploadInfoQuery(
|
||||
{
|
||||
id: parseInt(source?.id as string),
|
||||
},
|
||||
{ skip: source === undefined }
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextInput
|
||||
readOnlyVariant="default"
|
||||
isRequired
|
||||
id="aws-account-id"
|
||||
value={data ? data.aws?.account_id : ''}
|
||||
aria-label="aws account id"
|
||||
/>
|
||||
<HelperText>
|
||||
<HelperTextItem component="div" variant="indeterminate">
|
||||
This is the account associated with the source.
|
||||
</HelperTextItem>
|
||||
</HelperText>
|
||||
{isError && (
|
||||
<Alert
|
||||
variant={'danger'}
|
||||
isPlain
|
||||
isInline
|
||||
title={'AWS details unavailable'}
|
||||
>
|
||||
The AWS account ID for the selected source could not be resolved.
|
||||
There might be a problem with the source. Verify that the source is
|
||||
valid in Sources or select a different source.
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,82 +1,56 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { FormSpy } from '@data-driven-forms/react-form-renderer';
|
||||
import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
|
||||
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
|
||||
import { Alert } from '@patternfly/react-core';
|
||||
import { FormGroup, Spinner } from '@patternfly/react-core';
|
||||
import { Alert, Spinner } from '@patternfly/react-core';
|
||||
import { FormGroup } from '@patternfly/react-core';
|
||||
import {
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
} from '@patternfly/react-core/deprecated';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { extractProvisioningList } from '../../../store/helpers';
|
||||
import { useAppDispatch } from '../../../../../store/hooks';
|
||||
import { useGetSourceListQuery } from '../../../../../store/provisioningApi';
|
||||
import {
|
||||
useGetSourceListQuery,
|
||||
useGetSourceUploadInfoQuery,
|
||||
} from '../../../store/provisioningApi';
|
||||
changeAwsSource,
|
||||
resetAwsSource,
|
||||
} from '../../../../../store/wizardSlice';
|
||||
|
||||
export const AWSSourcesSelect = ({
|
||||
label,
|
||||
isRequired,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
const { change, getState } = useFormApi();
|
||||
const { input } = useFieldApi(props);
|
||||
import { V1ListSourceResponseItem } from '.';
|
||||
|
||||
type AwsSourcesSelectPropTypes = {
|
||||
source: V1ListSourceResponseItem | undefined;
|
||||
setSource: React.Dispatch<
|
||||
React.SetStateAction<V1ListSourceResponseItem | undefined>
|
||||
>;
|
||||
};
|
||||
|
||||
export const AwsSourcesSelect = ({
|
||||
source,
|
||||
setSource,
|
||||
}: AwsSourcesSelectPropTypes) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedSourceId, setSelectedSourceId] = useState(
|
||||
getState()?.values?.['aws-sources-select']
|
||||
);
|
||||
|
||||
const {
|
||||
data: rawSources,
|
||||
isFetching,
|
||||
isSuccess,
|
||||
isError,
|
||||
refetch,
|
||||
} = useGetSourceListQuery({ provider: 'aws' });
|
||||
const sources = extractProvisioningList(rawSources);
|
||||
const { data, isFetching, isLoading, isSuccess, isError, refetch } =
|
||||
useGetSourceListQuery({
|
||||
provider: 'aws',
|
||||
});
|
||||
|
||||
const {
|
||||
data: sourceDetails,
|
||||
isFetching: isFetchingDetails,
|
||||
isSuccess: isSuccessDetails,
|
||||
isError: isErrorDetails,
|
||||
} = useGetSourceUploadInfoQuery(
|
||||
{ id: selectedSourceId },
|
||||
{
|
||||
skip: !selectedSourceId,
|
||||
}
|
||||
);
|
||||
const sources = data?.data;
|
||||
|
||||
useEffect(() => {
|
||||
if (isFetchingDetails || !isSuccessDetails) return;
|
||||
change('aws-associated-account-id', sourceDetails?.aws?.account_id);
|
||||
}, [isFetchingDetails, isSuccessDetails]);
|
||||
|
||||
const onFormChange = ({ values }) => {
|
||||
if (
|
||||
values['aws-target-type'] !== 'aws-target-type-source' ||
|
||||
values[input.name] === undefined
|
||||
) {
|
||||
change(input.name, undefined);
|
||||
change('aws-associated-account-id', undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect = (_, sourceName) => {
|
||||
const sourceId = sources.find((source) => source.name === sourceName).id;
|
||||
setSelectedSourceId(sourceId);
|
||||
const handleSelect = (
|
||||
_event: React.MouseEvent<Element, MouseEvent>,
|
||||
value: string
|
||||
) => {
|
||||
const source = sources?.find((source) => source.name === value);
|
||||
setSource(source);
|
||||
source?.id && dispatch(changeAwsSource(source.id));
|
||||
setIsOpen(false);
|
||||
change(input.name, sourceId);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setSelectedSourceId();
|
||||
change(input.name, undefined);
|
||||
dispatch(resetAwsSource());
|
||||
setSource(undefined);
|
||||
};
|
||||
|
||||
const handleToggle = () => {
|
||||
|
|
@ -88,40 +62,36 @@ export const AWSSourcesSelect = ({
|
|||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
const selectOptions = sources?.map((source) => (
|
||||
<SelectOption key={source.id} value={source.name} />
|
||||
));
|
||||
|
||||
const loadingSpinner = (
|
||||
<SelectOption key={'fetching'} isNoResultsOption={true}>
|
||||
<Spinner size="lg" />
|
||||
</SelectOption>
|
||||
);
|
||||
|
||||
if (isFetching) {
|
||||
selectOptions?.push(loadingSpinner);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormSpy subscription={{ values: true }} onChange={onFormChange} />
|
||||
<FormGroup
|
||||
isRequired={isRequired}
|
||||
label={label}
|
||||
data-testid="sources"
|
||||
className={className}
|
||||
>
|
||||
<FormGroup isRequired label={'Source Name'} data-testid="sources">
|
||||
<Select
|
||||
ouiaId="source_select"
|
||||
variant={SelectVariant.typeahead}
|
||||
onToggle={handleToggle}
|
||||
onSelect={handleSelect}
|
||||
onClear={handleClear}
|
||||
selections={
|
||||
selectedSourceId
|
||||
? sources.find((source) => source.id === selectedSourceId)?.name
|
||||
: undefined
|
||||
}
|
||||
selections={source?.name}
|
||||
isOpen={isOpen}
|
||||
placeholderText="Select source"
|
||||
typeAheadAriaLabel="Select source"
|
||||
isDisabled={!isSuccess}
|
||||
isDisabled={!isSuccess || isLoading}
|
||||
>
|
||||
{isSuccess &&
|
||||
sources.map((source) => (
|
||||
<SelectOption key={source.id} value={source.name} />
|
||||
))}
|
||||
{isFetching && (
|
||||
<SelectOption isNoResultsOption={true}>
|
||||
<Spinner size="lg" />
|
||||
</SelectOption>
|
||||
)}
|
||||
{selectOptions}
|
||||
</Select>
|
||||
</FormGroup>
|
||||
<>
|
||||
|
|
@ -136,25 +106,7 @@ export const AWSSourcesSelect = ({
|
|||
ID manually.
|
||||
</Alert>
|
||||
)}
|
||||
{!isError && isErrorDetails && (
|
||||
<Alert
|
||||
variant={'danger'}
|
||||
isPlain
|
||||
isInline
|
||||
title={'AWS details unavailable'}
|
||||
>
|
||||
The AWS account ID for the selected source could not be resolved.
|
||||
There might be a problem with the source. Verify that the source is
|
||||
valid in Sources or select a different source.
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
AWSSourcesSelect.propTypes = {
|
||||
className: PropTypes.string,
|
||||
label: PropTypes.node,
|
||||
isRequired: PropTypes.bool,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,157 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
Radio,
|
||||
Text,
|
||||
Form,
|
||||
Title,
|
||||
FormGroup,
|
||||
TextInput,
|
||||
Gallery,
|
||||
GalleryItem,
|
||||
HelperText,
|
||||
HelperTextItem,
|
||||
Button,
|
||||
} from '@patternfly/react-core';
|
||||
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
|
||||
|
||||
import { AwsAccountId } from './AwsAccountId';
|
||||
import { AwsSourcesSelect } from './AwsSourcesSelect';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
|
||||
import {
|
||||
changeAwsAccount,
|
||||
resetAws,
|
||||
selectAwsAccount,
|
||||
} from '../../../../../store/wizardSlice';
|
||||
import { ValidatedTextInput } from '../../../ValidatedTextInput';
|
||||
import { isAwsAccountIdValid } from '../../../validators';
|
||||
|
||||
export type AwsShareMethod = 'manual' | 'sources';
|
||||
|
||||
// The Sources API only defines a V1ListSourceResponseItem[] type
|
||||
export type V1ListSourceResponseItem = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
source_type_id?: string;
|
||||
uid?: string;
|
||||
};
|
||||
|
||||
const SourcesButton = () => {
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
target="_blank"
|
||||
variant="link"
|
||||
icon={<ExternalLinkAltIcon />}
|
||||
iconPosition="right"
|
||||
isInline
|
||||
href={'settings/sources'}
|
||||
>
|
||||
Create and manage sources here
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
type AwsPropTypes = {
|
||||
shareMethod: AwsShareMethod;
|
||||
setShareMethod: React.Dispatch<React.SetStateAction<AwsShareMethod>>;
|
||||
};
|
||||
|
||||
const Aws = ({ shareMethod, setShareMethod }: AwsPropTypes) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [source, setSource] = useState<V1ListSourceResponseItem | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const shareWithAccount = useAppSelector((state) => selectAwsAccount(state));
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<Title headingLevel="h2">Target environment - Amazon Web Services</Title>
|
||||
<Text>
|
||||
Your image will be uploaded to AWS and shared with the account you
|
||||
provide below.
|
||||
</Text>
|
||||
<Text>
|
||||
<b>The shared image will expire within 14 days.</b> To permanently
|
||||
access the image, copy the image, which will be shared to your account
|
||||
by Red Hat, to your own AWS account.
|
||||
</Text>
|
||||
<FormGroup label="Share method:">
|
||||
<Radio
|
||||
id="radio-with-description"
|
||||
label="Use an account configured from Sources."
|
||||
name="radio-7"
|
||||
description="Use a configured sources to launch environments directly from the console."
|
||||
isChecked={shareMethod === 'sources'}
|
||||
onChange={() => {
|
||||
dispatch(resetAws());
|
||||
setSource(undefined);
|
||||
setShareMethod('sources');
|
||||
}}
|
||||
/>
|
||||
<Radio
|
||||
id="radio"
|
||||
label="Manually enter an account ID."
|
||||
name="radio-8"
|
||||
isChecked={shareMethod === 'manual'}
|
||||
onChange={() => {
|
||||
dispatch(resetAws());
|
||||
setSource(undefined);
|
||||
setShareMethod('manual');
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
{shareMethod === 'sources' && (
|
||||
<>
|
||||
<AwsSourcesSelect source={source} setSource={setSource} />
|
||||
<SourcesButton />
|
||||
<Gallery hasGutter>
|
||||
<GalleryItem>
|
||||
<TextInput
|
||||
readOnlyVariant="default"
|
||||
isRequired
|
||||
id="someid"
|
||||
value="us-east-1"
|
||||
/>
|
||||
<HelperText>
|
||||
<HelperTextItem component="div" variant="indeterminate">
|
||||
Images are built in the default region but can be copied to
|
||||
other regions later.
|
||||
</HelperTextItem>
|
||||
</HelperText>
|
||||
</GalleryItem>
|
||||
<GalleryItem>
|
||||
<AwsAccountId source={source} />
|
||||
</GalleryItem>
|
||||
</Gallery>
|
||||
</>
|
||||
)}
|
||||
{shareMethod === 'manual' && (
|
||||
<>
|
||||
<FormGroup label="AWS account ID" isRequired>
|
||||
<ValidatedTextInput
|
||||
ariaLabel="aws account id"
|
||||
value={shareWithAccount || ''}
|
||||
validator={isAwsAccountIdValid}
|
||||
onChange={(_event, value) => dispatch(changeAwsAccount(value))}
|
||||
helperText="Should be 12 characters long."
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup label="Default Region" isRequired>
|
||||
<TextInput
|
||||
value={'us-east-1'}
|
||||
type="text"
|
||||
aria-label="default region"
|
||||
readOnlyVariant="default"
|
||||
/>
|
||||
</FormGroup>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default Aws;
|
||||
10
src/Components/CreateImageWizardV2/validators.ts
Normal file
10
src/Components/CreateImageWizardV2/validators.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export const isAwsAccountIdValid = (awsAccountId: string | undefined) => {
|
||||
if (
|
||||
awsAccountId !== undefined &&
|
||||
/^\d+$/.test(awsAccountId) &&
|
||||
awsAccountId.length === 12
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue