src: Rename "V2" folders to just Wizard

This replaces all occurences of "CreateImageWizardV2" with just "CreateImageWizard" as it is the only version now.
This commit is contained in:
regexowl 2024-07-16 17:10:37 +02:00 committed by Ondřej Ezr
parent b1e5a8c7c6
commit 4fb37c187e
93 changed files with 20 additions and 22 deletions

View file

@ -0,0 +1,63 @@
import React, { useEffect } from 'react';
import {
Alert,
HelperText,
HelperTextItem,
TextInput,
FormGroup,
} from '@patternfly/react-core';
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
import { useGetSourceUploadInfoQuery } from '../../../../../store/provisioningApi';
import {
changeAwsAccountId,
selectAwsSourceId,
} from '../../../../../store/wizardSlice';
export const AwsAccountId = () => {
const dispatch = useAppDispatch();
const sourceId = useAppSelector(selectAwsSourceId);
const { data, isError } = useGetSourceUploadInfoQuery(
{
id: parseInt(sourceId as string),
},
{ skip: sourceId === undefined || sourceId === '' }
);
useEffect(() => {
dispatch(changeAwsAccountId(data?.aws?.account_id || ''));
}, [data?.aws?.account_id, dispatch]);
return (
<>
<FormGroup label="Associated account ID" isRequired>
<TextInput
readOnlyVariant="default"
isRequired
id="aws-account-id"
value={sourceId && data ? data.aws?.account_id : ''}
aria-label="aws account id"
/>
</FormGroup>
<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>
)}
</>
);
};

View file

