Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wizard: Add kernel append input (HMS-5299) #2734

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
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
@@ -1,9 +1,161 @@
import React from 'react';
import React, { useState } from 'react';

import { FormGroup } from '@patternfly/react-core';
import {
Button,
Chip,
ChipGroup,
FormGroup,
HelperText,
HelperTextItem,
TextInputGroup,
TextInputGroupMain,
TextInputGroupUtilities,
} from '@patternfly/react-core';
import { PlusCircleIcon, TimesIcon } from '@patternfly/react-icons';

import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
import { useGetOscapCustomizationsQuery } from '../../../../../store/imageBuilderApi';
import {
addKernelArg,
removeKernelArg,
selectComplianceProfileID,
selectDistribution,
selectKernel,
} from '../../../../../store/wizardSlice';
import { isKernelArgumentValid } from '../../../validators';

const KernelArguments = () => {
return <FormGroup isRequired={false} label="Append"></FormGroup>;
const dispatch = useAppDispatch();
const kernelAppend = useAppSelector(selectKernel).append;

const release = useAppSelector(selectDistribution);
const complianceProfileID = useAppSelector(selectComplianceProfileID);

const { data: oscapProfileInfo } = useGetOscapCustomizationsQuery(
{
distribution: release,
// @ts-ignore if complianceProfileID is undefined the query is going to get skipped, so it's safe here to ignore the linter here
profile: complianceProfileID,
},
{
skip: !complianceProfileID,
}
);

const [inputValue, setInputValue] = useState('');
const [errorText, setErrorText] = useState('');

const requiredByOpenSCAP = kernelAppend.filter((arg) =>
oscapProfileInfo?.kernel?.append?.split(' ').includes(arg.name)
);
const notRequiredByOpenSCAP = kernelAppend.filter(
(arg) => !oscapProfileInfo?.kernel?.append?.split(' ').includes(arg.name)
);

const onTextInputChange = (
_event: React.FormEvent<HTMLInputElement>,
value: string
) => {
setInputValue(value);
setErrorText('');
};

const addArgument = (value: string) => {
if (
isKernelArgumentValid(value) &&
!kernelAppend.some((arg) => arg.name === value)
) {
dispatch(addKernelArg({ name: value }));
setInputValue('');
setErrorText('');
}

if (kernelAppend.some((arg) => arg.name === value)) {
setErrorText(`Kernel argument already exists.`);
}

if (!isKernelArgumentValid(value)) {
setErrorText('Invalid format.');
}
};

const handleKeyDown = (e: React.KeyboardEvent, value: string) => {
if (e.key === 'Enter') {
e.preventDefault();
addArgument(value);
}
};

const handleAddItem = (e: React.MouseEvent, value: string) => {
addArgument(value);
};

const handleClear = () => {
setInputValue('');
setErrorText('');
};

return (
<FormGroup isRequired={false} label="Append">
<TextInputGroup>
<TextInputGroupMain
placeholder="Add kernel argument"
onChange={onTextInputChange}
value={inputValue}
onKeyDown={(e) => handleKeyDown(e, inputValue)}
/>
<TextInputGroupUtilities>
<Button
variant="plain"
onClick={(e) => handleAddItem(e, inputValue)}
isDisabled={!inputValue}
aria-label="Add kernel argument"
>
<PlusCircleIcon className="pf-v5-u-primary-color-100" />
</Button>
<Button
variant="plain"
onClick={handleClear}
isDisabled={!inputValue}
aria-label="Clear input"
>
<TimesIcon />
</Button>
</TextInputGroupUtilities>
</TextInputGroup>
{errorText && (
<HelperText>
<HelperTextItem variant={'error'}>{errorText}</HelperTextItem>
</HelperText>
)}
<ChipGroup
categoryName="Required by OpenSCAP"
numChips={20}
className="pf-v5-u-mt-sm pf-v5-u-w-100"
>
{requiredByOpenSCAP.map((arg) => (
<Chip
key={arg.name}
onClick={() => dispatch(removeKernelArg(arg.name))}
isReadOnly
>
{arg.name}
</Chip>
))}
</ChipGroup>

<ChipGroup numChips={20} className="pf-v5-u-mt-sm pf-v5-u-w-100">
{notRequiredByOpenSCAP.map((arg) => (
<Chip
key={arg.name}
onClick={() => dispatch(removeKernelArg(arg.name))}
>
{arg.name}
</Chip>
))}
</ChipGroup>
</FormGroup>
);
};

