WizardV2: Add draggable mount point

This commit is contained in:
Amir 2024-04-08 18:30:49 +03:00 committed by Klara Simickova
parent 0a62e0d286
commit e5bfc19194
3 changed files with 252 additions and 51 deletions

View file

@ -3,7 +3,6 @@ import React, { useEffect, useState } from 'react';
import {
Alert,
Button,
Popover,
Text,
TextContent,
TextInput,
@ -12,15 +11,13 @@ import {
WizardFooterWrapper,
} from '@patternfly/react-core';
import { Select, SelectOption } from '@patternfly/react-core/deprecated';
import {
HelpIcon,
MinusCircleIcon,
PlusCircleIcon,
} from '@patternfly/react-icons';
import { MinusCircleIcon, PlusCircleIcon } from '@patternfly/react-icons';
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
import { Td, Tr } from '@patternfly/react-table';
import { v4 as uuidv4 } from 'uuid';
import FileSystemTable from './FileSystemTable';
import { UNIT_GIB, UNIT_KIB, UNIT_MIB } from '../../../../constants';
import { useAppDispatch, useAppSelector } from '../../../../store/hooks';
import {
@ -158,47 +155,7 @@ const FileSystemConfiguration = () => {
title="Filesystem customizations are not applied to 'Bare metal - Installer' images"
/>
)}
<Table aria-label="File system table" variant="compact">
<Thead>
<Tr>
<Th />
<Th>Mount point</Th>
<Th></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 />
<Th />
</Tr>
</Thead>
<Tbody data-testid="file-system-configuration-tbody">
{partitions &&
partitions.map((partition) => (
<Row key={partition.id} partition={partition} />
))}
</Tbody>
</Table>
<FileSystemTable />
<TextContent>
<Button
ouiaId="add-partition"
@ -217,6 +174,9 @@ const FileSystemConfiguration = () => {
type RowPropTypes = {
partition: Partition;
onDrop?: (event: React.DragEvent<HTMLTableRowElement>) => void;
onDragEnd?: (event: React.DragEvent<HTMLTableRowElement>) => void;
onDragStart?: (event: React.DragEvent<HTMLTableRowElement>) => void;
};
const getPrefix = (mountpoint: string) => {
@ -227,7 +187,12 @@ const getSuffix = (mountpoint: string) => {
return mountpoint.substring(prefix.length);
};
const Row = ({ partition }: RowPropTypes) => {
export const Row = ({
partition,
onDragEnd,
onDragStart,
onDrop,
}: RowPropTypes) => {
const dispatch = useAppDispatch();
const partitions = useAppSelector(selectPartitions);
const handleRemovePartition = (id: string) => {
@ -237,8 +202,18 @@ const Row = ({ partition }: RowPropTypes) => {
const duplicates = getDuplicateMountPoints(partitions);
return (
<Tr>
<Td />
<Tr
draggable
id={partition.id}
onDrop={onDrop}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
>
<Td
draggableRow={{
id: `draggable-row-${partition.id}`,
}}
/>
<Td className="pf-m-width-20">
<MountpointPrefix partition={partition} />
{!isNextButtonPristine &&

View file

@ -0,0 +1,220 @@
import React, { useRef, useState } from 'react';
import { Popover, TextContent, Text, Button } from '@patternfly/react-core';
import { HelpIcon } from '@patternfly/react-icons';
import styles from '@patternfly/react-styles/css/components/Table/table';
import {
Table,
Th,
Thead,
Tbody,
Tr,
TrProps,
TbodyProps,
} from '@patternfly/react-table';
import { Row } from './FileSystemConfiguration';
import { useAppDispatch, useAppSelector } from '../../../../store/hooks';
import {
changePartitionOrder,
selectPartitions,
} from '../../../../store/wizardSlice';
const FileSystemTable = () => {
const [draggedItemId, setDraggedItemId] = useState<string | null>(null);
const [draggingToItemIndex, setDraggingToItemIndex] = useState<number | null>(
null
);
const [isDragging, setIsDragging] = useState(false);
const [tempItemOrder, setTempItemOrder] = useState<string[]>([]);
const bodyRef = useRef<HTMLTableSectionElement>(null);
const partitions = useAppSelector(selectPartitions);
const itemOrder = partitions.map((partition) => partition.id);
const dispatch = useAppDispatch();
const isValidDrop = (
evt: React.DragEvent<HTMLTableSectionElement | HTMLTableRowElement>
) => {
const ulRect = bodyRef.current?.getBoundingClientRect();
if (!ulRect) return false;
return (
evt.clientX > ulRect.x &&
evt.clientX < ulRect.x + ulRect.width &&
evt.clientY > ulRect.y &&
evt.clientY < ulRect.y + ulRect.height
);
};
const onDragStart: TrProps['onDragStart'] = (evt) => {
evt.dataTransfer.effectAllowed = 'move';
evt.dataTransfer.setData('text/plain', evt.currentTarget.id);
const draggedItemId = evt.currentTarget.id;
evt.currentTarget.classList.add(styles.modifiers.ghostRow);
evt.currentTarget.setAttribute('aria-pressed', 'true');
setDraggedItemId(draggedItemId);
setIsDragging(true);
};
const onDragCancel = () => {
const children = bodyRef.current?.children;
if (children) {
Array.from(children).forEach((el) => {
el.classList.remove(styles.modifiers.ghostRow);
el.setAttribute('aria-pressed', 'false');
});
}
setDraggedItemId(null);
setDraggingToItemIndex(null);
setIsDragging(false);
};
const onDragLeave: TbodyProps['onDragLeave'] = (evt) => {
if (!isValidDrop(evt)) {
move(itemOrder);
setDraggingToItemIndex(null);
}
};
const onDrop: TrProps['onDrop'] = (evt) => {
if (isValidDrop(evt)) {
dispatch(changePartitionOrder(tempItemOrder));
} else {
onDragCancel();
}
};
const onDragOver: TbodyProps['onDragOver'] = (evt) => {
evt.preventDefault();
const curListItem = (evt.target as HTMLTableSectionElement).closest('tr');
if (
!curListItem ||
!bodyRef.current?.contains(curListItem) ||
curListItem.id === draggedItemId
) {
return null;
} else {
const dragId = curListItem.id;
const newDraggingToItemIndex = Array.from(
bodyRef.current.children
).findIndex((item) => item.id === dragId);
if (newDraggingToItemIndex !== draggingToItemIndex && draggedItemId) {
const tempItemOrder = moveItem(
[...itemOrder],
draggedItemId,
newDraggingToItemIndex
);
move(tempItemOrder);
setDraggingToItemIndex(newDraggingToItemIndex);
setTempItemOrder(tempItemOrder);
}
}
};
const onDragEnd: TrProps['onDragEnd'] = (evt) => {
const target = evt.target as HTMLTableRowElement;
target.classList.remove(styles.modifiers.ghostRow);
target.setAttribute('aria-pressed', 'false');
setDraggedItemId(null);
setDraggingToItemIndex(null);
setIsDragging(false);
};
const moveItem = (arr: string[], i1: string, toIndex: number) => {
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: string[]) => {
const ulNode = bodyRef.current;
if (!ulNode) {
return;
}
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 as Node);
}
itemOrder.forEach((id) => {
const node = nodes.find((n) => n.id === id);
if (node) {
ulNode.appendChild(node);
}
});
};
return (
<Table
className={isDragging ? styles.modifiers.dragOver : ''}
aria-label="File system table"
variant="compact"
>
<Thead>
<Tr>
<Th />
<Th>Mount point</Th>
<Th></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 />
<Th />
</Tr>
</Thead>
<Tbody
onDragOver={onDragOver}
onDrop={onDragOver}
onDragLeave={onDragLeave}
ref={bodyRef}
data-testid="file-system-configuration-tbody"
>
{partitions &&
partitions.map((partition) => (
<Row
onDrop={onDrop}
onDragEnd={onDragEnd}
onDragStart={onDragStart}
key={partition.id}
partition={partition}
/>
))}
</Tbody>
</Table>
);
};
export default FileSystemTable;