- Use React beautiful dnd library for servers list section

- The dnded servers list order can be sent for persistence now
- Removed the previous React DND and Html5 Backend libraries as it
is more lower level and hard to enable the drag indicator within AutoComplete
This commit is contained in:
Shemy Gan committed 2025-01-14 16:10:34 -05:00
1 parent b5409f0c0f
commit e8a9c0da36
10 files changed
+309 -285

No files matched your search

@@ -67,10 +67,12 @@ PasswordEndAdornment.propTypes = {
setFieldType: PropTypes.func,
};
export const ServerStartAdornment = () => {
export const ServerStartAdornment = (props) => {
return (
<InputAdornment position="start">
<ReorderRoundedIcon />
<Stack {...props.dragHandleProps}>
<ReorderRoundedIcon />
</Stack>
</InputAdornment>
);
};
@@ -1,4 +0,0 @@
export default {
CARD: 'card',
}
@@ -1,50 +0,0 @@
import {
ServerEndAdornment,
} from "../../../../Inputs/TextInput/Adornments";
import Search from "../../../../Inputs/Search";
import { useState } from "react";
import ReorderRoundedIcon from "@mui/icons-material/ReorderRounded";
import { Stack, IconButton } from "@mui/material";
import React from "react";
/**
*
* @param {*} id The Id of the Server component
* @param {*} removeItem The function used to remove a single server
* @param {*} onChange The Change handler function to handle when the server value is changed
* used to update the server(monitor) lists
* @param {*} monitors The server monitors options
* @param {*} value the option label of the server/monitor, namely the monitor name field
* @returns
*/
const Server = (props) => {
const [search, setSearch] = useState("");
const handleSearch = (val) => {
setSearch(val);
};
return (
<Stack direction={"row"}>
<IconButton>
<ReorderRoundedIcon />
</IconButton>
<Search
id={props.id}
multiple={false}
isAdorned={false}
options={props.monitors ? props.monitors : []}
filteredBy="name"
inputValue={search}
value={props.value}
endAdornment={
<ServerEndAdornment
id={props.id}
removeItem={props.removeItem}
/>
}
handleInputChange={handleSearch}
handleChange={props.onChange}
/>
</Stack>
);
};
export default Server;
@@ -1,109 +0,0 @@
import React, { useRef } from "react";
import { useDrag, useDrop } from "react-dnd";
import ItemTypes from "./ItemTypes";
import Card from "@mui/material/Card";
import Server from "./Server"
import { CardContent } from "@mui/material";
/**
* CustomCard is a React Forward Ref component that
* passes properties to its wrapped component with
* ref that carrys the drag and drop feature etc
*/
const CustomCard = React.forwardRef((props, ref) => (
<Card ref={ref}>
<CardContent>
<Server
id={props.id}
removeItem={props.removeCard}
value={props.value}
onChange={props.onChange}
monitors={props.monitors}
/>
</CardContent>
</Card>
));
/**
* @param {*} id The Id of the Server component
* @param {*} index The index of the Card in its natural order of placement
* @param {*} moveCard The function to actually move a card
* @param {*} removeCard The function to remote the card
* @param {*} onChange The Change handler function to handle when the server value is changed
* used to update the server(monitor) lists
* @param {*} monitors The server monitors options
* @param {*} value the option label of the server/monitor, namely the monitor name field
* @returns The Card component representing a singler server/monitor that is capable of being
* dragged and droped
*/
const MyCard = ({ id, index, moveCard, removeCard, value, onChange, monitors }) => {
const ref = useRef(null);
const [, drop] = useDrop({
accept: ItemTypes.CARD,
hover(item, monitor) {
if (!ref.current) {
return;
}
const dragIndex = item.index;
const hoverIndex = index;
// Don't replace items with themselves
if (dragIndex === hoverIndex) {
return;
}
// Determine rectangle on screen
const hoverBoundingRect = ref.current.getBoundingClientRect();
// Get vertical middle
const hoverMiddleY =
(hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
// Determine mouse position
const clientOffset = monitor.getClientOffset();
// Get pixels to the top
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
// Only perform the move when the mouse has crossed half of the items height
// When dragging downwards, only move when the cursor is below 50%
// When dragging upwards, only move when the cursor is above 50%
// Dragging downwards
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return;
}
// Dragging upwards
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return;
}
// Time to actually perform the action
moveCard(dragIndex, hoverIndex);
// Note: we're mutating the monitor item here!
// Generally it's better to avoid mutations,
// but it's good here for the sake of performance
// to avoid expensive index searches.
item.index = hoverIndex;
}
});
const [ _, drag] = useDrag({
type: ItemTypes.CARD,
item: {id: id, index:index},
collect: monitor => ({
isDragging: monitor.isDragging()
})
});
drag(drop(ref));
return (
<CustomCard
ref={ref}
id={id}
removeCard={removeCard}
value={value}
onChange={onChange}
monitors={monitors}
/>
);
};
export default MyCard;
@@ -1,15 +1,15 @@
import { useState, useCallback, useContext, useEffect } from "react";
import { useState, useContext, useEffect } from "react";
import { Button, Box, Stack, Typography } from "@mui/material";
import ConfigBox from "../../../Components/ConfigBox";
import { useTheme } from "@emotion/react";
import TabPanel from "@mui/lab/TabPanel";
import Card from "./Card";
import update from "immutability-helper";
import { StatusFormContext } from "../../../Pages/Status/CreateStatusContext";
import { useSelector } from "react-redux";
import { logger } from "../../../Utils/Logger"
import { createToast } from "../../../Utils/toastUtils";
import { networkService } from "../../../main";
import ServersList from "./ServersList";
/**
* Content Panel is used to compose the second part of the status page
@@ -24,6 +24,7 @@ const ContentPanel = () => {
const {user, authToken } = useSelector((state) => state.auth);
const [monitors, setMonitors] = useState([]);
useEffect(() => {
const fetchMonitors = async () => {
try {
@@ -54,44 +55,6 @@ const ContentPanel = () => {
};
fetchMonitors();
}, [user, authToken]);
const moveCard = useCallback(
(dragIndex, hoverIndex) => {
const dragCard = cards[dragIndex];
setCards(
update(cards, {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, dragCard],
],
})
);
},
[cards]
);
const handleCardChange = (event, val) => {
event.preventDefault();
const { id } = event.target;
let idx = cards.findIndex((a) => {
let found = false;
let optionIdx = id.indexOf("-option");
if (optionIdx !== -1) found = a.id == id.substr(0, optionIdx);
else found = a.id == id;
return found;
});
let newCards;
if (idx >= 0) {
let x = JSON.parse(JSON.stringify(cards[idx]));
x.val = val;
newCards = update(cards, { $splice: [[idx, 1, x]] });
setCards(newCards);
} else {
newCards = update(cards, { $push: [{ id: id, val: val }] });
setCards(newCards);
}
setForm({ ...form, monitors: newCards.map(c=>c.val) });
};
const handleAddNew = () => {
if (cards.length === monitors.length) return;
const newCards = [...cards, { id: "" + Math.random(),val:{} }];
@@ -172,21 +135,14 @@ const ContentPanel = () => {
direction="column"
gap={theme.spacing(2)}
>
{cards.map((card, idx) => {
return (
<Card
key={idx}
index={idx}
id={card?.id ?? "" + Math.random()}
text={"" + idx}
moveCard={moveCard}
removeCard={removeCard}
value={card.val ?? {}}
onChange={handleCardChange}
monitors={monitors}
/>
);
})}
<ServersList
monitors={monitors}
cards={cards}
setCards={setCards}
form={form}
setForm={setForm}
removeItem={removeCard}
/>
</Stack>
)}
{errors["monitors"] && (
@@ -0,0 +1,58 @@
import { ServerStartAdornment, ServerEndAdornment } from "../../../../Inputs/TextInput/Adornments";
import Search from "../../../../Inputs/Search";
import {useState} from "react"
import React from "react";
import { Stack } from "@mui/material";
/**
*
* @param {*} id The Id of the Server component
* @param {*} monitors The server monitors options
* @param {*} value the option label of the server/monitor, namely the monitor name field
* @param {*} removeItem The function used to remove a single server
* @param {*} onChange The Change handler function to handle when the server value is changed
* used to update the server(monitor) lists*
* @param {*} dragHandleProps the dragHandleProps passed on to the designated dom node
* so that there will be a draging indicator when mouse over it
* @returns A single server whose value is one of the existing monitors
*/
const Server = ({ id, value, monitors, onChange, removeItem, dragHandleProps }) => {
const [search, setSearch] = useState("");
const handleSearch = (val) => {
setSearch(val);
};
return (
<Stack direction={"row"}>
<Search
id={id}
multiple={false}
isAdorned={false}
options={monitors ? monitors : []}
filteredBy="name"
inputValue={search}
value={value}
startAdornment={<ServerStartAdornment dragHandleProps={dragHandleProps} />}
endAdornment={
<ServerEndAdornment
id={id}
removeItem={removeItem}
/>
}
handleInputChange={handleSearch}
handleChange={onChange}
/>
</Stack>
);
};
Server.propTypes = {
id: PropTypes.string.isRequired,
monitors: PropTypes.array.isRequired,
value: PropTypes.string.isRequired,
removeItem: PropTypes.function.isRequired,
onChange: PropTypes.function.isRequired,
dragHandleProps: PropTypes.object.isRequired,
};
export default Server;
@@ -0,0 +1,136 @@
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
import Server from "./Server";
import update from "immutability-helper";
/**
*
* @param {*} monitors The server monitors options
* @param {*} cards A set of servers/monitors user can add/remove/drag/drop to be displayed in status page
* each card can be autocompleted/selected from the existing monitors list
* @param {*} setCards Function to set the cards
* @param {*} form The Status page form
* @param {*} setForm Function to set the form
* @param {*} removeItem The function used to remove a single server
* @returns A list of user selected Servers/Monitors
*/
const ServersList = ({ monitors, cards, setCards, form, setForm, removeItem }) => {
const grid = 8;
const handleCardChange = (event, val) => {
let newCards;
const { id } = event.target;
let idx = cards.findIndex((a) => {
let found = false;
let optionIdx = id.indexOf("-option");
if (optionIdx !== -1) found = a.id == id.substr(0, optionIdx);
else found = a.id == id;
return found;
});
if (idx >= 0) {
let x = JSON.parse(JSON.stringify(cards[idx]));
x.val = val;
newCards = update(cards, { $splice: [[idx, 1, x]] });
} else {
newCards = update(cards, { $push: [{ id: id, val: val }] });
}
setCards(newCards);
setForm({ ...form, monitors: newCards.map((c) => c.val) });
};
const moveCard = (dragIndex, hoverIndex) => {
const dragCard = cards[dragIndex];
const newCards = update(cards, {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, dragCard],
],
});
setCards(newCards);
setForm({ ...form, monitors: newCards.map((c) => c.val) });
};
const handleDragEnd = (result) => {
// dropped outside the list
if (!result.destination) {
return;
}
moveCard(result.source.index, result.destination.index);
};
const getItemStyle = (isDragging, draggableStyle) => ({
// some basic styles to make the items look a bit nicer
userSelect: "none",
padding: grid * 2,
margin: `0 0 ${grid}px 0`,
// change background colour if dragging
background: isDragging ? "lightgreen" : "lightgrey",
// styles we need to apply on draggables
...draggableStyle,
});
const getListStyle = (isDraggingOver) => ({
background: isDraggingOver ? "lightblue" : "white",
padding: grid,
width: 550,
});
return (
<DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId="droppable">
{(provided, snapshot) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
style={getListStyle(snapshot.isDraggingOver)}
>
{cards.map((item, index) => (
<Draggable
key={item.id}
draggableId={item.id}
index={index}
>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
style={getItemStyle(
snapshot.isDragging,
provided.draggableProps.style
)}
>
<Server
key={index}
id={item?.id ?? "" + Math.random()}
monitors={monitors}
value={item.val ?? {}}
onChange={handleCardChange}
removeItem={removeItem}
dragHandleProps={provided.dragHandleProps}
/>
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
};
ServersList.propTypes = {
monitors: PropTypes.array.isRequired,
cards: PropTypes.array.isRequired,
setCards: PropTypes.function.isRequired,
form: PropTypes.object.isRequired,
setForm: PropTypes.function.isRequired,
removeItem: PropTypes.function.isRequired
};
export default ServersList;
-4
View File
@@ -6,8 +6,6 @@ import { Provider } from "react-redux";
import { persistor, store } from "./store";
import { PersistGate } from "redux-persist/integration/react";
import NetworkServiceProvider from "./Utils/NetworkServiceProvider.jsx";
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import { networkService } from "./Utils/NetworkService";
export { networkService };
@@ -19,9 +17,7 @@ ReactDOM.createRoot(document.getElementById("root")).render(
>
<Router>
<NetworkServiceProvider>
<DndProvider backend={HTML5Backend}>
<App />
</DndProvider>
</NetworkServiceProvider>
</Router>
</PersistGate>