@ -0,0 +1,100 @@
import React, { useState } from 'react';
import { Alert, Spinner } from '@patternfly/react-core';
import { FormGroup } from '@patternfly/react-core';
import {
Select,
SelectOption,
SelectVariant,
} from '@patternfly/react-core/deprecated';
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
import { useGetSourceListQuery } from '../../../../../store/provisioningApi';
import {
changeAwsSourceId,
selectAwsSourceId,
} from '../../../../../store/wizardSlice';
export const AwsSourcesSelect = () => {
const dispatch = useAppDispatch();
const [isOpen, setIsOpen] = useState(false);
const sourceId = useAppSelector(selectAwsSourceId);
const { data, isFetching, isLoading, isSuccess, isError, refetch } =
useGetSourceListQuery({
provider: 'aws',
});
const sources = data?.data;
const chosenSource = sources?.find((source) => source.id === sourceId);
const handleSelect = (
_event: React.MouseEvent<Element, MouseEvent>,
value: string
) => {
const source = sources?.find((source) => source.name === value);
dispatch(changeAwsSourceId(source?.id));
setIsOpen(false);
};
const handleClear = () => {
dispatch(changeAwsSourceId(undefined));
};
const handleToggle = () => {
// Refetch upon opening (but not upon closing)
if (!isOpen) {
refetch();
}
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 (
<>
<FormGroup isRequired label={'Source name'} data-testid="sources">
<Select
ouiaId="source_select"
variant={SelectVariant.typeahead}
onToggle={handleToggle}
onSelect={handleSelect}
onClear={handleClear}
selections={chosenSource?.name}
isOpen={isOpen}
placeholderText="Select source"
typeAheadAriaLabel="Select source"
isDisabled={!isSuccess || isLoading}
>
{selectOptions}
</Select>
</FormGroup>
<>
{isError && (
<Alert
variant={'danger'}
isPlain={true}
isInline={true}
title={'Sources unavailable'}
>
Sources cannot be reached, try again later or enter an AWS account
ID manually.
</Alert>
)}
</>
</>
);
};

View file

@ -0,0 +1,148 @@
import React 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 {
changeAwsAccountId,
changeAwsShareMethod,
changeAwsSourceId,
selectAwsAccountId,
selectAwsShareMethod,
} from '../../../../../store/wizardSlice';
import { ValidatedTextInput } from '../../../ValidatedTextInput';
import { isAwsAccountIdValid } from '../../../validators';
export type AwsShareMethod = 'manual' | 'sources';
const SourcesButton = () => {
return (
<Button
component="a"
target="_blank"
variant="link"
icon={<ExternalLinkAltIcon />}
iconPosition="right"
isInline
href={'settings/sources'}
>
Create and manage sources here
</Button>
);
};
const Aws = () => {
const dispatch = useAppDispatch();
const shareMethod = useAppSelector(selectAwsShareMethod);
const shareWithAccount = useAppSelector(selectAwsAccountId);
return (
<Form>
<Title headingLevel="h1" size="xl">
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 source to launch environments directly from the console."
isChecked={shareMethod === 'sources'}
onChange={() => {
dispatch(changeAwsSourceId(undefined));
dispatch(changeAwsAccountId(''));
dispatch(changeAwsShareMethod('sources'));
}}
autoFocus
/>
<Radio
id="radio"
label="Manually enter an account ID."
name="radio-8"
isChecked={shareMethod === 'manual'}
onChange={() => {
dispatch(changeAwsSourceId(undefined));
dispatch(changeAwsAccountId(''));
dispatch(changeAwsShareMethod('manual'));
}}
/>
</FormGroup>
{shareMethod === 'sources' && (
<>
<AwsSourcesSelect />
<SourcesButton />
<Gallery hasGutter>
<GalleryItem>
<FormGroup label="Default region" isRequired>
<TextInput
readOnlyVariant="default"
isRequired
id="someid"
value="us-east-1"
/>
</FormGroup>
<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 />
</GalleryItem>
</Gallery>
</>
)}
{shareMethod === 'manual' && (
<>
<FormGroup label="AWS account ID" isRequired>
<ValidatedTextInput
ariaLabel="aws account id"
value={shareWithAccount || ''}
validator={isAwsAccountIdValid}
onChange={(_event, value) => dispatch(changeAwsAccountId(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;

View file

@ -0,0 +1,33 @@
import React from 'react';
import { Button, FormGroup } from '@patternfly/react-core';
import { useAppSelector } from '../../../../../store/hooks';
import { selectAzureTenantId } from '../../../../../store/wizardSlice';
export const AzureAuthButton = () => {
const tenantId = useAppSelector(selectAzureTenantId);
const guidRegex = new RegExp(
'^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$',
'i'
);
return (
<FormGroup>
<Button
component="a"
target="_blank"
variant="secondary"
isDisabled={!guidRegex.test(tenantId)}
href={
'https://login.microsoftonline.com/' +
tenantId +
'/oauth2/v2.0/authorize?client_id=b94bb246-b02c-4985-9c22-d44e66f657f4&scope=openid&' +
'response_type=code&response_mode=query&redirect_uri=https://portal.azure.com'
}
>
Authorize Image Builder
</Button>
</FormGroup>
);
};

View file

@ -0,0 +1,87 @@
import React, { useState } from 'react';
import { FormGroup, Spinner } from '@patternfly/react-core';
import {
Select,
SelectOption,
SelectVariant,
} from '@patternfly/react-core/deprecated';
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
import { useGetSourceUploadInfoQuery } from '../../../../../store/provisioningApi';
import {
changeAzureResourceGroup,
selectAzureResourceGroup,
selectAzureSource,
} from '../../../../../store/wizardSlice';
export const AzureResourceGroups = () => {
const azureSource = useAppSelector(selectAzureSource);
const azureResourceGroup = useAppSelector(selectAzureResourceGroup);
const dispatch = useAppDispatch();
const [isOpen, setIsOpen] = useState(false);
const { data: sourceDetails, isFetching } = useGetSourceUploadInfoQuery(
{ id: parseInt(azureSource as string) },
{
skip: !azureSource,
}
);
const resourceGroups =
(azureSource && sourceDetails?.azure?.resource_groups) || [];
const setResourceGroup = (
_event: React.MouseEvent<Element, MouseEvent>,
selection: string
) => {
const resource =
resourceGroups?.find((resource) => resource === selection) || '';
setIsOpen(false);
dispatch(changeAzureResourceGroup(resource));
};
const handleClear = () => {
dispatch(changeAzureResourceGroup(''));
};
const options: JSX.Element[] = [];
if (isFetching) {
options.push(
<SelectOption
isNoResultsOption={true}
data-testid="azure-resource-groups-loading"
>
<Spinner size="lg" />
</SelectOption>
);
}
return (
<FormGroup
isRequired
label={'Resource group'}
data-testid="azure-resource-groups"
>
<Select
ouiaId="resource_group_select"
variant={SelectVariant.typeahead}
onToggle={() => setIsOpen(!isOpen)}
onSelect={setResourceGroup}
onClear={handleClear}
selections={azureResourceGroup}
isOpen={isOpen}
placeholderText="Select resource group"
typeAheadAriaLabel="Select resource group"
>
{resourceGroups.map((name: string, index: number) => (
<SelectOption
key={index}
value={name}
aria-label={`Resource group ${name}`}
/>
))}
</Select>
</FormGroup>
);
};

View file

@ -0,0 +1,155 @@
import React, { useState, useEffect } from 'react';
import { Alert } from '@patternfly/react-core';
import { FormGroup, Spinner } from '@patternfly/react-core';
import {
Select,
SelectOption,
SelectVariant,
} from '@patternfly/react-core/deprecated';
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
import {
useGetSourceListQuery,
useGetSourceUploadInfoQuery,
} from '../../../../../store/provisioningApi';
import {
changeAzureResourceGroup,
changeAzureSource,
changeAzureSubscriptionId,
changeAzureTenantId,
selectAzureSource,
} from '../../../../../store/wizardSlice';
export const AzureSourcesSelect = () => {
const azureSource = useAppSelector(selectAzureSource);
const dispatch = useAppDispatch();
const [isOpen, setIsOpen] = useState(false);
const {
data: rawSources,
isFetching,
isSuccess,
isError,
refetch,
} = useGetSourceListQuery({ provider: 'azure' });
const {
data: sourceDetails,
isFetching: isFetchingDetails,
isSuccess: isSuccessDetails,
isError: isErrorDetails,
} = useGetSourceUploadInfoQuery(
{ id: parseInt(azureSource as string) },
{
skip: !azureSource,
}
);
useEffect(() => {
if (isFetchingDetails || !isSuccessDetails) return;
dispatch(changeAzureTenantId(sourceDetails?.azure?.tenant_id || ''));
dispatch(
changeAzureSubscriptionId(sourceDetails?.azure?.subscription_id || '')
);
}, [
isFetchingDetails,
isSuccessDetails,
sourceDetails?.azure?.tenant_id,
sourceDetails?.azure?.subscription_id,
dispatch,
]);
const handleSelect = (
_event: React.MouseEvent<Element, MouseEvent>,
sourceName: string
) => {
const sourceId = rawSources?.data?.find(
(source) => source?.name === sourceName
)?.id;
dispatch(changeAzureSource(sourceId || ''));
dispatch(changeAzureResourceGroup(''));
setIsOpen(false);
};
const handleClear = () => {
dispatch(changeAzureSource(''));
dispatch(changeAzureTenantId(''));
dispatch(changeAzureSubscriptionId(''));
dispatch(changeAzureResourceGroup(''));
};
const handleToggle = () => {
// Refetch upon opening (but not upon closing)
if (!isOpen) {
refetch();
}
setIsOpen(!isOpen);
};
const selectOptions = rawSources?.data?.map((source) => (
<SelectOption key={source.id} value={source.name} />
));
if (isSuccess) {
if (isFetching) {
selectOptions?.push(
<SelectOption key="loading" isNoResultsOption={true}>
<Spinner size="lg" />
</SelectOption>
);
}
}
return (
<>
<FormGroup isRequired label={'Source name'} data-testid="azure-sources">
<Select
ouiaId="source_select"
variant={SelectVariant.typeahead}
onToggle={handleToggle}
onSelect={handleSelect}
onClear={handleClear}
selections={
azureSource
? rawSources?.data?.find((source) => source.id === azureSource)
?.name
: undefined
}
isOpen={isOpen}
placeholderText="Select source"
typeAheadAriaLabel="Select source"
menuAppendTo="parent"
maxHeight="25rem"
isDisabled={!isSuccess}
>
{selectOptions}
</Select>
</FormGroup>
{isError && (
<Alert
variant={'danger'}
isPlain
isInline
title={'Sources unavailable'}
>
Sources cannot be reached, try again later or enter an account info
for upload manually.
</Alert>
)}
{!isError && isErrorDetails && (
<Alert
variant={'danger'}
isPlain
isInline
title={'Azure details unavailable'}
>
Could not fetch Tenant ID and Subscription ID from Azure for given
Source. Check Sources page for the source availability or select a
different Source.
</Alert>
)}
<></>
</>
);
};

View file

@ -0,0 +1,197 @@
import React from 'react';
import {
Radio,
Text,
Form,
Title,
FormGroup,
TextInput,
Gallery,
GalleryItem,
Button,
} from '@patternfly/react-core';
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
import { AzureAuthButton } from './AzureAuthButton';
import { AzureResourceGroups } from './AzureResourceGroups';
import { AzureSourcesSelect } from './AzureSourcesSelect';
import { AZURE_AUTH_URL } from '../../../../../constants';
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
import {
changeAzureResourceGroup,
changeAzureShareMethod,
changeAzureSource,
changeAzureSubscriptionId,
changeAzureTenantId,
selectAzureResourceGroup,
selectAzureShareMethod,
selectAzureSubscriptionId,
selectAzureTenantId,
} from '../../../../../store/wizardSlice';
import { ValidatedTextInput } from '../../../ValidatedTextInput';
import {
isAzureResourceGroupValid,
isAzureSubscriptionIdValid,
isAzureTenantGUIDValid,
} from '../../../validators';
export type AzureShareMethod = 'manual' | 'sources';
const SourcesButton = () => {
return (
<Button
component="a"
target="_blank"
variant="link"
icon={<ExternalLinkAltIcon />}
iconPosition="right"
isInline
href={'settings/sources'}
>
Create and manage sources here
</Button>
);
};
const Azure = () => {
const dispatch = useAppDispatch();
const shareMethod = useAppSelector(selectAzureShareMethod);
const tenantId = useAppSelector(selectAzureTenantId);
const subscriptionId = useAppSelector(selectAzureSubscriptionId);
const resourceGroup = useAppSelector(selectAzureResourceGroup);
return (
<Form>
<Title headingLevel="h1" size="xl">
Target environment - Microsoft Azure
</Title>
<Text>
Upon build, Image Builder sends the image to the selected authorized
Azure account. The image will be uploaded to the resource group in the
subscription you specify.
</Text>
<Text>
To authorize Image Builder to push images to Microsoft Azure, the
account owner must configure Image Builder as an authorized application
for a specific tenant ID and give it the role of &quot;Contributor&quot;
for the resource group you want to upload to. This applies even when
defining target by Source selection.
<br />
<Button
component="a"
target="_blank"
variant="link"
icon={<ExternalLinkAltIcon />}
iconPosition="right"
isInline
href={AZURE_AUTH_URL}
>
Learn more about OAuth 2.0
</Button>
</Text>
<FormGroup label="Share method:">
<Radio
id="radio-with-description"
label="Use an account configured from Sources."
name="radio-7"
description="Use a configured source to launch environments directly from the console."
isChecked={shareMethod === 'sources'}
onChange={() => {
dispatch(changeAzureSource(''));
dispatch(changeAzureTenantId(''));
dispatch(changeAzureSubscriptionId(''));
dispatch(changeAzureShareMethod('sources'));
dispatch(changeAzureResourceGroup(''));
}}
autoFocus
/>
<Radio
id="radio"
label="Manually enter the account information."
name="radio-8"
isChecked={shareMethod === 'manual'}
onChange={() => {
dispatch(changeAzureSource(''));
dispatch(changeAzureTenantId(''));
dispatch(changeAzureSubscriptionId(''));
dispatch(changeAzureShareMethod('manual'));
dispatch(changeAzureResourceGroup(''));
}}
/>
</FormGroup>
{shareMethod === 'sources' && (
<>
<AzureSourcesSelect />
<SourcesButton />
<Gallery hasGutter>
<GalleryItem>
<FormGroup label="Azure tenant GUID" isRequired>
<TextInput
aria-label="Azure tenant GUID"
readOnlyVariant="default"
isRequired
id="tenant id"
value={tenantId}
/>
</FormGroup>
</GalleryItem>
<GalleryItem>
<FormGroup label="Subscription ID" isRequired>
<TextInput
aria-label="Subscription ID"
label="Subscription ID"
readOnlyVariant="default"
isRequired
id="subscription id"
value={subscriptionId}
/>
</FormGroup>
</GalleryItem>
</Gallery>
<AzureAuthButton />
<AzureResourceGroups />
</>
)}
{shareMethod === 'manual' && (
<>
<FormGroup label="Azure tenant GUID" isRequired>
<ValidatedTextInput
ariaLabel="Azure tenant GUID"
value={tenantId || ''}
validator={isAzureTenantGUIDValid}
onChange={(_event, value) => dispatch(changeAzureTenantId(value))}
helperText="Please enter a valid tenant ID"
/>
</FormGroup>
<AzureAuthButton />
<FormGroup label="Subscription ID" isRequired>
<ValidatedTextInput
ariaLabel="subscription id"
value={subscriptionId}
validator={isAzureSubscriptionIdValid}
onChange={(_event, value) =>
dispatch(changeAzureSubscriptionId(value))
}
helperText="Please enter a valid subscription ID"
/>
</FormGroup>
<FormGroup label="Resource group" isRequired>
<ValidatedTextInput
ariaLabel="resource group"
value={resourceGroup}
validator={isAzureResourceGroupValid}
onChange={(_event, value) =>
dispatch(changeAzureResourceGroup(value))
}
helperText="Resource group names only allow alphanumeric characters, periods, underscores, hyphens, and parenthesis and cannot end in a period"
/>
</FormGroup>
</>
)}
</Form>
);
};
export default Azure;

View file

@ -0,0 +1,147 @@
import React from 'react';
import { Radio, Text, Form, Title, FormGroup } from '@patternfly/react-core';
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
import {
changeGcpAccountType,
changeGcpEmail,
changeGcpShareMethod,
selectGcpAccountType,
selectGcpEmail,
selectGcpShareMethod,
} from '../../../../../store/wizardSlice';
import { ValidatedTextInput } from '../../../ValidatedTextInput';
import { isGcpEmailValid } from '../../../validators';
export type GcpShareMethod = 'withGoogle' | 'withInsights';
export type GcpAccountType =
| 'user'
| 'serviceAccount'
| 'group'
| 'domain'
| undefined;
const Gcp = () => {
const dispatch = useAppDispatch();
const accountType = useAppSelector(selectGcpAccountType);
const shareMethod = useAppSelector(selectGcpShareMethod);
const gcpEmail = useAppSelector(selectGcpEmail);
return (
<Form>
<Title headingLevel="h1" size="xl">
Target environment - Google Cloud Platform
</Title>
<Text>
Select how to share your image. The image you create can be used to
launch instances on GCP, regardless of which method you select.
</Text>
<FormGroup label="Select image sharing" isRequired>
<Radio
id="share-with-google"
data-testid="share-with-google"
label="Share image with a Google account"
name="radio-1"
description={
<Text>
Your image will be uploaded to GCP and shared with the account you
provide below.
<b>The image expires in 14 days.</b> To keep permanent access to
your image, copy it to your GCP project.
</Text>
}
isChecked={shareMethod === 'withGoogle'}
onChange={() => {
dispatch(changeGcpShareMethod('withGoogle'));
}}
autoFocus
/>
<Radio
id="share-with-insights"
data-testid="share-with-insights"
label="Share image with Red Hat Insights only"
name="radio-2"
description={
<Text>
Your image will be uploaded to GCP and shared with Red Hat
Insights.
<b> The image expires in 14 days.</b> You cannot access or
recreate this image in your GCP project.
</Text>
}
isChecked={shareMethod === 'withInsights'}
onChange={() => {
dispatch(changeGcpShareMethod('withInsights'));
}}
/>
</FormGroup>
{shareMethod === 'withGoogle' && (
<>
<FormGroup label="Account type" isRequired>
<Radio
id="google-account"
data-testid="google-account"
label="Google account"
name="radio-3"
isChecked={accountType === 'user'}
onChange={() => {
dispatch(changeGcpAccountType('user'));
}}
/>
<Radio
id="service-account"
data-testid="service-account"
label="Service account"
name="radio-4"
isChecked={accountType === 'serviceAccount'}
onChange={() => {
dispatch(changeGcpAccountType('serviceAccount'));
}}
/>
<Radio
id="google-group"
data-testid="google-group"
label="Google group"
name="radio-5"
isChecked={accountType === 'group'}
onChange={() => {
dispatch(changeGcpAccountType('group'));
}}
/>
<Radio
id="google-domain"
data-testid="google-domain"
label="Google Workspace domain or Cloud Identity domain"
name="radio-6"
isChecked={accountType === 'domain'}
onChange={() => {
dispatch(changeGcpAccountType('domain'));
}}
/>
</FormGroup>
<FormGroup
label={
accountType === 'domain'
? 'Domain'
: 'Principal (e.g. e-mail address)'
}
isRequired
>
<ValidatedTextInput
ariaLabel="google principal"
dataTestId="principal"
value={gcpEmail || ''}
validator={isGcpEmailValid}
onChange={(_event, value) => dispatch(changeGcpEmail(value))}
helperText="Please enter a valid e-mail address."
/>
</FormGroup>
</>
)}
</Form>
);
};
export default Gcp;