Wizard: Migrate activation keys calls to RTK Query

This migrates calls to RHSM endpoints `activation_keys` and `/activation_keys/{name}` to RTK Query.

Tests were also updated to use Mock Service Worker instead of previous Jest mock function.
This commit is contained in:
regexowl 2023-03-30 09:23:20 +02:00 committed by Lucas Garfield
parent 02a584b36f
commit 5f99bc79cf
9 changed files with 374 additions and 360 deletions

View file

@ -1,7 +1,10 @@
import React, { useEffect, useState } from 'react';
import React, { useContext } from 'react';
import { useFormApi } from '@data-driven-forms/react-form-renderer';
import WizardContext from '@data-driven-forms/react-form-renderer/wizard-context';
import {
Alert,
Spinner,
Text,
TextContent,
TextList,
@ -21,119 +24,152 @@ import {
Tr,
} from '@patternfly/react-table';
import api from '../../../api';
import { useGetActivationKeyInformationQuery } from '../../../store/apiSlice';
const ActivationKeyInformation = () => {
const { getState } = useFormApi();
const { currentStep } = useContext(WizardContext);
const activationKey = getState()?.values?.['subscription-activation-key'];
const [role, setRole] = useState(undefined);
const [serviceLevel, setServiceLevel] = useState(undefined);
const [usage, setUsage] = useState(undefined);
const [additionalRepositories, setRepositories] = useState(undefined);
useEffect(() => {
const fetchKeyInformation = async () => {
const data = await api.getActivationKey(activationKey);
setRole(data?.role);
setServiceLevel(data?.serviceLevel);
setUsage(data?.usage);
setRepositories(data?.additionalRepositories);
};
fetchKeyInformation();
}, []);
const {
data: activationKeyInfo,
isFetching: isFetchingActivationKeyInfo,
isSuccess: isSuccessActivationKeyInfo,
isError: isErrorActivationKeyInfo,
} = useGetActivationKeyInformationQuery(activationKey, {
skip: !activationKey,
});
return (
<>
<TextContent>
<TextList component={TextListVariants.dl}>
<TextListItem component={TextListItemVariants.dt}>Name:</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{activationKey}
</TextListItem>
<TextListItem component={TextListItemVariants.dt}>Role:</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{role || 'Not defined'}
</TextListItem>
<TextListItem component={TextListItemVariants.dt}>SLA:</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{serviceLevel || 'Not defined'}
</TextListItem>
<TextListItem component={TextListItemVariants.dt}>
Usage:
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{usage || 'Not defined'}
</TextListItem>
<TextListItem component={TextListItemVariants.dt}>
Additional repositories:
<Popover
bodyContent={
<TextContent>
<Text>
The core repositories for your operating system version are
always enabled and do not need to be explicitly added to the
activation key.
</Text>
</TextContent>
}
>
<Button
variant="plain"
aria-label="About additional repositories"
className="pf-u-pl-sm pf-u-pt-0 pf-u-pb-0"
isSmall
>
<HelpIcon />
</Button>
</Popover>
</TextListItem>
<TextListItem
component={TextListItemVariants.dd}
className="pf-u-display-flex pf-u-align-items-flex-end"
>
{additionalRepositories?.length > 0 ? (
{isFetchingActivationKeyInfo && <Spinner isSVG size="lg" />}
{isSuccessActivationKeyInfo && (
<TextContent>
<TextList component={TextListVariants.dl}>
<TextListItem component={TextListItemVariants.dt}>
Name:
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{activationKey}
</TextListItem>
<TextListItem component={TextListItemVariants.dt}>
Role:
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{activationKeyInfo.body?.role || 'Not defined'}
</TextListItem>
<TextListItem component={TextListItemVariants.dt}>
SLA:
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{activationKeyInfo.body?.serviceLevel || 'Not defined'}
</TextListItem>
<TextListItem component={TextListItemVariants.dt}>
Usage:
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{activationKeyInfo.body?.usage || 'Not defined'}
</TextListItem>
<TextListItem component={TextListItemVariants.dt}>
Additional repositories:
<Popover
bodyContent={
<TextContent>
<Text component={TextVariants.h3}>
Additional repositories
<Text>
The core repositories for your operating system version
are always enabled and do not need to be explicitly added
to the activation key.
</Text>
<TableComposable
aria-label="Additional repositories table"
variant="compact"
>
<Thead>
<Tr>
<Th>Name</Th>
</Tr>
</Thead>
<Tbody data-testid="additional-repositories-table">
{additionalRepositories?.map((repo, index) => (
<Tr key={index}>
<Td>{repo.repositoryLabel}</Td>
</Tr>
))}
</Tbody>
</TableComposable>
</TextContent>
}
>
<Button
data-testid="repositories-popover-button"
variant="link"
aria-label="Show additional repositories"
className="pf-u-pl-0 pf-u-pt-0 pf-u-pb-0"
variant="plain"
aria-label="About additional repositories"
className="pf-u-pl-sm pf-u-pt-0 pf-u-pb-0"
isSmall
>
{additionalRepositories?.length} repositories
<HelpIcon />
</Button>
</Popover>
) : (
'None'
)}
</TextListItem>
</TextList>
</TextContent>
</TextListItem>
<TextListItem
component={TextListItemVariants.dd}
className="pf-u-display-flex pf-u-align-items-flex-end"
>
{activationKeyInfo.body?.additionalRepositories?.length > 0 ? (
<Popover
bodyContent={
<TextContent>
<Text component={TextVariants.h3}>
Additional repositories
</Text>
<TableComposable
aria-label="Additional repositories table"
variant="compact"
>
<Thead>
<Tr>
<Th>Name</Th>
</Tr>
</Thead>
<Tbody data-testid="additional-repositories-table">
{activationKeyInfo.body?.additionalRepositories?.map(
(repo, index) => (
<Tr key={index}>
<Td>{repo.repositoryLabel}</Td>
</Tr>
)
)}
</Tbody>
</TableComposable>
</TextContent>
}
>
<Button
data-testid="repositories-popover-button"
variant="link"
aria-label="Show additional repositories"
className="pf-u-pl-0 pf-u-pt-0 pf-u-pb-0"
>
{activationKeyInfo.body?.additionalRepositories?.length}{' '}
repositories
</Button>
</Popover>
) : (
'None'
)}
</TextListItem>
</TextList>
</TextContent>
)}
{isErrorActivationKeyInfo && (
<TextContent>
<TextList component={TextListVariants.dl}>
<TextListItem component={TextListItemVariants.dt}>
Name:
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{activationKey}
</TextListItem>
</TextList>
</TextContent>
)}
{currentStep.name === 'registration' && (
<>
<br />
<Alert
title="Information about the activation key unavailable"
variant="danger"
isPlain
isInline
>
Information about the activation key cannot be loaded. Please check
the key was not removed and try again later.
</Alert>
</>
)}
</>
);
};

View file

@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react';
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,
FormGroup,
Select,
SelectOption,
@ -11,26 +12,25 @@ import {
} from '@patternfly/react-core';
import PropTypes from 'prop-types';
import api from '../../../api';
import { useGetActivationKeysQuery } from '../../../store/apiSlice';
const ActivationKeys = ({ label, isRequired, ...props }) => {
const { change, getState } = useFormApi();
const { input } = useFieldApi(props);
const [activationKeys, setActivationKeys] = useState([]);
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [activationKeySelected, selectActivationKey] = useState(
getState()?.values?.['subscription-activation-key']
);
useEffect(() => {
setIsLoading(true);
const data = api.getActivationKeys();
data.then((keys) => {
setActivationKeys(keys);
setIsLoading(false);
});
const {
data: activationKeys,
isFetching: isFetchingActivationKeys,
isSuccess: isSuccessActivationKeys,
isError: isErrorActivationKeys,
refetch,
} = useGetActivationKeysQuery();
useEffect(() => {
if (insights.chrome.isProd()) {
change('subscription-server-url', 'subscription.rhsm.redhat.com');
change('subscription-base-url', 'https://cdn.redhat.com/');
@ -51,36 +51,57 @@ const ActivationKeys = ({ label, isRequired, ...props }) => {
change(input.name, undefined);
};
const handleToggle = () => {
if (!isOpen) {
refetch();
}
setIsOpen(!isOpen);
};
return (
<FormGroup
isRequired={isRequired}
label={label}
data-testid="subscription-activation-key"
>
<Select
ouiaId="activation_key_select"
variant={SelectVariant.typeahead}
onToggle={() => setIsOpen(!isOpen)}
onSelect={setActivationKey}
onClear={handleClear}
selections={activationKeySelected}
isOpen={isOpen}
placeholderText="Select activation key"
typeAheadAriaLabel="Select activation key"
<>
<FormGroup
isRequired={isRequired}
label={label}
data-testid="subscription-activation-key"
>
{isLoading && (
<SelectOption
isNoResultsOption={true}
data-testid="activation-keys-loading"
>
<Spinner isSVG size="lg" />
</SelectOption>
)}
{activationKeys.map((key, index) => (
<SelectOption key={index} value={key.name} />
))}
</Select>
</FormGroup>
<Select
ouiaId="activation_key_select"
variant={SelectVariant.typeahead}
onToggle={handleToggle}
onSelect={setActivationKey}
onClear={handleClear}
selections={activationKeySelected}
isOpen={isOpen}
placeholderText="Select activation key"
typeAheadAriaLabel="Select activation key"
isDisabled={!isSuccessActivationKeys}
>
{isSuccessActivationKeys &&
activationKeys.body.map((key, index) => (
<SelectOption key={index} value={key.name} />
))}
{isFetchingActivationKeys && (
<SelectOption
isNoResultsOption={true}
data-testid="activation-keys-loading"
>
<Spinner isSVG size="md" />
</SelectOption>
)}
</Select>
</FormGroup>
{isErrorActivationKeys && (
<Alert
title="Activation keys unavailable"
variant="danger"
isPlain
isInline
>
Activation keys cannot be reached, try again later.
</Alert>
)}
</>
);
};

View file

@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import { useFormApi } from '@data-driven-forms/react-form-renderer';
import {
Alert,
Button,
Popover,
Spinner,
@ -27,6 +28,7 @@ import {
useGetAWSSourcesQuery,
useGetAzureSourcesQuery,
} from '../../../store/apiSlice';
import { useGetActivationKeyInformationQuery } from '../../../store/apiSlice';
import { googleAccType } from '../steps/googleCloud';
const ExpirationWarning = () => {
@ -429,85 +431,103 @@ export const RegisterLaterList = () => {
export const RegisterNowList = () => {
const { getState } = useFormApi();
const activationKey = getState()?.values?.['subscription-activation-key'];
const { isError } = useGetActivationKeyInformationQuery(activationKey, {
skip: !activationKey,
});
return (
<TextContent>
<TextList component={TextListVariants.dl}>
<TextListItem
component={TextListItemVariants.dt}
className="pf-u-min-width"
>
Registration type
</TextListItem>
<TextListItem
component={TextListItemVariants.dd}
data-testid="review-registration"
>
<TextList isPlain>
{getState()?.values?.['register-system']?.startsWith(
'register-now'
) && (
<TextListItem>
Register with Red Hat Subscription Manager (RHSM)
<br />
</TextListItem>
)}
{(getState()?.values?.['register-system'] ===
'register-now-insights' ||
getState()?.values?.['register-system'] ===
'register-now-rhc') && (
<TextListItem>
Connect to Red Hat Insights
<br />
</TextListItem>
)}
{getState()?.values?.['register-system'] === 'register-now-rhc' && (
<TextListItem>
Use remote host configuration (RHC) utility
<br />
</TextListItem>
)}
</TextList>
</TextListItem>
<TextListItem component={TextListItemVariants.dt}>
Activation key
<Popover
bodyContent={
<TextContent>
<Text>
Activation keys enable you to register a system with
appropriate subscriptions, system purpose, and repositories
attached.
<br />
<br />
If using an activation key with command line registration, you
must provide your organization&apos;s ID. Your
organization&apos;s ID is{' '}
{getState()?.values?.['subscription-organization-id'] !==
undefined ? (
getState()?.values?.['subscription-organization-id']
) : (
<Spinner size="md" />
)}
</Text>
</TextContent>
}
<>
<TextContent>
<TextList component={TextListVariants.dl}>
<TextListItem
component={TextListItemVariants.dt}
className="pf-u-min-width"
>
<Button
variant="plain"
aria-label="About activation key"
className="pf-u-pl-sm pf-u-pt-0 pf-u-pb-0"
isSmall
Registration type
</TextListItem>
<TextListItem
component={TextListItemVariants.dd}
data-testid="review-registration"
>
<TextList isPlain>
{getState()?.values?.['register-system']?.startsWith(
'register-now'
) && (
<TextListItem>
Register with Red Hat Subscription Manager (RHSM)
<br />
</TextListItem>
)}
{(getState()?.values?.['register-system'] ===
'register-now-insights' ||
getState()?.values?.['register-system'] ===
'register-now-rhc') && (
<TextListItem>
Connect to Red Hat Insights
<br />
</TextListItem>
)}
{getState()?.values?.['register-system'] ===
'register-now-rhc' && (
<TextListItem>
Use remote host configuration (RHC) utility
<br />
</TextListItem>
)}
</TextList>
</TextListItem>
<TextListItem component={TextListItemVariants.dt}>
Activation key
<Popover
bodyContent={
<TextContent>
<Text>
Activation keys enable you to register a system with
appropriate subscriptions, system purpose, and repositories
attached.
<br />
<br />
If using an activation key with command line registration,
you must provide your organization&apos;s ID. Your
organization&apos;s ID is{' '}
{getState()?.values?.['subscription-organization-id'] !==
undefined ? (
getState()?.values?.['subscription-organization-id']
) : (
<Spinner size="md" />
)}
</Text>
</TextContent>
}
>
<HelpIcon />
</Button>
</Popover>
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
<ActivationKeyInformation />
</TextListItem>
</TextList>
<br />
</TextContent>
<Button
variant="plain"
aria-label="About activation key"
className="pf-u-pl-sm pf-u-pt-0 pf-u-pb-0"
isSmall
>
<HelpIcon />
</Button>
</Popover>
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
<ActivationKeyInformation />
</TextListItem>
</TextList>
<br />
</TextContent>
{isError && (
<Alert
title="Information about the activation key unavailable"
variant="danger"
isPlain
isInline
>
Information about the activation key cannot be loaded. Please check
the key was not removed and try again later.
</Alert>
)}
</>
);
};

View file

@ -1,6 +1,6 @@
import axios from 'axios';
import { CONTENT_SOURCES, IMAGE_BUILDER_API, RHSM_API } from './constants';
import { CONTENT_SOURCES, IMAGE_BUILDER_API } from './constants';
const postHeaders = { headers: { 'Content-Type': 'application/json' } };
@ -77,18 +77,6 @@ async function getVersion() {
return request.data;
}
async function getActivationKeys() {
const path = '/activation_keys';
const request = await axios.get(RHSM_API.concat(path));
return request.data.body;
}
async function getActivationKey(name) {
const path = `/activation_keys/${name}`;
const request = await axios.get(RHSM_API.concat(path));
return request.data.body;
}
// get clones of a compose
async function getClones(id, limit, offset) {
const params = new URLSearchParams({
@ -127,6 +115,4 @@ export default {
getPackagesContentSources,
getRepositories,
getVersion,
getActivationKeys,
getActivationKey,
};

View file

@ -1,6 +1,10 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { IMAGE_BUILDER_API, PROVISIONING_SOURCES_ENDPOINT } from '../constants';
import {
IMAGE_BUILDER_API,
PROVISIONING_SOURCES_ENDPOINT,
RHSM_API,
} from '../constants';
export const apiSlice = createApi({
reducerPath: 'api',
@ -53,6 +57,12 @@ export const apiSlice = createApi({
query: (distribution) =>
`${IMAGE_BUILDER_API}/architectures/${distribution}`,
}),
getActivationKeys: builder.query({
query: () => `${RHSM_API}/activation_keys`,
}),
getActivationKeyInformation: builder.query({
query: (activationKey) => `${RHSM_API}/activation_keys/${activationKey}`,
}),
}),
});
@ -61,5 +71,7 @@ export const {
useGetArchitecturesByDistributionQuery,
useGetAzureSourcesQuery,
useGetAzureSourceDetailQuery,
useGetActivationKeysQuery,
useGetActivationKeyInformationQuery,
usePrefetch,
} = apiSlice;

View file

@ -32,17 +32,6 @@ describe('Step Upload to Azure', () => {
// scrollTo is not defined in jsdom
window.HTMLElement.prototype.scrollTo = function () {};
// mock the activation key api call
const mockActivationKeys = [{ name: 'name0' }, { name: 'name1' }];
jest
.spyOn(api, 'getActivationKeys')
.mockImplementation(() => Promise.resolve(mockActivationKeys));
const mockActivationKey = { body: [{ name: 'name0' }, { name: 'name1' }] };
jest.spyOn(api, 'getActivationKey').mockImplementation((name) => {
return Promise.resolve(mockActivationKey[name]);
});
jest
.spyOn(api, 'getRepositories')
.mockImplementation(() => Promise.resolve(mockRepositoryResults));

View file

@ -143,17 +143,6 @@ beforeAll(() => {
// scrollTo is not defined in jsdom
window.HTMLElement.prototype.scrollTo = function () {};
// mock the activation key api call
const mockActivationKeys = [{ name: 'name0' }, { name: 'name1' }];
jest
.spyOn(api, 'getActivationKeys')
.mockImplementation(() => Promise.resolve(mockActivationKeys));
const mockActivationKey = { body: [{ name: 'name0' }, { name: 'name1' }] };
jest.spyOn(api, 'getActivationKey').mockImplementation((name) => {
return Promise.resolve(mockActivationKey[name]);
});
global.insights = {
chrome: {
auth: {
@ -804,57 +793,6 @@ describe('Click through all steps', () => {
screen.getByRole('button', { name: /Next/ }).click();
// registration
const mockActivationKeys = [
{ id: '0', name: 'name0' },
{ id: 1, name: 'name1' },
];
jest
.spyOn(api, 'getActivationKeys')
.mockImplementation(() => Promise.resolve(mockActivationKeys));
const mockActivationKey = {
name0: {
additionalRepositories: [
{
repositoryLabel: 'repository0',
},
{
repositoryLabel: 'repository1',
},
{
repositoryLabel: 'repository2',
},
],
id: '0',
name: 'name0',
releaseVersion: '',
role: '',
serviceLevel: 'Self-Support',
usage: 'Production',
},
name1: {
additionalRepositories: [
{
repositoryLabel: 'repository3',
},
{
repositoryLabel: 'repository4',
},
{
repositoryLabel: 'repository5',
},
],
id: '1',
name: 'name1',
releaseVersion: '',
role: '',
serviceLevel: 'Premium',
usage: 'Production',
},
};
jest.spyOn(api, 'getActivationKey').mockImplementation((name) => {
return Promise.resolve(mockActivationKey[name]);
});
const activationKeyDropdown = await screen.findByRole('textbox', {
name: 'Select activation key',
});

View file

@ -110,17 +110,6 @@ beforeAll(() => {
// scrollTo is not defined in jsdom
window.HTMLElement.prototype.scrollTo = function () {};
// mock the activation key api call
const mockActivationKeys = [{ name: 'name0' }, { name: 'name1' }];
jest
.spyOn(api, 'getActivationKeys')
.mockImplementation(() => Promise.resolve(mockActivationKeys));
const mockActivationKey = { body: [{ name: 'name0' }, { name: 'name1' }] };
jest.spyOn(api, 'getActivationKey').mockImplementation((name) => {
return Promise.resolve(mockActivationKey[name]);
});
global.insights = {
chrome: {
auth: {
@ -1380,57 +1369,6 @@ describe('Click through all steps', () => {
screen.getByRole('button', { name: /Next/ }).click();
// registration
const mockActivationKeys = [
{ id: '0', name: 'name0' },
{ id: 1, name: 'name1' },
];
jest
.spyOn(api, 'getActivationKeys')
.mockImplementation(() => Promise.resolve(mockActivationKeys));
const mockActivationKey = {
name0: {
additionalRepositories: [
{
repositoryLabel: 'repository0',
},
{
repositoryLabel: 'repository1',
},
{
repositoryLabel: 'repository2',
},
],
id: '0',
name: 'name0',
releaseVersion: '',
role: '',
serviceLevel: 'Self-Support',
usage: 'Production',
},
name1: {
additionalRepositories: [
{
repositoryLabel: 'repository3',
},
{
repositoryLabel: 'repository4',
},
{
repositoryLabel: 'repository5',
},
],
id: '1',
name: 'name1',
releaseVersion: '',
role: '',
serviceLevel: 'Premium',
usage: 'Production',
},
};
jest.spyOn(api, 'getActivationKey').mockImplementation((name) => {
return Promise.resolve(mockActivationKey[name]);
});
const registrationRadio = screen.getByTestId('registration-radio-now');
await user.click(registrationRadio);

View file

@ -1,6 +1,6 @@
import { rest } from 'msw';
import { PROVISIONING_SOURCES_ENDPOINT } from '../../constants';
import { PROVISIONING_SOURCES_ENDPOINT, RHSM_API } from '../../constants';
const baseURL = 'http://localhost';
@ -245,4 +245,78 @@ export const handlers = [
}
}
),
rest.get(baseURL.concat(`${RHSM_API}/activation_keys`), (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
body: [
{
id: 0,
name: 'name0',
},
{
id: 1,
name: 'name1',
},
],
})
);
}),
rest.get(
baseURL.concat(`${RHSM_API}/activation_keys/:key`),
(req, res, ctx) => {
const { key } = req.params;
if (key === 'name0') {
return res(
ctx.status(200),
ctx.json({
body: {
additionalRepositories: [
{
repositoryLabel: 'repository0',
},
{
repositoryLabel: 'repository1',
},
{
repositoryLabel: 'repository2',
},
],
id: '0',
name: 'name0',
releaseVersion: '',
role: '',
serviceLevel: 'Self-Support',
usage: 'Production',
},
})
);
} else if (key === 'name1') {
return res(
ctx.status(200),
ctx.json({
body: {
additionalRepositories: [
{
repositoryLabel: 'repository3',
},
{
repositoryLabel: 'repository4',
},
{
repositoryLabel: 'repository5',
},
],
id: '1',
name: 'name1',
releaseVersion: '',
role: '',
serviceLevel: 'Premium',
usage: 'Production',
},
})
);
}
}
),
];