feat: networks modal and initial form with state

pull/74/head
Artem Golub 3 years ago
parent fa53ce6f63
commit 9065d0cba1

@ -0,0 +1,132 @@
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="hidden sm:block">
<div className="border-b border-gray-200 px-8">
<nav className="-mb-px flex 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>
<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;

@ -0,0 +1,143 @@
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="hidden sm:block">
<div className="border-b border-gray-200 px-8">
<nav className="-mb-px flex 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>
<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;

@ -3,7 +3,8 @@ import TextField from "../../global/FormElements/InputField";
const General = () => { const General = () => {
return ( return (
<> <>
<TextField label="Name" name="name" /> <TextField label="Network name" name="canvasConfig.node_name" />
<TextField label="Name" name="networkConfig.name" />
</> </>
); );
}; };

@ -1,51 +1,50 @@
import { useState } from "react"; import { useState } from "react";
import { Formik } from "formik";
import * as yup from "yup";
import { XIcon } from "@heroicons/react/outline"; import { XIcon } from "@heroicons/react/outline";
import General from "./General"; import NetworkCreate from "./Create";
import IPam from "./IPam";
import Labels from "./Labels";
import { topLevelNetworkConfigInitialValues } from "../../../utils";
import { CallbackFunction } from "../../../types"; import { CallbackFunction } from "../../../types";
import NetworkEdit from "./Edit";
import { attachUUID } from "../../../utils";
interface IModalNetworkProps { interface IModalNetworkProps {
networks: Record<string, any>;
onCreateNetwork: CallbackFunction;
onUpdateNetwork: CallbackFunction;
onDeleteNetwork: CallbackFunction;
onHide: CallbackFunction; onHide: CallbackFunction;
} }
const ModalNetwork = (props: IModalNetworkProps) => { const ModalNetwork = (props: IModalNetworkProps) => {
const { onHide } = props; const {
const [openTab, setOpenTab] = useState("General"); networks,
const handleCreate = (values: any, formik: any) => { onCreateNetwork,
formik.resetForm(); 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 validationSchema = yup.object({ const handleUpdate = (values: any) => {
name: yup onUpdateNetwork(values);
.string() setSelectedNetwork(values);
.max(256, "name should be 256 characters or less") };
.required("name is required") const handleDelete = () => {
}); onDeleteNetwork(selectedNetwork.key);
const tabs = [ setSelectedNetwork(null);
{ };
name: "General", const handleNew = () => {
href: "#", setSelectedNetwork(null);
current: true, };
hidden: false const onNetworkSelect = (e: any) => {
}, const networkUuid = e.target.value;
{ setSelectedNetwork(networks[networkUuid]);
name: "Ipam",
href: "#",
current: false,
hidden: false
},
{
name: "Labels",
href: "#",
current: false,
hidden: false
}
];
const classNames = (...classes: string[]) => {
return classes.filter(Boolean).join(" ");
}; };
return ( return (
@ -69,70 +68,46 @@ const ModalNetwork = (props: IModalNetworkProps) => {
</button> </button>
</div> </div>
<Formik {networks && Object.keys(networks).length > 0 && (
initialValues={{ <div className="flex flex-row space-x-1 mx-4 mt-2">
...topLevelNetworkConfigInitialValues(), <select
key: "volume", id="network"
type: "VOLUME", name="network"
inputs: [], className="input-util"
outputs: [], value={selectedNetwork ? selectedNetwork.key : ""}
config: {} onChange={onNetworkSelect}
}} >
enableReinitialize={true} <option>select network to edit</option>
onSubmit={(values, formik) => { {Object.keys(networks).map((networkUuid: string) => (
handleCreate(values, formik); <option value={networkUuid} key={networkUuid}>
}} {networks[networkUuid].canvasConfig.node_name}
validationSchema={validationSchema} </option>
> ))}
{(formik) => ( </select>
<>
<div className="hidden sm:block"> {selectedNetwork && (
<div className="border-b border-gray-200 px-8"> <button
<nav className="-mb-px flex space-x-8" aria-label="Tabs"> className="btn-util"
{tabs.map((tab) => ( type="button"
<a onClick={handleNew}
key={tab.name} >
href={tab.href} New
className={classNames( </button>
tab.name === openTab )}
? "border-indigo-500 text-indigo-600" </div>
: "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>
<div className="relative px-4 py-3 flex-auto"> {!selectedNetwork && (
{openTab === "General" && <General />} <NetworkCreate onCreateNetwork={handleCreate} />
{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"> {selectedNetwork && (
<button <NetworkEdit
className="btn-util" network={selectedNetwork}
type="button" onUpdateNetwork={handleUpdate}
onClick={() => { onDeleteNetwork={handleDelete}
formik.submitForm(); />
}} )}
>
Add
</button>
</div>
</>
)}
</Formik>
</div> </div>
</div> </div>
</div> </div>

@ -8,7 +8,8 @@ import {
IServiceNodeItem, IServiceNodeItem,
IVolumeNodeItem, IVolumeNodeItem,
IServiceNodePosition, IServiceNodePosition,
IProject IProject,
INetworkTopLevel
} from "../../types"; } from "../../types";
import eventBus from "../../events/eventBus"; import eventBus from "../../events/eventBus";
import { useMutation } from "react-query"; import { useMutation } from "react-query";
@ -18,7 +19,7 @@ import {
createProject createProject
} from "../../hooks/useProject"; } from "../../hooks/useProject";
import useWindowDimensions from "../../hooks/useWindowDimensions"; import useWindowDimensions from "../../hooks/useWindowDimensions";
import { flattenGraphData } from "../../utils/generators"; import { generatePayload } from "../../utils/generators";
import { nodeLibraries } from "../../utils/data/libraries"; import { nodeLibraries } from "../../utils/data/libraries";
import { import {
getClientNodeItem, getClientNodeItem,
@ -46,6 +47,7 @@ export default function Project() {
const stateNodesRef = const stateNodesRef =
useRef<Dictionary<IServiceNodeItem | IVolumeNodeItem>>(); useRef<Dictionary<IServiceNodeItem | IVolumeNodeItem>>();
const stateConnectionsRef = useRef<[[string, string]] | []>(); const stateConnectionsRef = useRef<[[string, string]] | []>();
const stateNetworksRef = useRef({});
const [generatedCode, setGeneratedCode] = useState<string>(); const [generatedCode, setGeneratedCode] = useState<string>();
const [formattedCode, setFormattedCode] = useState<string>(""); const [formattedCode, setFormattedCode] = useState<string>("");
@ -67,6 +69,7 @@ export default function Project() {
const [copyText, setCopyText] = useState("Copy"); const [copyText, setCopyText] = useState("Copy");
const [nodes, setNodes] = useState({}); const [nodes, setNodes] = useState({});
const [connections, setConnections] = useState<[[string, string]] | []>([]); const [connections, setConnections] = useState<[[string, string]] | []>([]);
const [networks, setNetworks] = useState<Record<string, any>>({});
const [projectName, setProjectName] = useState("Untitled"); const [projectName, setProjectName] = useState("Untitled");
const [canvasPosition, setCanvasPosition] = useState({ const [canvasPosition, setCanvasPosition] = useState({
top: 0, top: 0,
@ -87,6 +90,7 @@ export default function Project() {
stateNodesRef.current = nodes; stateNodesRef.current = nodes;
stateConnectionsRef.current = connections; stateConnectionsRef.current = connections;
stateNetworksRef.current = networks;
const handleNameChange = (e: any) => { const handleNameChange = (e: any) => {
setProjectName(e.target.value); setProjectName(e.target.value);
@ -109,7 +113,8 @@ export default function Project() {
canvas: { canvas: {
position: canvasPosition, position: canvasPosition,
nodes: nodes, nodes: nodes,
connections: connections connections: connections,
networks: networks
} }
} }
}; };
@ -152,6 +157,7 @@ export default function Project() {
setProjectName(data.name); setProjectName(data.name);
setNodes(clientNodeItems); setNodes(clientNodeItems);
setConnections(canvasData.canvas.connections); setConnections(canvasData.canvas.connections);
setNetworks(canvasData.canvas.networks);
setCanvasPosition(canvasData.canvas.position); setCanvasPosition(canvasData.canvas.position);
}, [data]); }, [data]);
@ -166,7 +172,8 @@ export default function Project() {
const debouncedOnGraphUpdate = useMemo( const debouncedOnGraphUpdate = useMemo(
() => () =>
debounce((graphData) => { debounce((graphData) => {
const flatData = flattenGraphData(graphData); graphData.networks = stateNetworksRef.current;
const flatData = generatePayload(graphData);
generateHttp(flatData) generateHttp(flatData)
.then(checkHttpStatus) .then(checkHttpStatus)
.then((data) => { .then((data) => {
@ -220,6 +227,26 @@ export default function Project() {
setNodes({ ...nodes, [clientNodeItem.key]: clientNodeItem }); setNodes({ ...nodes, [clientNodeItem.key]: clientNodeItem });
}; };
const onCreateNetwork = (values: any) => {
setNetworks({ ...networks, [values.key]: values });
};
const onUpdateNetwork = (values: any) => {
setNetworks({ ...networks, [values.key]: values });
};
const onDeleteNetwork = (uuid: string) => {
const _networks = Object.keys(networks).reduce((ret: any, key) => {
if (networks[key].key !== uuid) {
ret[key] = networks[key];
}
return ret;
}, {});
setNetworks({ ..._networks });
};
const onUpdateEndpoint = (nodeItem: IServiceNodeItem) => { const onUpdateEndpoint = (nodeItem: IServiceNodeItem) => {
setNodes({ ...nodes, [nodeItem.key]: nodeItem }); setNodes({ ...nodes, [nodeItem.key]: nodeItem });
}; };
@ -283,7 +310,13 @@ export default function Project() {
return ( return (
<> <>
{showNetworksModal ? ( {showNetworksModal ? (
<ModalNetwork onHide={() => setShowNetworksModal(false)} /> <ModalNetwork
networks={networks}
onHide={() => setShowNetworksModal(false)}
onCreateNetwork={(values: any) => onCreateNetwork(values)}
onUpdateNetwork={(values: any) => onUpdateNetwork(values)}
onDeleteNetwork={(uuid: string) => onDeleteNetwork(uuid)}
/>
) : null} ) : null}
{showVolumesModal ? ( {showVolumesModal ? (

@ -4,6 +4,8 @@
body { body {
margin: 0; margin: 0;
height: 100%;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif; sans-serif;
@ -110,6 +112,9 @@ path,
.btn-util { .btn-util {
@apply inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-1 focus:ring-offset-1 focus:ring-indigo-500; @apply inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-1 focus:ring-offset-1 focus:ring-indigo-500;
} }
.btn-util-red {
@apply inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-1 focus:ring-offset-1 focus:ring-red-500;
}
.btn-util-selected { .btn-util-selected {
@apply text-white bg-indigo-500 hover:bg-indigo-500 focus:ring-indigo-500; @apply text-white bg-indigo-500 hover:bg-indigo-500 focus:ring-indigo-500;
} }

@ -339,6 +339,7 @@ export interface IProjectPayload {
}; };
nodes: any; nodes: any;
connections: any; connections: any;
networks: any;
}; };
}; };
} }
@ -346,7 +347,7 @@ export interface IProjectPayload {
export interface IGeneratePayload { export interface IGeneratePayload {
data: { data: {
version: number; version: number;
networks: Partial<INetworkTopLevel>[]; networks: Record<string, Partial<INetworkTopLevel>>;
services: Record<string, Partial<IService>>; services: Record<string, Partial<IService>>;
volumes: Record<string, Partial<IVolumeTopLevel>>; volumes: Record<string, Partial<IVolumeTopLevel>>;
}; };

@ -1,11 +1,12 @@
import { IGeneratePayload } from "../types"; import { IGeneratePayload } from "../types";
export const flattenGraphData = (graphData: any): IGeneratePayload => { export const generatePayload = (data: any): IGeneratePayload => {
const nodes = graphData["nodes"]; const nodes = data["nodes"];
const networks = data["networks"] || {};
const base: IGeneratePayload = { const base: IGeneratePayload = {
data: { data: {
version: 3, version: 3,
networks: [], networks: {},
services: {}, services: {},
volumes: {} volumes: {}
} }
@ -23,5 +24,10 @@ export const flattenGraphData = (graphData: any): IGeneratePayload => {
} }
}); });
Object.keys(networks).forEach((key) => {
base.data.networks[networks[key].canvasConfig.node_name] =
networks[key].networkConfig;
});
return base; return base;
}; };

@ -209,6 +209,12 @@ export const volumeConfigCanvasInitialValues = (): ICanvasConfig => {
}; };
}; };
export const networkConfigCanvasInitialValues = (): ICanvasConfig => {
return {
node_name: "unnamed"
};
};
export const serviceConfigCanvasInitialValues = (): ICanvasConfig => { export const serviceConfigCanvasInitialValues = (): ICanvasConfig => {
return { return {
node_name: "unnamed", node_name: "unnamed",

Loading…
Cancel
Save