chore: add tabs with forms, service interface

pull/68/head
Artem Golub 3 years ago committed by Samuel Rowe
parent 02549703ee
commit cb3c1b1fc3

3
.gitignore vendored

@ -313,3 +313,6 @@ Icon
Network Trash Folder Network Trash Folder
Temporary Items Temporary Items
.apdisk .apdisk
# Django
static

@ -41,9 +41,7 @@ shell_server:
frontend_build: frontend_build:
@ cd ./services/frontend/src && npm install && npm run build @ cd ./services/frontend/src && npm install && npm run build
local_setup: frontend_build up local_server_init:
@ echo "Waiting for PostgreSQL..." \ docker exec -it ${CONTAINER} python /home/server/manage.py makemigrations \
&& sleep 5 \
&& docker exec -it ${CONTAINER} python /home/server/manage.py makemigrations \
&& docker exec -it ${CONTAINER} python /home/server/manage.py migrate \ && docker exec -it ${CONTAINER} python /home/server/manage.py migrate \
&& docker exec -it ${BACKEND_CONTAINER_NAME} python /home/server/manage.py collectstatic --noinput && docker exec -it ${CONTAINER} python /home/server/manage.py collectstatic --noinput

@ -26,3 +26,4 @@ $ cd services/frontend && npm run start
## Docs ## Docs
- https://docs.jsplumbtoolkit.com/community/ - https://docs.jsplumbtoolkit.com/community/
- https://github.com/compose-spec/compose-spec/blob/master/spec.md

@ -10,7 +10,9 @@ import docker
from better_profanity import profanity from better_profanity import profanity
from ruamel.yaml import YAML from ruamel.yaml import YAML
from ruamel.yaml.scalarstring import SingleQuotedScalarString, DoubleQuotedScalarString from ruamel.yaml.scalarstring import (
SingleQuotedScalarString,
DoubleQuotedScalarString)
from api.models import Project from api.models import Project
@ -18,7 +20,7 @@ from api.models import Project
try: try:
import textwrap import textwrap
textwrap.indent textwrap.indent
except AttributeError: # undefined function (wasn't added until Python 3.3) except AttributeError:
def indent(text, amount, ch=' '): def indent(text, amount, ch=' '):
padding = amount * ch padding = amount * ch
return ''.join(padding+line for line in text.splitlines(True)) return ''.join(padding+line for line in text.splitlines(True))
@ -267,32 +269,31 @@ def format_build(specified_version, build):
return build_str return build_str
for _key, _val in build.items(): for _key, _val in build.items():
if _key in ['args', 'cache_from', 'labels']:
if _val: if _val:
if _key in ['args', 'cache_from', 'labels']:
ret[_key] = format_key_val_pairs(_val) ret[_key] = format_key_val_pairs(_val)
else: else:
if _val:
ret[_key] = _val ret[_key] = _val
return ret return ret
def _remove_missing_and_underscored_keys(d): def _remove_missing_and_underscored_keys(str):
if not d: return d if not str: return str
for key in list(d.keys()): for key in list(str.keys()):
if isinstance(d[key], list): if isinstance(str[key], list):
d[key] = list(filter(None, d[key])) str[key] = list(filter(None, str[key]))
if not d.get(key): if not str.get(key):
del d[key] del str[key]
elif isinstance(d[key], dict): elif isinstance(str[key], dict):
d[key] = _remove_missing_and_underscored_keys(d[key]) str[key] = _remove_missing_and_underscored_keys(str[key])
if d[key] is None or d[key] == {}: if str[key] is None or str[key] == {}:
del d[key] del str[key]
return d return str
def format_deploy(specified_version, deploy): def format_deploy(deploy):
ret = deploy ret = deploy
with contextlib.suppress(Exception): with contextlib.suppress(Exception):
@ -418,7 +419,7 @@ def format_services_version_three(specified_version, services, connections, volu
if connected_services := get_connected_services(service_key, connections, services): if connected_services := get_connected_services(service_key, connections, services):
service_formatted['depends_on'] = [] service_formatted['depends_on'] = []
for connected_service in connected_services: for connected_service in connected_services:
service_formatted['depends_on'].append(f"{connected_service['name']}") service_formatted['depends_on'].append(f"{connected_service['service_name']}")
with contextlib.suppress(KeyError): with contextlib.suppress(KeyError):
if service['container_name']: if service['container_name']:
service_formatted['container_name'] = service['container_name'] service_formatted['container_name'] = service['container_name']
@ -458,9 +459,9 @@ def format_services_version_three(specified_version, services, connections, volu
service_formatted['build'] = build service_formatted['build'] = build
if int(float(specified_version)) >= 3: if int(float(specified_version)) >= 3:
with contextlib.suppress(KeyError): with contextlib.suppress(KeyError):
if deploy := format_deploy(specified_version, service['deploy']): if deploy := format_deploy(service['deploy']):
service_formatted['deploy'] = deploy service_formatted['deploy'] = deploy
services_formatted[service['name']] = service_formatted services_formatted[service['service_name']] = service_formatted
return services_formatted return services_formatted
@ -534,9 +535,7 @@ def generate(cname):
sys.exit(1) sys.exit(1)
cattrs = c.containers.get(cid).attrs cattrs = c.containers.get(cid).attrs
cfile = {} cfile = {cattrs['Name'][1:]: {}}
networks = {}
cfile[cattrs['Name'][1:]] = {}
ct = cfile[cattrs['Name'][1:]] ct = cfile[cattrs['Name'][1:]]
values = { values = {
@ -580,9 +579,7 @@ def generate(cname):
} }
networklist = c.networks.list() networklist = c.networks.list()
for network in networklist: networks = {network.attrs['Name']: {'external': (not network.attrs['Internal'])} for network in networklist if network.attrs['Name'] in values['networks'].keys()}
if network.attrs['Name'] in values['networks'].keys():
networks[network.attrs['Name']] = {'external': (not network.attrs['Internal'])}
# Check for command and add it if present. # Check for command and add it if present.
if cattrs['Config']['Cmd'] != None: if cattrs['Config']['Cmd'] != None:

@ -153,7 +153,9 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/ # https://docs.djangoproject.com/en/4.0/howto/static-files/
STATIC_URL = "static/" PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(PROJECT_ROOT, 'static')
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field

@ -1,15 +1,15 @@
import { FC, useState, useEffect } from "react"; import { FC, useState, useEffect } from "react";
import { values } from "lodash"; import { Dictionary, values } from "lodash";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import eventBus from "../../events/eventBus"; import eventBus from "../../events/eventBus";
import { Popover } from "./Popover"; import { Popover } from "./Popover";
import { IGraphData, CallbackFunction } from "../../types"; import { IGraphData, CallbackFunction, IClientNodeItem } from "../../types";
import { useJsPlumb } from "../useJsPlumb"; import { useJsPlumb } from "../useJsPlumb";
const CANVAS_ID: string = "canvas-container-" + uuidv4(); const CANVAS_ID: string = "canvas-container-" + uuidv4();
interface ICanvasProps { interface ICanvasProps {
nodes: any; nodes: Dictionary<IClientNodeItem>;
connections: any; connections: any;
canvasPosition: any; canvasPosition: any;
onNodeUpdate: CallbackFunction; onNodeUpdate: CallbackFunction;
@ -200,11 +200,11 @@ export const Canvas: FC<ICanvasProps> = (props) => {
></Popover> ></Popover>
)} )}
<div className="node-label w-full py-2 px-4"> <div className="node-label w-full py-2 px-4">
<div className="text-sm font-semibold"> <div className="text-sm font-semibold overflow-x-hidden">
{x.configuration.prettyName} {x.canvasConfig.service_name}
</div> </div>
<div className="text-xs text-gray-500"> <div className="text-xs text-gray-500 overflow-x-hidden">
{x.configuration.prettyName} {x.serviceConfig?.container_name}
</div> </div>
</div> </div>
</div> </div>

@ -47,10 +47,10 @@ const ModalConfirmDelete = (props: IModalConfirmDeleteProps) => {
</div> </div>
</div> </div>
<div className="flex items-center justify-end px-4 py-3 border-t border-solid border-blueGray-200 rounded-b"> <div className="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-2 items-center justify-end px-4 py-3 border-t border-solid border-gray-200 rounded-b">
<button <button
type="button" type="button"
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-3 py-1 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 sm:mt-0 sm:w-auto sm:text-sm" className="w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-3 py-1 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 sm:w-auto sm:text-sm"
onClick={onHide} onClick={onHide}
> >
Cancel Cancel
@ -58,7 +58,7 @@ const ModalConfirmDelete = (props: IModalConfirmDeleteProps) => {
<button <button
type="button" type="button"
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-3 py-1 bg-red-600 text-sm font-medium text-white hover:bg-red-700 sm:ml-3 sm:w-auto sm:text-sm" className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-3 py-1 bg-red-600 text-sm font-medium text-white hover:bg-red-700 sm:w-auto sm:text-sm"
onClick={() => { onClick={() => {
onHide(); onHide();
onConfirm(); onConfirm();

@ -1,71 +0,0 @@
const Container = (props: any) => {
const { formik } = props;
return (
<>
<div className="grid grid-cols-3 gap-4">
<div className="col-span-3">
<label
htmlFor={`container-name`}
className="block text-xs font-medium text-gray-700"
>
Name
</label>
<div key={`container-name`}>
<input
type="text"
className="input-util"
name={`configuration.container.name`}
value={formik.values.configuration.container.name || ""}
onChange={formik.handleChange}
/>
</div>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="col-span-3">
<label
htmlFor={`container-image`}
className="block text-xs font-medium text-gray-700 mt-2"
>
Image
</label>
<div key={`container-image`}>
<input
type="text"
className="input-util"
name={`configuration.container.image`}
value={formik.values.configuration.container.image || ""}
onChange={formik.handleChange}
/>
</div>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="col-span-3">
<label
htmlFor={`container-pull-policy`}
className="block text-xs font-medium text-gray-700 mt-2"
>
Pull policy
</label>
<div key={`container-pull-policy`}>
<input
type="text"
className="input-util"
name={`configuration.container.imagePullPolicy`}
value={
formik.values.configuration.container.imagePullPolicy || ""
}
onChange={formik.handleChange}
/>
</div>
</div>
</div>
</>
);
};
export default Container;

@ -1,152 +0,0 @@
import { useState } from "react";
import { useFormik } from "formik";
import { XIcon } from "@heroicons/react/outline";
import General from "./General";
import Container from "./Container";
import Resource from "./Resource";
import { initialValues, formatName } from "./../../../utils";
import { CallbackFunction } from "../../../types";
interface IModalProps {
onHide: CallbackFunction;
onAddEndpoint: CallbackFunction;
}
const ModalCreate = (props: IModalProps) => {
const { onHide, onAddEndpoint } = props;
const [openTab, setOpenTab] = useState("General");
const formik = useFormik({
initialValues: {
configuration: {
...initialValues()
},
key: "template",
type: "TEMPLATE",
inputs: ["op_source"],
outputs: [],
config: {}
},
onSubmit: () => undefined
});
const tabs = [
{ name: "General", href: "#", current: true, hidden: false },
{
name: "Container",
href: "#",
current: false,
hidden: formik.values.configuration.type === "container" ? false : true
},
{
name: "Resource",
href: "#",
current: false,
hidden: formik.values.configuration.type === "resource" ? false : true
}
];
const classNames = (...classes: string[]) => {
return classes.filter(Boolean).join(" ");
};
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">Add template</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>
<div>
<div className="sm:hidden">
<label htmlFor="tabs" className="sr-only">
Select a tab
</label>
<select
id="tabs"
name="tabs"
className="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
defaultValue={tabs.find((tab) => tab.current)?.name}
>
{tabs.map((tab) => (
<option key={tab.name}>{tab.name}</option>
))}
</select>
</div>
<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">
<form onSubmit={formik.handleSubmit}>
{openTab === "General" && <General formik={formik} />}
{openTab === "Container" && <Container formik={formik} />}
{openTab === "Resource" && <Resource formik={formik} />}
</form>
</div>
</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.values.configuration.name = formatName(
formik.values.configuration.prettyName
);
onAddEndpoint(formik.values);
formik.resetForm();
setOpenTab("General");
}}
>
Add
</button>
</div>
</div>
</div>
</div>
</div>
</>
);
};
export default ModalCreate;

@ -1,175 +0,0 @@
import React from "react";
import { useFormik } from "formik";
import { XIcon } from "@heroicons/react/outline";
import General from "./General";
import Container from "./Container";
import Resource from "./Resource";
import { initialValues, formatName } from "./../../../utils";
import { IClientNodeItem, CallbackFunction } from "../../../types";
interface IModalProps {
node: IClientNodeItem | null;
onHide: CallbackFunction;
onUpdateEndpoint: CallbackFunction;
}
const ModalEdit = (props: IModalProps) => {
const { node, onHide, onUpdateEndpoint } = props;
const [selectedNode, setSelectedNode] =
React.useState<IClientNodeItem | null>(null);
const [openTab, setOpenTab] = React.useState("General");
const formik = useFormik({
initialValues: {
configuration: {
...initialValues()
}
},
onSubmit: () => undefined
});
const tabs = [
{
name: "General",
href: "#",
current: true,
hidden: false
},
{
name: "Container",
href: "#",
current: false,
hidden: formik.values.configuration.type === "container" ? false : true
},
{
name: "Resource",
href: "#",
current: false,
hidden: formik.values.configuration.type === "resource" ? false : true
}
];
const classNames = (...classes: string[]) => {
return classes.filter(Boolean).join(" ");
};
React.useEffect(() => {
if (node) {
setSelectedNode(node);
}
}, [node]);
React.useEffect(() => {
formik.resetForm();
if (selectedNode) {
formik.initialValues.configuration = { ...selectedNode.configuration };
}
}, [selectedNode]);
React.useEffect(() => {
return () => {
formik.resetForm();
};
}, []);
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">Update template</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>
<div>
<div className="sm:hidden">
<label htmlFor="tabs" className="sr-only">
Select a tab
</label>
<select
id="tabs"
name="tabs"
className="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
defaultValue={tabs.find((tab) => tab.current)?.name}
>
{tabs.map((tab) => (
<option key={tab.name}>{tab.name}</option>
))}
</select>
</div>
<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, index) => (
<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">
<form onSubmit={formik.handleSubmit}>
{openTab === "General" && <General formik={formik} />}
{openTab === "Container" && <Container formik={formik} />}
{openTab === "Resource" && <Resource formik={formik} />}
</form>
</div>
</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={() => {
const updated = { ...selectedNode };
formik.values.configuration.name = formatName(
formik.values.configuration.prettyName
);
updated.configuration = formik.values.configuration;
onUpdateEndpoint(updated);
}}
>
Update
</button>
</div>
</div>
</div>
</div>
</div>
</>
);
};
export default ModalEdit;

@ -1,76 +0,0 @@
import React from "react";
const General = (props: any) => {
const { formik } = props;
return (
<>
<div className="grid grid-cols-3 gap-4">
<div className="col-span-3">
<label
htmlFor="prettyName"
className="block text-xs font-medium text-gray-700"
>
Name
</label>
<div className="mt-1">
<input
id="prettyName"
name="configuration.prettyName"
type="text"
autoComplete="none"
className="input-util"
onChange={formik.handleChange}
value={formik.values.configuration.prettyName}
/>
</div>
</div>
</div>
<div className="mt-2">
<label
htmlFor="about"
className="block text-xs font-medium text-gray-700"
>
Description
</label>
<div className="mt-1">
<textarea
id="description"
name="configuration.description"
onChange={formik.handleChange}
value={formik.values.configuration.description}
rows={2}
className="input-util"
placeholder=""
></textarea>
</div>
</div>
<div className="grid grid-cols-3 gap-4 mt-2">
<div className="col-span-3">
<label
htmlFor="templateType"
className="block text-xs font-medium text-gray-700"
>
Type
</label>
<div className="mt-1">
<select
id="templateType"
name="configuration.type"
className="max-w-lg block focus:ring-indigo-500 focus:border-indigo-500 w-full shadow-sm sm:max-w-xs sm:text-sm border-gray-300 rounded-md"
value={formik.values.configuration.type}
onChange={formik.handleChange}
>
<option value="">Select type</option>
<option value="container">Container</option>
<option value="resource">Resource</option>
</select>
</div>
</div>
</div>
</>
);
};
export default General;

@ -1,50 +0,0 @@
import React from "react";
const Resource = (props: any) => {
const { formik } = props;
return (
<>
<div className="grid grid-cols-3 gap-4">
<div className="col-span-3">
<label
htmlFor={`resource-action`}
className="block text-xs font-medium text-gray-700"
>
Action
</label>
<div key={`resource-action`}>
<input
type="text"
className="input-util"
name={`configuration.resource.action`}
value={formik.values.configuration.resource.action || ""}
onChange={formik.handleChange}
/>
</div>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="col-span-3">
<label
htmlFor={`resource-manifest`}
className="block text-xs font-medium text-gray-700 mt-2"
>
Manifest
</label>
<textarea
id="resource-manifest"
rows={2}
className="input-util"
placeholder=""
name={`configuration.resource.manifest`}
value={formik.values.configuration.resource.manifest || ""}
onChange={formik.handleChange}
></textarea>
</div>
</div>
</>
);
};
export default Resource;

@ -1,6 +1,11 @@
import { useState } from "react";
import { useFormik } from "formik"; import { useFormik } from "formik";
import { XIcon } from "@heroicons/react/outline"; import { XIcon } from "@heroicons/react/outline";
import { serviceInitialValues, formatName } from "../../../utils"; import General from "./General";
import Environment from "./Environment";
import Volumes from "./Volumes";
import Labels from "./Labels";
import { canvasConfigInitialValues } from "../../../utils";
import { CallbackFunction } from "../../../types"; import { CallbackFunction } from "../../../types";
interface IModalServiceProps { interface IModalServiceProps {
@ -10,10 +15,15 @@ interface IModalServiceProps {
const ModalServiceCreate = (props: IModalServiceProps) => { const ModalServiceCreate = (props: IModalServiceProps) => {
const { onHide, onAddEndpoint } = props; const { onHide, onAddEndpoint } = props;
const [openTab, setOpenTab] = useState("General");
const formik = useFormik({ const formik = useFormik({
initialValues: { initialValues: {
configuration: { canvasConfig: {
...serviceInitialValues() ...canvasConfigInitialValues()
},
serviceConfig: {
container_name: ""
}, },
key: "service", key: "service",
type: "SERVICE", type: "SERVICE",
@ -23,6 +33,35 @@ const ModalServiceCreate = (props: IModalServiceProps) => {
}, },
onSubmit: () => undefined onSubmit: () => undefined
}); });
const tabs = [
{
name: "General",
href: "#",
current: true,
hidden: false
},
{
name: "Environment",
href: "#",
current: false,
hidden: false
},
{
name: "Volumes",
href: "#",
current: false,
hidden: false
},
{
name: "Labels",
href: "#",
current: false,
hidden: false
}
];
const classNames = (...classes: string[]) => {
return classes.filter(Boolean).join(" ");
};
return ( return (
<div className="fixed z-50 inset-0 overflow-y-auto"> <div className="fixed z-50 inset-0 overflow-y-auto">
@ -45,49 +84,41 @@ const ModalServiceCreate = (props: IModalServiceProps) => {
</button> </button>
</div> </div>
<div className="relative px-4 py-3 flex-auto"> <div>
<div className="grid grid-cols-3 gap-4"> <div className="hidden sm:block">
<div className="col-span-3"> <div className="border-b border-gray-200 px-8">
<label <nav className="-mb-px flex space-x-8" aria-label="Tabs">
htmlFor="prettyName" {tabs.map((tab) => (
className="block text-xs font-medium text-gray-700" <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);
}}
> >
Name {tab.name}
</label> </a>
<div className="mt-1"> ))}
<input </nav>
id="prettyName"
name="configuration.prettyName"
type="text"
autoComplete="none"
className="input-util"
onChange={formik.handleChange}
value={formik.values.configuration.prettyName}
/>
</div>
</div> </div>
</div> </div>
<div className="mt-2"> <div className="relative px-4 py-3 flex-auto">
<div className="col-span-3"> <form onSubmit={formik.handleSubmit}>
<label {openTab === "General" && <General formik={formik} />}
htmlFor="template" {openTab === "Environment" && <Environment formik={formik} />}
className="block text-xs font-medium text-gray-700" {openTab === "Volumes" && <Volumes formik={formik} />}
> {openTab === "Labels" && <Labels formik={formik} />}
Template </form>
</label>
<div className="mt-1">
<input
id="template"
name="configuration.template"
type="text"
autoComplete="none"
className="input-util"
onChange={formik.handleChange}
value={formik.values.configuration.template}
/>
</div>
</div>
</div> </div>
</div> </div>
@ -96,9 +127,6 @@ const ModalServiceCreate = (props: IModalServiceProps) => {
className="btn-util" className="btn-util"
type="button" type="button"
onClick={() => { onClick={() => {
formik.values.configuration.name = formatName(
formik.values.configuration.prettyName
);
onAddEndpoint(formik.values); onAddEndpoint(formik.values);
formik.resetForm(); formik.resetForm();
}} }}

@ -1,48 +1,95 @@
import React from "react"; import { useState, useEffect } from "react";
import { useFormik } from "formik"; import { useFormik } from "formik";
import { XIcon } from "@heroicons/react/outline"; import { XIcon } from "@heroicons/react/outline";
import { serviceInitialValues, formatName } from "../../../utils"; import General from "./General";
import Environment from "./Environment";
import Volumes from "./Volumes";
import Labels from "./Labels";
import { canvasConfigInitialValues } from "../../../utils";
import {
CallbackFunction,
ICanvasConfig,
IClientNodeItem,
IService
} from "../../../types";
interface IModalServiceProps { interface IModalServiceProps {
node: any; node: any;
onHide: any; onHide: CallbackFunction;
onUpdateEndpoint: any; onUpdateEndpoint: CallbackFunction;
} }
const ModalServiceEdit = (props: IModalServiceProps) => { const ModalServiceEdit = (props: IModalServiceProps) => {
const { node, onHide, onUpdateEndpoint } = props; const { node, onHide, onUpdateEndpoint } = props;
const [selectedNode, setSelectedNode] = React.useState<any>(null); const [openTab, setOpenTab] = useState("General");
const [selectedNode, setSelectedNode] = useState<IClientNodeItem>();
const formik = useFormik({ const formik = useFormik({
initialValues: { initialValues: {
configuration: { canvasConfig: {
...serviceInitialValues() ...canvasConfigInitialValues()
},
serviceConfig: {
container_name: ""
} }
}, },
onSubmit: () => undefined onSubmit: () => undefined
}); });
const tabs = [
{
name: "General",
href: "#",
current: true,
hidden: false
},
{
name: "Environment",
href: "#",
current: false,
hidden: false
},
{
name: "Volumes",
href: "#",
current: false,
hidden: false
},
{
name: "Labels",
href: "#",
current: false,
hidden: false
}
];
const classNames = (...classes: string[]) => {
return classes.filter(Boolean).join(" ");
};
React.useEffect(() => { useEffect(() => {
if (node) { if (node) {
setSelectedNode(node); setSelectedNode(node);
} }
}, [node]); }, [node]);
React.useEffect(() => { useEffect(() => {
formik.resetForm(); formik.resetForm();
if (selectedNode) { if (selectedNode) {
formik.initialValues.configuration = { ...selectedNode.configuration }; formik.initialValues.canvasConfig = {
...selectedNode.canvasConfig
} as ICanvasConfig;
formik.initialValues.serviceConfig = {
...selectedNode.serviceConfig
} as IService;
} }
}, [selectedNode]); }, [selectedNode]);
React.useEffect(() => { useEffect(() => {
return () => { return () => {
formik.resetForm(); formik.resetForm();
}; };
}, []); }, []);
return ( return (
<>
<div className="fixed z-50 inset-0 overflow-y-auto"> <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 className="justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 outline-none focus:outline-none">
<div <div
@ -63,49 +110,41 @@ const ModalServiceEdit = (props: IModalServiceProps) => {
</button> </button>
</div> </div>
<div className="relative px-4 py-3 flex-auto"> <div>
<div className="grid grid-cols-3 gap-4"> <div className="hidden sm:block">
<div className="col-span-3"> <div className="border-b border-gray-200 px-8">
<label <nav className="-mb-px flex space-x-8" aria-label="Tabs">
htmlFor="prettyName" {tabs.map((tab) => (
className="block text-xs font-medium text-gray-700" <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);
}}
> >
Name {tab.name}
</label> </a>
<div className="mt-1"> ))}
<input </nav>
id="prettyName"
name="configuration.prettyName"
type="text"
autoComplete="none"
className="input-util"
onChange={formik.handleChange}
value={formik.values.configuration.prettyName}
/>
</div>
</div> </div>
</div> </div>
<div className="mt-2"> <div className="relative px-4 py-3 flex-auto">
<div className="col-span-3"> <form onSubmit={formik.handleSubmit}>
<label {openTab === "General" && <General formik={formik} />}
htmlFor="template" {openTab === "Environment" && <Environment formik={formik} />}
className="block text-xs font-medium text-gray-700" {openTab === "Volumes" && <Volumes formik={formik} />}
> {openTab === "Labels" && <Labels formik={formik} />}
Template </form>
</label>
<div className="mt-1">
<input
id="template"
name="configuration.template"
type="text"
autoComplete="none"
className="input-util"
onChange={formik.handleChange}
value={formik.values.configuration.template}
/>
</div>
</div>
</div> </div>
</div> </div>
@ -115,10 +154,8 @@ const ModalServiceEdit = (props: IModalServiceProps) => {
type="button" type="button"
onClick={() => { onClick={() => {
const updated = { ...selectedNode }; const updated = { ...selectedNode };
formik.values.configuration.name = formatName( updated.canvasConfig = formik.values.canvasConfig;
formik.values.configuration.prettyName updated.serviceConfig = formik.values.serviceConfig;
);
updated.configuration = formik.values.configuration;
onUpdateEndpoint(updated); onUpdateEndpoint(updated);
}} }}
> >
@ -129,7 +166,6 @@ const ModalServiceEdit = (props: IModalServiceProps) => {
</div> </div>
</div> </div>
</div> </div>
</>
); );
}; };

@ -0,0 +1,6 @@
const Environment = (props: any) => {
const { formik } = props;
return <></>;
};
export default Environment;

@ -0,0 +1,56 @@
const General = (props: any) => {
const { formik } = props;
return (
<>
<div className="relative pb-3 flex-auto">
<div className="grid grid-cols-6 gap-4">
<div className="col-span-3">
<label
htmlFor="service_name"
className="block text-xs font-medium text-gray-700"
>
Service name
</label>
<div className="mt-1">
<input
id="service_name"
name="canvasConfig.service_name"
type="text"
autoComplete="none"
className="input-util"
onChange={formik.handleChange}
value={formik.values.canvasConfig.service_name}
/>
</div>
</div>
</div>
</div>
<div className="relative pb-3 flex-auto">
<div className="grid grid-cols-6 gap-4">
<div className="col-span-3">
<label
htmlFor="container_name"
className="block text-xs font-medium text-gray-700"
>
Container name
</label>
<div className="mt-1">
<input
id="container_name"
name="serviceConfig.container_name"
type="text"
autoComplete="none"
className="input-util"
onChange={formik.handleChange}
value={formik.values.serviceConfig.container_name}
/>
</div>
</div>
</div>
</div>
</>
);
};
export default General;

@ -0,0 +1,6 @@
const Labels = (props: any) => {
const { formik } = props;
return <></>;
};
export default Labels;

@ -0,0 +1,6 @@
const Volumes = (props: any) => {
const { formik } = props;
return <></>;
};
export default Volumes;

@ -208,7 +208,7 @@ export default function Project() {
const sections = flattenLibraries(nodeLibraries); const sections = flattenLibraries(nodeLibraries);
const clientNodeItem = getClientNodeItem( const clientNodeItem = getClientNodeItem(
values, values,
ensure(sections.find((l) => l.Type === values.type)) ensure(sections.find((l) => l.type === values.type))
); );
clientNodeItem.position = { left: 60, top: 30 }; clientNodeItem.position = { left: 60, top: 30 };
setNodes({ ...nodes, [clientNodeItem.key]: clientNodeItem }); setNodes({ ...nodes, [clientNodeItem.key]: clientNodeItem });

@ -2,6 +2,10 @@ import { AnchorId } from "@jsplumb/common";
import { Dictionary } from "lodash"; import { Dictionary } from "lodash";
import { NodeGroupType } from "./enums"; import { NodeGroupType } from "./enums";
type KeyValPair = {
[x: string]: string | number;
};
export type CallbackFunction = (...args: any[]) => any; export type CallbackFunction = (...args: any[]) => any;
export interface IServiceNodePosition { export interface IServiceNodePosition {
@ -30,25 +34,20 @@ export interface IContainer {
} }
export interface INodeLibraryItem { export interface INodeLibraryItem {
Id: number; id: number;
Name: string; name: string;
Type: string; type: string;
Description: string; description: string;
NoInputs: number; noInputs: number;
NoOutputs: number; noOutputs: number;
IsActive: boolean; isActive: boolean;
} }
export interface INodeGroup { export interface INodeGroup {
Id: number; id: number;
Name: NodeGroupType; name: NodeGroupType;
Description: string; description: string;
NodeTypes: INodeLibraryItem[]; nodeTypes: INodeLibraryItem[];
}
export interface IDockerCompose {
version: string;
services: any[];
} }
interface INodeItem { interface INodeItem {
@ -63,11 +62,8 @@ export interface IFlatConnection {
target: string; target: string;
} }
export interface IBaseConfiguration { export interface ICanvasConfig {
prettyName: string; service_name: string;
name: string;
description: string;
type: string;
} }
export interface IGraphData { export interface IGraphData {
@ -75,23 +71,214 @@ export interface IGraphData {
connections: Dictionary<IFlatConnection>; connections: Dictionary<IFlatConnection>;
} }
export interface IClientNodeItem extends INodeItem {
outputs: string[];
configuration: IBaseConfiguration;
}
export interface IServiceNodeItem extends INodeItem {
configuration: IBaseConfiguration;
}
export interface IAnchor { export interface IAnchor {
id: string; id: string;
position: AnchorId; position: AnchorId;
} }
export interface IVolume {
type: string;
source: string;
target: string;
read_only: boolean;
bind: {
propagation: string;
create_host_path: boolean;
selinux: string;
};
volume: {
nocopy: boolean;
};
tmpfs: {
size: string | number;
};
consistency: string;
}
export interface IService { export interface IService {
name: string; build: {
labels: any; context: string;
dockerfile: string;
args: KeyValPair[];
ssh: string[];
cache_from: string[];
cache_to: string[];
extra_hosts: string[];
isolation: string;
labels: KeyValPair[];
shm_size: string | number;
target: string;
};
cpu_count: string | number;
cpu_percent: string | number;
cpu_shares: string | number;
cpu_period: string | number;
cpu_quota: string | number;
cpu_rt_runtime: string | number;
cpu_rt_period: string | number;
cpuset: number | number[];
cap_add: string[];
cap_drop: string[];
cgroup_parent: string;
command: string | string[];
configs: string[] | KeyValPair[];
container_name: string;
credential_spec: KeyValPair;
depends_on: string[] | { [key: string]: string | number | KeyValPair };
deploy: {
endpoint_mode: string;
labels: string[] | { [key: string]: string };
mode: string;
placement: {
constraints: KeyValPair[] | KeyValPair;
preferences: KeyValPair[] | KeyValPair;
};
replicas: number;
resources: {
limits: {
cpus: string;
memory: string;
pids: number;
};
reservations: {
cpus: string;
memory: string;
devices: { [key: string]: string | number | string[] }[];
};
};
restart_policy: {
condition: string;
delay: string;
max_attempts: number;
window: string;
};
rollback_config: {
parallelism: number;
delay: string;
failure_action: string;
monitor: string;
max_failure_ratio: string;
order: string;
};
update_config: {
parallelism: number;
delay: string;
failure_action: string;
monitor: string;
max_failure_ratio: string;
order: string;
};
};
device_cgroup_rules: string[];
devices: string[];
dns: string | string[];
dns_opt: string[];
dns_search: string | string[];
domainname: string;
entrypoint: string | string[];
env_file: string | string[];
environment: string[] | KeyValPair;
expose: string[];
extends: KeyValPair;
external_links: string[];
extra_hosts: string[];
group_add: string[];
healthcheck: {
test: string[];
interval: string;
timeout: string;
retries: number;
start_period: string;
};
hostname: string;
image: string;
init: boolean;
ipc: string;
isolation: string;
labels: string[] | KeyValPair;
links: string[];
logging: {
driver: string;
options: KeyValPair;
};
network_mode: string;
networks:
| string[]
| {
[x: string]: {
aliases: string[];
ipv4_address: string;
ipv6_address: string;
link_local_ips: string[];
priority: number;
};
};
mac_address: string;
mem_swappiness: number;
memswap_limit: string | number;
oom_kill_disable: boolean;
oom_score_adj: number;
pid: string | number;
platform: string;
ports:
| string[]
| {
target: number;
host_ip: string;
published: string | number;
protocol: string;
mode: string;
};
privileged: boolean;
profiles: string;
pull_policy: string;
read_only: boolean;
restart: string;
runtime: string;
secrets:
| string[]
| {
source: string;
target: string;
uid: string;
gid: string;
mode: number;
};
security_opt: string[];
shm_size: string;
stdin_open: boolean;
stop_grace_period: string;
stop_signal: string;
storage_opt: {
size: string;
};
sysctls: string[] | KeyValPair;
tmpfs: string | string[];
tty: boolean;
ulimits: {
nproc: number;
nofile: {
soft: number;
hard: number;
};
};
user: string;
userns_mode: string;
volumes: string[] | IVolume;
volumes_from: string[];
working_dir: string;
tag: string;
}
export interface IDockerCompose {
version: string;
services: IService[];
}
export interface IClientNodeItem extends INodeItem {
outputs: string[];
canvasConfig: ICanvasConfig;
serviceConfig: Partial<IService>;
} }
export interface IProjectPayload { export interface IProjectPayload {
@ -115,12 +302,14 @@ export interface IProjectPayload {
}; };
} }
export interface ISaturatedService extends Partial<IService>, ICanvasConfig {}
export interface IGeneratePayload { export interface IGeneratePayload {
data: { data: {
configs: []; configs: [];
networks: []; networks: [];
secrets: []; secrets: [];
services: IService[]; services: ISaturatedService[];
connections: [[string, string]]; connections: [[string, string]];
version: number; version: number;
volumes: []; volumes: [];

@ -3,18 +3,18 @@ import { INodeGroup } from "../../types";
export const nodeLibraries: INodeGroup[] = [ export const nodeLibraries: INodeGroup[] = [
{ {
Id: 1, id: 1,
Name: NodeGroupType.Services, name: NodeGroupType.Services,
Description: "Services", description: "Services",
NodeTypes: [ nodeTypes: [
{ {
Id: 1, id: 1,
Name: "service", name: "service",
Type: "SERVICE", type: "SERVICE",
Description: "Service node", description: "Service node",
NoInputs: 1, noInputs: 1,
NoOutputs: 1, noOutputs: 1,
IsActive: true isActive: true
} }
] ]
} }

@ -1,2 +1,2 @@
export const ServiceNodeConfiguration = export const ServiceNodeConfiguration =
'{"prettyName":"","name":"","key":"service","type":"SERVICE","inputs":["op_source"],"outputs":[]}'; '{"canvasConfig":{"name":""},"key":"service","type":"SERVICE","inputs":["op_source"],"outputs":[]}';

@ -1,14 +1,14 @@
import { IClientNodeItem, IService, IGeneratePayload } from "../types"; import { IClientNodeItem, IGeneratePayload, ISaturatedService } from "../types";
import { Dictionary } from "lodash"; import { Dictionary } from "lodash";
const getServices = (graphNodes: Dictionary<IClientNodeItem>): IService[] => { const getServices = (
const ret: IService[] = []; graphNodes: Dictionary<IClientNodeItem>
): ISaturatedService[] => {
const ret: ISaturatedService[] = [];
for (const [, value] of Object.entries(graphNodes)) { for (const [, value] of Object.entries(graphNodes)) {
ret.push({ ret.push({
name: value.configuration.name, ...value.canvasConfig,
labels: { ...value.serviceConfig
key: value.key
}
}); });
} }

@ -13,26 +13,11 @@ import {
import { LOCAL_STORAGE } from "../constants"; import { LOCAL_STORAGE } from "../constants";
import { import {
IClientNodeItem, IClientNodeItem,
IServiceNodeItem,
INodeLibraryItem, INodeLibraryItem,
INodeGroup, INodeGroup,
IContainer ICanvasConfig
} from "../types"; } from "../types";
interface IConf {
prettyName: string;
name: string;
description: string;
type: string;
container?: IContainer;
}
interface IServiceConf {
prettyName: string;
name: string;
template: string;
}
export function ensure<T>( export function ensure<T>(
argument: T | undefined | null, argument: T | undefined | null,
message = "This value was promised to be there." message = "This value was promised to be there."
@ -44,8 +29,8 @@ export function ensure<T>(
return argument; return argument;
} }
export const parseSingleNode = (configurationStr: string): IServiceNodeItem => { export const parseSingleNode = (configurationStr: string): IClientNodeItem => {
let node: IServiceNodeItem = {} as IServiceNodeItem; let node: IClientNodeItem = {} as IClientNodeItem;
const configurationObj = JSON.parse(configurationStr); const configurationObj = JSON.parse(configurationStr);
if (isPlainObject(configurationObj)) { if (isPlainObject(configurationObj)) {
@ -62,8 +47,8 @@ export const formatName = (name: string): string => {
export const parseConfiguration = ( export const parseConfiguration = (
configurationStr: string configurationStr: string
): IServiceNodeItem[] => { ): IClientNodeItem[] => {
let nodes: IServiceNodeItem[] = []; let nodes: IClientNodeItem[] = [];
const configurationObj = JSON.parse(configurationStr); const configurationObj = JSON.parse(configurationStr);
if (isPlainObject(configurationObj)) { if (isPlainObject(configurationObj)) {
@ -86,7 +71,7 @@ export const parseConfiguration = (
export const flattenLibraries = ( export const flattenLibraries = (
sections: INodeGroup[] sections: INodeGroup[]
): INodeLibraryItem[] => { ): INodeLibraryItem[] => {
return flattenDeep(sections.map((x) => x.NodeTypes)); return flattenDeep(sections.map((x) => x.nodeTypes));
}; };
const getEndPointUuids = ( const getEndPointUuids = (
@ -120,21 +105,16 @@ export const attachUUID = (key: string): string => {
}; };
export const getClientNodeItem = ( export const getClientNodeItem = (
nodeItem: IServiceNodeItem, nodeItem: IClientNodeItem,
library: INodeLibraryItem library: INodeLibraryItem
): IClientNodeItem => { ): IClientNodeItem => {
const uniqueKey = attachUUID(nodeItem.key); const uniqueKey = attachUUID(nodeItem.key);
return { return {
...nodeItem,
key: uniqueKey, key: uniqueKey,
type: nodeItem.type, inputs: getEndPointUuids(uniqueKey, "ip", library.noInputs),
position: nodeItem.position, outputs: getEndPointUuids(uniqueKey, "op", library.noOutputs)
inputs: getEndPointUuids(uniqueKey, "ip", library.NoInputs),
configuration: {
...nodeItem.configuration,
name: formatName(nodeItem.configuration.prettyName)
},
outputs: getEndPointUuids(uniqueKey, "op", library.NoOutputs)
}; };
}; };
@ -169,7 +149,7 @@ export const getConnections = (
}; };
export const getClientNodesAndConnections = ( export const getClientNodesAndConnections = (
nodeItems: IServiceNodeItem[], nodeItems: IClientNodeItem[],
sections: INodeGroup[] sections: INodeGroup[]
): Dictionary<IClientNodeItem> => { ): Dictionary<IClientNodeItem> => {
if (!Array.isArray(nodeItems) || !Array.isArray(sections)) { if (!Array.isArray(nodeItems) || !Array.isArray(sections)) {
@ -180,7 +160,7 @@ export const getClientNodesAndConnections = (
const clientItems = nodeItems.map((x) => { const clientItems = nodeItems.map((x) => {
return getClientNodeItem( return getClientNodeItem(
x, x,
ensure(libraries.find((l) => l.Type === x.type)) ensure(libraries.find((l) => l.type === x.type))
); );
}); });
@ -192,25 +172,9 @@ export const getNodeKeyFromConnectionId = (uuid: string) => {
return key; return key;
}; };
export const initialValues = (): IConf => { export const canvasConfigInitialValues = (): ICanvasConfig => {
return {
prettyName: "Unnamed",
name: "unnamed",
description: "",
type: "",
container: {
name: "",
image: "",
imagePullPolicy: ""
}
};
};
export const serviceInitialValues = (): IServiceConf => {
return { return {
prettyName: "Unnamed", service_name: "unnamed"
name: "unnamed",
template: ""
}; };
}; };

@ -1,8 +1,8 @@
import { IServiceNodeItem } from "../types";
import * as d3 from "d3"; import * as d3 from "d3";
import { IClientNodeItem } from "../types";
import { getNodeKeyFromConnectionId } from "./index"; import { getNodeKeyFromConnectionId } from "./index";
interface INodeItemWithParent extends IServiceNodeItem { interface INodeItemWithParent extends IClientNodeItem {
parent: string; parent: string;
} }
@ -10,7 +10,7 @@ const nodeWidth = 150;
const nodeHeight = 60; const nodeHeight = 60;
export const getHierarchyTree = ( export const getHierarchyTree = (
nodes: IServiceNodeItem[] nodes: IClientNodeItem[]
): d3.HierarchyPointNode<INodeItemWithParent> => { ): d3.HierarchyPointNode<INodeItemWithParent> => {
const data = nodes.map((node): INodeItemWithParent => { const data = nodes.map((node): INodeItemWithParent => {
return { return {
@ -48,9 +48,9 @@ export const getHierarchyTree = (
}; };
export const getNodesPositions = ( export const getNodesPositions = (
nodes: IServiceNodeItem[] nodes: IClientNodeItem[]
): [IServiceNodeItem[], number, number] => { ): [IClientNodeItem[], number, number] => {
const nodeWithPosition: IServiceNodeItem[] = []; const nodeWithPosition: IClientNodeItem[] = [];
const tree = getHierarchyTree(nodes); const tree = getHierarchyTree(nodes);
let x0 = Infinity; let x0 = Infinity;
let x1 = -x0; let x1 = -x0;

Loading…
Cancel
Save