Skip to content

Commit 8023d2a

Browse files
feat: sync template logic with new backend
- Added new "Content versioning" step that allows users to choose between rolling releases and extended support releases (EUS/E4S) - Implemented validation logic to conditionally show the versioning step based on feature availability and architecture constraints - Updated template request model to include `extended_release` and `extended_release_version` fields - Initialized template request with default extended support values to prevent undefined state - Integrated repository parameters API to fetch extended release features and distribution minor versions
1 parent 1d8f41f commit 8023d2a

8 files changed

Lines changed: 317 additions & 193 deletions

File tree

src/Pages/Templates/TemplatesTable/components/AddOrEditTemplate/AddOrEditTemplateContext.tsx

Lines changed: 39 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {
1+
import React, {
22
createContext,
33
ReactNode,
44
useCallback,
@@ -11,7 +11,7 @@ import { TemplateRequest } from 'services/Templates/TemplateApi';
1111
import { QueryClient, useQueryClient } from 'react-query';
1212
import { useContentListQuery, useRepositoryParams } from 'services/Content/ContentQueries';
1313
import { ContentOrigin, NameLabel, DistributionMinorVersion } from 'services/Content/ContentApi';
14-
import { hardcodeRedHatReposByArchAndVersion } from '../templateHelpers';
14+
import { hardcodeRedHatReposByArchAndVersion, hasExtendedSupport } from '../templateHelpers';
1515
import { useNavigate } from 'react-router-dom';
1616
import { useFetchTemplate } from 'services/Templates/TemplateQueries';
1717
import useRootPath from 'Hooks/useRootPath';
@@ -24,30 +24,20 @@ export interface AddOrEditTemplateContextInterface {
2424
distribution_versions: NameLabel[];
2525
extended_release_features: NameLabel[];
2626
distribution_minor_versions: DistributionMinorVersion[];
27+
useExtendedSupport: boolean;
28+
setUseExtendedSupport: (value: React.SetStateAction<boolean>) => void;
2729
templateRequest: Partial<TemplateRequest>;
2830
setTemplateRequest: (value: React.SetStateAction<Partial<TemplateRequest>>) => void;
2931
selectedRedhatRepos: Set<string>;
3032
setSelectedRedhatRepos: (uuidSet: Set<string>) => void;
3133
selectedCustomRepos: Set<string>;
3234
setSelectedCustomRepos: (uuidSet: Set<string>) => void;
3335
hardcodedRedhatRepositoryUUIDS: Set<string>;
34-
checkIfCurrentStepValid: (index: number) => boolean;
36+
hasInvalidSteps: (index: number) => boolean;
3537
isEdit?: boolean;
3638
editUUID?: string;
3739
}
3840

39-
// template will need to have attributes to store update stream and minor version
40-
// this task is not yet being worked on
41-
42-
/*
43-
Here I will be:
44-
- Tracking my FE state so that it can be USED WITHIN the Add/Edit wizard
45-
- Getting Red Hat repos (based on user subscription - eus 9.6). EPEL & custom aren't affected (no extra logic)
46-
- Pull the ExtendedRelease & ExtendedReleaseVersion from the repos API (safety)
47-
- Prepare the request object (data) that I will be sending to our API to create a new template - setTemplateRequest
48-
49-
*/
50-
5141
export const AddOrEditTemplateContext = createContext({} as AddOrEditTemplateContextInterface);
5242

5343
export const AddOrEditTemplateContextProvider = ({ children }: { children: ReactNode }) => {
@@ -58,45 +48,55 @@ export const AddOrEditTemplateContextProvider = ({ children }: { children: React
5848
const rootPath = useRootPath();
5949

6050
if (isError) navigate(rootPath);
61-
const [templateRequest, setTemplateRequest] = useState<Partial<TemplateRequest>>({});
51+
const [templateRequest, setTemplateRequest] = useState<Partial<TemplateRequest>>({
52+
extended_release: '',
53+
extended_release_version: '',
54+
});
55+
56+
const [useExtendedSupport, setUseExtendedSupport] = useState(false);
6257
const [selectedRedhatRepos, setSelectedRedhatRepos] = useState<Set<string>>(new Set());
6358
const [selectedCustomRepos, setSelectedCustomRepos] = useState<Set<string>>(new Set());
6459
const [hardcodedRedhatRepositories, setHardcodeRepositories] = useState<string[]>([]);
6560
const [hardcodedRedhatRepositoryUUIDS, setHardcodeRepositoryUUIDS] = useState<Set<string>>(
6661
new Set(),
6762
);
6863

69-
// /api/content-sources/v1.0/repository_parameters/
70-
const hasExtendedSupportSubscription = true; // TODO: Get from the Feature Service or our BE
71-
// seems like I'd get it from: "feature_name": "RHEL-E4S-x86_64",
72-
73-
// Like I mentioned before, would need to pull this information from the repo_params API
74-
// distributionMinorVersions - 9.4
75-
// extendedReleaseFeatures - ues, e4s, "none" (object - name, label)
64+
const {
65+
data: {
66+
distribution_versions = [],
67+
distribution_arches = [],
68+
extended_release_features = [],
69+
distribution_minor_versions = [],
70+
} = {},
71+
} = useRepositoryParams();
7672

77-
const stepsValidArray = useMemo(() => {
73+
const stepValidationSequence = useMemo(() => {
7874
const { arch, date, name, version, use_latest, extended_release, extended_release_version } =
7975
templateRequest;
8076

77+
// Valid if: feature is unavailable, unused, or all required fields filled
78+
const isVersioningStepValid =
79+
!hasExtendedSupport(extended_release_features) ||
80+
!useExtendedSupport ||
81+
(extended_release && extended_release_version);
82+
8183
return [
82-
true,
83-
arch && version,
84-
// if they selected [] Extended support releases, then:
85-
// extended_release & extended_release_version
86-
// This data must be present for the step to be valid
87-
!!selectedRedhatRepos.size,
88-
true,
89-
use_latest || isDateValid(date ?? ''),
90-
!!name && name.length < 256,
84+
true, // [0] No step
85+
arch && version, // [1] "Define content" step
86+
isVersioningStepValid, // [2] "Content versioning" step
87+
!!selectedRedhatRepos.size, // [3] "Red Hat repositories" step
88+
true, // [4] "Other repositories" step - optional step
89+
use_latest || isDateValid(date ?? ''), // [5] "Setup date" step
90+
!!name && name.length < 256, // [6] "Detail" step
9191
] as boolean[];
9292
}, [templateRequest, selectedRedhatRepos.size]);
9393

94-
const checkIfCurrentStepValid = useCallback(
94+
const hasInvalidSteps = useCallback(
9595
(stepIndex: number) => {
96-
const stepsToCheck = stepsValidArray.slice(0, stepIndex + 1);
96+
const stepsToCheck = stepValidationSequence.slice(0, stepIndex + 1);
9797
return !stepsToCheck.every((step) => step);
9898
},
99-
[selectedRedhatRepos.size, stepsValidArray],
99+
[selectedRedhatRepos.size, stepValidationSequence],
100100
);
101101

102102
const queryClient = useQueryClient();
@@ -188,20 +188,6 @@ export const AddOrEditTemplateContextProvider = ({ children }: { children: React
188188
}));
189189
}, [templateRequestDependencies]);
190190

191-
const {
192-
data: {
193-
distribution_versions,
194-
distribution_arches,
195-
extended_release_features,
196-
distribution_minor_versions,
197-
} = {
198-
distribution_versions: [],
199-
distribution_arches: [],
200-
extended_release_features: [],
201-
distribution_minor_versions: [],
202-
},
203-
} = useRepositoryParams();
204-
205191
return (
206192
<AddOrEditTemplateContext.Provider
207193
key={uuid}
@@ -218,9 +204,11 @@ export const AddOrEditTemplateContextProvider = ({ children }: { children: React
218204
selectedCustomRepos,
219205
setSelectedCustomRepos,
220206
hardcodedRedhatRepositoryUUIDS,
221-
checkIfCurrentStepValid,
207+
hasInvalidSteps,
222208
isEdit: !!uuid,
223209
editUUID: uuid,
210+
useExtendedSupport,
211+
setUseExtendedSupport,
224212
}}
225213
>
226214
{children}

src/Pages/Templates/TemplatesTable/components/AddOrEditTemplate/AddOrEditTemplateModal.tsx

Lines changed: 62 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import RedhatRepositoriesStep from './steps/RedhatRepositoriesStep';
2020
import CustomRepositoriesStep from './steps/CustomRepositoriesStep';
2121
import { TemplateRequest } from 'services/Templates/TemplateApi';
2222

23-
import DefineContentStep from './steps/DefineContentStep';
2423
import SetUpDateStep from './steps/SetUpDateStep';
2524
import DetailStep from './steps/DetailStep';
2625
import ReviewStep from './steps/ReviewStep';
@@ -31,31 +30,37 @@ import { useEffect, useRef } from 'react';
3130
import { AddTemplateButton } from './AddTemplateButton';
3231
import useRootPath from 'Hooks/useRootPath';
3332
import { TEMPLATES_ROUTE } from 'Routes/constants';
33+
import DefineContentStep from './steps/DefineContentStep';
34+
import ExtendedSupportStep from './steps/ExtendedSupportStep';
35+
import { hasExtendedSupport } from '../templateHelpers';
3436

3537
const useStyles = createUseStyles({
3638
minHeightForSpinner: {
3739
minHeight: '60vh',
3840
},
3941
});
4042

41-
export interface Props {
42-
isDisabled: boolean;
43-
addRepo: (snapshot: boolean) => void;
44-
}
45-
4643
const DEFAULT_STEP_ID = 'define-content';
44+
const DEFAULT_STEP_INDEX = 2;
4745

4846
const stepIdToIndex: Record<string, number> = {
49-
'define-content': 2,
50-
'redhat-repositories': 3,
51-
'custom-repositories': 4,
52-
'set-up-date': 5,
53-
detail: 6,
54-
review: 7,
47+
[DEFAULT_STEP_ID]: DEFAULT_STEP_INDEX,
48+
'content-versioning': 3,
49+
'redhat-repositories': 4,
50+
'custom-repositories': 5,
51+
'set-up-date': 6,
52+
detail: 7,
53+
review: 8,
5554
};
5655

5756
// Component to sync URL with wizard state (must be inside Wizard)
58-
const WizardUrlSync = ({ onCancel }: { onCancel: () => void }) => {
57+
const WizardUrlSync = ({
58+
onCancel,
59+
usesArmArch,
60+
}: {
61+
onCancel: () => void;
62+
usesArmArch: boolean;
63+
}) => {
5964
const { goToStepById, activeStep } = useWizardContext();
6065
const [urlSearchParams] = useSearchParams();
6166

@@ -65,13 +70,18 @@ const WizardUrlSync = ({ onCancel }: { onCancel: () => void }) => {
6570
if (tabParam && tabParam !== activeStep?.id) {
6671
if (stepIdToIndex[tabParam]) {
6772
goToStepById(tabParam);
68-
} else if (tabParam === 'content') {
73+
} else if (tabParam === 'define-content') {
74+
// ARM architecture doesn't support extended support releases, so skip the content versioning step
75+
if (usesArmArch) {
76+
goToStepById('redhat-repositories');
77+
return;
78+
}
6979
goToStepById(DEFAULT_STEP_ID);
7080
} else {
7181
onCancel();
7282
}
7383
}
74-
}, [tabParam, activeStep?.id, goToStepById, onCancel]);
84+
}, [tabParam, activeStep?.id, goToStepById, onCancel, usesArmArch]);
7585

7686
return null;
7787
};
@@ -86,15 +96,28 @@ const AddOrEditTemplateBase = () => {
8696
// Store the original 'from' value on mount (before step navigation changes location.state)
8797
const fromRef = useRef(location.state?.from);
8898

89-
const { isEdit, templateRequest, checkIfCurrentStepValid, editUUID } =
90-
useAddOrEditTemplateContext();
99+
const {
100+
isEdit,
101+
templateRequest,
102+
hasInvalidSteps,
103+
editUUID,
104+
extended_release_features,
105+
distribution_minor_versions,
106+
} = useAddOrEditTemplateContext();
107+
108+
const usesArmArch = templateRequest.arch === 'aarch64';
109+
110+
const selectedVersionMismatch = distribution_minor_versions.every(
111+
(distribution) => distribution.major !== templateRequest.version,
112+
);
91113

92114
// useSafeUUIDParam in AddOrEditTemplateContext already validates the UUID
93115
// If in edit mode and UUID is invalid, it will be an empty string
94116
if (isEdit && !editUUID) throw new Error('UUID is invalid');
95117

96118
const tabParam = urlSearchParams.get('tab');
97-
const initialIndex = tabParam && stepIdToIndex[tabParam] ? stepIdToIndex[tabParam] : 2;
119+
const initialIndex =
120+
tabParam && stepIdToIndex[tabParam] ? stepIdToIndex[tabParam] : DEFAULT_STEP_INDEX;
98121

99122
useEffect(() => {
100123
if (!urlSearchParams.get('tab')) {
@@ -149,7 +172,7 @@ const AddOrEditTemplateBase = () => {
149172
<Wizard
150173
header={
151174
<>
152-
<WizardUrlSync onCancel={onCancel} />
175+
<WizardUrlSync onCancel={onCancel} usesArmArch={usesArmArch} />
153176
<WizardHeader
154177
title={`${isEdit ? 'Edit' : 'Create'} content template`}
155178
titleId={`${isEdit ? 'edit' : 'create'}_content_template`}
@@ -176,25 +199,35 @@ const AddOrEditTemplateBase = () => {
176199
name='Define content'
177200
id='define-content'
178201
key='define-content-key'
179-
footer={{ ...sharedFooterProps, isNextDisabled: checkIfCurrentStepValid(1) }}
202+
footer={{ ...sharedFooterProps, isNextDisabled: hasInvalidSteps(1) }}
180203
>
181204
<DefineContentStep />
182205
</WizardStep>,
183206
<WizardStep
184-
isDisabled={checkIfCurrentStepValid(1)}
207+
key='content-versioning-key'
208+
isDisabled={hasInvalidSteps(1) || selectedVersionMismatch || usesArmArch}
209+
isHidden={!hasExtendedSupport(extended_release_features)}
210+
name='Content versioning'
211+
id='content-versioning'
212+
footer={{ ...sharedFooterProps, isNextDisabled: hasInvalidSteps(2) }}
213+
>
214+
<ExtendedSupportStep />
215+
</WizardStep>,
216+
<WizardStep
217+
isDisabled={hasInvalidSteps(2)}
185218
name='Red Hat repositories'
186219
id='redhat-repositories'
187220
key='redhat-repositories-key'
188-
footer={{ ...sharedFooterProps, isNextDisabled: checkIfCurrentStepValid(2) }}
221+
footer={{ ...sharedFooterProps, isNextDisabled: hasInvalidSteps(3) }}
189222
>
190223
<RedhatRepositoriesStep />
191224
</WizardStep>,
192225
<WizardStep
193-
isDisabled={checkIfCurrentStepValid(2)}
226+
isDisabled={hasInvalidSteps(3)}
194227
name='Other repositories'
195228
id='custom-repositories'
196229
key='custom-repositories-key'
197-
footer={{ ...sharedFooterProps, isNextDisabled: checkIfCurrentStepValid(3) }}
230+
footer={{ ...sharedFooterProps, isNextDisabled: hasInvalidSteps(4) }}
198231
>
199232
<CustomRepositoriesStep />
200233
</WizardStep>,
@@ -203,22 +236,22 @@ const AddOrEditTemplateBase = () => {
203236
<WizardStep
204237
name='Set up date'
205238
id='set-up-date'
206-
isDisabled={checkIfCurrentStepValid(3)}
207-
footer={{ ...sharedFooterProps, isNextDisabled: checkIfCurrentStepValid(4) }}
239+
isDisabled={hasInvalidSteps(4)}
240+
footer={{ ...sharedFooterProps, isNextDisabled: hasInvalidSteps(5) }}
208241
>
209242
<SetUpDateStep />
210243
</WizardStep>
211244
{/* <WizardStep name='Systems (optional)' id='systems' /> */}
212245
<WizardStep
213-
isDisabled={checkIfCurrentStepValid(4)}
214-
footer={{ ...sharedFooterProps, isNextDisabled: checkIfCurrentStepValid(5) }}
246+
isDisabled={hasInvalidSteps(5)}
247+
footer={{ ...sharedFooterProps, isNextDisabled: hasInvalidSteps(6) }}
215248
name='Detail'
216249
id='detail'
217250
>
218251
<DetailStep />
219252
</WizardStep>
220253
<WizardStep
221-
isDisabled={checkIfCurrentStepValid(5)}
254+
isDisabled={hasInvalidSteps(6)}
222255
name='Review'
223256
id='review'
224257
footer={
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import {
2+
defaultTemplateItem,
3+
testRepositoryParamsResponse,
4+
} from '../../../../../../testingHelpers';
5+
import { useAddOrEditTemplateContext } from '../AddOrEditTemplateContext';
6+
import ExtendedSupportStep from './ExtendedSupportStep';
7+
import { render } from '@testing-library/react';
8+
9+
jest.mock('../AddOrEditTemplateContext', () => ({
10+
useAddOrEditTemplateContext: jest.fn(),
11+
}));
12+
13+
const defaultMockContext = {
14+
isEdit: false,
15+
templateRequest: defaultTemplateItem,
16+
setTemplateRequest: () => undefined,
17+
distribution_arches: testRepositoryParamsResponse.distribution_arches,
18+
distribution_versions: testRepositoryParamsResponse.distribution_versions,
19+
extended_release_features: testRepositoryParamsResponse.extended_release_features,
20+
distribution_minor_versions: testRepositoryParamsResponse.distribution_minor_versions,
21+
};
22+
23+
beforeEach(() => {
24+
(useAddOrEditTemplateContext as jest.Mock).mockImplementation(() => defaultMockContext);
25+
});
26+
27+
it('expect ExtendedSupportStep to render correct initial state', () => {
28+
const { getByRole, queryByRole } = render(<ExtendedSupportStep />);
29+
30+
const heading = getByRole('heading', { name: 'Content versioning', level: 1 });
31+
expect(heading).toBeInTheDocument();
32+
33+
const radio = getByRole('radio', { name: 'Latest release' });
34+
expect(radio).toBeChecked();
35+
36+
const updateStreamToggle = queryByRole('button', { name: 'Update stream toggle' });
37+
expect(updateStreamToggle).not.toBeInTheDocument();
38+
39+
const minorVersionToggle = queryByRole('button', { name: 'Minor version toggle' });
40+
expect(minorVersionToggle).not.toBeInTheDocument();
41+
});

0 commit comments

Comments
 (0)