update style across the project
The eslint updates require style changes in all components.
This commit is contained in:
parent
7959f2a563
commit
4fa71cede8
56 changed files with 5973 additions and 5177 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
"Contributor" 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
|
||||
"Contributor" 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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 "Create image"
|
||||
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 "Create image" 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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue