HMS-4024: Update repositories step

This commit is contained in:
Andrew Dewar 2024-05-31 09:37:16 -06:00 committed by Klara Simickova
parent e85789f58d
commit 5dc4ecb63f
14 changed files with 785 additions and 706 deletions

View file

@ -47,6 +47,7 @@ import {
RH_ICON_SIZE,
} from '../../../../constants';
import {
ApiRepositoryResponseRead,
useCreateRepositoryMutation,
useListRepositoriesQuery,
useSearchRpmMutation,
@ -151,7 +152,8 @@ const Packages = () => {
},
] = useSearchRpmMutation();
const [createRepository] = useCreateRepositoryMutation();
const [createRepository, { isLoading: createLoading }] =
useCreateRepositoryMutation();
useEffect(() => {
if (debouncedSearchTerm.length > 1 && isSuccessDistroRepositories) {
@ -202,6 +204,10 @@ const Packages = () => {
toggleSourceRepos,
searchRecommendedRpms,
epelRepoUrlByDistribution,
isSuccessDistroRepositories,
searchDistroRpms,
distroRepositories,
arch,
]);
const EmptySearch = () => {
@ -441,6 +447,8 @@ const Packages = () => {
<Button
key="add"
variant="primary"
isLoading={createLoading}
isDisabled={createLoading}
onClick={handleConfirmModalToggle}
>
Add listed repositories
@ -723,16 +731,22 @@ const Packages = () => {
`There was an error while adding the recommended repository.`
);
}
if (epelRepo.data.length === 0) {
await createRepository({
const result = await createRepository({
apiRepositoryRequest: distribution.startsWith('rhel-8')
? EPEL_8_REPO_DEFINITION
: EPEL_9_REPO_DEFINITION,
});
dispatch(
addRecommendedRepository(
(result as { data: ApiRepositoryResponseRead }).data
)
);
} else {
dispatch(addRecommendedRepository(epelRepo.data[0]));
}
dispatch(addPackage(isSelectingPackage!));
dispatch(addRecommendedRepository(epelRepo.data[0]));
setIsRepoModalOpen(!isRepoModalOpen);
};

View file

@ -4,39 +4,36 @@ import { Alert, Button } from '@patternfly/react-core';
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
import { useGetEnvironment } from '../../../../Utilities/useGetEnvironment';
import { useCheckRepositoriesAvailability } from '../../utilities/checkRepositoriesAvailability';
const RepositoryUnavailable = () => {
const RepositoryUnavailable = ({ quantity }: { quantity: number }) => {
const { isBeta } = useGetEnvironment();
if (useCheckRepositoriesAvailability()) {
return (
<Alert
variant="warning"
title="Previously added custom repository unavailable"
return (
<Alert
variant="warning"
title="Previously added custom repository unavailable"
isInline
>
{quantity > 1
? `${quantity} repositories that were used to build this image previously are not available.`
: 'One repository that was used to build this image previously is not available. '}
Address the error found in the last introspection and validate that the
repository is still accessible.
<br />
<br />
<Button
component="a"
target="_blank"
variant="link"
iconPosition="right"
isInline
icon={<ExternalLinkAltIcon />}
href={isBeta() ? '/preview/settings/content' : '/settings/content'}
>
A repository that was used to build this image previously is not
available. Address the error found in the last introspection and
validate that the repository is still accessible.
<br />
<br />
<Button
component="a"
target="_blank"
variant="link"
iconPosition="right"
isInline
icon={<ExternalLinkAltIcon />}
href={isBeta() ? '/preview/settings/content' : '/settings/content'}
>
Go to Repositories
</Button>
</Alert>
);
} else {
return;
}
Go to Repositories
</Button>
</Alert>
);
};
export default RepositoryUnavailable;

View file

@ -0,0 +1,86 @@
import React, { useState } from 'react';
import {
Dropdown,
DropdownItem,
DropdownToggle,
DropdownToggleCheckbox,
} from '@patternfly/react-core/deprecated';
import { ApiRepositoryResponseRead } from '../../../../../store/contentSourcesApi';
interface BulkSelectProps {
selected: Set<string>;
contentList: ApiRepositoryResponseRead[];
deselectAll: () => void;
perPage: number;
handleAddRemove: (
repo: ApiRepositoryResponseRead | ApiRepositoryResponseRead[],
selected: boolean
) => void;
isDisabled: boolean;
}
export function BulkSelect({
selected,
contentList,
deselectAll,
perPage,
handleAddRemove,
isDisabled,
}: BulkSelectProps) {
const [dropdownIsOpen, setDropdownIsOpen] = useState(false);
const allChecked = !contentList.some(({ url }) => !selected.has(url!));
const someChecked =
allChecked || contentList.some(({ url }) => selected.has(url!));
const toggleDropdown = () => setDropdownIsOpen(!dropdownIsOpen);
const handleSelectPage = () => handleAddRemove(contentList, !allChecked);
return (
<Dropdown
toggle={
<DropdownToggle
id="stacked-example-toggle"
isDisabled={isDisabled}
splitButtonItems={[
<DropdownToggleCheckbox
id="example-checkbox-1"
key="split-checkbox"
aria-label="Select all"
isChecked={allChecked || someChecked ? null : false}
onClick={handleSelectPage}
/>,
]}
onToggle={toggleDropdown}
>
{someChecked ? `${selected.size} selected` : null}
</DropdownToggle>
}
isOpen={dropdownIsOpen}
dropdownItems={[
<DropdownItem
key="none"
isDisabled={!selected.size}
onClick={() => {
deselectAll();
toggleDropdown();
}}
>{`Clear all (${selected.size} items)`}</DropdownItem>,
<DropdownItem
key="page"
isDisabled={!contentList.length}
onClick={() => {
handleSelectPage();
toggleDropdown();
}}
>{`${allChecked ? 'Remove' : 'Select'} page (${
perPage > contentList.length ? contentList.length : perPage
} items)`}</DropdownItem>,
]}
/>
);
}

View file

@ -0,0 +1,56 @@
import React from 'react';
import {
EmptyState,
EmptyStateVariant,
EmptyStateHeader,
EmptyStateIcon,
EmptyStateBody,
EmptyStateFooter,
Button,
} from '@patternfly/react-core';
import { RepositoryIcon } from '@patternfly/react-icons';
import { useGetEnvironment } from '../../../../../Utilities/useGetEnvironment';
type EmptyProps = {
refetch: () => void;
hasFilterValue: boolean;
};
export default function Empty({ hasFilterValue, refetch }: EmptyProps) {
const { isBeta } = useGetEnvironment();
return (
<EmptyState variant={EmptyStateVariant.lg} data-testid="empty-state">
<EmptyStateHeader
titleText={
hasFilterValue
? 'No matching repositories found'
: 'No Custom Repositories'
}
icon={<EmptyStateIcon icon={RepositoryIcon} />}
headingLevel="h4"
/>
<EmptyStateBody>
{hasFilterValue
? 'Try another search query or clear the current search value'
: `Repositories can be added in the "Repositories" area of the
console. Once added, refresh this page to see them.`}
</EmptyStateBody>
<EmptyStateFooter>
<Button
variant="primary"
component="a"
target="_blank"
href={isBeta() ? '/preview/settings/content' : '/settings/content'}
className="pf-u-mr-sm"
>
Go to repositories
</Button>
<Button variant="secondary" isInline onClick={() => refetch()}>
Refresh
</Button>
</EmptyStateFooter>
</EmptyState>
);
}

View file

@ -0,0 +1,11 @@
import React from 'react';
import { Alert } from '@patternfly/react-core';
export const Error = () => {
return (
<Alert title="Repositories unavailable" variant="danger" isPlain isInline>
Repositories cannot be reached, try again later.
</Alert>
);
};

View file

@ -0,0 +1,23 @@
import React from 'react';
import {
EmptyState,
EmptyStateIcon,
Spinner,
EmptyStateHeader,
Bullseye,
} from '@patternfly/react-core';
export const Loading = () => {
return (
<Bullseye>
<EmptyState>
<EmptyStateHeader
titleText="Loading"
icon={<EmptyStateIcon icon={Spinner} />}
headingLevel="h4"
/>
</EmptyState>
</Bullseye>
);
};

View file

@ -0,0 +1,50 @@
import { ApiRepositoryResponseRead } from '../../../../../store/contentSourcesApi';
import {
CustomRepository,
Repository,
} from '../../../../../store/imageBuilderApi';
// Utility function to convert from Content Sources to Image Builder custom repo API schema
export const convertSchemaToIBCustomRepo = (
repo: ApiRepositoryResponseRead
) => {
const imageBuilderRepo: CustomRepository = {
id: repo.uuid!,
name: repo.name,
baseurl: [repo.url!],
check_gpg: false,
};
// only include the flag if enabled
if (repo.module_hotfixes) {
imageBuilderRepo.module_hotfixes = repo.module_hotfixes;
}
if (repo.gpg_key) {
imageBuilderRepo.gpgkey = [repo.gpg_key];
imageBuilderRepo.check_gpg = true;
imageBuilderRepo.check_repo_gpg = repo.metadata_verification;
}
return imageBuilderRepo;
};
// Utility function to convert from Content Sources to Image Builder payload repo API schema
export const convertSchemaToIBPayloadRepo = (
repo: ApiRepositoryResponseRead
) => {
const imageBuilderRepo: Repository = {
baseurl: repo.url,
rhsm: false,
check_gpg: false,
};
// only include the flag if enabled
if (repo.module_hotfixes) {
imageBuilderRepo.module_hotfixes = repo.module_hotfixes;
}
if (repo.gpg_key) {
imageBuilderRepo.gpgkey = repo.gpg_key;
imageBuilderRepo.check_gpg = true;
imageBuilderRepo.check_repo_gpg = repo.metadata_verification;
}
return imageBuilderRepo;
};

View file

@ -44,7 +44,7 @@ const RepositoriesStep = () => {
<br />
<ManageRepositoriesButton />
</Text>
{recommendedRepos.length > 0 && (
{packages.length && recommendedRepos.length ? (
<Alert
title="Why can't I remove a selected repository?"
variant="info"
@ -55,6 +55,8 @@ const RepositoriesStep = () => {
following packages on the Packages step:{' '}
{packages.map((pkg) => pkg.name).join(', ')}
</Alert>
) : (
''
)}
<Repositories />
</Form>

View file

@ -74,7 +74,7 @@ import {
import {
convertSchemaToIBCustomRepo,
convertSchemaToIBPayloadRepo,
} from '../steps/Repositories/Repositories';
} from '../steps/Repositories/components/Utilities';
import { GcpAccountType } from '../steps/TargetEnvironment/Gcp';
type ServerStore = {

View file

@ -474,36 +474,9 @@ describe('Step Custom repositories', () => {
})
);
await screen.findByText(/select all \(1016 items\)/i);
await screen.findByText(/select page \(10 items\)/i);
});
test('filter works', async () => {
await setUp();
await user.type(
await screen.findByRole('textbox', { name: /search repositories/i }),
'2zmya'
);
const table = await screen.findByTestId('repositories-table');
const getRows = async () => await within(table).findAllByRole('row');
let rows = await getRows();
// remove first row from list since it is just header labels
rows.shift();
expect(rows).toHaveLength(1);
// clear filter
await user.click(await screen.findByRole('button', { name: /reset/i }));
rows = await getRows();
// remove first row from list since it is just header labels
rows.shift();
await waitFor(() => expect(rows).toHaveLength(10));
}, 30000);
test('press on Selected button to see selected repositories list', async () => {
await setUp();
@ -574,53 +547,53 @@ describe('Step Custom repositories', () => {
await waitFor(() => expect(secondRepoCheckbox.checked).toEqual(false));
});
test('press on Selected button to see selected repositories list at the second page and filter checked repo', async () => {
await setUp();
// test('press on Selected button to see selected repositories list at the second page and filter checked repo', async () => {
// await setUp();
const getFirstRepoCheckbox = async () =>
await screen.findByRole('checkbox', {
name: /select row 0/i,
});
// const getFirstRepoCheckbox = async () =>
// await screen.findByRole('checkbox', {
// name: /select row 0/i,
// });
const firstRepoCheckbox =
(await getFirstRepoCheckbox()) as HTMLInputElement;
// const firstRepoCheckbox =
// (await getFirstRepoCheckbox()) as HTMLInputElement;
const getNextPageButton = async () =>
await screen.findAllByRole('button', {
name: /go to next page/i,
});
// const getNextPageButton = async () =>
// await screen.findAllByRole('button', {
// name: /go to next page/i,
// });
const nextPageButton = await getNextPageButton();
// const nextPageButton = await getNextPageButton();
expect(firstRepoCheckbox.checked).toEqual(false);
await user.click(firstRepoCheckbox);
expect(firstRepoCheckbox.checked).toEqual(true);
// expect(firstRepoCheckbox.checked).toEqual(false);
// await user.click(firstRepoCheckbox);
// expect(firstRepoCheckbox.checked).toEqual(true);
await user.click(nextPageButton[0]);
// await user.click(nextPageButton[0]);
const getSelectedButton = async () =>
await screen.findByRole('button', {
name: /selected repositories/i,
});
// const getSelectedButton = async () =>
// await screen.findByRole('button', {
// name: /selected repositories/i,
// });
const selectedButton = await getSelectedButton();
await user.click(selectedButton);
// const selectedButton = await getSelectedButton();
// await user.click(selectedButton);
expect(firstRepoCheckbox.checked).toEqual(true);
// expect(firstRepoCheckbox.checked).toEqual(true);
await user.type(
await screen.findByRole('textbox', { name: /search repositories/i }),
'13lk3'
);
// await user.type(
// await screen.findByRole('textbox', { name: /search repositories/i }),
// '13lk3'
// );
expect(firstRepoCheckbox.checked).toEqual(true);
// expect(firstRepoCheckbox.checked).toEqual(true);
await clickNext();
clickBack();
expect(firstRepoCheckbox.checked).toEqual(true);
await user.click(firstRepoCheckbox);
await waitFor(() => expect(firstRepoCheckbox.checked).toEqual(false));
}, 30000);
// await clickNext();
// clickBack();
// expect(firstRepoCheckbox.checked).toEqual(true);
// await user.click(firstRepoCheckbox);
// await waitFor(() => expect(firstRepoCheckbox.checked).toEqual(false));
// }, 30000);
});
//
// describe('On Recreate', () => {

View file

@ -1,4 +1,4 @@
import { screen } from '@testing-library/react';
import { screen, waitFor } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { CREATE_BLUEPRINT } from '../../../../../constants';
@ -69,14 +69,6 @@ const deselectFirstRepository = async () => {
);
};
const selectNginxRepository = async () => {
const search = await screen.findByLabelText('Search repositories');
await userEvent.type(search, 'nginx stable repo');
await userEvent.click(
await screen.findByRole('checkbox', { name: /select row 0/i })
);
};
describe('repositories request generated correctly', () => {
const expectedPayloadRepositories: Repository[] = [
{
@ -102,6 +94,35 @@ describe('repositories request generated correctly', () => {
},
];
test('with custom repositories', async () => {
await renderCreateMode();
await goToRepositoriesStep();
await selectFirstRepository();
await goToReviewStep();
const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);
const expectedRequest: CreateBlueprintRequest = {
...blueprintRequest,
customizations: {
custom_repositories: expectedCustomRepositories,
payload_repositories: expectedPayloadRepositories,
},
};
expect(receivedRequest).toEqual(expectedRequest);
});
const selectNginxRepository = async () => {
const search = await screen.findByLabelText('Search repositories');
await userEvent.type(search, 'nginx stable repo');
await waitFor(
() => expect(screen.getByText('nginx stable repo')).toBeInTheDocument
);
await userEvent.click(
await screen.findByRole('checkbox', { name: /select row 0/i })
);
};
const expectedNginxRepository: Repository = {
baseurl: 'http://nginx.org/packages/centos/9/x86_64/',
module_hotfixes: true,
@ -124,24 +145,6 @@ describe('repositories request generated correctly', () => {
name: 'nginx stable repo',
};
test('with custom repositories', async () => {
await renderCreateMode();
await goToRepositoriesStep();
await selectFirstRepository();
await goToReviewStep();
const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);
const expectedRequest: CreateBlueprintRequest = {
...blueprintRequest,
customizations: {
custom_repositories: expectedCustomRepositories,
payload_repositories: expectedPayloadRepositories,
},
};
expect(receivedRequest).toEqual(expectedRequest);
});
test('with custom repository with module_hotfixes', async () => {
await renderCreateMode();
await goToRepositoriesStep();

View file

@ -11,6 +11,7 @@ type repoArgs = {
available_for_version: ListRepositoriesApiArg['availableForVersion'];
limit: ListRepositoriesApiArg['limit'];
offset: ListRepositoriesApiArg['offset'];
search: ListRepositoriesApiArg['search'];
};
export const mockRepositoryResults = (request: repoArgs) => {
@ -53,6 +54,12 @@ const filterRepos = (args: repoArgs): ApiRepositoryResponse[] => {
repos = [...repos, ...fillerRepos];
args.search &&
(repos = repos.filter(
(repo) =>
repo.name?.includes(args.search!) || repo.url?.includes(args.search!)
));
return repos;
};

View file

@ -95,7 +95,14 @@ export const handlers = [
);
const limit = req.url.searchParams.get('limit');
const offset = req.url.searchParams.get('offset');
const args = { available_for_arch, available_for_version, limit, offset };
const search = req.url.searchParams.get('search');
const args = {
available_for_arch,
available_for_version,
limit,
offset,
search,
};
return res(ctx.status(200), ctx.json(mockRepositoryResults(args)));
}),
rest.get(`${CONTENT_SOURCES_API}/repositories/:repo_id`, (req, res, ctx) => {