export default KernelArguments;
22 changes: 19 additions & 3 deletions src/Components/CreateImageWizard/steps/Oscap/Oscap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@
changeEnabledServices,
changeMaskedServices,
changeDisabledServices,
changeKernelAppend,
selectComplianceType,
clearKernelAppend,
addKernelArg,
} from '../../../../store/wizardSlice';
import { useHasSpecificTargetOnly } from '../../utilities/hasSpecificTargetOnly';
import { parseSizeUnit } from '../../utilities/parseSizeUnit';
Expand Down Expand Up @@ -154,7 +155,7 @@
}
}
}
}, [isSuccessPolicies]);

Check warning on line 158 in src/Components/CreateImageWizard/steps/Oscap/Oscap.tsx

View workflow job for this annotation

GitHub Actions / dev-check

React Hook useEffect has missing dependencies: 'dispatch', 'policies', 'policyID', and 'policyTitle'. Either include them or remove the dependency array

const handleToggle = () => {
if (!isOpen && complianceType === 'openscap') {
Expand All @@ -174,7 +175,7 @@
clearOscapPackages(currentProfileData?.packages || []);
dispatch(changeFileSystemConfigurationType('automatic'));
handleServices(undefined);
dispatch(changeKernelAppend(''));
dispatch(clearKernelAppend());
};

const handlePackages = (
Expand Down Expand Up @@ -228,6 +229,21 @@
dispatch(changeDisabledServices(services?.disabled || []));
};

const handleKernelAppend = (kernelAppend: string | undefined) => {
dispatch(clearKernelAppend());

if (kernelAppend) {
const kernelArgsArray = kernelAppend.split(' ');
for (const arg in kernelArgsArray) {
dispatch(
addKernelArg({
name: kernelArgsArray[arg],
})
);
}
}
};

const handleSelect = (
_event: React.MouseEvent<Element, MouseEvent>,
selection: OScapSelectOptionValueType | ComplianceSelectOptionValueType
Expand All @@ -251,7 +267,7 @@
handlePartitions(oscapPartitions);
handlePackages(oldOscapPackages, newOscapPackages);
handleServices(response.services);
dispatch(changeKernelAppend(response.kernel?.append || ''));
handleKernelAppend(response.kernel?.append);
if (complianceType === 'openscap') {
dispatch(
changeCompliance({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
policyTitle: pol.title,
})
);
}, [isSuccessPolicyInfo]);

Check warning on line 81 in src/Components/CreateImageWizard/steps/Oscap/OscapProfileInformation.tsx

View workflow job for this annotation

GitHub Actions / dev-check

React Hook useEffect has missing dependencies: 'dispatch' and 'policyInfo'. Either include them or remove the dependency array

const enabledServicesDisplayString =
oscapProfileInfo?.services?.enabled?.join(' ');
Expand Down Expand Up @@ -130,19 +130,6 @@
>
{oscapProfile?.profile_id}
</TextListItem>
<TextListItem
component={TextListItemVariants.dt}
className="pf-v5-u-min-width"
>
Kernel arguments:
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
<CodeBlock>
<CodeBlockCode>
{oscapProfileInfo?.kernel?.append}
</CodeBlockCode>
</CodeBlock>
</TextListItem>
<TextListItem
component={TextListItemVariants.dt}
className="pf-v5-u-min-width"
Expand Down
4 changes: 2 additions & 2 deletions src/Components/CreateImageWizard/steps/Oscap/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ import {
changeDisabledServices,
removePackage,
changeFileSystemConfigurationType,
changeKernelAppend,
selectDistribution,
selectComplianceType,
clearKernelAppend,
} from '../../../../store/wizardSlice';
import { useGetEnvironment } from '../../../../Utilities/useGetEnvironment';

Expand Down Expand Up @@ -84,7 +84,7 @@ const OscapStep = () => {
dispatch(changeEnabledServices([]));
dispatch(changeMaskedServices([]));
dispatch(changeDisabledServices([]));
dispatch(changeKernelAppend(''));
dispatch(clearKernelAppend());
};

return (
Expand Down
24 changes: 20 additions & 4 deletions src/Components/CreateImageWizard/utilities/requestMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
GcpUploadRequestOptions,
ImageRequest,
ImageTypes,
Kernel,
OpenScap,
OpenScapCompliance,
OpenScapProfile,
Expand Down Expand Up @@ -317,7 +318,10 @@ function commonRequestToState(
disabled: request.customizations?.services?.disabled || [],
},
kernel: {
append: request.customizations?.kernel?.append || '',
append:
request.customizations?.kernel?.append
?.split(' ')
.map((arg) => ({ name: arg })) || [],
},
timezone: {
timezone: request.customizations.timezone?.timezone || '',
Expand Down Expand Up @@ -521,9 +525,7 @@ const getCustomizations = (state: RootState, orgID: string): Customizations => {
users: getUsers(state),
services: getServices(state),
hostname: selectHostname(state) || undefined,
kernel: selectKernel(state).append
? { append: selectKernel(state).append }
: undefined,
kernel: getKernel(state),
groups: undefined,
timezone: getTimezone(state),
locale: getLocale(state),
Expand Down Expand Up @@ -560,6 +562,20 @@ const getServices = (state: RootState): Services | undefined => {
};
};

const getKernel = (state: RootState): Kernel | undefined => {
const kernelAppend = selectKernel(state).append;

if (kernelAppend && kernelAppend.length > 0) {
return {
append: selectKernel(state)
.append.map((arg) => arg.name)
.join(' '),
};
}

return undefined;
};

const getOpenscap = (state: RootState): OpenScap | undefined => {
const complianceType = selectComplianceType(state);
const profile = selectComplianceProfileID(state);
Expand Down
8 changes: 8 additions & 0 deletions src/Components/CreateImageWizard/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ export const isHostnameValid = (hostname: string) => {
);
};

export const isKernelArgumentValid = (arg: string) => {
if (!arg) {
return true;
}

return /^[a-zA-Z0-9=-_,"' ]*$/.test(arg);
};

export const isPortValid = (port: string) => {
return /^(\d{1,5}|[a-z]{1,6})(-\d{1,5})?:[a-z]{1,6}$/.test(port);
};
33 changes: 28 additions & 5 deletions src/store/wizardSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ type UserSshKeyPayload = {
sshKey: string;
};

export type KernelArgument = {
name: string;
};

export type wizardState = {
env: {
serverUrl: string;
Expand Down Expand Up @@ -127,7 +131,7 @@ export type wizardState = {
disabled: string[];
};
kernel: {
append: string;
append: KernelArgument[];
};
locale: Locale;
details: {
Expand Down Expand Up @@ -204,7 +208,7 @@ export const initialState: wizardState = {
disabled: [],
},
kernel: {
append: '',
append: [],
},
locale: {
languages: [],
Expand Down Expand Up @@ -800,8 +804,25 @@ export const wizardSlice = createSlice({
changeDisabledServices: (state, action: PayloadAction<string[]>) => {
state.services.disabled = action.payload;
},
changeKernelAppend: (state, action: PayloadAction<string>) => {
state.kernel.append = action.payload;
addKernelArg: (state, action: PayloadAction<KernelArgument>) => {
const existingArgIndex = state.kernel.append.findIndex(
(arg) => arg.name === action.payload.name
);

if (existingArgIndex !== -1) {
state.kernel.append[existingArgIndex] = action.payload;
} else {
state.kernel.append.push(action.payload);
}
},
removeKernelArg: (state, action: PayloadAction<KernelArgument['name']>) => {
state.kernel.append.splice(
state.kernel.append.findIndex((arg) => arg.name === action.payload),
1
);
},
clearKernelAppend: (state) => {
state.kernel.append = [];
},
changeTimezone: (state, action: PayloadAction<string>) => {
state.timezone.timezone = action.payload;
Expand Down Expand Up @@ -922,7 +943,9 @@ export const {
changeEnabledServices,
changeMaskedServices,
changeDisabledServices,
changeKernelAppend,
addKernelArg,
removeKernelArg,
clearKernelAppend,
changeTimezone,
addNtpServer,
removeNtpServer,
Expand Down
Loading
Loading