update style across the project

The eslint updates require style changes in all components.
This commit is contained in:
Jacob Kozol 2022-05-23 11:38:16 +02:00 committed by Sanne Raymaekers
parent 7959f2a563
commit 4fa71cede8
56 changed files with 5973 additions and 5177 deletions

View file

@ -1,71 +1,86 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { FormGroup, Spinner, Select, SelectOption, SelectVariant } from '@patternfly/react-core';
import {
FormGroup,
Spinner,
Select,
SelectOption,
SelectVariant,
} from '@patternfly/react-core';
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
import api from '../../../api';
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']);
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);
});
}, []);
useEffect(() => {
setIsLoading(true);
const data = api.getActivationKeys();
data.then((keys) => {
setActivationKeys(keys);
setIsLoading(false);
});
}, []);
const setActivationKey = (_, selection) => {
selectActivationKey(selection);
setIsOpen(false);
change(input.name, selection);
};
const setActivationKey = (_, selection) => {
selectActivationKey(selection);
setIsOpen(false);
change(input.name, selection);
};
const handleClear = () => {
selectActivationKey();
change(input.name, undefined);
};
const handleClear = () => {
selectActivationKey();
change(input.name, undefined);
};
return (
<FormGroup isRequired={ isRequired } label={ label } data-testid='subscription-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">
{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>);
return (
<FormGroup
isRequired={isRequired}
label={label}
data-testid="subscription-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"
>
{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>
);
};
ActivationKeys.propTypes = {
label: PropTypes.node,
isRequired: PropTypes.bool
label: PropTypes.node,
isRequired: PropTypes.bool,
};
ActivationKeys.defaultProps = {
label: '',
isRequired: false
label: '',
isRequired: false,
};
export default ActivationKeys;

View file

@ -3,24 +3,32 @@ import { Button, FormGroup } from '@patternfly/react-core';
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
const AzureAuthButton = () => {
const { getState } = useFormApi();
const { getState } = useFormApi();
const tenantId = getState()?.values?.['azure-tenant-id'];
const guidRegex = new RegExp('^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$', 'i');
const tenantId = getState()?.values?.['azure-tenant-id'];
const guidRegex = new RegExp(
'^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$',
'i'
);
return (
<FormGroup>
<Button
component="a"
target="_blank"
variant="secondary"
isDisabled={ !guidRegex.test(tenantId) }
href={ 'https://login.microsoftonline.com/' + tenantId +
'/oauth2/v2.0/authorize?client_id=b94bb246-b02c-4985-9c22-d44e66f657f4&scope=openid&' +
'response_type=code&response_mode=query&redirect_uri=https://portal.azure.com' }>
Authorize Image Builder
</Button>
</FormGroup>);
return (
<FormGroup>
<Button
component="a"
target="_blank"
variant="secondary"
isDisabled={!guidRegex.test(tenantId)}
href={
'https://login.microsoftonline.com/' +
tenantId +
'/oauth2/v2.0/authorize?client_id=b94bb246-b02c-4985-9c22-d44e66f657f4&scope=openid&' +
'response_type=code&response_mode=query&redirect_uri=https://portal.azure.com'
}
>
Authorize Image Builder
</Button>
</FormGroup>
);
};
export default AzureAuthButton;

View file

@ -3,34 +3,41 @@ import { Button, ExpandableSection, Text, Title } from '@patternfly/react-core';
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
const AzureAuthExpandable = () => {
const [ expanded, setExpanded ] = useState(true);
const [expanded, setExpanded] = useState(true);
return (
<>
<ExpandableSection
className='azureAuthExpandable'
toggleText={ <Title headingLevel="h3">Authorizing an Azure account</Title> }
onToggle={ () => setExpanded(!expanded) }
isExpanded={ expanded }>
<Text>
To authorize Image Builder to push images to Microsoft Azure, the account owner
must configure Image Builder as an authorized application for a specific tenant ID and give it the role of
&quot;Contributor&quot; to at least one resource group.<br />
</Text>
<small>
<Button
component="a"
target="_blank"
variant="link"
icon={ <ExternalLinkAltIcon /> }
iconPosition="right"
isInline
href="https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow">
Learn more about OAuth 2.0
</Button>
</small>
</ExpandableSection>
</>);
return (
<>
<ExpandableSection
className="azureAuthExpandable"
toggleText={
<Title headingLevel="h3">Authorizing an Azure account</Title>
}
onToggle={() => setExpanded(!expanded)}
isExpanded={expanded}
>
<Text>
To authorize Image Builder to push images to Microsoft Azure, the
account owner must configure Image Builder as an authorized
application for a specific tenant ID and give it the role of
&quot;Contributor&quot; to at least one resource group.
<br />
</Text>
<small>
<Button
component="a"
target="_blank"
variant="link"
icon={<ExternalLinkAltIcon />}
iconPosition="right"
isInline
href="https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow"
>
Learn more about OAuth 2.0
</Button>
</small>
</ExpandableSection>
</>
);
};
export default AzureAuthExpandable;

View file

@ -4,45 +4,62 @@ import { FormSpy } from '@data-driven-forms/react-form-renderer';
import WizardContext from '@data-driven-forms/react-form-renderer/wizard-context';
import PropTypes from 'prop-types';
const CustomButtons = ({ buttonLabels: { cancel, submit, back }}) => {
const [ isSaving, setIsSaving ] = useState(false);
const { handlePrev, formOptions } = useContext(WizardContext);
return <FormSpy>
{() => (
<React.Fragment>
<Button
variant="primary"
type="button"
isDisabled={ !formOptions.valid || formOptions.getState().validating || isSaving }
isLoading={ isSaving }
onClick={ () => {
formOptions.onSubmit({
values: formOptions.getState().values,
setIsSaving
});
} }>
{ isSaving ? 'Creating image' : submit}
</Button>
<Button type="button" variant="secondary" onClick={ handlePrev } isDisabled={ isSaving }>
{back}
</Button>
<div className="pf-c-wizard__footer-cancel">
<Button type="button" variant="link" onClick={ formOptions.onCancel } isDisabled={ isSaving }>
{cancel}
</Button>
</div>
</React.Fragment>
)}
</FormSpy>;
const CustomButtons = ({ buttonLabels: { cancel, submit, back } }) => {
const [isSaving, setIsSaving] = useState(false);
const { handlePrev, formOptions } = useContext(WizardContext);
return (
<FormSpy>
{() => (
<React.Fragment>
<Button
variant="primary"
type="button"
isDisabled={
!formOptions.valid ||
formOptions.getState().validating ||
isSaving
}
isLoading={isSaving}
onClick={() => {
formOptions.onSubmit({
values: formOptions.getState().values,
setIsSaving,
});
}}
>
{isSaving ? 'Creating image' : submit}
</Button>
<Button
type="button"
variant="secondary"
onClick={handlePrev}
isDisabled={isSaving}
>
{back}
</Button>
<div className="pf-c-wizard__footer-cancel">
<Button
type="button"
variant="link"
onClick={formOptions.onCancel}
isDisabled={isSaving}
>
{cancel}
</Button>
</div>
</React.Fragment>
)}
</FormSpy>
);
};
CustomButtons.propTypes = {
buttonLabels: PropTypes.shape({
cancel: PropTypes.node,
submit: PropTypes.node,
back: PropTypes.node,
}),
isSaving: PropTypes.bool
buttonLabels: PropTypes.shape({
cancel: PropTypes.node,
submit: PropTypes.node,
back: PropTypes.node,
}),
isSaving: PropTypes.bool,
};
export default CustomButtons;

View file

@ -1,48 +1,46 @@
import React, {
useState,
useEffect,
} from 'react';
import {
ToggleGroup,
ToggleGroupItem,
} from '@patternfly/react-core';
import React, { useState, useEffect } from 'react';
import { ToggleGroup, ToggleGroupItem } from '@patternfly/react-core';
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
const FileSystemConfigToggle = ({ ...props }) => {
const { change, getState } = useFormApi();
const { input } = useFieldApi(props);
const [ selected, setSelected ] =
useState(getState()?.values?.['file-system-config-toggle'] || 'auto');
const { change, getState } = useFormApi();
const { input } = useFieldApi(props);
const [selected, setSelected] = useState(
getState()?.values?.['file-system-config-toggle'] || 'auto'
);
useEffect(() => {
change(input.name, selected);
}, [ selected ]);
useEffect(() => {
change(input.name, selected);
}, [selected]);
const onClick = (_, evt) => {
setSelected(evt.currentTarget.id);
};
const onClick = (_, evt) => {
setSelected(evt.currentTarget.id);
};
return (
<>
<ToggleGroup
data-testid="fsc-paritioning-toggle"
aria-label="Automatic partitioning toggle">
<ToggleGroupItem
onChange={ onClick }
text="Use automatic partitioning"
buttonId="auto"
isSelected={ selected === 'auto' } />
<ToggleGroupItem
onChange={ onClick }
text="Manually configure partitions"
buttonId="manual"
isSelected={ selected === 'manual' }
data-testid="file-system-config-toggle-manual" />
</ToggleGroup>
</>
);
return (
<>
<ToggleGroup
data-testid="fsc-paritioning-toggle"
aria-label="Automatic partitioning toggle"
>
<ToggleGroupItem
onChange={onClick}
text="Use automatic partitioning"
buttonId="auto"
isSelected={selected === 'auto'}
/>
<ToggleGroupItem
onChange={onClick}
text="Manually configure partitions"
buttonId="manual"
isSelected={selected === 'manual'}
data-testid="file-system-config-toggle-manual"
/>
</ToggleGroup>
</>
);
};
export default FileSystemConfigToggle;

View file

@ -1,24 +1,24 @@
import React, {
useEffect,
useState,
useRef,
} from 'react';
import { HelpIcon, MinusCircleIcon, PlusCircleIcon } from '@patternfly/react-icons';
import React, { useEffect, useState, useRef } from 'react';
import {
Alert,
Button,
Popover,
Text,
TextContent,
TextVariants,
HelpIcon,
MinusCircleIcon,
PlusCircleIcon,
} from '@patternfly/react-icons';
import {
Alert,
Button,
Popover,
Text,
TextContent,
TextVariants,
} from '@patternfly/react-core';
import {
TableComposable,
Thead,
Tbody,
Tr,
Th,
Td,
TableComposable,
Thead,
Tbody,
Tr,
Th,
Td,
} from '@patternfly/react-table';
import styles from '@patternfly/react-styles/css/components/Table/table';
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
@ -30,298 +30,352 @@ import MountPoint from './MountPoint';
import SizeUnit from './SizeUnit';
let initialRow = {
id: uuidv4(),
mountpoint: '/',
fstype: 'xfs',
size: 10,
unit: UNIT_GIB,
id: uuidv4(),
mountpoint: '/',
fstype: 'xfs',
size: 10,
unit: UNIT_GIB,
};
const FileSystemConfiguration = ({ ...props }) => {
const { change, getState } = useFormApi();
const { input } = useFieldApi(props);
const [ draggedItemId, setDraggedItemId ] = useState(null);
const [ draggingToItemIndex, setDraggingToItemIndex ] = useState(null);
const [ isDragging, setIsDragging ] = useState(false);
const [ itemOrder, setItemOrder ] = useState([ initialRow.id ]);
const [ tempItemOrder, setTempItemOrder ] = useState([]);
const bodyref = useRef();
const [ rows, setRows ] = useState([ initialRow ]);
const { change, getState } = useFormApi();
const { input } = useFieldApi(props);
const [draggedItemId, setDraggedItemId] = useState(null);
const [draggingToItemIndex, setDraggingToItemIndex] = useState(null);
const [isDragging, setIsDragging] = useState(false);
const [itemOrder, setItemOrder] = useState([initialRow.id]);
const [tempItemOrder, setTempItemOrder] = useState([]);
const bodyref = useRef();
const [rows, setRows] = useState([initialRow]);
useEffect(() => {
const fsc = getState()?.values?.['file-system-configuration'];
if (!fsc) {
return;
useEffect(() => {
const fsc = getState()?.values?.['file-system-configuration'];
if (!fsc) {
return;
}
const newRows = [];
const newOrder = [];
fsc.map((r) => {
const id = uuidv4();
newRows.push({
id,
mountpoint: r.mountpoint,
fstype: 'xfs',
size: r.size,
unit: r.unit,
});
newOrder.push(id);
});
setRows(newRows);
setItemOrder(newOrder);
}, []);
useEffect(() => {
change(
input.name,
itemOrder.map((r) => {
for (const r2 of rows) {
if (r2.id === r) {
return {
mountpoint: r2.mountpoint,
size: r2.size,
unit: r2.unit,
};
}
}
const newRows = [];
const newOrder = [];
fsc.map(r => {
const id = uuidv4();
newRows.push({
id,
mountpoint: r.mountpoint,
fstype: 'xfs',
size: r.size,
unit: r.unit,
});
newOrder.push(id);
});
setRows(newRows);
setItemOrder(newOrder);
}, []);
useEffect(() => {
change(input.name, itemOrder.map(r => {
for (const r2 of rows) {
if (r2.id === r) {
return {
mountpoint: r2.mountpoint,
size: r2.size,
unit: r2.unit,
};
}
}
}));
}, [ rows, itemOrder ]);
const addRow = () => {
const id = uuidv4();
setRows(rows.concat([{
id,
mountpoint: '/home',
fstype: 'xfs',
size: 1,
unit: UNIT_GIB,
}]));
setItemOrder(itemOrder.concat([ id ]));
};
const removeRow = id => {
let removeIndex = rows.map(e => e.id).indexOf(id);
let newRows = [ ...rows ];
newRows.splice(removeIndex, 1);
let removeOrderIndex = itemOrder.indexOf(id);
let newOrder = [ ...itemOrder ];
newOrder.splice(removeOrderIndex, 1);
setRows(newRows);
setItemOrder(newOrder);
};
const moveItem = (arr, i1, toIndex) => {
const fromIndex = arr.indexOf(i1);
if (fromIndex === toIndex) {
return arr;
}
const temp = arr.splice(fromIndex, 1);
arr.splice(toIndex, 0, temp[0]);
return arr;
};
const move = itemOrder => {
const ulNode = bodyref.current;
const nodes = Array.from(ulNode.children);
if (nodes.map(node => node.id).every((id, i) => id === itemOrder[i])) {
return;
}
while (ulNode.firstChild) {
ulNode.removeChild(ulNode.lastChild);
}
itemOrder.forEach(id => {
ulNode.appendChild(nodes.find(n => n.id === id));
});
};
const onDragOver = evt => {
evt.preventDefault();
const curListItem = evt.target.closest('tr');
if (!curListItem || !bodyref.current.contains(curListItem)) {
return null;
}
const dragId = curListItem.id;
const newDraggingToItemIndex = Array.from(bodyref.current.children).findIndex(item => item.id === dragId);
if (newDraggingToItemIndex !== draggingToItemIndex) {
const tempItemOrder = moveItem([ ...itemOrder ], draggedItemId, newDraggingToItemIndex);
move(tempItemOrder);
setDraggingToItemIndex(newDraggingToItemIndex);
setTempItemOrder(tempItemOrder);
}
};
const isValidDrop = evt => {
const ulRect = bodyref.current.getBoundingClientRect();
return (
evt.clientX > ulRect.x &&
evt.clientX < ulRect.x + ulRect.width &&
evt.clientY > ulRect.y &&
evt.clientY < ulRect.y + ulRect.height
);
};
const onDragLeave = evt => {
if (!isValidDrop(evt)) {
move(itemOrder);
setDraggingToItemIndex(null);
}
};
const onDrop = evt => {
if (isValidDrop(evt)) {
setItemOrder(tempItemOrder);
}
};
const onDragStart = evt => {
evt.dataTransfer.effectAllowed = 'move';
evt.dataTransfer.setData('text/plain', evt.currentTarget.id);
evt.currentTarget.classList.add(styles.modifiers.ghostRow);
evt.currentTarget.setAttribute('aria-pressed', 'true');
setDraggedItemId(evt.currentTarget.id);
setIsDragging(true);
};
const onDragEnd = evt => {
evt.target.classList.remove(styles.modifiers.ghostRow);
evt.target.setAttribute('aria-pressed', 'false');
setDraggedItemId(null);
setDraggingToItemIndex(null);
setIsDragging(false);
};
const setMountpoint = (id, mp) => {
let newRows = [ ...rows ];
for (let i = 0; i < newRows.length; i++) {
if (newRows[i].id === id) {
let newRow = { ...newRows[i] };
newRow.mountpoint = mp;
newRows.splice(i, 1, newRow);
break;
}
}
setRows(newRows);
};
const setSize = (id, s, u) => {
let newRows = [ ...rows ];
for (let i = 0; i < newRows.length; i++) {
if (newRows[i].id === id) {
let newRow = { ...newRows[i] };
newRow.size = s;
newRow.unit = u;
newRows.splice(i, 1, newRow);
break;
}
}
setRows(newRows);
};
return (
<>
<TextContent>
<Text component={ TextVariants.h3 }>Configure partitions</Text>
</TextContent>
{ rows.length > 1 && getState()?.errors?.['file-system-configuration']?.duplicates &&
<Alert variant="danger" isInline
title="Duplicate mount points: All mount points must be unique. Remove the duplicate or choose a new mount point." />
}
{ rows.length >= 1 && getState()?.errors?.['file-system-configuration']?.root === false &&
<Alert variant="danger" isInline
title="No root partition configured." />
}
<TextContent>
<Text>
Partitions have been generated and given default values based on best practices from Red Hat,
and your selections in previous steps of the wizard.
</Text>
</TextContent>
<TableComposable aria-label="File system table" className={ isDragging && styles.modifiers.dragOver } variant="compact">
<Thead>
<Tr>
<Th />
<Th>Mount point</Th>
<Th>Type</Th>
<Th>Minimum size
<Popover
hasAutoWidth
bodyContent={ <TextContent>
<Text>
Image Builder may extend this size based on requirements, selected packages, and configurations.
</Text>
</TextContent> }>
<Button
variant="plain"
aria-label="File system configuration info"
aria-describedby="file-system-configuration-info"
className="pf-c-form__group-label-help">
<HelpIcon />
</Button>
</Popover>
</Th>
<Th />
</Tr>
</Thead>
<Tbody
ref={ bodyref }
onDragOver={ onDragOver }
onDrop={ onDragOver }
onDragLeave={ onDragLeave }
data-testid="file-system-configuration-tbody">
{rows.map((row, rowIndex) => (
<Tr key={ rowIndex } id={ row.id } draggable onDrop={ onDrop } onDragEnd={ onDragEnd } onDragStart={ onDragStart }>
<Td draggableRow={ {
id: `draggable-row-${row.id}`
} } />
<Td className="pf-m-width-30">
<MountPoint
key={ row.id + '-mountpoint' }
mountpoint={ row.mountpoint }
onChange={ mp => setMountpoint(row.id, mp) } />
{ getState().errors['file-system-configuration']?.duplicates &&
getState().errors['file-system-configuration']?.duplicates.indexOf(row.mountpoint) !== -1 &&
<Alert variant="danger" isInline isPlain title="Duplicate mount point." /> }
</Td>
<Td className="pf-m-width-20">
{ /* always xfs */ }
{row.fstype}
</Td>
<Td className="pf-m-width-30">
<SizeUnit
key={ row.id + '-sizeunit' }
size={ row.size }
unit={ row.unit }
onChange={ (s, u) => setSize(row.id, s, u) } />
</Td>
<Td className="pf-m-width-10">
<Button
variant="link"
icon={ <MinusCircleIcon /> }
onClick={ () => removeRow(row.id) } />
</Td>
</Tr>
))}
</Tbody>
</TableComposable>
<TextContent>
<Button
data-testid="file-system-add-partition"
className="pf-u-text-align-left"
variant="link"
icon={ <PlusCircleIcon /> }
onClick={ addRow }>
Add partition
</Button>
</TextContent>
</>
})
);
}, [rows, itemOrder]);
const addRow = () => {
const id = uuidv4();
setRows(
rows.concat([
{
id,
mountpoint: '/home',
fstype: 'xfs',
size: 1,
unit: UNIT_GIB,
},
])
);
setItemOrder(itemOrder.concat([id]));
};
const removeRow = (id) => {
let removeIndex = rows.map((e) => e.id).indexOf(id);
let newRows = [...rows];
newRows.splice(removeIndex, 1);
let removeOrderIndex = itemOrder.indexOf(id);
let newOrder = [...itemOrder];
newOrder.splice(removeOrderIndex, 1);
setRows(newRows);
setItemOrder(newOrder);
};
const moveItem = (arr, i1, toIndex) => {
const fromIndex = arr.indexOf(i1);
if (fromIndex === toIndex) {
return arr;
}
const temp = arr.splice(fromIndex, 1);
arr.splice(toIndex, 0, temp[0]);
return arr;
};
const move = (itemOrder) => {
const ulNode = bodyref.current;
const nodes = Array.from(ulNode.children);
if (nodes.map((node) => node.id).every((id, i) => id === itemOrder[i])) {
return;
}
while (ulNode.firstChild) {
ulNode.removeChild(ulNode.lastChild);
}
itemOrder.forEach((id) => {
ulNode.appendChild(nodes.find((n) => n.id === id));
});
};
const onDragOver = (evt) => {
evt.preventDefault();
const curListItem = evt.target.closest('tr');
if (!curListItem || !bodyref.current.contains(curListItem)) {
return null;
}
const dragId = curListItem.id;
const newDraggingToItemIndex = Array.from(
bodyref.current.children
).findIndex((item) => item.id === dragId);
if (newDraggingToItemIndex !== draggingToItemIndex) {
const tempItemOrder = moveItem(
[...itemOrder],
draggedItemId,
newDraggingToItemIndex
);
move(tempItemOrder);
setDraggingToItemIndex(newDraggingToItemIndex);
setTempItemOrder(tempItemOrder);
}
};
const isValidDrop = (evt) => {
const ulRect = bodyref.current.getBoundingClientRect();
return (
evt.clientX > ulRect.x &&
evt.clientX < ulRect.x + ulRect.width &&
evt.clientY > ulRect.y &&
evt.clientY < ulRect.y + ulRect.height
);
};
const onDragLeave = (evt) => {
if (!isValidDrop(evt)) {
move(itemOrder);
setDraggingToItemIndex(null);
}
};
const onDrop = (evt) => {
if (isValidDrop(evt)) {
setItemOrder(tempItemOrder);
}
};
const onDragStart = (evt) => {
evt.dataTransfer.effectAllowed = 'move';
evt.dataTransfer.setData('text/plain', evt.currentTarget.id);
evt.currentTarget.classList.add(styles.modifiers.ghostRow);
evt.currentTarget.setAttribute('aria-pressed', 'true');
setDraggedItemId(evt.currentTarget.id);
setIsDragging(true);
};
const onDragEnd = (evt) => {
evt.target.classList.remove(styles.modifiers.ghostRow);
evt.target.setAttribute('aria-pressed', 'false');
setDraggedItemId(null);
setDraggingToItemIndex(null);
setIsDragging(false);
};
const setMountpoint = (id, mp) => {
let newRows = [...rows];
for (let i = 0; i < newRows.length; i++) {
if (newRows[i].id === id) {
let newRow = { ...newRows[i] };
newRow.mountpoint = mp;
newRows.splice(i, 1, newRow);
break;
}
}
setRows(newRows);
};
const setSize = (id, s, u) => {
let newRows = [...rows];
for (let i = 0; i < newRows.length; i++) {
if (newRows[i].id === id) {
let newRow = { ...newRows[i] };
newRow.size = s;
newRow.unit = u;
newRows.splice(i, 1, newRow);
break;
}
}
setRows(newRows);
};
return (
<>
<TextContent>
<Text component={TextVariants.h3}>Configure partitions</Text>
</TextContent>
{rows.length > 1 &&
getState()?.errors?.['file-system-configuration']?.duplicates && (
<Alert
variant="danger"
isInline
title="Duplicate mount points: All mount points must be unique. Remove the duplicate or choose a new mount point."
/>
)}
{rows.length >= 1 &&
getState()?.errors?.['file-system-configuration']?.root === false && (
<Alert
variant="danger"
isInline
title="No root partition configured."
/>
)}
<TextContent>
<Text>
Partitions have been generated and given default values based on best
practices from Red Hat, and your selections in previous steps of the
wizard.
</Text>
</TextContent>
<TableComposable
aria-label="File system table"
className={isDragging && styles.modifiers.dragOver}
variant="compact"
>
<Thead>
<Tr>
<Th />
<Th>Mount point</Th>
<Th>Type</Th>
<Th>
Minimum size
<Popover
hasAutoWidth
bodyContent={
<TextContent>
<Text>
Image Builder may extend this size based on requirements,
selected packages, and configurations.
</Text>
</TextContent>
}
>
<Button
variant="plain"
aria-label="File system configuration info"
aria-describedby="file-system-configuration-info"
className="pf-c-form__group-label-help"
>
<HelpIcon />
</Button>
</Popover>
</Th>
<Th />
</Tr>
</Thead>
<Tbody
ref={bodyref}
onDragOver={onDragOver}
onDrop={onDragOver}
onDragLeave={onDragLeave}
data-testid="file-system-configuration-tbody"
>
{rows.map((row, rowIndex) => (
<Tr
key={rowIndex}
id={row.id}
draggable
onDrop={onDrop}
onDragEnd={onDragEnd}
onDragStart={onDragStart}
>
<Td
draggableRow={{
id: `draggable-row-${row.id}`,
}}
/>
<Td className="pf-m-width-30">
<MountPoint
key={row.id + '-mountpoint'}
mountpoint={row.mountpoint}
onChange={(mp) => setMountpoint(row.id, mp)}
/>
{getState().errors['file-system-configuration']?.duplicates &&
getState().errors[
'file-system-configuration'
]?.duplicates.indexOf(row.mountpoint) !== -1 && (
<Alert
variant="danger"
isInline
isPlain
title="Duplicate mount point."
/>
)}
</Td>
<Td className="pf-m-width-20">
{/* always xfs */}
{row.fstype}
</Td>
<Td className="pf-m-width-30">
<SizeUnit
key={row.id + '-sizeunit'}
size={row.size}
unit={row.unit}
onChange={(s, u) => setSize(row.id, s, u)}
/>
</Td>
<Td className="pf-m-width-10">
<Button
variant="link"
icon={<MinusCircleIcon />}
onClick={() => removeRow(row.id)}
/>
</Td>
</Tr>
))}
</Tbody>
</TableComposable>
<TextContent>
<Button
data-testid="file-system-add-partition"
className="pf-u-text-align-left"
variant="link"
icon={<PlusCircleIcon />}
onClick={addRow}
>
Add partition
</Button>
</TextContent>
</>
);
};
export default FileSystemConfiguration;

View file

@ -1,58 +1,64 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { FormGroup, Select, SelectOption, SelectVariant } from '@patternfly/react-core';
import {
FormGroup,
Select,
SelectOption,
SelectVariant,
} from '@patternfly/react-core';
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
import { RELEASES } from '../../../constants';
import isRhel from '../../../Utilities/isRhel';
const ImageOutputReleaseSelect = ({ label, isRequired, ...props }) => {
const { change, getState } = useFormApi();
const { input } = useFieldApi(props);
const [ isOpen, setIsOpen ] = useState(false);
const { change, getState } = useFormApi();
const { input } = useFieldApi(props);
const [isOpen, setIsOpen] = useState(false);
const setRelease = (_, selection) => {
change(input.name, selection);
setIsOpen(false);
};
const setRelease = (_, selection) => {
change(input.name, selection);
setIsOpen(false);
};
const handleClear = () => {
change(input.name, null);
};
const handleClear = () => {
change(input.name, null);
};
return (
<FormGroup isRequired={ isRequired } label={ label }>
<Select
variant={ SelectVariant.single }
onToggle={ () => setIsOpen(!isOpen) }
onSelect={ setRelease }
onClear={ handleClear }
selections={ RELEASES[getState()?.values?.[input.name]] }
isOpen={ isOpen }>
{
Object.entries(RELEASES)
.filter(([ key ]) => {
// Only show non-RHEL distros in beta
if (insights.chrome.isBeta()) {
return true;
}
return (
<FormGroup isRequired={isRequired} label={label}>
<Select
variant={SelectVariant.single}
onToggle={() => setIsOpen(!isOpen)}
onSelect={setRelease}
onClear={handleClear}
selections={RELEASES[getState()?.values?.[input.name]]}
isOpen={isOpen}
>
{Object.entries(RELEASES)
.filter(([key]) => {
// Only show non-RHEL distros in beta
if (insights.chrome.isBeta()) {
return true;
}
return isRhel(key);
})
.map(([ key, release ], index) => {
return <SelectOption key={ index } value={ key }>
{ release }
</SelectOption>;
})
}
</Select>
</FormGroup>
);
return isRhel(key);
})
.map(([key, release], index) => {
return (
<SelectOption key={index} value={key}>
{release}
</SelectOption>
);
})}
</Select>
</FormGroup>
);
};
ImageOutputReleaseSelect.propTypes = {
label: PropTypes.node,
isRequired: PropTypes.bool
label: PropTypes.node,
isRequired: PropTypes.bool,
};
export default ImageOutputReleaseSelect;

View file

@ -1,86 +1,95 @@
import React, {
useEffect,
useState,
} from 'react';
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import {
Select,
SelectOption,
SelectVariant,
TextInput,
Select,
SelectOption,
SelectVariant,
TextInput,
} from '@patternfly/react-core';
import path from 'path';
const MountPoint = ({ ...props }) => {
// check '/' last!
const validPrefixes = [ '/app', '/data', '/home', '/opt', '/srv', '/tmp', '/usr', '/usr/local', '/var', '/' ];
const [ isOpen, setIsOpen ] = useState(false);
const [ prefix, setPrefix ] = useState('/');
const [ suffix, setSuffix ] = useState('');
// check '/' last!
const validPrefixes = [
'/app',
'/data',
'/home',
'/opt',
'/srv',
'/tmp',
'/usr',
'/usr/local',
'/var',
'/',
];
const [isOpen, setIsOpen] = useState(false);
const [prefix, setPrefix] = useState('/');
const [suffix, setSuffix] = useState('');
// split
useEffect(() => {
for (let p of validPrefixes) {
if (props.mountpoint.startsWith(p)) {
setPrefix(p);
setSuffix(props.mountpoint.substring(p.length));
return;
}
}
}, []);
// split
useEffect(() => {
for (let p of validPrefixes) {
if (props.mountpoint.startsWith(p)) {
setPrefix(p);
setSuffix(props.mountpoint.substring(p.length));
return;
}
}
}, []);
useEffect(() => {
let suf = suffix;
let mp = prefix;
if (suf) {
if (mp !== '/' && suf[0] !== '/') {
suf = '/' + suf;
}
useEffect(() => {
let suf = suffix;
let mp = prefix;
if (suf) {
if (mp !== '/' && suf[0] !== '/') {
suf = '/' + suf;
}
mp += suf;
}
mp += suf;
}
props.onChange(path.normalize(mp));
}, [ prefix, suffix ]);
props.onChange(path.normalize(mp));
}, [prefix, suffix]);
const onToggle = (isOpen) => {
setIsOpen(isOpen);
};
const onToggle = (isOpen) => {
setIsOpen(isOpen);
};
const onSelect = (event, selection) => {
setPrefix(selection);
setIsOpen(false);
};
const onSelect = (event, selection) => {
setPrefix(selection);
setIsOpen(false);
};
return (
<>
<Select
className="pf-u-w-50"
isOpen={ isOpen }
onToggle={ onToggle }
onSelect={ onSelect }
selections={ prefix }
variant={ SelectVariant.single }>
{validPrefixes.map((pfx, index) => {
return <SelectOption key={ index } value={ pfx } />;
})
}
</Select>
{ prefix !== '/' &&
<TextInput
className="pf-u-w-50"
type="text"
value={ suffix }
aria-label="Mount point suffix text input"
onChange={ v => setSuffix(v) } />
}
</>
);
return (
<>
<Select
className="pf-u-w-50"
isOpen={isOpen}
onToggle={onToggle}
onSelect={onSelect}
selections={prefix}
variant={SelectVariant.single}
>
{validPrefixes.map((pfx, index) => {
return <SelectOption key={index} value={pfx} />;
})}
</Select>
{prefix !== '/' && (
<TextInput
className="pf-u-w-50"
type="text"
value={suffix}
aria-label="Mount point suffix text input"
onChange={(v) => setSuffix(v)}
/>
)}
</>
);
};
MountPoint.propTypes = {
mountpoint: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
mountpoint: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
};
export default MountPoint;

View file

@ -4,391 +4,459 @@ import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
import api from '../../../api';
import PropTypes from 'prop-types';
import {
DualListSelector,
DualListSelectorPane,
DualListSelectorList,
DualListSelectorListItem,
DualListSelectorControlsWrapper,
DualListSelectorControl,
SearchInput,
TextContent
DualListSelector,
DualListSelectorPane,
DualListSelectorList,
DualListSelectorListItem,
DualListSelectorControlsWrapper,
DualListSelectorControl,
SearchInput,
TextContent,
} from '@patternfly/react-core';
import { AngleDoubleLeftIcon, AngleLeftIcon, AngleDoubleRightIcon, AngleRightIcon } from '@patternfly/react-icons';
import {
AngleDoubleLeftIcon,
AngleLeftIcon,
AngleDoubleRightIcon,
AngleRightIcon,
} from '@patternfly/react-icons';
// the fields isHidden and isSelected should not be included in the package list sent for image creation
const removePackagesDisplayFields = (packages) => packages.map((pack) => ({
const removePackagesDisplayFields = (packages) =>
packages.map((pack) => ({
name: pack.name,
summary: pack.summary,
}));
}));
const Packages = ({ defaultArch, ...props }) => {
const { change, getState } = useFormApi();
const { input } = useFieldApi(props);
const [ packagesSearchName, setPackagesSearchName ] = useState(undefined);
const [ filterAvailable, setFilterAvailable ] = useState(undefined);
const [ filterChosen, setFilterChosen ] = useState(undefined);
const [ packagesAvailable, setPackagesAvailable ] = useState([]);
const [ packagesAvailableFound, setPackagesAvailableFound ] = useState(true);
const [ packagesChosen, setPackagesChosen ] = useState([]);
const [ packagesChosenFound, setPackagesChosenFound ] = useState(true);
const [ focus, setFocus ] = useState('');
const { change, getState } = useFormApi();
const { input } = useFieldApi(props);
const [packagesSearchName, setPackagesSearchName] = useState(undefined);
const [filterAvailable, setFilterAvailable] = useState(undefined);
const [filterChosen, setFilterChosen] = useState(undefined);
const [packagesAvailable, setPackagesAvailable] = useState([]);
const [packagesAvailableFound, setPackagesAvailableFound] = useState(true);
const [packagesChosen, setPackagesChosen] = useState([]);
const [packagesChosenFound, setPackagesChosenFound] = useState(true);
const [focus, setFocus] = useState('');
// this effect only triggers on mount
useEffect(() => {
const selectedPackages = getState()?.values?.['selected-packages'];
if (selectedPackages) {
setPackagesChosen(selectedPackages);
}
}, []);
// this effect only triggers on mount
useEffect(() => {
const selectedPackages = getState()?.values?.['selected-packages'];
if (selectedPackages) {
setPackagesChosen(selectedPackages);
}
}, []);
const searchResultsComparator = useCallback((searchTerm) => {
return (a, b) => {
a = a.name.toLowerCase();
b = b.name.toLowerCase();
const searchResultsComparator = useCallback((searchTerm) => {
return (a, b) => {
a = a.name.toLowerCase();
b = b.name.toLowerCase();
// check exact match first
if (a === searchTerm) {
return -1;
}
// check exact match first
if (a === searchTerm) {
return -1;
}
if (b === searchTerm) {
return 1;
}
if (b === searchTerm) {
return 1;
}
// check for packages that start with the search term
if (a.startsWith(searchTerm) && !b.startsWith(searchTerm)) {
return -1;
}
// check for packages that start with the search term
if (a.startsWith(searchTerm) && !b.startsWith(searchTerm)) {
return -1;
}
if (b.startsWith(searchTerm) && !a.startsWith(searchTerm)) {
return 1;
}
if (b.startsWith(searchTerm) && !a.startsWith(searchTerm)) {
return 1;
}
// if both (or neither) start with the search term
// sort alphabetically
if (a < b) {
return -1;
}
// if both (or neither) start with the search term
// sort alphabetically
if (a < b) {
return -1;
}
if (b < a) {
return 1;
}
if (b < a) {
return 1;
}
return 0;
};
return 0;
};
});
const setPackagesAvailableSorted = (
packageList,
filter = filterAvailable
) => {
const sortResults = packageList.sort(searchResultsComparator(filter));
setPackagesAvailable(sortResults);
};
const setPackagesChosenSorted = (packageList) => {
const sortResults = packageList.sort(searchResultsComparator(filterChosen));
setPackagesChosen(sortResults);
};
// filter the packages by name
const filterPackagesAvailable = (packageList) => {
return packageList.filter((availablePackage) => {
// returns true if no packages in the available or chosen list have the same name
return !packagesChosen.some(
(chosenPackage) => availablePackage.name === chosenPackage.name
);
});
};
const getAllPackages = async () => {
const args = [
getState()?.values?.release,
getState()?.values?.architecture || defaultArch,
packagesSearchName,
];
let { data, meta } = await api.getPackages(...args);
if (data?.length === meta.count) {
return data;
} else if (data) {
({ data } = await api.getPackages(...args, meta.count));
return data;
}
};
// call api to list available packages
const handlePackagesAvailableSearch = async () => {
setFilterAvailable(packagesSearchName);
const packageList = await getAllPackages();
if (packageList) {
const packagesAvailableFiltered = filterPackagesAvailable(packageList);
setPackagesAvailableSorted(packagesAvailableFiltered, packagesSearchName);
setPackagesAvailableFound(
packagesAvailableFiltered.length ? true : false
);
} else {
setPackagesAvailable([]);
setPackagesAvailableFound(false);
}
};
// filter displayed selected packages
const handlePackagesChosenSearch = (val) => {
let found = false;
const filteredPackagesChosen = packagesChosen.map((pack) => {
if (!pack.name.includes(val)) {
pack.isHidden = true;
} else {
pack.isHidden = false;
found = true;
}
return pack;
});
const setPackagesAvailableSorted = (packageList, filter = filterAvailable) => {
const sortResults = packageList.sort(searchResultsComparator(filter));
setPackagesAvailable(sortResults);
setFilterChosen(val);
setPackagesChosenFound(found);
setPackagesChosenSorted(filteredPackagesChosen);
};
const keydownHandler = (event) => {
if (event.key === 'Enter') {
if (focus === 'available') {
event.stopPropagation();
handlePackagesAvailableSearch();
}
}
};
useEffect(() => {
document.addEventListener('keydown', keydownHandler, true);
return () => {
document.removeEventListener('keydown', keydownHandler, true);
};
});
const setPackagesChosenSorted = (packageList) => {
const sortResults = packageList.sort(searchResultsComparator(filterChosen));
setPackagesChosen(sortResults);
};
const areFound = (filter, packageList) => {
if (filter === undefined) {
return true;
} else if (packageList.some((pack) => pack.name.includes(filter))) {
return true;
} else {
return false;
}
};
// filter the packages by name
const filterPackagesAvailable = (packageList) => {
return packageList.filter((availablePackage) => {
// returns true if no packages in the available or chosen list have the same name
return !packagesChosen.some((chosenPackage) => availablePackage.name === chosenPackage.name);
});
};
const isHidden = (filter, pack) =>
filter && !pack.name.includes(filter) ? true : false;
const getAllPackages = async () => {
const args = [
getState()?.values?.release,
getState()?.values?.architecture || defaultArch,
packagesSearchName
];
let { data, meta } = await api.getPackages(...args);
if (data?.length === meta.count) {
return data;
} else if (data) {
({ data } = await api.getPackages(...args, meta.count));
return data;
}
};
// call api to list available packages
const handlePackagesAvailableSearch = async () => {
setFilterAvailable(packagesSearchName);
const packageList = await getAllPackages();
if (packageList) {
const packagesAvailableFiltered = filterPackagesAvailable(packageList);
setPackagesAvailableSorted(packagesAvailableFiltered, packagesSearchName);
setPackagesAvailableFound(packagesAvailableFiltered.length ? true : false);
} else {
setPackagesAvailable([]);
setPackagesAvailableFound(false);
}
};
// filter displayed selected packages
const handlePackagesChosenSearch = (val) => {
let found = false;
const filteredPackagesChosen = packagesChosen.map((pack) => {
if (!pack.name.includes(val)) {
pack.isHidden = true;
} else {
pack.isHidden = false;
found = true;
}
return pack;
});
setFilterChosen(val);
setPackagesChosenFound(found);
setPackagesChosenSorted(filteredPackagesChosen);
};
const keydownHandler = (event) => {
if (event.key === 'Enter') {
if (focus === 'available') {
event.stopPropagation();
handlePackagesAvailableSearch();
}
}
};
useEffect(() => {
document.addEventListener('keydown', keydownHandler, true);
return () => {
document.removeEventListener('keydown', keydownHandler, true);
};
});
const areFound = (filter, packageList) => {
if (filter === undefined) {
return true;
} else if (packageList.some(pack => pack.name.includes(filter))) {
return true;
} else {
return false;
}
};
const isHidden = (filter, pack) => filter && !pack.name.includes(filter) ? true : false;
const updateState = (updatedPackagesAvailable, updatedPackagesChosen) => {
setPackagesChosenSorted(updatedPackagesChosen);
setPackagesAvailableSorted(updatedPackagesAvailable);
setPackagesAvailableFound(areFound(filterAvailable, updatedPackagesAvailable));
setPackagesChosenFound(areFound(filterChosen, updatedPackagesChosen));
// set the steps field to the current chosen packages list
change(input.name, removePackagesDisplayFields(updatedPackagesChosen));
};
const moveSelectedToChosen = () => {
const newPackagesChosen = [];
const updatedPackagesAvailable = packagesAvailable.filter((pack) => {
if (pack.selected) {
pack.selected = false;
pack.isHidden = isHidden(filterChosen, pack);
newPackagesChosen.push(pack);
return false;
}
return true;
});
const updatedPackagesChosen = [ ...newPackagesChosen, ...packagesChosen ];
updateState(updatedPackagesAvailable, updatedPackagesChosen);
};
const moveSelectedToAvailable = () => {
const newPackagesAvailable = [];
const updatedPackagesChosen = packagesChosen.filter((pack) => {
if (pack.selected) {
pack.selected = false;
pack.isHidden = false;
pack.name.includes(filterAvailable) ? newPackagesAvailable.push(pack) : null;
return false;
}
return true;
});
const updatedPackagesAvailable = [ ...newPackagesAvailable, ...packagesAvailable ];
updateState(updatedPackagesAvailable, updatedPackagesChosen);
};
const moveAllToChosen = () => {
const newPackagesChosen = packagesAvailable.map(pack => {
return { ...pack, selected: false, isHidden: isHidden(filterChosen, pack) };
});
const updatedPackagesAvailable = [];
const updatedPackagesChosen = [ ...newPackagesChosen, ...packagesChosen ];
updateState(updatedPackagesAvailable, updatedPackagesChosen);
};
const moveAllToAvailable = () => {
const updatedPackagesChosen = packagesChosen.filter(pack => pack.isHidden);
const newPackagesAvailable = filterAvailable === undefined ? [] :
packagesChosen
.filter(pack => !pack.isHidden && pack.name.includes(filterAvailable))
.map(pack => { return { ...pack, selected: false };});
const updatedPackagesAvailable = [ ...newPackagesAvailable, ...packagesAvailable ];
updateState(updatedPackagesAvailable, updatedPackagesChosen);
};
const onOptionSelect = (event, index, isChosen) => {
if (isChosen) {
const newChosen = [ ...packagesChosen ];
newChosen[index].selected = !packagesChosen[index].selected;
setPackagesChosenSorted(newChosen);
} else {
const newAvailable = [ ...packagesAvailable ];
newAvailable[index].selected = !packagesAvailable[index].selected;
setPackagesAvailableSorted(newAvailable);
}
};
const firstInputElement = useRef(null);
useEffect(() => {
firstInputElement.current?.focus();
}, []);
const handleClearAvailableSearch = () => {
setPackagesSearchName(undefined);
setFilterAvailable(undefined);
setPackagesAvailable([]);
setPackagesAvailableFound(true);
};
const handleClearChosenSearch = () => {
setFilterChosen(undefined);
setPackagesChosenSorted(packagesChosen.map(pack => {
return { ...pack, isHidden: false };}));
setPackagesChosenFound(true);
};
return (
<DualListSelector>
<DualListSelectorPane
title="Available packages"
searchInput={ <SearchInput
placeholder="Search for a package"
data-testid="search-available-pkgs-input"
value={ packagesSearchName }
ref={ firstInputElement }
onFocus={ () => setFocus('available') }
onBlur={ () => setFocus('') }
onChange={ (val) => setPackagesSearchName(val) }
submitSearchButtonLabel="Search button for available packages"
onSearch={ handlePackagesAvailableSearch }
resetButtonLabel="Clear available packages search"
onClear={ handleClearAvailableSearch } /> }>
<DualListSelectorList data-testid="available-pkgs-list">
{!packagesAvailable.length ? (
<p className="pf-u-text-align-center pf-u-mt-md">
{!packagesAvailableFound
? 'No packages found'
: <>Search above to add additional<br />packages to your image</>
}
</p>
) : (packagesAvailable.map((pack, index) => {
return !pack.isHidden ? (
<DualListSelectorListItem
key={ index }
isSelected={ pack.selected }
onOptionSelect={ (e) => onOptionSelect(e, index, false) }>
<TextContent key={ `${pack.name}-${index}` }>
<span className="pf-c-dual-list-selector__item-text">{ pack.name }</span>
<small>{ pack.summary }</small>
</TextContent>
</DualListSelectorListItem>
) : null;
}))}
</DualListSelectorList>
</DualListSelectorPane>
<DualListSelectorControlsWrapper
aria-label="Selector controls">
<DualListSelectorControl
isDisabled={ !packagesAvailable.some(option => option.selected) }
onClick={ () => moveSelectedToChosen() }
aria-label="Add selected"
tooltipContent="Add selected">
<AngleRightIcon />
</DualListSelectorControl>
<DualListSelectorControl
isDisabled={ !packagesAvailable.length }
onClick={ () => moveAllToChosen() }
aria-label="Add all"
tooltipContent="Add all">
<AngleDoubleRightIcon />
</DualListSelectorControl>
<DualListSelectorControl
isDisabled={ !packagesChosen.length || !packagesChosenFound }
onClick={ () => moveAllToAvailable() }
aria-label="Remove all"
tooltipContent="Remove all">
<AngleDoubleLeftIcon />
</DualListSelectorControl>
<DualListSelectorControl
onClick={ () => moveSelectedToAvailable() }
isDisabled={ !packagesChosen.some(option => option.selected) || !packagesChosenFound }
aria-label="Remove selected"
tooltipContent="Remove selected">
<AngleLeftIcon />
</DualListSelectorControl>
</DualListSelectorControlsWrapper>
<DualListSelectorPane
title="Chosen packages"
searchInput={ <SearchInput
placeholder="Search for a package"
data-testid="search-chosen-pkgs-input"
value={ filterChosen }
onFocus={ () => setFocus('chosen') }
onBlur={ () => setFocus('') }
onChange={ (val) => handlePackagesChosenSearch(val) }
resetButtonLabel="Clear chosen packages search"
onClear={ handleClearChosenSearch } /> }
isChosen>
<DualListSelectorList data-testid="chosen-pkgs-list">
{!packagesChosen.length ? (
<p className="pf-u-text-align-center pf-u-mt-md">
No packages added
</p>
) : !packagesChosenFound ? (
<p className="pf-u-text-align-center pf-u-mt-md">
No packages found
</p>
) : (packagesChosen.map((pack, index) => {
return !pack.isHidden ? (
<DualListSelectorListItem
key={ index }
isSelected={ pack.selected }
onOptionSelect={ (e) => onOptionSelect(e, index, true) }>
<TextContent key={ `${pack.name}-${index}` }>
<span className="pf-c-dual-list-selector__item-text">{ pack.name }</span>
<small>{ pack.summary }</small>
</TextContent>
</DualListSelectorListItem>
) : null;
}))}
</DualListSelectorList>
</DualListSelectorPane>
</DualListSelector>
const updateState = (updatedPackagesAvailable, updatedPackagesChosen) => {
setPackagesChosenSorted(updatedPackagesChosen);
setPackagesAvailableSorted(updatedPackagesAvailable);
setPackagesAvailableFound(
areFound(filterAvailable, updatedPackagesAvailable)
);
setPackagesChosenFound(areFound(filterChosen, updatedPackagesChosen));
// set the steps field to the current chosen packages list
change(input.name, removePackagesDisplayFields(updatedPackagesChosen));
};
const moveSelectedToChosen = () => {
const newPackagesChosen = [];
const updatedPackagesAvailable = packagesAvailable.filter((pack) => {
if (pack.selected) {
pack.selected = false;
pack.isHidden = isHidden(filterChosen, pack);
newPackagesChosen.push(pack);
return false;
}
return true;
});
const updatedPackagesChosen = [...newPackagesChosen, ...packagesChosen];
updateState(updatedPackagesAvailable, updatedPackagesChosen);
};
const moveSelectedToAvailable = () => {
const newPackagesAvailable = [];
const updatedPackagesChosen = packagesChosen.filter((pack) => {
if (pack.selected) {
pack.selected = false;
pack.isHidden = false;
pack.name.includes(filterAvailable)
? newPackagesAvailable.push(pack)
: null;
return false;
}
return true;
});
const updatedPackagesAvailable = [
...newPackagesAvailable,
...packagesAvailable,
];
updateState(updatedPackagesAvailable, updatedPackagesChosen);
};
const moveAllToChosen = () => {
const newPackagesChosen = packagesAvailable.map((pack) => {
return {
...pack,
selected: false,
isHidden: isHidden(filterChosen, pack),
};
});
const updatedPackagesAvailable = [];
const updatedPackagesChosen = [...newPackagesChosen, ...packagesChosen];
updateState(updatedPackagesAvailable, updatedPackagesChosen);
};
const moveAllToAvailable = () => {
const updatedPackagesChosen = packagesChosen.filter(
(pack) => pack.isHidden
);
const newPackagesAvailable =
filterAvailable === undefined
? []
: packagesChosen
.filter(
(pack) => !pack.isHidden && pack.name.includes(filterAvailable)
)
.map((pack) => {
return { ...pack, selected: false };
});
const updatedPackagesAvailable = [
...newPackagesAvailable,
...packagesAvailable,
];
updateState(updatedPackagesAvailable, updatedPackagesChosen);
};
const onOptionSelect = (event, index, isChosen) => {
if (isChosen) {
const newChosen = [...packagesChosen];
newChosen[index].selected = !packagesChosen[index].selected;
setPackagesChosenSorted(newChosen);
} else {
const newAvailable = [...packagesAvailable];
newAvailable[index].selected = !packagesAvailable[index].selected;
setPackagesAvailableSorted(newAvailable);
}
};
const firstInputElement = useRef(null);
useEffect(() => {
firstInputElement.current?.focus();
}, []);
const handleClearAvailableSearch = () => {
setPackagesSearchName(undefined);
setFilterAvailable(undefined);
setPackagesAvailable([]);
setPackagesAvailableFound(true);
};
const handleClearChosenSearch = () => {
setFilterChosen(undefined);
setPackagesChosenSorted(
packagesChosen.map((pack) => {
return { ...pack, isHidden: false };
})
);
setPackagesChosenFound(true);
};
return (
<DualListSelector>
<DualListSelectorPane
title="Available packages"
searchInput={
<SearchInput
placeholder="Search for a package"
data-testid="search-available-pkgs-input"
value={packagesSearchName}
ref={firstInputElement}
onFocus={() => setFocus('available')}
onBlur={() => setFocus('')}
onChange={(val) => setPackagesSearchName(val)}
submitSearchButtonLabel="Search button for available packages"
onSearch={handlePackagesAvailableSearch}
resetButtonLabel="Clear available packages search"
onClear={handleClearAvailableSearch}
/>
}
>
<DualListSelectorList data-testid="available-pkgs-list">
{!packagesAvailable.length ? (
<p className="pf-u-text-align-center pf-u-mt-md">
{!packagesAvailableFound ? (
'No packages found'
) : (
<>
Search above to add additional
<br />
packages to your image
</>
)}
</p>
) : (
packagesAvailable.map((pack, index) => {
return !pack.isHidden ? (
<DualListSelectorListItem
key={index}
isSelected={pack.selected}
onOptionSelect={(e) => onOptionSelect(e, index, false)}
>
<TextContent key={`${pack.name}-${index}`}>
<span className="pf-c-dual-list-selector__item-text">
{pack.name}
</span>
<small>{pack.summary}</small>
</TextContent>
</DualListSelectorListItem>
) : null;
})
)}
</DualListSelectorList>
</DualListSelectorPane>
<DualListSelectorControlsWrapper aria-label="Selector controls">
<DualListSelectorControl
isDisabled={!packagesAvailable.some((option) => option.selected)}
onClick={() => moveSelectedToChosen()}
aria-label="Add selected"
tooltipContent="Add selected"
>
<AngleRightIcon />
</DualListSelectorControl>
<DualListSelectorControl
isDisabled={!packagesAvailable.length}
onClick={() => moveAllToChosen()}
aria-label="Add all"
tooltipContent="Add all"
>
<AngleDoubleRightIcon />
</DualListSelectorControl>
<DualListSelectorControl
isDisabled={!packagesChosen.length || !packagesChosenFound}
onClick={() => moveAllToAvailable()}
aria-label="Remove all"
tooltipContent="Remove all"
>
<AngleDoubleLeftIcon />
</DualListSelectorControl>
<DualListSelectorControl
onClick={() => moveSelectedToAvailable()}
isDisabled={
!packagesChosen.some((option) => option.selected) ||
!packagesChosenFound
}
aria-label="Remove selected"
tooltipContent="Remove selected"
>
<AngleLeftIcon />
</DualListSelectorControl>
</DualListSelectorControlsWrapper>
<DualListSelectorPane
title="Chosen packages"
searchInput={
<SearchInput
placeholder="Search for a package"
data-testid="search-chosen-pkgs-input"
value={filterChosen}
onFocus={() => setFocus('chosen')}
onBlur={() => setFocus('')}
onChange={(val) => handlePackagesChosenSearch(val)}
resetButtonLabel="Clear chosen packages search"
onClear={handleClearChosenSearch}
/>
}
isChosen
>
<DualListSelectorList data-testid="chosen-pkgs-list">
{!packagesChosen.length ? (
<p className="pf-u-text-align-center pf-u-mt-md">
No packages added
</p>
) : !packagesChosenFound ? (
<p className="pf-u-text-align-center pf-u-mt-md">
No packages found
</p>
) : (
packagesChosen.map((pack, index) => {
return !pack.isHidden ? (
<DualListSelectorListItem
key={index}
isSelected={pack.selected}
onOptionSelect={(e) => onOptionSelect(e, index, true)}
>
<TextContent key={`${pack.name}-${index}`}>
<span className="pf-c-dual-list-selector__item-text">
{pack.name}
</span>
<small>{pack.summary}</small>
</TextContent>
</DualListSelectorListItem>
) : null;
})
)}
</DualListSelectorList>
</DualListSelectorPane>
</DualListSelector>
);
};
Packages.propTypes = {
defaultArch: PropTypes.string,
defaultArch: PropTypes.string,
};
export default Packages;

View file

@ -3,15 +3,23 @@ import Radio from '@data-driven-forms/pf4-component-mapper/radio';
import PropTypes from 'prop-types';
const RadioWithPopover = ({ Popover, ...props }) => {
const ref = useRef();
return <Radio { ...props } label={ <span ref={ ref } className="ins-c-image--builder__popover">{props.label}
<Popover />
</span> } />;
const ref = useRef();
return (
<Radio
{...props}
label={
<span ref={ref} className="ins-c-image--builder__popover">
{props.label}
<Popover />
</span>
}
/>
);
};
RadioWithPopover.propTypes = {
Popover: PropTypes.elementType.isRequired,
label: PropTypes.node
Popover: PropTypes.elementType.isRequired,
label: PropTypes.node,
};
export default RadioWithPopover;

View file

@ -1,21 +1,33 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import {
Button,
DescriptionList, DescriptionListTerm, DescriptionListGroup, DescriptionListDescription,
List, ListItem,
Popover,
Spinner,
Tabs, Tab, TabTitleText,
Text, TextContent, TextVariants, TextList, TextListVariants, TextListItem, TextListItemVariants,
Button,
DescriptionList,
DescriptionListTerm,
DescriptionListGroup,
DescriptionListDescription,
List,
ListItem,
Popover,
Spinner,
Tabs,
Tab,
TabTitleText,
Text,
TextContent,
TextVariants,
TextList,
TextListVariants,
TextListItem,
TextListItemVariants,
} from '@patternfly/react-core';
import {
TableComposable,
Thead,
Tbody,
Tr,
Th,
Td,
TableComposable,
Thead,
Tbody,
Tr,
Th,
Td,
} from '@patternfly/react-table';
import { HelpIcon } from '@patternfly/react-icons';
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
@ -24,308 +36,386 @@ import { RELEASES, UNIT_GIB, UNIT_MIB } from '../../../constants';
import isRhel from '../../../Utilities/isRhel';
const FSReviewTable = ({ ...props }) => {
return (
<TableComposable
aria-label="File system configuration table"
variant="compact">
<Thead>
<Tr>
<Th>Mount point</Th>
<Th>Type</Th>
<Th>Minimum size</Th>
</Tr>
</Thead>
<Tbody data-testid="file-system-configuration-tbody-review">
{props.fsc.map((r, ri) =>
<Tr key={ ri }>
<Td className="pf-m-width-60">{ r.mountpoint }</Td>
<Td className="pf-m-width-10">xfs</Td>
<Td className="pf-m-width-30">{ r.size } { r.unit === UNIT_GIB ? 'GiB' : r.unit === UNIT_MIB ? 'MiB' : 'KiB' }</Td>
</Tr>
)}
</Tbody>
</TableComposable>
);
return (
<TableComposable
aria-label="File system configuration table"
variant="compact"
>
<Thead>
<Tr>
<Th>Mount point</Th>
<Th>Type</Th>
<Th>Minimum size</Th>
</Tr>
</Thead>
<Tbody data-testid="file-system-configuration-tbody-review">
{props.fsc.map((r, ri) => (
<Tr key={ri}>
<Td className="pf-m-width-60">{r.mountpoint}</Td>
<Td className="pf-m-width-10">xfs</Td>
<Td className="pf-m-width-30">
{r.size}{' '}
{r.unit === UNIT_GIB
? 'GiB'
: r.unit === UNIT_MIB
? 'MiB'
: 'KiB'}
</Td>
</Tr>
))}
</Tbody>
</TableComposable>
);
};
FSReviewTable.propTypes = {
fsc: PropTypes.arrayOf(PropTypes.object).isRequired,
fsc: PropTypes.arrayOf(PropTypes.object).isRequired,
};
const ReviewStep = () => {
const [ activeTabKey, setActiveTabKey ] = useState(0);
const [ orgId, setOrgId ] = useState();
const [ minSize, setMinSize ] = useState();
const { change, getState } = useFormApi();
const [activeTabKey, setActiveTabKey] = useState(0);
const [orgId, setOrgId] = useState();
const [minSize, setMinSize] = useState();
const { change, getState } = useFormApi();
useEffect(() => {
const registerSystem = getState()?.values?.['register-system'];
if (registerSystem === 'register-now' || registerSystem === 'register-now-insights') {
(async () => {
const userData = await insights?.chrome?.auth?.getUser();
const id = userData?.identity?.internal?.org_id;
setOrgId(id);
change('subscription-organization-id', id);
})();
}
useEffect(() => {
const registerSystem = getState()?.values?.['register-system'];
if (
registerSystem === 'register-now' ||
registerSystem === 'register-now-insights'
) {
(async () => {
const userData = await insights?.chrome?.auth?.getUser();
const id = userData?.identity?.internal?.org_id;
setOrgId(id);
change('subscription-organization-id', id);
})();
}
if (getState()?.values?.['file-system-config-toggle'] === 'manual' &&
getState()?.values?.['file-system-configuration']) {
let size = 0;
for (const fsc of getState().values['file-system-configuration']) {
size += (fsc.size * fsc.unit);
}
if (
getState()?.values?.['file-system-config-toggle'] === 'manual' &&
getState()?.values?.['file-system-configuration']
) {
let size = 0;
for (const fsc of getState().values['file-system-configuration']) {
size += fsc.size * fsc.unit;
}
size = (size / UNIT_GIB).toFixed(1);
if (size < 1) {
setMinSize(`Less than 1 GiB`);
} else {
setMinSize(`${size} GiB`);
}
}
});
size = (size / UNIT_GIB).toFixed(1);
if (size < 1) {
setMinSize(`Less than 1 GiB`);
} else {
setMinSize(`${size} GiB`);
}
}
});
const handleTabClick = (event, tabIndex) => {
setActiveTabKey(tabIndex);
};
const handleTabClick = (event, tabIndex) => {
setActiveTabKey(tabIndex);
};
return (
<>
<Text>
Review the information and click &quot;Create image&quot;
to create the image using the following criteria.
</Text>
<DescriptionList isCompact isHorizontal>
<DescriptionListGroup>
{getState()?.values?.['image-name'] &&
<>
<DescriptionListTerm>Image name</DescriptionListTerm>
<DescriptionListDescription>
{getState()?.values?.['image-name']}
</DescriptionListDescription>
</>
}
<DescriptionListTerm>Release</DescriptionListTerm>
<DescriptionListDescription>
{RELEASES[getState()?.values?.release]}
</DescriptionListDescription>
</DescriptionListGroup>
</DescriptionList>
<Tabs isFilled activeKey={ activeTabKey } onSelect={ handleTabClick } className="pf-u-w-75">
<Tab eventKey={ 0 } title={ <TabTitleText>Target environment</TabTitleText> } data-testid='tab-target' autoFocus>
<List isPlain iconSize="large">
{getState()?.values?.['target-environment']?.aws &&
<ListItem icon={ <img className='provider-icon' src='/apps/frontend-assets/partners-icons/aws.svg' /> }>
<TextContent>
<Text component={ TextVariants.h3 }>
Amazon Web Services
</Text>
<TextList component={ TextListVariants.dl }>
<TextListItem component={ TextListItemVariants.dt }>Account ID</TextListItem>
<TextListItem component={ TextListItemVariants.dd }>
{getState()?.values?.['aws-account-id']}
</TextListItem>
</TextList>
</TextContent>
</ListItem>
}
{getState()?.values?.['target-environment']?.gcp &&
<ListItem
className='pf-c-list__item pf-u-mt-md'
icon={ <img className='provider-icon' src='/apps/frontend-assets/partners-icons/google-cloud-short.svg' /> }>
<TextContent>
<Text component={ TextVariants.h3 }>Google Cloud Platform</Text>
<TextList component={ TextListVariants.dl }>
<TextListItem component={ TextListItemVariants.dt }>
{googleAccType?.[getState()?.values?.['google-account-type']]}
</TextListItem>
<TextListItem component={ TextListItemVariants.dd }>
{getState()?.values?.['google-email'] || getState()?.values?.['google-domain']}
</TextListItem>
</TextList>
</TextContent>
</ListItem>
}
{getState()?.values?.['target-environment']?.azure &&
<ListItem
className='pf-c-list__item pf-u-mt-md'
icon={ <img className='provider-icon' src='/apps/frontend-assets/partners-icons/microsoft-azure-short.svg' /> }>
<TextContent>
<Text component={ TextVariants.h3 }>Microsoft Azure</Text>
<TextList component={ TextListVariants.dl }>
<TextListItem component={ TextListItemVariants.dt }>
Subscription ID
</TextListItem>
<TextListItem component={ TextListItemVariants.dd }>
{getState()?.values?.['azure-subscription-id']}
</TextListItem>
<TextListItem component={ TextListItemVariants.dt }>
Tenant ID
</TextListItem>
<TextListItem component={ TextListItemVariants.dd }>
{getState()?.values?.['azure-tenant-id']}
</TextListItem>
<TextListItem component={ TextListItemVariants.dt }>
Resource group
</TextListItem>
<TextListItem component={ TextListItemVariants.dd }>
{getState()?.values?.['azure-resource-group']}
</TextListItem>
</TextList>
</TextContent>
</ListItem>
}
{getState()?.values?.['target-environment']?.vsphere &&
<ListItem>
<TextContent>
<Text component={ TextVariants.h3 }>
VMWare
</Text>
</TextContent>
</ListItem>
}
{getState()?.values?.['target-environment']?.['guest-image'] &&
<ListItem>
<TextContent>
<Text component={ TextVariants.h3 }>
Virtualization - Guest image
</Text>
</TextContent>
</ListItem>
}
{getState()?.values?.['target-environment']?.['image-installer'] &&
<ListItem>
<TextContent>
<Text component={ TextVariants.h3 }>
Bare metal - Installer
</Text>
</TextContent>
</ListItem>
}
</List>
</Tab>
{isRhel(getState()?.values?.release) &&
<Tab eventKey={ 1 } title={ <TabTitleText>Registration</TabTitleText> } data-testid='tab-registration'>
{getState()?.values?.['register-system'] === 'register-later' &&
<TextContent>
<TextList component={ TextListVariants.dl }>
<TextListItem component={ TextListItemVariants.dt }>
Subscription
</TextListItem>
<TextListItem component={ TextListItemVariants.dd }>
Register the system later
</TextListItem>
</TextList>
</TextContent>
}
{(getState()?.values?.['register-system'] === 'register-now' ||
getState()?.values?.['register-system'] === 'register-now-insights') &&
<TextContent>
<TextList component={ TextListVariants.dl }>
<TextListItem component={ TextListItemVariants.dt }>
Subscription
</TextListItem>
<TextListItem component={ TextListItemVariants.dd }>
{getState()?.values?.['register-system'] === 'register-now-insights' &&
'Register with Subscriptions and Red Hat Insights'
}
{getState()?.values?.['register-system'] === 'register-now' &&
'Register with Subscriptions'
}
</TextListItem>
<TextListItem component={ TextListItemVariants.dt }>
Activation key
</TextListItem>
<TextListItem component={ TextListItemVariants.dd }>
{getState()?.values?.['subscription-activation-key']}
</TextListItem>
<TextListItem component={ TextListItemVariants.dt }>
Organization ID
</TextListItem>
{orgId !== undefined ? (
<TextListItem component={ TextListItemVariants.dd } data-testid='organization-id'>
{orgId}
</TextListItem>
) : (
<TextListItem component={ TextListItemVariants.dd }>
<Spinner />
</TextListItem>
)}
</TextList>
</TextContent>
}
</Tab>
return (
<>
<Text>
Review the information and click &quot;Create image&quot; to create the
image using the following criteria.
</Text>
<DescriptionList isCompact isHorizontal>
<DescriptionListGroup>
{getState()?.values?.['image-name'] && (
<>
<DescriptionListTerm>Image name</DescriptionListTerm>
<DescriptionListDescription>
{getState()?.values?.['image-name']}
</DescriptionListDescription>
</>
)}
<DescriptionListTerm>Release</DescriptionListTerm>
<DescriptionListDescription>
{RELEASES[getState()?.values?.release]}
</DescriptionListDescription>
</DescriptionListGroup>
</DescriptionList>
<Tabs
isFilled
activeKey={activeTabKey}
onSelect={handleTabClick}
className="pf-u-w-75"
>
<Tab
eventKey={0}
title={<TabTitleText>Target environment</TabTitleText>}
data-testid="tab-target"
autoFocus
>
<List isPlain iconSize="large">
{getState()?.values?.['target-environment']?.aws && (
<ListItem
icon={
<img
className="provider-icon"
src="/apps/frontend-assets/partners-icons/aws.svg"
/>
}
<Tab eventKey={ 2 } title={ <TabTitleText>System configuration</TabTitleText> } data-testid='tab-system'>
<TextContent>
<Text component={ TextVariants.h3 }>File system configuration</Text>
<TextList component={ TextListVariants.dl }>
<TextListItem component={ TextListItemVariants.dt }>
Partitioning
</TextListItem>
<TextListItem component={ TextListItemVariants.dd } data-testid='partitioning-auto-manual'>
{getState()?.values?.['file-system-config-toggle'] === 'manual' ? 'Manual' : 'Automatic'}
{getState()?.values?.['file-system-config-toggle'] === 'manual' &&
<>
{' '}
<Popover
position="bottom"
headerContent="Partitions"
hasAutoWidth
minWidth="30rem"
bodyContent={ <FSReviewTable fsc={ getState().values['file-system-configuration'] } /> }>
<Button
data-testid='file-system-configuration-popover'
variant="link"
aria-label="File system configuration info"
aria-describedby="file-system-configuration-info">
View partitions
</Button>
</Popover>
</>
}
</TextListItem>
{getState()?.values?.['file-system-config-toggle'] === 'manual' &&
<>
<TextListItem component={ TextListItemVariants.dt }>
Image size (minimum)
<Popover
hasAutoWidth
bodyContent={ <TextContent>
<Text>
Image Builder may extend this size based on requirements,
selected packages, and configurations.
</Text>
</TextContent> }>
<Button
variant="plain"
aria-label="File system configuration info"
aria-describedby="file-system-configuration-info"
className="pf-c-form__group-label-help">
<HelpIcon />
</Button>
</Popover>
</TextListItem>
<TextListItem component={ TextListItemVariants.dd }>
{ minSize }
</TextListItem>
</>
}
</TextList>
<Text component={ TextVariants.h3 }>Packages</Text>
<TextList component={ TextListVariants.dl }>
<TextListItem component={ TextListItemVariants.dt }>
Chosen
</TextListItem>
<TextListItem component={ TextListItemVariants.dd } data-testid='chosen-packages-count'>
{getState()?.values?.['selected-packages']?.length || 0}
</TextListItem>
</TextList>
</TextContent>
</Tab>
</Tabs>
</>
);
>
<TextContent>
<Text component={TextVariants.h3}>Amazon Web Services</Text>
<TextList component={TextListVariants.dl}>
<TextListItem component={TextListItemVariants.dt}>
Account ID
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{getState()?.values?.['aws-account-id']}
</TextListItem>
</TextList>
</TextContent>
</ListItem>
)}
{getState()?.values?.['target-environment']?.gcp && (
<ListItem
className="pf-c-list__item pf-u-mt-md"
icon={
<img
className="provider-icon"
src="/apps/frontend-assets/partners-icons/google-cloud-short.svg"
/>
}
>
<TextContent>
<Text component={TextVariants.h3}>Google Cloud Platform</Text>
<TextList component={TextListVariants.dl}>
<TextListItem component={TextListItemVariants.dt}>
{
googleAccType?.[
getState()?.values?.['google-account-type']
]
}
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{getState()?.values?.['google-email'] ||
getState()?.values?.['google-domain']}
</TextListItem>
</TextList>
</TextContent>
</ListItem>
)}
{getState()?.values?.['target-environment']?.azure && (
<ListItem
className="pf-c-list__item pf-u-mt-md"
icon={
<img
className="provider-icon"
src="/apps/frontend-assets/partners-icons/microsoft-azure-short.svg"
/>
}
>
<TextContent>
<Text component={TextVariants.h3}>Microsoft Azure</Text>
<TextList component={TextListVariants.dl}>
<TextListItem component={TextListItemVariants.dt}>
Subscription ID
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{getState()?.values?.['azure-subscription-id']}
</TextListItem>
<TextListItem component={TextListItemVariants.dt}>
Tenant ID
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{getState()?.values?.['azure-tenant-id']}
</TextListItem>
<TextListItem component={TextListItemVariants.dt}>
Resource group
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{getState()?.values?.['azure-resource-group']}
</TextListItem>
</TextList>
</TextContent>
</ListItem>
)}
{getState()?.values?.['target-environment']?.vsphere && (
<ListItem>
<TextContent>
<Text component={TextVariants.h3}>VMWare</Text>
</TextContent>
</ListItem>
)}
{getState()?.values?.['target-environment']?.['guest-image'] && (
<ListItem>
<TextContent>
<Text component={TextVariants.h3}>
Virtualization - Guest image
</Text>
</TextContent>
</ListItem>
)}
{getState()?.values?.['target-environment']?.[
'image-installer'
] && (
<ListItem>
<TextContent>
<Text component={TextVariants.h3}>
Bare metal - Installer
</Text>
</TextContent>
</ListItem>
)}
</List>
</Tab>
{isRhel(getState()?.values?.release) && (
<Tab
eventKey={1}
title={<TabTitleText>Registration</TabTitleText>}
data-testid="tab-registration"
>
{getState()?.values?.['register-system'] === 'register-later' && (
<TextContent>
<TextList component={TextListVariants.dl}>
<TextListItem component={TextListItemVariants.dt}>
Subscription
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
Register the system later
</TextListItem>
</TextList>
</TextContent>
)}
{(getState()?.values?.['register-system'] === 'register-now' ||
getState()?.values?.['register-system'] ===
'register-now-insights') && (
<TextContent>
<TextList component={TextListVariants.dl}>
<TextListItem component={TextListItemVariants.dt}>
Subscription
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{getState()?.values?.['register-system'] ===
'register-now-insights' &&
'Register with Subscriptions and Red Hat Insights'}
{getState()?.values?.['register-system'] ===
'register-now' && 'Register with Subscriptions'}
</TextListItem>
<TextListItem component={TextListItemVariants.dt}>
Activation key
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{getState()?.values?.['subscription-activation-key']}
</TextListItem>
<TextListItem component={TextListItemVariants.dt}>
Organization ID
</TextListItem>
{orgId !== undefined ? (
<TextListItem
component={TextListItemVariants.dd}
data-testid="organization-id"
>
{orgId}
</TextListItem>
) : (
<TextListItem component={TextListItemVariants.dd}>
<Spinner />
</TextListItem>
)}
</TextList>
</TextContent>
)}
</Tab>
)}
<Tab
eventKey={2}
title={<TabTitleText>System configuration</TabTitleText>}
data-testid="tab-system"
>
<TextContent>
<Text component={TextVariants.h3}>File system configuration</Text>
<TextList component={TextListVariants.dl}>
<TextListItem component={TextListItemVariants.dt}>
Partitioning
</TextListItem>
<TextListItem
component={TextListItemVariants.dd}
data-testid="partitioning-auto-manual"
>
{getState()?.values?.['file-system-config-toggle'] === 'manual'
? 'Manual'
: 'Automatic'}
{getState()?.values?.['file-system-config-toggle'] ===
'manual' && (
<>
{' '}
<Popover
position="bottom"
headerContent="Partitions"
hasAutoWidth
minWidth="30rem"
bodyContent={
<FSReviewTable
fsc={getState().values['file-system-configuration']}
/>
}
>
<Button
data-testid="file-system-configuration-popover"
variant="link"
aria-label="File system configuration info"
aria-describedby="file-system-configuration-info"
>
View partitions
</Button>
</Popover>
</>
)}
</TextListItem>
{getState()?.values?.['file-system-config-toggle'] ===
'manual' && (
<>
<TextListItem component={TextListItemVariants.dt}>
Image size (minimum)
<Popover
hasAutoWidth
bodyContent={
<TextContent>
<Text>
Image Builder may extend this size based on
requirements, selected packages, and configurations.
</Text>
</TextContent>
}
>
<Button
variant="plain"
aria-label="File system configuration info"
aria-describedby="file-system-configuration-info"
className="pf-c-form__group-label-help"
>
<HelpIcon />
</Button>
</Popover>
</TextListItem>
<TextListItem component={TextListItemVariants.dd}>
{minSize}
</TextListItem>
</>
)}
</TextList>
<Text component={TextVariants.h3}>Packages</Text>
<TextList component={TextListVariants.dl}>
<TextListItem component={TextListItemVariants.dt}>
Chosen
</TextListItem>
<TextListItem
component={TextListItemVariants.dd}
data-testid="chosen-packages-count"
>
{getState()?.values?.['selected-packages']?.length || 0}
</TextListItem>
</TextList>
</TextContent>
</Tab>
</Tabs>
</>
);
};
export default ReviewStep;

View file

@ -1,74 +1,75 @@
import React, {
useEffect,
useState,
} from 'react';
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import {
Select,
SelectOption,
SelectVariant,
TextInput,
Select,
SelectOption,
SelectVariant,
TextInput,
} from '@patternfly/react-core';
import { UNIT_KIB, UNIT_MIB, UNIT_GIB } from '../../../constants';
const SizeUnit = ({ ...props }) => {
const [ isOpen, setIsOpen ] = useState(false);
const [ unit, setUnit ] = useState(props.unit || UNIT_GIB);
const [ size, setSize ] = useState(props.size || 1);
const [isOpen, setIsOpen] = useState(false);
const [unit, setUnit] = useState(props.unit || UNIT_GIB);
const [size, setSize] = useState(props.size || 1);
useEffect(() => {
props.onChange(size, unit);
}, [ unit, size ]);
useEffect(() => {
props.onChange(size, unit);
}, [unit, size]);
const onToggle = (isOpen) => {
setIsOpen(isOpen);
};
const onToggle = (isOpen) => {
setIsOpen(isOpen);
};
const onSelect = (event, selection) => {
switch (selection) {
case 'KiB':
setUnit(UNIT_KIB);
break;
case 'MiB':
setUnit(UNIT_MIB);
break;
case 'GiB':
setUnit(UNIT_GIB);
break;
const onSelect = (event, selection) => {
switch (selection) {
case 'KiB':
setUnit(UNIT_KIB);
break;
case 'MiB':
setUnit(UNIT_MIB);
break;
case 'GiB':
setUnit(UNIT_GIB);
break;
}
setIsOpen(false);
};
return (
<>
<TextInput
className="pf-u-w-50"
type="text"
value={size}
aria-label="Size text input"
onChange={(v) => setSize(isNaN(parseInt(v)) ? 0 : parseInt(v))}
/>
<Select
className="pf-u-w-50"
isOpen={isOpen}
onToggle={onToggle}
onSelect={onSelect}
selections={
unit === UNIT_KIB ? 'KiB' : unit === UNIT_MIB ? 'MiB' : 'GiB'
}
setIsOpen(false);
};
return (
<>
<TextInput
className="pf-u-w-50"
type="text"
value={ size }
aria-label="Size text input"
onChange={ v => setSize(isNaN(parseInt(v)) ? 0 : parseInt(v)) } />
<Select
className="pf-u-w-50"
isOpen={ isOpen }
onToggle={ onToggle }
onSelect={ onSelect }
selections={ unit === UNIT_KIB ? 'KiB' : unit === UNIT_MIB ? 'MiB' : 'GiB' }
variant={ SelectVariant.single }
aria-label="Unit select">
{[ 'KiB', 'MiB', 'GiB' ].map((u, index) => {
return <SelectOption key={ index } value={ u } />;
})}
</Select>
</>
);
variant={SelectVariant.single}
aria-label="Unit select"
>
{['KiB', 'MiB', 'GiB'].map((u, index) => {
return <SelectOption key={index} value={u} />;
})}
</Select>
</>
);
};
SizeUnit.propTypes = {
size: PropTypes.number.isRequired,
unit: PropTypes.number.isRequired,
onChange: PropTypes.func.isRequired,
size: PropTypes.number.isRequired,
unit: PropTypes.number.isRequired,
onChange: PropTypes.func.isRequired,
};
export default SizeUnit;

View file

@ -2,125 +2,164 @@ import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
import { Checkbox, FormGroup, Text, TextVariants, Tile } from '@patternfly/react-core';
import {
Checkbox,
FormGroup,
Text,
TextVariants,
Tile,
} from '@patternfly/react-core';
const TargetEnvironment = ({ label, isRequired, ...props }) => {
const { getState, change } = useFormApi();
const { input } = useFieldApi({ label, isRequired, ...props });
const [ environment, setEnvironment ] = useState({
aws: false,
azure: false,
gcp: false,
vsphere: false,
'guest-image': false,
'image-installer': false,
const { getState, change } = useFormApi();
const { input } = useFieldApi({ label, isRequired, ...props });
const [environment, setEnvironment] = useState({
aws: false,
azure: false,
gcp: false,
vsphere: false,
'guest-image': false,
'image-installer': false,
});
useEffect(() => {
if (getState()?.values?.[input.name]) {
setEnvironment(getState().values[input.name]);
}
}, []);
const handleSetEnvironment = (env) =>
setEnvironment((prevEnv) => {
const newEnv = {
...prevEnv,
[env]: !prevEnv[env],
};
change(input.name, newEnv);
return newEnv;
});
useEffect(() => {
if (getState()?.values?.[input.name]) {
setEnvironment(getState().values[input.name]);
}
}, []);
const handleKeyDown = (e, env) => {
if (e.key === ' ') {
handleSetEnvironment(env);
}
};
const handleSetEnvironment = (env) => setEnvironment((prevEnv) => {
const newEnv = ({
...prevEnv,
[env]: !prevEnv[env]
});
change(input.name, newEnv);
return newEnv;
});
const handleKeyDown = (e, env) => {
if (e.key === ' ') {
handleSetEnvironment(env);
}
};
return (
<>
<FormGroup isRequired={ isRequired } label={ label } data-testid="target-select">
<FormGroup label={ <Text component={ TextVariants.small }>Public cloud</Text> } data-testid="target-public">
<div className="tiles">
<Tile
className="tile pf-u-mr-sm"
data-testid="upload-aws"
title="Amazon Web Services"
icon={ <img
className='provider-icon'
src={ '/apps/frontend-assets/partners-icons/aws.svg' } /> }
onClick={ () => handleSetEnvironment('aws') }
onKeyDown = { (e) => handleKeyDown(e, 'aws') }
isSelected={ environment.aws }
isStacked
isDisplayLarge />
<Tile
className="tile pf-u-mr-sm"
data-testid="upload-google"
title="Google Cloud Platform"
icon={ <img
className='provider-icon'
src={ '/apps/frontend-assets/partners-icons/google-cloud-short.svg' } /> }
onClick={ () => handleSetEnvironment('gcp') }
isSelected={ environment.gcp }
onKeyDown = { (e) => handleKeyDown(e, 'gcp') }
isStacked
isDisplayLarge />
<Tile
className="tile pf-u-mr-sm"
data-testid="upload-azure"
title="Microsoft Azure"
icon={ <img
className='provider-icon'
src={ '/apps/frontend-assets/partners-icons/microsoft-azure-short.svg' } /> }
onClick={ () => handleSetEnvironment('azure') }
onKeyDown = { (e) => handleKeyDown(e, 'azure') }
isSelected={ environment.azure }
isStacked
isDisplayLarge />
</div>
</FormGroup>
<FormGroup label={ <Text component={ TextVariants.small }>Private cloud</Text> } data-testid="target-private">
<Checkbox
label="VMWare"
isChecked={ environment.vsphere }
onChange={ () => handleSetEnvironment('vsphere') }
aria-label="VMWare checkbox"
id="checkbox-vmware"
name="VMWare"
data-testid="checkbox-vmware" />
</FormGroup>
<FormGroup label={ <Text component={ TextVariants.small }>Other</Text> } data-testid="target-other">
<Checkbox
label="Virtualization - Guest image"
isChecked={ environment['guest-image'] }
onChange={ () => handleSetEnvironment('guest-image') }
aria-label="Virtualization guest image checkbox"
id="checkbox-guest-image"
name="Virtualization guest image"
data-testid="checkbox-guest-image" />
<Checkbox
label="Bare metal - Installer"
isChecked={ environment['image-installer'] }
onChange={ () => handleSetEnvironment('image-installer') }
aria-label="Bare metal installer checkbox"
id="checkbox-image-installer"
name="Bare metal installer"
data-testid="checkbox-image-installer" />
</FormGroup>
</FormGroup>
</>
);
return (
<>
<FormGroup
isRequired={isRequired}
label={label}
data-testid="target-select"
>
<FormGroup
label={<Text component={TextVariants.small}>Public cloud</Text>}
data-testid="target-public"
>
<div className="tiles">
<Tile
className="tile pf-u-mr-sm"
data-testid="upload-aws"
title="Amazon Web Services"
icon={
<img
className="provider-icon"
src={'/apps/frontend-assets/partners-icons/aws.svg'}
/>
}
onClick={() => handleSetEnvironment('aws')}
onKeyDown={(e) => handleKeyDown(e, 'aws')}
isSelected={environment.aws}
isStacked
isDisplayLarge
/>
<Tile
className="tile pf-u-mr-sm"
data-testid="upload-google"
title="Google Cloud Platform"
icon={
<img
className="provider-icon"
src={
'/apps/frontend-assets/partners-icons/google-cloud-short.svg'
}
/>
}
onClick={() => handleSetEnvironment('gcp')}
isSelected={environment.gcp}
onKeyDown={(e) => handleKeyDown(e, 'gcp')}
isStacked
isDisplayLarge
/>
<Tile
className="tile pf-u-mr-sm"
data-testid="upload-azure"
title="Microsoft Azure"
icon={
<img
className="provider-icon"
src={
'/apps/frontend-assets/partners-icons/microsoft-azure-short.svg'
}
/>
}
onClick={() => handleSetEnvironment('azure')}
onKeyDown={(e) => handleKeyDown(e, 'azure')}
isSelected={environment.azure}
isStacked
isDisplayLarge
/>
</div>
</FormGroup>
<FormGroup
label={<Text component={TextVariants.small}>Private cloud</Text>}
data-testid="target-private"
>
<Checkbox
label="VMWare"
isChecked={environment.vsphere}
onChange={() => handleSetEnvironment('vsphere')}
aria-label="VMWare checkbox"
id="checkbox-vmware"
name="VMWare"
data-testid="checkbox-vmware"
/>
</FormGroup>
<FormGroup
label={<Text component={TextVariants.small}>Other</Text>}
data-testid="target-other"
>
<Checkbox
label="Virtualization - Guest image"
isChecked={environment['guest-image']}
onChange={() => handleSetEnvironment('guest-image')}
aria-label="Virtualization guest image checkbox"
id="checkbox-guest-image"
name="Virtualization guest image"
data-testid="checkbox-guest-image"
/>
<Checkbox
label="Bare metal - Installer"
isChecked={environment['image-installer']}
onChange={() => handleSetEnvironment('image-installer')}
aria-label="Bare metal installer checkbox"
id="checkbox-image-installer"
name="Bare metal installer"
data-testid="checkbox-image-installer"
/>
</FormGroup>
</FormGroup>
</>
);
};
TargetEnvironment.propTypes = {
label: PropTypes.node,
isRequired: PropTypes.bool
label: PropTypes.node,
isRequired: PropTypes.bool,
};
TargetEnvironment.defaultProps = {
label: '',
isRequired: false
label: '',
isRequired: false,
};
export default TargetEnvironment;