Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
HelperText,
HelperTextItem,
Spinner,
Tooltip,
} from '@patternfly/react-core';

import {
Expand All @@ -16,7 +17,10 @@ import {
usePullImageMutation,
} from '@/store/api/backend';
import { Distributions } from '@/store/api/backend/hosted';
import { KNOWN_IMAGES } from '@/store/api/backend/onprem/constants';
import {
IMAGE_REGISTRY_HOST,
KNOWN_IMAGES,
} from '@/store/api/backend/onprem/constants';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import {
changeDistribution,
Expand All @@ -33,6 +37,42 @@ import {
import ImageSelect from './ImageSelect';
import RegistryAuth from './RegistryAuth';

type PullButtonProps = {
onPull: () => void;
isPulling: boolean;
isAuthenticated: boolean;
isDisabled?: boolean;
};

const PullButton = ({
onPull,
isPulling,
isAuthenticated,
isDisabled,
}: PullButtonProps) => {
const button = (
<Button
variant='secondary'
onClick={onPull}
isDisabled={isDisabled || isPulling}
isAriaDisabled={!isAuthenticated}
icon={isPulling ? <Spinner size='sm' /> : undefined}
>
{isPulling ? 'Pulling image...' : 'Pull latest image'}
</Button>
);

if (isAuthenticated) {
return button;
}

return (
<Tooltip content={`Log in to ${IMAGE_REGISTRY_HOST} to pull images.`}>
{button}
</Tooltip>
);
};

const OfficialImageSource = () => {
const dispatch = useAppDispatch();
const arch = useAppSelector(selectArchitecture);
Expand All @@ -42,24 +82,30 @@ const OfficialImageSource = () => {
const hasOfficialSelection = useAppSelector(selectIsOfficialImage);

const { data: authStatus, isLoading: isAuthLoading } =
useGetRegistryAuthStatusQuery();
useGetRegistryAuthStatusQuery(undefined, {
refetchOnMountOrArgChange: true,
});
const isAuthenticated = authStatus?.status === 'authenticated';

const images = useMemo(() => {
if (!isAuthenticated) {
return [];
}

return KNOWN_IMAGES.map((known) => ({ ...known, arch }));
}, [isAuthenticated, arch]);
const images = useMemo(
() => KNOWN_IMAGES.map((known) => ({ ...known, arch })),
[arch],
);

// Local images can be removed outside the wizard (e.g. podman rmi),
// so bypass the cache and re-check whenever this section mounts.
const { data: imageExists } = useGetImageExistsQuery(
{ reference: selectedRef! },
{ skip: !selectedRef },
{ skip: !selectedRef, refetchOnMountOrArgChange: true },
);

const [pullImage, { isLoading: isPulling, isError: isPullError }] =
usePullImageMutation();
// The mutation state is scoped to the reference it was started with,
// so switching to another image doesn't show its busy/error state.
const [pullImage, pullState] = usePullImageMutation();
const isPulling =
pullState.isLoading && pullState.originalArgs?.reference === selectedRef;
const isPullError =
pullState.isError && pullState.originalArgs?.reference === selectedRef;

const showSelectionError = forceShowErrors && !hasOfficialSelection;
const showPullValidation = hasOfficialSelection && imageExists === false;
Expand All @@ -77,48 +123,42 @@ const OfficialImageSource = () => {
return (
<>
<RegistryAuth />
{isAuthenticated && (
<Flex
spaceItems={{ default: 'spaceItemsMd' }}
alignItems={{ default: 'alignItemsFlexStart' }}
>
<FlexItem>
<ImageSelect
items={images}
selectedRef={selectedRef}
ariaDescribedBy={errorId}
onSelect={(_event, selection) => {
const selected = images.find(
(img) => img.reference === selection,
<Flex
spaceItems={{ default: 'spaceItemsMd' }}
alignItems={{ default: 'alignItemsFlexStart' }}
>
<FlexItem>
<ImageSelect
items={images}
selectedRef={selectedRef}
ariaDescribedBy={errorId}
onSelect={(_event, selection) => {
const selected = images.find(
(img) => img.reference === selection,
);
if (selected) {
dispatch(changeImageSource(selected.reference));
dispatch(changeDistribution(selected.distro as Distributions));
dispatch(
changeImageTypes([selected.type as SupportedImageTypes]),
);
if (selected) {
dispatch(changeImageSource(selected.reference));
dispatch(
changeDistribution(selected.distro as Distributions),
);
dispatch(
changeImageTypes([selected.type as SupportedImageTypes]),
);
}
}}
getLabel={(item) => item.name}
placeholder={'Select an official image'}
}
}}
getLabel={(item) => item.name}
placeholder={'Select an official image'}
/>
</FlexItem>
{hasOfficialSelection && (
<FlexItem className='pf-v6-u-mt-md'>
<PullButton
onPull={() => pullImage({ reference: selectedRef! })}
isPulling={isPulling}
isAuthenticated={isAuthenticated}
isDisabled={isAuthLoading}
/>
</FlexItem>
{hasOfficialSelection && (
<FlexItem className='pf-v6-u-mt-md'>
<Button
variant='secondary'
onClick={() => pullImage({ reference: selectedRef! })}
isDisabled={isAuthLoading || isPulling}
icon={isPulling ? <Spinner size='sm' /> : undefined}
>
{isPulling ? 'Pulling image...' : 'Pull latest image'}
</Button>
</FlexItem>
)}
</Flex>
)}
)}
</Flex>
{showSelectionError && (
<FormHelperText>
<HelperText>
Expand All @@ -134,7 +174,9 @@ const OfficialImageSource = () => {
<HelperTextItem variant='error' id='official-image-pull-error'>
{isPullError
? 'Failed to pull image. Please try again.'
: 'Image must be pulled before proceeding.'}
: isAuthenticated
? 'Bootc container must be pulled before proceeding.'
: `Bootc container is not in local storage. Log in to ${IMAGE_REGISTRY_HOST} to pull it.`}
</HelperTextItem>
</HelperText>
</FormHelperText>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React, { useState } from 'react';

import {
Alert,
AlertActionLink,
Button,
Card,
CardBody,
Expand All @@ -9,10 +11,6 @@ import {
CardTitle,
Content,
ContentVariants,
EmptyState,
EmptyStateActions,
EmptyStateBody,
EmptyStateFooter,
Flex,
FlexItem,
FormGroup,
Expand All @@ -31,23 +29,21 @@ import {
import { IMAGE_REGISTRY_HOST } from '@/store/api/backend/onprem/constants';
import { OnPremError } from '@/store/api/shared';

const EmptyCard = ({ openForm }: { openForm: (arg0: boolean) => void }) => {
const LoginPrompt = ({ openForm }: { openForm: (arg0: boolean) => void }) => {
return (
<Card variant='secondary' className='pf-v6-u-mt-md pf-v6-u-py-md'>
<EmptyState titleText='Login to select an image' headingLevel='h4'>
<EmptyStateBody>
<Content>Registry images come from {IMAGE_REGISTRY_HOST}.</Content>
<Content>
Sign in to browse available images for this release.
</Content>
</EmptyStateBody>
<EmptyStateFooter>
<EmptyStateActions>
<Button onClick={() => openForm(true)}>Login</Button>
</EmptyStateActions>
</EmptyStateFooter>
</EmptyState>
</Card>
<Alert
variant='info'
isInline
title='Log in to pull the latest images'
className='pf-v6-u-mt-md'
actionLinks={
<AlertActionLink onClick={() => openForm(true)}>Log in</AlertActionLink>
}
>
You can build from images already on this system without logging in.
Pulling the latest images from {IMAGE_REGISTRY_HOST} requires a Red Hat
login.
</Alert>
);
};

Expand Down Expand Up @@ -210,7 +206,11 @@ const RegistryStatus = ({ username }: { username: string }) => {
const RegistryAuth = () => {
const [isFormVisible, setIsFormVisible] = useState(false);

const { data, isLoading } = useGetRegistryAuthStatusQuery();
// The login can change outside the wizard (e.g. podman logout), so
// bypass the cache and re-check whenever this section mounts.
const { data, isLoading } = useGetRegistryAuthStatusQuery(undefined, {
refetchOnMountOrArgChange: true,
});

if (isLoading) {
return (
Expand All @@ -226,7 +226,7 @@ const RegistryAuth = () => {
}

if (!isFormVisible) {
return <EmptyCard openForm={setIsFormVisible} />;
return <LoginPrompt openForm={setIsFormVisible} />;
}

return <LoginCard closeForm={setIsFormVisible} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
CardTitle,
FormGroup,
Gallery,
Label,
} from '@patternfly/react-core';

import { IMAGE_REGISTRY_HOST } from '@/store/api/backend/onprem/constants';
Expand Down Expand Up @@ -47,7 +46,7 @@ const OnPremImageSourceSelect = () => {
}}
>
<CardTitle id='official-card-title'>
Official Red Hat images <Label isCompact>Login required</Label>
Official Red Hat images
</CardTitle>
</CardHeader>
<CardBody>Remote images from {IMAGE_REGISTRY_HOST}</CardBody>
Expand Down
Loading
Loading