CreateImageWizard: File system configuration
This commit is contained in:
parent
1e864a2076
commit
350acbd21b
15 changed files with 906 additions and 17 deletions
|
|
@ -0,0 +1,48 @@
|
|||
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');
|
||||
|
||||
useEffect(() => {
|
||||
change(input.name, selected);
|
||||
}, [ selected ]);
|
||||
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileSystemConfigToggle;
|
||||
|
|
@ -0,0 +1,333 @@
|
|||
import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { 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,
|
||||
} 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';
|
||||
import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { UNIT_GIB } from '../../../constants';
|
||||
import MountPoint from './MountPoint';
|
||||
import SizeUnit from './SizeUnit';
|
||||
|
||||
let initialRow = {
|
||||
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 ]);
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}));
|
||||
}, [ 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) || curListItem.id === draggedItemId) {
|
||||
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);
|
||||
} else {
|
||||
evt.currentTarget.classList.remove(styles.modifiers.ghostRow);
|
||||
evt.currentTarget.setAttribute('aria-pressed', 'false');
|
||||
setDraggedItemId(null);
|
||||
setDraggingToItemIndex(null);
|
||||
setIsDragging(false);
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
TextInput,
|
||||
} from '@patternfly/react-core';
|
||||
import path from 'path';
|
||||
|
||||
const MountPoint = ({ ...props }) => {
|
||||
// check '/' last!
|
||||
const validPrefixes = [ '/home', '/opt', '/srv', '/usr/local', '/var', '/boot', '/' ];
|
||||
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;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let suf = suffix;
|
||||
let mp = prefix;
|
||||
if (suf) {
|
||||
if (mp !== '/' && suf[0] !== '/') {
|
||||
suf = '/' + suf;
|
||||
}
|
||||
|
||||
mp += suf;
|
||||
}
|
||||
|
||||
props.onChange(path.normalize(mp));
|
||||
}, [ prefix, suffix ]);
|
||||
|
||||
const onToggle = (isOpen) => {
|
||||
setIsOpen(isOpen);
|
||||
};
|
||||
|
||||
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>
|
||||
<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,
|
||||
};
|
||||
|
||||
export default MountPoint;
|
||||
|
|
@ -1,18 +1,61 @@
|
|||
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
|
||||
Text, TextContent, TextVariants, TextList, TextListVariants, TextListItem, TextListItemVariants,
|
||||
} from '@patternfly/react-core';
|
||||
import {
|
||||
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';
|
||||
import { releaseValues } from '../steps/imageOutput';
|
||||
import { googleAccType } from '../steps/googleCloud';
|
||||
import { UNIT_GIB, UNIT_MIB } from '../../../constants';
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
FSReviewTable.propTypes = {
|
||||
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();
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -25,6 +68,21 @@ const ReviewStep = () => {
|
|||
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);
|
||||
}
|
||||
|
||||
size = (size / UNIT_GIB).toFixed(1);
|
||||
if (size < 1) {
|
||||
setMinSize(`Less than 1 GiB`);
|
||||
} else {
|
||||
setMinSize(`${size} GiB`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const handleTabClick = (event, tabIndex) => {
|
||||
|
|
@ -192,6 +250,60 @@ const ReviewStep = () => {
|
|||
}
|
||||
<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 }>
|
||||
|
|
|
|||
74
src/Components/CreateImageWizard/formComponents/SizeUnit.js
Normal file
74
src/Components/CreateImageWizard/formComponents/SizeUnit.js
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
props.onChange(size, unit);
|
||||
}, [ unit, size ]);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TextInput
|
||||
className="pf-u-w-50"
|
||||
type="text"
|
||||
value={ size }
|
||||
aria-label="Size text input"
|
||||
onChange={ v => setSize(isNaN(parseInt(v)) ? '' : 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
SizeUnit.propTypes = {
|
||||
size: PropTypes.number.isRequired,
|
||||
unit: PropTypes.number.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default SizeUnit;
|
||||
Loading…
Add table
Add a link
Reference in a new issue