diff --git a/README.md b/README.md index bb5291c..4750022 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Container ToolKit +![Alt text](/screenshots/ui.png?raw=true "UI") + ## Local setup and development On a Mac/Linux/Windows you need Docker, Docker Compose installed. Optionally GCC to run make commands for convenience, or just run the commands from the Makefile by hand. diff --git a/screenshots/ui.png b/screenshots/ui.png new file mode 100644 index 0000000..b89da62 Binary files /dev/null and b/screenshots/ui.png differ diff --git a/services/.DS_Store b/services/.DS_Store deleted file mode 100644 index 2a559d6..0000000 Binary files a/services/.DS_Store and /dev/null differ diff --git a/services/frontend/src/components/Canvas/Popover.tsx b/services/frontend/src/components/Canvas/Popover.tsx new file mode 100644 index 0000000..0cf4048 --- /dev/null +++ b/services/frontend/src/components/Canvas/Popover.tsx @@ -0,0 +1,22 @@ +import { TrashIcon, PencilIcon } from "@heroicons/react/solid"; +export const Popover = ({ + onEditClick, + onDeleteClick +}: { + onEditClick: Function + onDeleteClick: Function +}) => { + return ( +
+
+ +
+ onDeleteClick()} className="w-3 h-3 text-red-400"> + onEditClick()} className="w-3 h-3"> +
+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/services/frontend/src/components/Canvas/index.tsx b/services/frontend/src/components/Canvas/index.tsx index 4c6f712..0e0538c 100644 --- a/services/frontend/src/components/Canvas/index.tsx +++ b/services/frontend/src/components/Canvas/index.tsx @@ -14,10 +14,11 @@ import { position } from "../../reducers"; import Remove from "../Remove"; +import eventBus from "../../events/eventBus"; +import { Popover } from "./Popover"; +import ModalConfirmDelete from "../Modal/Service/ConfirmDelete"; import ModalServiceCreate from "../Modal/Service/Create"; import ModalServiceEdit from "../Modal/Service/Edit"; -import ModalCreate from "../Modal/Node/Create"; -import ModalEdit from "../Modal/Node/Edit"; import { useClickOutside } from "../../utils/clickOutside"; import { IClientNodeItem, IGraphData } from "../../types"; import { nodeLibraries } from "../../utils/data/libraries"; @@ -47,17 +48,21 @@ export const Canvas: FC = (props) => { const [instanceConnections, setInstanceConnections] = useState(state.connections); const [copyText, setCopyText] = useState("Copy"); const [selectedNode, setSelectedNode] = useState(null); - const [showModalCreateStep, setShowModalCreateStep] = useState(false); - const [showModalEditStep, setShowModalEditStep] = useState(false); - const [showModalCreate, setShowModalCreate] = useState(false); - const [showModalEdit, setShowModalEdit] = useState(false); - + const [showModalCreateService, setShowModalCreateService] = useState(false); + const [showModalEditService, setShowModalEditService] = useState(false); + const [showModalConfirmDeleteService, setShowModalConfirmDeleteService] = useState(false); + const [showVolumesModal, setShowVolumesModal] = useState(false); + const [showNetworksModal, setShowNetworksModal] = useState(false); + + const [nodeDragging, setNodeDragging] = useState(); + const [nodeHovering, setNodeHovering] = useState(); const [dragging, setDragging] = useState(false); const [_scale, _setScale] = useState(1); const [_left, _setLeft] = useState(0); const [_top, _setTop] = useState(0); const [_initX, _setInitX] = useState(0); const [_initY, _setInitY] = useState(0); + const [containerCallbackRef, setZoom, setStyle, removeEndpoint] = useJsPlumb( instanceNodes, instanceConnections, @@ -76,8 +81,7 @@ export const Canvas: FC = (props) => { stateRef.current = state.nodes; useClickOutside(drop, () => { - setShowModalCreateStep(false); - setShowModalCreate(false); + setShowModalCreateService(false); }); useEffect(() => { @@ -256,38 +260,49 @@ export const Canvas: FC = (props) => { _setScale(state.canvasPosition.scale); }, [state.canvasPosition]); + useEffect(() => { + eventBus.on("EVENT_DRAG_START", (data: any) => { + setNodeDragging(data.detail.message.id); + }); + + eventBus.on("EVENT_DRAG_STOP", (data: any) => { + setNodeDragging(null); + }); + + return () => { + eventBus.remove("EVENT_DRAG_START", () => {}); + eventBus.remove("EVENT_DRAG_STOP", () => { }); + }; + }, []); + return ( <> - {showModalCreateStep + {showModalCreateService ? setShowModalCreateStep(false)} + onHide={() => setShowModalCreateService(false)} onAddEndpoint={(values: any) => onAddEndpoint(values)} /> : null } - {showModalEditStep + {showModalEditService ? setShowModalEditStep(false)} + onHide={() => setShowModalEditService(false)} onUpdateEndpoint={(values: any) => onUpdateEndpoint(values)} /> : null } - {showModalCreate - ? setShowModalCreate(false)} - onAddEndpoint={(values: any) => onAddEndpoint(values)} - /> - : null - } - - {showModalEdit - ? setShowModalEdit(false)} - onUpdateEndpoint={(nodeItem: IClientNodeItem) => onUpdateEndpoint(nodeItem)} + {showModalConfirmDeleteService + ? setShowModalConfirmDeleteService(false)} + onConfirm={() => { + setShowModalEditService(false); + if (selectedNode) { + onRemoveEndpoint(selectedNode.key); + } + }} /> : null } @@ -300,14 +315,14 @@ export const Canvas: FC = (props) => {
- - -
@@ -335,10 +350,28 @@ export const Canvas: FC = (props) => { {values(instanceNodes).map((x) => (
setNodeHovering(x.key)} + onMouseLeave={() => { + if (nodeHovering === x.key) { + setNodeHovering(null); + } + }} > + {((nodeHovering === x.key) && (nodeDragging !== x.key)) && + { + setSelectedNode(x); + setShowModalEditService(true); + }} + onDeleteClick={() => { + setSelectedNode(x); + setShowModalConfirmDeleteService(true); + }} + > + }
{x.configuration.prettyName} diff --git a/services/frontend/src/components/Modal/Service/ConfirmDelete.tsx b/services/frontend/src/components/Modal/Service/ConfirmDelete.tsx new file mode 100644 index 0000000..20e6f8d --- /dev/null +++ b/services/frontend/src/components/Modal/Service/ConfirmDelete.tsx @@ -0,0 +1,71 @@ +import { XIcon, ExclamationIcon } from "@heroicons/react/outline"; + +interface IModalConfirmDeleteProps { + onConfirm: any; + onHide: any; +} + +const ModalConfirmDelete = (props: IModalConfirmDeleteProps) => { + const { onConfirm, onHide } = props; + + return ( +
+
+
+
+
+
+

Confirm delete

+ +
+ +
+
+
+
+
+
+

+ Careful! This action cannot be undone. +

+
+
+
+
+ +
+ + + +
+
+
+
+
+ ) +} + +export default ModalConfirmDelete; diff --git a/services/frontend/src/components/Modal/Service/Create.tsx b/services/frontend/src/components/Modal/Service/Create.tsx index ec71611..43d5e2d 100644 --- a/services/frontend/src/components/Modal/Service/Create.tsx +++ b/services/frontend/src/components/Modal/Service/Create.tsx @@ -27,78 +27,76 @@ const ModalServiceCreate = (props: IModalServiceProps) => { }); return ( - <> -
-
-
-
-
-
-

Add service

- -
+
+
+
+
+
+
+

Add service

+ +
-
-
-
- -
- -
+
+
+
+ +
+
+
-
-
- -
- -
+
+
+ +
+
+
-
- -
+
+
- +
); } diff --git a/services/frontend/src/components/useJsPlumb.ts b/services/frontend/src/components/useJsPlumb.ts index 4dda84c..8fc41b2 100644 --- a/services/frontend/src/components/useJsPlumb.ts +++ b/services/frontend/src/components/useJsPlumb.ts @@ -13,8 +13,10 @@ import { import { BrowserJsPlumbInstance, newInstance, + EVENT_DRAG_START, EVENT_DRAG_STOP, - EVENT_CONNECTION_DBL_CLICK + EVENT_CONNECTION_DBL_CLICK, + DragStartPayload } from "@jsplumb/browser-ui"; import { defaultOptions, @@ -23,6 +25,7 @@ import { sourceEndpoint, targetEndpoint } from "../utils/options"; +import eventBus from "../events/eventBus"; import { getConnections } from "../utils"; import { IClientNodeItem } from "../types"; import { Dictionary, isEqual } from "lodash"; @@ -90,11 +93,13 @@ export const useJsPlumb = ( if (nodeConnections) { Object.values(nodeConnections).forEach((conn) => { + instance.destroyConnector(conn); instance.deleteConnection(conn); }); }; instance.removeAllEndpoints(document.getElementById(node.key) as Element); + instance.repaintEverything(); }, [instance]); const getAnchors = (port: string[], anchorIds: AnchorId[]): IAnchor[] => { @@ -234,7 +239,7 @@ export const useJsPlumb = ( 'connections': getConnections(instance.getConnections({}, true) as Connection[]) }); } - }, [instance, addEndpoints, onGraphUpdate]); + }, [instance, addEndpoints, stateRef.current]); useEffect(() => { if (!instance) return; @@ -263,6 +268,14 @@ export const useJsPlumb = ( container: containerRef.current }); + jsPlumbInstance.bind(EVENT_DRAG_START, function (params: DragStartPayload) { + eventBus.dispatch("EVENT_DRAG_START", { message: { "id": params.el.id } }); + }); + + jsPlumbInstance.bind(EVENT_DRAG_STOP, function (params: DragStartPayload) { + eventBus.dispatch("EVENT_DRAG_STOP", { message: {"id": params.el.id} }); + }); + jsPlumbInstance.bind(INTERCEPT_BEFORE_DROP, function (params: BeforeDropParams) { return onbeforeDropIntercept(jsPlumbInstance, params); }); @@ -318,7 +331,6 @@ export const useJsPlumb = ( return () => { reset(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return [containerCallbackRef, setZoom, setStyle, removeEndpoint]; diff --git a/services/frontend/src/events/eventBus.ts b/services/frontend/src/events/eventBus.ts new file mode 100644 index 0000000..a326c36 --- /dev/null +++ b/services/frontend/src/events/eventBus.ts @@ -0,0 +1,13 @@ +const eventBus = { + on(event: string, callback: { (data: any): void; (data: any): void; (arg: any): any; }) { + document.addEventListener(event, (e) => callback(e)); + }, + dispatch(event: string, data: { message: { id: string; } | { id: string; }; }) { + document.dispatchEvent(new CustomEvent(event, { detail: data })); + }, + remove(event: string, callback: { (): void; (this: Document, ev: any): any; }) { + document.removeEventListener(event, callback); + }, +}; + +export default eventBus; \ No newline at end of file