import { Button, ButtonVariant, Checkbox, Divider, ExpandableSection, ExpandableSectionToggle, Flex, FlexItem, Grid, } from '@patternfly/react-core'; import { MinusCircleIcon } from '@patternfly/react-icons'; import { FieldArray, FieldArrayRenderProps, useField } from 'formik'; import cloneDeep from 'lodash/cloneDeep'; import React from 'react'; import { getFormikArrayItemFieldName, LoadingState } from '../../../../../common'; import ConfirmationModal from '../../../../../common/components/ui/ConfirmationModal'; const fieldName = 'hosts'; export type HostComponentProps = { fieldName: string; hostIdx: number; }; export type HostArrayProps<HostFieldType> = { emptyHostData: HostFieldType; enableCopyAboveConfiguration?: boolean; CollapsedHostComponent: React.FC<HostComponentProps>; ExpandedHostComponent: React.FC<HostComponentProps>; }; type HostsProps<HostFieldType> = HostArrayProps<HostFieldType> & FieldArrayRenderProps; type SingleHostProps<HostFieldType> = { hostIdx: number; onRemove: () => void; onToggleExpand: (isExpanded: boolean) => void; isExpanded: boolean; emptyHostData: HostFieldType; CollapsedHostComponent: React.FC<HostComponentProps>; ExpandedHostComponent: React.FC<HostComponentProps>; fieldName: string; enableRemoveHost: boolean; }; export const RemoveItemButton: React.FC<{ onRemove: () => void; showRemoveButton: boolean; dataTestId: string; }> = ({ onRemove, showRemoveButton, dataTestId }) => ( //use css visibility instead of conditional rendering to avoid button jumping when hovering <Button aria-label="remove host" style={{ visibility: showRemoveButton ? 'visible' : 'hidden' }} variant="plain" > <MinusCircleIcon onClick={onRemove} data-testid={dataTestId} /> </Button> ); const getHostName = (hostIdx: number) => `Host ${hostIdx + 1}`; const SingleHost = <HostFieldType,>({ fieldName, hostIdx, onRemove, onToggleExpand, isExpanded, CollapsedHostComponent, ExpandedHostComponent, enableRemoveHost, }: SingleHostProps<HostFieldType>) => { //TODO: fix RemovableField reusable component to support this use case const [showRemoveButton, setShowRemoveButton] = React.useState(false); const updateShowRemoveButton = (value: boolean) => { if (!enableRemoveHost) { setShowRemoveButton(false); } else { setShowRemoveButton(value); } }; const hostFieldName = getFormikArrayItemFieldName(fieldName, hostIdx); return ( <Grid onMouseEnter={() => updateShowRemoveButton(true)} onMouseLeave={() => updateShowRemoveButton(false)} hasGutter > <Flex> <FlexItem> <ExpandableSectionToggle isExpanded={isExpanded} onToggle={onToggleExpand} direction="down" data-testid={`toggle-host-${hostIdx}`} > {getHostName(hostIdx)} </ExpandableSectionToggle> </FlexItem> {hostIdx > 0 && ( <FlexItem align={{ default: 'alignRight' }}> <RemoveItemButton onRemove={onRemove} showRemoveButton={showRemoveButton} dataTestId={`remove-host-${hostIdx}`} /> </FlexItem> )} </Flex> {isExpanded && ( <ExpandableSection isDetached key={hostIdx} isExpanded={true}> <ExpandedHostComponent fieldName={hostFieldName} hostIdx={hostIdx} /> </ExpandableSection> )} {!isExpanded && <CollapsedHostComponent fieldName={hostFieldName} hostIdx={hostIdx} />} </Grid> ); }; type ExpandedHosts = { [hostIdx: number]: boolean }; const getExpandedHostsInitialValue = (numHosts: number): ExpandedHosts => { const ret = {}; for (let i = 0; i < numHosts; ++i) { ret[i] = false; } return ret; }; const getExpandedHostsDefaultValue = (numHosts: number): ExpandedHosts => { const ret = getExpandedHostsInitialValue(numHosts); if (numHosts === 1) { ret[0] = true; } return ret; }; const Hosts = <HostFieldType,>({ push, remove, enableCopyAboveConfiguration, emptyHostData, ...props }: HostsProps<HostFieldType>) => { const [field, { error }] = useField<HostFieldType[]>({ name: fieldName, }); const [expandedHosts, setExpandedHosts] = React.useState<ExpandedHosts>( getExpandedHostsDefaultValue(field.value.length), ); const [copyConfiguration, setCopyConfiguration] = React.useState<boolean>(false); const [hostIdxToRemove, setHostIdxToRemove] = React.useState<number | null>(null); if (field.value === undefined) { return <LoadingState />; } const onAddHost = () => { let newHostData: HostFieldType; if (copyConfiguration) { newHostData = cloneDeep(field.value[field.value.length - 1]); } else { newHostData = cloneDeep(emptyHostData); } const newExpandedHosts = getExpandedHostsInitialValue(field.value.length + 1); newExpandedHosts[field.value.length] = true; setExpandedHosts(newExpandedHosts); push(newHostData); }; return ( <> {field.value.map((data, hostIdx) => { const onToggleExpand = (isExpanded: boolean) => { const newExpandedHosts = cloneDeep(expandedHosts); newExpandedHosts[hostIdx] = isExpanded; setExpandedHosts(newExpandedHosts); }; return ( <React.Fragment key={hostIdx}> <SingleHost hostIdx={hostIdx} onToggleExpand={onToggleExpand} isExpanded={expandedHosts[hostIdx]} onRemove={() => setHostIdxToRemove(hostIdx)} fieldName={fieldName} emptyHostData={emptyHostData} enableRemoveHost={field.value.length > 1} {...props} /> <Divider /> </React.Fragment> ); })} <Flex> <FlexItem> <Button variant="secondary" onClick={onAddHost} data-testid="add-host" isDisabled={!!error} > Add another host </Button> </FlexItem> {enableCopyAboveConfiguration && ( <FlexItem alignSelf={{ default: 'alignSelfCenter' }}> <Checkbox label="Copy the above configuration" isChecked={copyConfiguration} onChange={setCopyConfiguration} aria-label="copy host configuration" name="copy-host-configuration" id="copy-host-configuration" data-testid="copy-host-cofiguration" /> </FlexItem> )} </Flex> {hostIdxToRemove && ( <ConfirmationModal title={`Delete ${getHostName(hostIdxToRemove)}?`} titleIconVariant="warning" confirmationButtonText="Delete" confirmationButtonVariant={ButtonVariant.danger} content={ <> <p>{`All the network configurations of ${getHostName( hostIdxToRemove, )} will be lost`}</p> </> } onClose={() => setHostIdxToRemove(null)} onConfirm={() => { remove(hostIdxToRemove); setHostIdxToRemove(null); }} /> )} </> ); }; const StaticIpHostsArray = <HostFieldType,>({ ...props }: HostArrayProps<HostFieldType>) => { const renderHosts = React.useCallback( (arrayRenderProps: FieldArrayRenderProps) => ( <Hosts {...Object.assign(props, arrayRenderProps)} /> ), // eslint-disable-next-line react-hooks/exhaustive-deps [], ); return <FieldArray name={fieldName} render={renderHosts} />; }; export default StaticIpHostsArray;