mirror of https://github.com/ctk-hq/ctk
commit
859ddc0398
@ -1,133 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { Formik } from "formik";
|
|
||||||
import * as yup from "yup";
|
|
||||||
import {
|
|
||||||
topLevelNetworkConfigInitialValues,
|
|
||||||
networkConfigCanvasInitialValues
|
|
||||||
} from "../../../utils";
|
|
||||||
import General from "./General";
|
|
||||||
import IPam from "./IPam";
|
|
||||||
import Labels from "./Labels";
|
|
||||||
import { CallbackFunction } from "../../../types";
|
|
||||||
|
|
||||||
interface INetworkCreateProps {
|
|
||||||
onCreateNetwork: CallbackFunction;
|
|
||||||
}
|
|
||||||
|
|
||||||
const NetworkCreate = (props: INetworkCreateProps) => {
|
|
||||||
const { onCreateNetwork } = props;
|
|
||||||
const [openTab, setOpenTab] = useState("General");
|
|
||||||
const handleCreate = (values: any, formik: any) => {
|
|
||||||
onCreateNetwork(values);
|
|
||||||
formik.resetForm();
|
|
||||||
};
|
|
||||||
const validationSchema = yup.object({
|
|
||||||
canvasConfig: yup.object({
|
|
||||||
node_name: yup
|
|
||||||
.string()
|
|
||||||
.max(256, "network name should be 256 characters or less")
|
|
||||||
.required("network name is required")
|
|
||||||
}),
|
|
||||||
networkConfig: yup.object({
|
|
||||||
name: yup
|
|
||||||
.string()
|
|
||||||
.max(256, "name should be 256 characters or less")
|
|
||||||
.required("name is required")
|
|
||||||
})
|
|
||||||
});
|
|
||||||
const tabs = [
|
|
||||||
{
|
|
||||||
name: "General",
|
|
||||||
href: "#",
|
|
||||||
current: true,
|
|
||||||
hidden: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Ipam",
|
|
||||||
href: "#",
|
|
||||||
current: false,
|
|
||||||
hidden: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Labels",
|
|
||||||
href: "#",
|
|
||||||
current: false,
|
|
||||||
hidden: false
|
|
||||||
}
|
|
||||||
];
|
|
||||||
const classNames = (...classes: string[]) => {
|
|
||||||
return classes.filter(Boolean).join(" ");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Formik
|
|
||||||
initialValues={{
|
|
||||||
canvasConfig: {
|
|
||||||
...networkConfigCanvasInitialValues()
|
|
||||||
},
|
|
||||||
networkConfig: {
|
|
||||||
...topLevelNetworkConfigInitialValues()
|
|
||||||
},
|
|
||||||
key: "network",
|
|
||||||
type: "NETWORK"
|
|
||||||
}}
|
|
||||||
enableReinitialize={true}
|
|
||||||
onSubmit={(values, formik) => {
|
|
||||||
handleCreate(values, formik);
|
|
||||||
}}
|
|
||||||
validationSchema={validationSchema}
|
|
||||||
>
|
|
||||||
{(formik) => (
|
|
||||||
<>
|
|
||||||
<div className="border-b border-gray-200 px-4 md:px-8">
|
|
||||||
<nav
|
|
||||||
className="-mb-px flex space-x-4 md:space-x-8"
|
|
||||||
aria-label="Tabs"
|
|
||||||
>
|
|
||||||
{tabs.map((tab) => (
|
|
||||||
<a
|
|
||||||
key={tab.name}
|
|
||||||
href={tab.href}
|
|
||||||
className={classNames(
|
|
||||||
tab.name === openTab
|
|
||||||
? "border-indigo-500 text-indigo-600"
|
|
||||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300",
|
|
||||||
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
|
|
||||||
tab.hidden ? "hidden" : ""
|
|
||||||
)}
|
|
||||||
aria-current={tab.current ? "page" : undefined}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setOpenTab(tab.name);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tab.name}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative px-4 py-3 flex-auto">
|
|
||||||
{openTab === "General" && <General />}
|
|
||||||
{openTab === "IPam" && <IPam />}
|
|
||||||
{openTab === "Labels" && <Labels />}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-end px-4 py-3 border-t border-solid border-blueGray-200 rounded-b">
|
|
||||||
<button
|
|
||||||
className="btn-util"
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
formik.submitForm();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NetworkCreate;
|
|
||||||
@ -1,144 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { Formik } from "formik";
|
|
||||||
import * as yup from "yup";
|
|
||||||
import { TrashIcon } from "@heroicons/react/outline";
|
|
||||||
import General from "./General";
|
|
||||||
import IPam from "./IPam";
|
|
||||||
import Labels from "./Labels";
|
|
||||||
import {
|
|
||||||
CallbackFunction,
|
|
||||||
ICanvasConfig,
|
|
||||||
INetworkTopLevel
|
|
||||||
} from "../../../types";
|
|
||||||
|
|
||||||
interface INetworkEditProps {
|
|
||||||
onUpdateNetwork: CallbackFunction;
|
|
||||||
onDeleteNetwork: CallbackFunction;
|
|
||||||
network: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const NetworkEdit = (props: INetworkEditProps) => {
|
|
||||||
const { onUpdateNetwork, onDeleteNetwork, network } = props;
|
|
||||||
const [openTab, setOpenTab] = useState("General");
|
|
||||||
const handleUpdate = (values: any) => {
|
|
||||||
const updated = { ...network };
|
|
||||||
updated.canvasConfig = values.canvasConfig;
|
|
||||||
updated.networkConfig = values.networkConfig;
|
|
||||||
onUpdateNetwork(updated);
|
|
||||||
};
|
|
||||||
const validationSchema = yup.object({
|
|
||||||
canvasConfig: yup.object({
|
|
||||||
node_name: yup
|
|
||||||
.string()
|
|
||||||
.max(256, "network name should be 256 characters or less")
|
|
||||||
.required("network name is required")
|
|
||||||
}),
|
|
||||||
networkConfig: yup.object({
|
|
||||||
name: yup
|
|
||||||
.string()
|
|
||||||
.max(256, "name should be 256 characters or less")
|
|
||||||
.required("name is required")
|
|
||||||
})
|
|
||||||
});
|
|
||||||
const tabs = [
|
|
||||||
{
|
|
||||||
name: "General",
|
|
||||||
href: "#",
|
|
||||||
current: true,
|
|
||||||
hidden: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Ipam",
|
|
||||||
href: "#",
|
|
||||||
current: false,
|
|
||||||
hidden: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Labels",
|
|
||||||
href: "#",
|
|
||||||
current: false,
|
|
||||||
hidden: false
|
|
||||||
}
|
|
||||||
];
|
|
||||||
const classNames = (...classes: string[]) => {
|
|
||||||
return classes.filter(Boolean).join(" ");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Formik
|
|
||||||
initialValues={{
|
|
||||||
canvasConfig: {
|
|
||||||
...network.canvasConfig
|
|
||||||
} as ICanvasConfig,
|
|
||||||
networkConfig: {
|
|
||||||
...network.networkConfig
|
|
||||||
} as INetworkTopLevel
|
|
||||||
}}
|
|
||||||
enableReinitialize={true}
|
|
||||||
onSubmit={(values) => {
|
|
||||||
handleUpdate(values);
|
|
||||||
}}
|
|
||||||
validationSchema={validationSchema}
|
|
||||||
>
|
|
||||||
{(formik) => (
|
|
||||||
<>
|
|
||||||
<div className="border-b border-gray-200 px-4 md:px-8">
|
|
||||||
<nav
|
|
||||||
className="-mb-px flex space-x-4 md:space-x-8"
|
|
||||||
aria-label="Tabs"
|
|
||||||
>
|
|
||||||
{tabs.map((tab) => (
|
|
||||||
<a
|
|
||||||
key={tab.name}
|
|
||||||
href={tab.href}
|
|
||||||
className={classNames(
|
|
||||||
tab.name === openTab
|
|
||||||
? "border-indigo-500 text-indigo-600"
|
|
||||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300",
|
|
||||||
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
|
|
||||||
tab.hidden ? "hidden" : ""
|
|
||||||
)}
|
|
||||||
aria-current={tab.current ? "page" : undefined}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setOpenTab(tab.name);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tab.name}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative px-4 py-3 flex-auto">
|
|
||||||
{openTab === "General" && <General />}
|
|
||||||
{openTab === "IPam" && <IPam />}
|
|
||||||
{openTab === "Labels" && <Labels />}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center justify-end px-4 py-3 border-t border-solid border-blueGray-200 rounded-b">
|
|
||||||
<button
|
|
||||||
className="btn-util-red"
|
|
||||||
type="button"
|
|
||||||
onClick={onDeleteNetwork}
|
|
||||||
>
|
|
||||||
<TrashIcon className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="btn-util"
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
formik.submitForm();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NetworkEdit;
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import TextField from "../../global/FormElements/TextField";
|
|
||||||
|
|
||||||
const General = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<TextField label="Network name" name="canvasConfig.node_name" />
|
|
||||||
<TextField label="Name" name="networkConfig.name" />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default General;
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
const IPam = () => {
|
|
||||||
return <></>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default IPam;
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
const Labels = () => {
|
|
||||||
return <></>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Labels;
|
|
||||||
@ -1,118 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { XIcon } from "@heroicons/react/outline";
|
|
||||||
import NetworkCreate from "./Create";
|
|
||||||
import { CallbackFunction } from "../../../types";
|
|
||||||
import NetworkEdit from "./Edit";
|
|
||||||
import { attachUUID } from "../../../utils";
|
|
||||||
|
|
||||||
interface IModalNetworkProps {
|
|
||||||
networks: Record<string, any>;
|
|
||||||
onCreateNetwork: CallbackFunction;
|
|
||||||
onUpdateNetwork: CallbackFunction;
|
|
||||||
onDeleteNetwork: CallbackFunction;
|
|
||||||
onHide: CallbackFunction;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModalNetwork = (props: IModalNetworkProps) => {
|
|
||||||
const {
|
|
||||||
networks,
|
|
||||||
onCreateNetwork,
|
|
||||||
onUpdateNetwork,
|
|
||||||
onDeleteNetwork,
|
|
||||||
onHide
|
|
||||||
} = props;
|
|
||||||
const [selectedNetwork, setSelectedNetwork] = useState<any | null>();
|
|
||||||
const handleCreate = (values: any) => {
|
|
||||||
const uniqueKey = attachUUID(values.key);
|
|
||||||
const network = {
|
|
||||||
...values,
|
|
||||||
key: uniqueKey
|
|
||||||
};
|
|
||||||
onCreateNetwork(network);
|
|
||||||
setSelectedNetwork(network);
|
|
||||||
};
|
|
||||||
const handleUpdate = (values: any) => {
|
|
||||||
onUpdateNetwork(values);
|
|
||||||
setSelectedNetwork(values);
|
|
||||||
};
|
|
||||||
const handleDelete = () => {
|
|
||||||
onDeleteNetwork(selectedNetwork.key);
|
|
||||||
setSelectedNetwork(null);
|
|
||||||
};
|
|
||||||
const handleNew = () => {
|
|
||||||
setSelectedNetwork(null);
|
|
||||||
};
|
|
||||||
const onNetworkSelect = (e: any) => {
|
|
||||||
const networkUuid = e.target.value;
|
|
||||||
setSelectedNetwork(networks[networkUuid]);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed z-50 inset-0 overflow-y-auto">
|
|
||||||
<div className="justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 outline-none focus:outline-none">
|
|
||||||
<div
|
|
||||||
onClick={onHide}
|
|
||||||
className="opacity-25 fixed inset-0 z-40 bg-black"
|
|
||||||
></div>
|
|
||||||
<div className="relative w-auto my-6 mx-auto max-w-5xl z-50">
|
|
||||||
<div className="border-0 rounded-lg shadow-lg relative flex flex-col w-full bg-white outline-none focus:outline-none">
|
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-b border-solid border-blueGray-200 rounded-t">
|
|
||||||
<h3 className="text-sm font-semibold">Top level networks</h3>
|
|
||||||
<button
|
|
||||||
className="p-1 ml-auto text-black float-right outline-none focus:outline-none"
|
|
||||||
onClick={onHide}
|
|
||||||
>
|
|
||||||
<span className="block outline-none focus:outline-none">
|
|
||||||
<XIcon className="w-4" />
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{networks && Object.keys(networks).length > 0 && (
|
|
||||||
<div className="flex flex-row space-x-1 mx-4 mt-2">
|
|
||||||
<select
|
|
||||||
id="network"
|
|
||||||
name="network"
|
|
||||||
className="input-util"
|
|
||||||
value={selectedNetwork ? selectedNetwork.key : ""}
|
|
||||||
onChange={onNetworkSelect}
|
|
||||||
>
|
|
||||||
<option>select network to edit</option>
|
|
||||||
{Object.keys(networks).map((networkUuid: string) => (
|
|
||||||
<option value={networkUuid} key={networkUuid}>
|
|
||||||
{networks[networkUuid].canvasConfig.node_name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
{selectedNetwork && (
|
|
||||||
<button
|
|
||||||
className="btn-util"
|
|
||||||
type="button"
|
|
||||||
onClick={handleNew}
|
|
||||||
>
|
|
||||||
New
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!selectedNetwork && (
|
|
||||||
<NetworkCreate onCreateNetwork={handleCreate} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedNetwork && (
|
|
||||||
<NetworkEdit
|
|
||||||
network={selectedNetwork}
|
|
||||||
onUpdateNetwork={handleUpdate}
|
|
||||||
onDeleteNetwork={handleDelete}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModalNetwork;
|
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
import { FunctionComponent, ReactElement, useMemo, useState } from "react";
|
||||||
|
import { Formik } from "formik";
|
||||||
|
import General from "./General";
|
||||||
|
import IPam from "./IPam";
|
||||||
|
import Labels from "./Labels";
|
||||||
|
import { CallbackFunction } from "../../../types";
|
||||||
|
import { getInitialValues, tabs, validationSchema } from "./form-utils";
|
||||||
|
import { classNames } from "../../../utils/styles";
|
||||||
|
import { Button, styled } from "@mui/joy";
|
||||||
|
|
||||||
|
interface ICreateNetworkModalProps {
|
||||||
|
onCreateNetwork: CallbackFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Actions = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
padding: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CreateNetworkModal: FunctionComponent<ICreateNetworkModalProps> = (
|
||||||
|
props: ICreateNetworkModalProps
|
||||||
|
): ReactElement => {
|
||||||
|
const { onCreateNetwork } = props;
|
||||||
|
const [openTab, setOpenTab] = useState("General");
|
||||||
|
const initialValues = useMemo(() => getInitialValues(), []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={initialValues}
|
||||||
|
enableReinitialize={true}
|
||||||
|
onSubmit={onCreateNetwork}
|
||||||
|
validationSchema={validationSchema}
|
||||||
|
>
|
||||||
|
{(formik) => (
|
||||||
|
<>
|
||||||
|
<div className="border-b border-gray-200 px-4 md:px-8">
|
||||||
|
<nav
|
||||||
|
className="-mb-px flex space-x-4 md:space-x-8"
|
||||||
|
aria-label="Tabs"
|
||||||
|
>
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<a
|
||||||
|
key={tab.name}
|
||||||
|
href={tab.href}
|
||||||
|
className={classNames(
|
||||||
|
tab.name === openTab
|
||||||
|
? "border-indigo-500 text-indigo-600"
|
||||||
|
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300",
|
||||||
|
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
|
||||||
|
tab.hidden ? "hidden" : ""
|
||||||
|
)}
|
||||||
|
aria-current={tab.current ? "page" : undefined}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setOpenTab(tab.name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.name}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative px-4 py-3 flex-auto">
|
||||||
|
{openTab === "General" && <General />}
|
||||||
|
{openTab === "IPAM" && <IPam />}
|
||||||
|
{openTab === "Labels" && <Labels />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Actions>
|
||||||
|
<Button size="sm" variant="solid" onClick={formik.submitForm}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Actions>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateNetworkModal;
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { Formik } from "formik";
|
||||||
|
import General from "./General";
|
||||||
|
import IPam from "./IPam";
|
||||||
|
import Labels from "./Labels";
|
||||||
|
import { CallbackFunction } from "../../../types";
|
||||||
|
import { getInitialValues, tabs, validationSchema } from "./form-utils";
|
||||||
|
import { classNames } from "../../../utils/styles";
|
||||||
|
import { Button, styled } from "@mui/joy";
|
||||||
|
|
||||||
|
interface IEditNetworkModalProps {
|
||||||
|
onUpdateNetwork: CallbackFunction;
|
||||||
|
network: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Actions = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
padding: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const EditNetworkModal = (props: IEditNetworkModalProps) => {
|
||||||
|
const { onUpdateNetwork, network } = props;
|
||||||
|
const [openTab, setOpenTab] = useState("General");
|
||||||
|
const initialValues = useMemo(() => getInitialValues(network), [network]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={initialValues}
|
||||||
|
enableReinitialize={true}
|
||||||
|
onSubmit={onUpdateNetwork}
|
||||||
|
validationSchema={validationSchema}
|
||||||
|
>
|
||||||
|
{(formik) => (
|
||||||
|
<>
|
||||||
|
<div className="border-b border-gray-200 px-4 md:px-8">
|
||||||
|
<nav
|
||||||
|
className="-mb-px flex space-x-4 md:space-x-8"
|
||||||
|
aria-label="Tabs"
|
||||||
|
>
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<a
|
||||||
|
key={tab.name}
|
||||||
|
href={tab.href}
|
||||||
|
className={classNames(
|
||||||
|
tab.name === openTab
|
||||||
|
? "border-indigo-500 text-indigo-600"
|
||||||
|
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300",
|
||||||
|
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
|
||||||
|
tab.hidden ? "hidden" : ""
|
||||||
|
)}
|
||||||
|
aria-current={tab.current ? "page" : undefined}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setOpenTab(tab.name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.name}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative px-4 py-3 flex-auto">
|
||||||
|
{openTab === "General" && <General />}
|
||||||
|
{openTab === "IPAM" && <IPam />}
|
||||||
|
{openTab === "Labels" && <Labels />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Actions>
|
||||||
|
<Button size="sm" variant="solid" onClick={formik.submitForm}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Actions>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditNetworkModal;
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
import { Button, styled } from "@mui/joy";
|
||||||
|
import { FunctionComponent, ReactElement } from "react";
|
||||||
|
|
||||||
|
const Root = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: ${({ theme }) => theme.spacing(2, 5, 5, 5)};
|
||||||
|
text-align: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Image = styled("img")`
|
||||||
|
width: 300px;
|
||||||
|
height: auto;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CreateNew = styled(Button)`
|
||||||
|
margin-top: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export interface IEmptyNetworksProps {
|
||||||
|
onCreate: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmptyNetworks: FunctionComponent<IEmptyNetworksProps> = (
|
||||||
|
props: IEmptyNetworksProps
|
||||||
|
): ReactElement => {
|
||||||
|
const { onCreate } = props;
|
||||||
|
return (
|
||||||
|
<Root>
|
||||||
|
<Image src="https://res.cloudinary.com/hypertool/image/upload/v1657816359/hypertool-assets/empty-projects_fdcxtk.svg" />
|
||||||
|
<p className="mt-4 text-md text-gray-500 dark:text-gray-400">
|
||||||
|
We tried our best, but could not find any networks.
|
||||||
|
</p>
|
||||||
|
<CreateNew variant="solid" size="sm" onClick={onCreate}>
|
||||||
|
Create new network
|
||||||
|
</CreateNew>
|
||||||
|
</Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmptyNetworks;
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import { styled } from "@mui/joy";
|
||||||
|
import TextField from "../../global/FormElements/TextField";
|
||||||
|
|
||||||
|
const Root = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const General = () => {
|
||||||
|
return (
|
||||||
|
<Root>
|
||||||
|
<TextField label="Entry name" name="entryName" />
|
||||||
|
<TextField label="Network name" name="networkName" />
|
||||||
|
</Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default General;
|
||||||
@ -0,0 +1,162 @@
|
|||||||
|
import { ReactElement } from "react";
|
||||||
|
import { styled } from "@mui/joy";
|
||||||
|
|
||||||
|
import TextField from "../../global/FormElements/TextField";
|
||||||
|
import { IFieldType } from "../../Record";
|
||||||
|
import Records from "../../Records";
|
||||||
|
|
||||||
|
const Fields = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Field = styled("div")`
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Remove = styled("div")`
|
||||||
|
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Configuration = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
row-gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
padding-left: ${({ theme }) => theme.spacing(4)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ConfigurationTop = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
column-gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ConfigurationBorder = styled("div")`
|
||||||
|
border-bottom: dotted 1px black;
|
||||||
|
height: 1px;
|
||||||
|
width: 40px;
|
||||||
|
margin: 8px 0px 0px 0px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const IPam = () => {
|
||||||
|
return (
|
||||||
|
<Fields>
|
||||||
|
<TextField label="Driver" name="driver" />
|
||||||
|
|
||||||
|
<Records
|
||||||
|
name="configurations"
|
||||||
|
modal="network"
|
||||||
|
title="Configurations"
|
||||||
|
referred="configuration"
|
||||||
|
fields={(index: number) => [
|
||||||
|
{
|
||||||
|
name: `configurations[${index}].subnet`,
|
||||||
|
label: "Subnet",
|
||||||
|
type: "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `configurations[${index}].ipRange`,
|
||||||
|
label: "IP Range",
|
||||||
|
type: "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `configurations[${index}].gateway`,
|
||||||
|
label: "Gateway",
|
||||||
|
type: "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `configurations[${index}].auxAddresses`,
|
||||||
|
type: "records",
|
||||||
|
// TODO: Remove placeholder from the main object.
|
||||||
|
placeholder: "",
|
||||||
|
options: {
|
||||||
|
name: `configurations[${index}].auxAddresses`,
|
||||||
|
modal: "configuration",
|
||||||
|
title: "Aux addresses",
|
||||||
|
referred: "aux address",
|
||||||
|
fields: (index2: number) => [
|
||||||
|
{
|
||||||
|
name: `configurations[${index}].auxAddresses[${index2}].hostName`,
|
||||||
|
label: "Host name",
|
||||||
|
type: "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `configurations[${index}].auxAddresses[${index2}].ipAddress`,
|
||||||
|
label: "IP address",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
newValue: {
|
||||||
|
hostName: "",
|
||||||
|
ipAddress: ""
|
||||||
|
},
|
||||||
|
renderField: (element: ReactElement): ReactElement => (
|
||||||
|
<Field>{element}</Field>
|
||||||
|
),
|
||||||
|
renderRemove: (element: ReactElement): ReactElement => (
|
||||||
|
<Remove>{element}</Remove>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
newValue={{
|
||||||
|
subnet: "",
|
||||||
|
ipRange: "",
|
||||||
|
gateway: "",
|
||||||
|
auxAddresses: []
|
||||||
|
}}
|
||||||
|
renderLayout={(elements: ReactElement[]): ReactElement => (
|
||||||
|
<Configuration>
|
||||||
|
<ConfigurationTop>
|
||||||
|
{elements[0]}
|
||||||
|
{elements[1]}
|
||||||
|
{elements[2]}
|
||||||
|
</ConfigurationTop>
|
||||||
|
{elements[3]}
|
||||||
|
</Configuration>
|
||||||
|
)}
|
||||||
|
renderField={(
|
||||||
|
element: ReactElement,
|
||||||
|
field: IFieldType
|
||||||
|
): ReactElement => (
|
||||||
|
<Field style={{ paddingLeft: field.type === "records" ? 16 : 0 }}>
|
||||||
|
{element}
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
renderRemove={(element: ReactElement): ReactElement => (
|
||||||
|
<Remove>{element}</Remove>
|
||||||
|
)}
|
||||||
|
renderBorder={() => <ConfigurationBorder />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Records
|
||||||
|
name="options"
|
||||||
|
modal="network"
|
||||||
|
title="Options"
|
||||||
|
referred="option"
|
||||||
|
fields={(index: number) => [
|
||||||
|
{
|
||||||
|
name: `options[${index}].key`,
|
||||||
|
placeholder: "Key",
|
||||||
|
required: true,
|
||||||
|
type: "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `options[${index}].value`,
|
||||||
|
placeholder: "Value",
|
||||||
|
required: true,
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
newValue={{ key: "", value: "" }}
|
||||||
|
renderField={(element: ReactElement): ReactElement => (
|
||||||
|
<Field>{element}</Field>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Fields>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IPam;
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import { styled } from "@mui/joy";
|
||||||
|
import { FunctionComponent, ReactElement } from "react";
|
||||||
|
import Records from "../../Records";
|
||||||
|
|
||||||
|
const Root = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Labels: FunctionComponent = (): ReactElement => {
|
||||||
|
return (
|
||||||
|
<Root>
|
||||||
|
<Records
|
||||||
|
name="labels"
|
||||||
|
modal="network"
|
||||||
|
title="Labels"
|
||||||
|
referred="label"
|
||||||
|
fields={(index: number) => [
|
||||||
|
{
|
||||||
|
name: `labels[${index}].key`,
|
||||||
|
placeholder: "Key",
|
||||||
|
required: true,
|
||||||
|
type: "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `labels[${index}].value`,
|
||||||
|
placeholder: "Value",
|
||||||
|
required: true,
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
newValue={{ key: "", value: "" }}
|
||||||
|
/>
|
||||||
|
</Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Labels;
|
||||||
@ -0,0 +1,104 @@
|
|||||||
|
import { MinusSmIcon, PlusSmIcon } from "@heroicons/react/outline";
|
||||||
|
import { Button, styled } from "@mui/joy";
|
||||||
|
import IconButton from "@mui/joy/IconButton";
|
||||||
|
import { FunctionComponent, ReactElement } from "react";
|
||||||
|
|
||||||
|
export interface INetworkListProps {
|
||||||
|
networks: Record<string, any>;
|
||||||
|
selectedUuid: string;
|
||||||
|
onEdit: (networkUuid: string) => void;
|
||||||
|
onNew: () => void;
|
||||||
|
onRemove: (networkUuid: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IListItemProps {
|
||||||
|
selected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Root = styled("div")`
|
||||||
|
padding: ${({ theme }) => theme.spacing(0)};
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-right: solid #eaeaea 1px;
|
||||||
|
margin: ${({ theme }) => theme.spacing(1, 2, 1, 0)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Top = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Bottom = styled("div")`
|
||||||
|
padding: ${({ theme }) => theme.spacing(1, 2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ListItem = styled("div")<IListItemProps>`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
padding: ${({ theme }) => theme.spacing(1, 2)};
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: ${({ selected }) => selected && "#f5f5f5"};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ListItemText = styled("h5")`
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 14px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const RemoveButton = styled(IconButton)`
|
||||||
|
width: 24px;
|
||||||
|
max-height: 16px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const NetworkList: FunctionComponent<INetworkListProps> = (
|
||||||
|
props: INetworkListProps
|
||||||
|
): ReactElement => {
|
||||||
|
const { onNew, onRemove, onEdit, networks, selectedUuid } = props;
|
||||||
|
|
||||||
|
const handleEdit = (networkUuid: string) => () => onEdit(networkUuid);
|
||||||
|
|
||||||
|
const handleRemove = (networkUuid: string) => () => onRemove(networkUuid);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Root>
|
||||||
|
<Top>
|
||||||
|
{Object.keys(networks).map((networkUuid: string) => (
|
||||||
|
<ListItem
|
||||||
|
key={networkUuid}
|
||||||
|
onClick={handleEdit(networkUuid)}
|
||||||
|
selected={networkUuid === selectedUuid}
|
||||||
|
>
|
||||||
|
<ListItemText>
|
||||||
|
{networks[networkUuid].canvasConfig.node_name}
|
||||||
|
</ListItemText>
|
||||||
|
<RemoveButton
|
||||||
|
variant="soft"
|
||||||
|
size="sm"
|
||||||
|
color="danger"
|
||||||
|
onClick={handleRemove(networkUuid)}
|
||||||
|
>
|
||||||
|
<MinusSmIcon className="h-4 w-4" />
|
||||||
|
</RemoveButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</Top>
|
||||||
|
|
||||||
|
<Bottom>
|
||||||
|
<Button variant="plain" size="sm" onClick={onNew}>
|
||||||
|
<PlusSmIcon className="h-5 w-5" />
|
||||||
|
New network
|
||||||
|
</Button>
|
||||||
|
</Bottom>
|
||||||
|
</Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NetworkList;
|
||||||
@ -0,0 +1,163 @@
|
|||||||
|
import lodash from "lodash";
|
||||||
|
import * as yup from "yup";
|
||||||
|
import { IEditNetworkForm, INetworkNodeItem } from "../../../types";
|
||||||
|
|
||||||
|
export const validationSchema = yup.object({
|
||||||
|
entryName: yup
|
||||||
|
.string()
|
||||||
|
.max(256, "Entry name should be 256 characters or less")
|
||||||
|
.required("Entry name is required"),
|
||||||
|
|
||||||
|
networkName: yup
|
||||||
|
.string()
|
||||||
|
.max(256, "Network name should be 256 characters or less")
|
||||||
|
.required("Network name is required"),
|
||||||
|
|
||||||
|
driver: yup
|
||||||
|
.string()
|
||||||
|
.max(256, "Driver should be 256 characters or less")
|
||||||
|
.default("default"),
|
||||||
|
|
||||||
|
configurations: yup.array(
|
||||||
|
yup.object({
|
||||||
|
subnet: yup.string(),
|
||||||
|
ipRange: yup.string(),
|
||||||
|
gateway: yup.string(),
|
||||||
|
auxAddresses: yup.array(
|
||||||
|
yup.object({
|
||||||
|
hostName: yup.string().required("Host name is required"),
|
||||||
|
ipAddress: yup.string().required("IP address is required")
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
),
|
||||||
|
|
||||||
|
options: yup.array(
|
||||||
|
yup.object({
|
||||||
|
key: yup.string().required("Key is required"),
|
||||||
|
value: yup.string().required("Value is required")
|
||||||
|
})
|
||||||
|
),
|
||||||
|
|
||||||
|
labels: yup.array(
|
||||||
|
yup.object({
|
||||||
|
key: yup.string().required("Key is required"),
|
||||||
|
value: yup.string().required("Value is required")
|
||||||
|
})
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const tabs = [
|
||||||
|
{
|
||||||
|
name: "General",
|
||||||
|
href: "#",
|
||||||
|
current: true,
|
||||||
|
hidden: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPAM",
|
||||||
|
href: "#",
|
||||||
|
current: false,
|
||||||
|
hidden: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Labels",
|
||||||
|
href: "#",
|
||||||
|
current: false,
|
||||||
|
hidden: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const initialValues: IEditNetworkForm = {
|
||||||
|
entryName: "",
|
||||||
|
networkName: "",
|
||||||
|
driver: "default",
|
||||||
|
configurations: [],
|
||||||
|
options: [],
|
||||||
|
labels: []
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getInitialValues = (node?: INetworkNodeItem): IEditNetworkForm => {
|
||||||
|
if (!node) {
|
||||||
|
return {
|
||||||
|
...initialValues
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { canvasConfig, networkConfig } = node;
|
||||||
|
const { node_name = "" } = canvasConfig;
|
||||||
|
const { name = "", ipam, labels } = networkConfig;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...initialValues,
|
||||||
|
entryName: node_name,
|
||||||
|
networkName: name,
|
||||||
|
driver: ipam?.driver ?? "",
|
||||||
|
configurations:
|
||||||
|
ipam?.config.map((item) => ({
|
||||||
|
subnet: item.subnet,
|
||||||
|
ipRange: item.ip_range,
|
||||||
|
gateway: item.gateway,
|
||||||
|
auxAddresses: Object.entries(item.aux_addresses).map(
|
||||||
|
([hostName, ipAddress]) => ({
|
||||||
|
hostName,
|
||||||
|
ipAddress
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})) ?? [],
|
||||||
|
options: Object.keys(ipam?.options || {}).map((key) => {
|
||||||
|
if (!ipam) {
|
||||||
|
throw new Error("Control should not reach here.");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
value: ipam.options[key].toString()
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
labels: Object.entries(labels as any).map(([key, value]: any) => ({
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFinalValues = (
|
||||||
|
values: IEditNetworkForm,
|
||||||
|
previous?: INetworkNodeItem
|
||||||
|
): INetworkNodeItem => {
|
||||||
|
const { labels, driver, configurations, options } = values;
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: "network",
|
||||||
|
type: "NETWORK",
|
||||||
|
inputs: previous?.inputs ?? [],
|
||||||
|
outputs: previous?.outputs ?? [],
|
||||||
|
config: (previous as any)?.config ?? {},
|
||||||
|
canvasConfig: {
|
||||||
|
node_name: values.entryName
|
||||||
|
},
|
||||||
|
networkConfig: {
|
||||||
|
name: values.networkName,
|
||||||
|
ipam: {
|
||||||
|
driver,
|
||||||
|
config: configurations.map((configuration) => ({
|
||||||
|
subnet: configuration.subnet,
|
||||||
|
ip_range: configuration.ipRange,
|
||||||
|
gateway: configuration.gateway,
|
||||||
|
aux_addresses: Object.fromEntries(
|
||||||
|
configuration.auxAddresses.map((auxAddress) => [
|
||||||
|
auxAddress.hostName,
|
||||||
|
auxAddress.ipAddress
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})),
|
||||||
|
options: Object.fromEntries(
|
||||||
|
options.map((option) => [option.key, option.value])
|
||||||
|
)
|
||||||
|
},
|
||||||
|
labels: Object.fromEntries(
|
||||||
|
labels.map((label) => [label.key, label.value])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
};
|
||||||
@ -0,0 +1,141 @@
|
|||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { XIcon } from "@heroicons/react/outline";
|
||||||
|
import CreateNetworkModal from "./CreateNetworkModal";
|
||||||
|
import { CallbackFunction } from "../../../types";
|
||||||
|
import EditNetworkModal from "./EditNetworkModal";
|
||||||
|
import { attachUUID } from "../../../utils";
|
||||||
|
import { getFinalValues } from "./form-utils";
|
||||||
|
import EmptyNetworks from "./EmptyNetworks";
|
||||||
|
import NetworkList from "./NetworkList";
|
||||||
|
import { styled } from "@mui/joy";
|
||||||
|
|
||||||
|
interface IModalNetworkProps {
|
||||||
|
networks: Record<string, any>;
|
||||||
|
onCreateNetwork: CallbackFunction;
|
||||||
|
onUpdateNetwork: CallbackFunction;
|
||||||
|
onDeleteNetwork: CallbackFunction;
|
||||||
|
onHide: CallbackFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const NetworkFormContainer = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ModalNetwork = (props: IModalNetworkProps) => {
|
||||||
|
const {
|
||||||
|
networks,
|
||||||
|
onCreateNetwork,
|
||||||
|
onUpdateNetwork,
|
||||||
|
onDeleteNetwork,
|
||||||
|
onHide
|
||||||
|
} = props;
|
||||||
|
const [selectedNetwork, setSelectedNetwork] = useState<any | null>();
|
||||||
|
const [showCreate, setShowCreate] = useState(false);
|
||||||
|
const handleCreate = (values: any) => {
|
||||||
|
const finalValues = getFinalValues(values);
|
||||||
|
const uniqueKey = attachUUID(finalValues.key);
|
||||||
|
const network = {
|
||||||
|
...finalValues,
|
||||||
|
key: uniqueKey
|
||||||
|
};
|
||||||
|
onCreateNetwork(network);
|
||||||
|
setSelectedNetwork(network);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdate = (values: any) => {
|
||||||
|
const finalValues = getFinalValues(values, selectedNetwork);
|
||||||
|
onUpdateNetwork(finalValues);
|
||||||
|
setSelectedNetwork(finalValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = useCallback(
|
||||||
|
(networkUuid: string) => {
|
||||||
|
onDeleteNetwork(networkUuid);
|
||||||
|
/* Show the new network form only when the selected node was deleted. */
|
||||||
|
/* BUG: If a selected node is deleted, it still remains in the form. */
|
||||||
|
if (selectedNetwork?.key === networkUuid) {
|
||||||
|
setSelectedNetwork(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onDeleteNetwork, selectedNetwork]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleNew = useCallback(() => {
|
||||||
|
setShowCreate(true);
|
||||||
|
setSelectedNetwork(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleEdit = useCallback(
|
||||||
|
(networkUuid: string) => {
|
||||||
|
setSelectedNetwork(networks[networkUuid]);
|
||||||
|
},
|
||||||
|
[networks]
|
||||||
|
);
|
||||||
|
|
||||||
|
const networkKeys = Object.keys(networks);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed z-50 inset-0 overflow-y-auto">
|
||||||
|
<div className="justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 outline-none focus:outline-none">
|
||||||
|
<div
|
||||||
|
onClick={onHide}
|
||||||
|
className="opacity-25 fixed inset-0 z-40 bg-black"
|
||||||
|
></div>
|
||||||
|
<div className="relative w-auto my-6 mx-auto max-w-5xl z-50">
|
||||||
|
<div className="border-0 rounded-lg shadow-lg relative flex flex-col w-full bg-white outline-none focus:outline-none">
|
||||||
|
<div className="flex items-center justify-between px-4 py-3 border-b border-solid border-blueGray-200 rounded-t">
|
||||||
|
<h3 className="text-sm font-semibold">Networks</h3>
|
||||||
|
<button
|
||||||
|
className="p-1 ml-auto text-black float-right outline-none focus:outline-none"
|
||||||
|
onClick={onHide}
|
||||||
|
>
|
||||||
|
<span className="block outline-none focus:outline-none">
|
||||||
|
<XIcon className="w-4" />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{networkKeys.length === 0 && !showCreate && (
|
||||||
|
<EmptyNetworks onCreate={handleNew} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(networkKeys.length > 0 || showCreate) && (
|
||||||
|
<Container>
|
||||||
|
{networkKeys.length > 0 && (
|
||||||
|
<NetworkList
|
||||||
|
networks={networks}
|
||||||
|
onNew={handleNew}
|
||||||
|
onRemove={handleRemove}
|
||||||
|
onEdit={handleEdit}
|
||||||
|
selectedUuid={selectedNetwork?.key}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<NetworkFormContainer>
|
||||||
|
{!selectedNetwork && (
|
||||||
|
<CreateNetworkModal onCreateNetwork={handleCreate} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedNetwork && (
|
||||||
|
<EditNetworkModal
|
||||||
|
network={selectedNetwork}
|
||||||
|
onUpdateNetwork={handleUpdate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</NetworkFormContainer>
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalNetwork;
|
||||||
@ -0,0 +1,128 @@
|
|||||||
|
import { Button, styled } from "@mui/joy";
|
||||||
|
import { Fragment, FunctionComponent, ReactElement, useCallback } from "react";
|
||||||
|
import { PlusIcon } from "@heroicons/react/outline";
|
||||||
|
import Record, { IFieldType } from "./Record";
|
||||||
|
import { useFormikContext } from "formik";
|
||||||
|
import lodash from "lodash";
|
||||||
|
|
||||||
|
export interface IRecordsProps {
|
||||||
|
modal: string;
|
||||||
|
title: string;
|
||||||
|
referred: string;
|
||||||
|
name: string;
|
||||||
|
fields: (index: number) => IFieldType[];
|
||||||
|
newValue: any;
|
||||||
|
renderLayout?: (elements: ReactElement[]) => ReactElement;
|
||||||
|
renderField?: (element: ReactElement, field: IFieldType) => ReactElement;
|
||||||
|
renderRemove?: (element: ReactElement) => ReactElement;
|
||||||
|
renderBorder?: () => ReactElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Group = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const GroupTitle = styled("h5")`
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #374151;
|
||||||
|
font-weight: 500;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const RecordList = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
row-gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
margin-top: ${({ theme }) => theme.spacing(1)};
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const AddButton = styled(Button)`
|
||||||
|
min-width: 140px;
|
||||||
|
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Description = styled("p")`
|
||||||
|
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||||
|
text-align: center;
|
||||||
|
color: #7a7a7a;
|
||||||
|
font-size: 14px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Records: FunctionComponent<IRecordsProps> = (
|
||||||
|
props: IRecordsProps
|
||||||
|
): ReactElement => {
|
||||||
|
const {
|
||||||
|
modal,
|
||||||
|
title,
|
||||||
|
referred,
|
||||||
|
name,
|
||||||
|
fields,
|
||||||
|
newValue,
|
||||||
|
renderLayout,
|
||||||
|
renderField,
|
||||||
|
renderRemove,
|
||||||
|
renderBorder
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const formik = useFormikContext();
|
||||||
|
const items = lodash.get(formik.values, name);
|
||||||
|
|
||||||
|
const handleNew = useCallback(() => {
|
||||||
|
formik.setFieldValue(`${name}[${items.length}]`, newValue);
|
||||||
|
}, [formik]);
|
||||||
|
|
||||||
|
const handleRemove = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
const newOptions = items.filter(
|
||||||
|
(_: unknown, currentIndex: number) => currentIndex != index
|
||||||
|
);
|
||||||
|
formik.setFieldValue(name, newOptions);
|
||||||
|
},
|
||||||
|
[formik]
|
||||||
|
);
|
||||||
|
|
||||||
|
const empty = items && items.length === 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group>
|
||||||
|
<GroupTitle>{title}</GroupTitle>
|
||||||
|
{!empty && (
|
||||||
|
<RecordList>
|
||||||
|
{items.map((_: unknown, index: number) => (
|
||||||
|
<Fragment key={index}>
|
||||||
|
<Record
|
||||||
|
index={index}
|
||||||
|
fields={fields(index)}
|
||||||
|
onRemove={handleRemove}
|
||||||
|
renderLayout={renderLayout}
|
||||||
|
renderField={renderField}
|
||||||
|
renderRemove={renderRemove}
|
||||||
|
/>
|
||||||
|
{renderBorder && renderBorder()}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</RecordList>
|
||||||
|
)}
|
||||||
|
{empty && (
|
||||||
|
<Description>
|
||||||
|
This {modal} does not have any {referred}.
|
||||||
|
<br />
|
||||||
|
Click "+ New {referred}" to add a new {referred}.
|
||||||
|
</Description>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AddButton size="sm" variant="plain" onClick={handleNew}>
|
||||||
|
<PlusIcon className="h-4 w-4 mr-2" />
|
||||||
|
New {referred}
|
||||||
|
</AddButton>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Records;
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
export const classNames = (...classes: string[]) => {
|
||||||
|
return classes.filter(Boolean).join(" ");
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue