Merge pull request #77 from nuxxapp/fix/random-fixes

Fix random issues
pull/79/head
Samuel Rowe 3 years ago committed by GitHub
commit 39533a64c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -2,11 +2,6 @@
:heart: Thanks for taking the time and for your help improving this project! :heart: Thanks for taking the time and for your help improving this project!
## Getting Help ##
If you have a question about nuxx or have encountered problems using it,
start by asking a question on [slack][slack]
## Submitting a Pull Request ## ## Submitting a Pull Request ##
Do you have an improvement? Do you have an improvement?
@ -24,8 +19,3 @@ Squash or rebase commits so that all changes from a branch are
committed to master as a single commit. All pull requests are squashed when committed to master as a single commit. All pull requests are squashed when
merged, but rebasing prior to merge gives you better control over the commit merged, but rebasing prior to merge gives you better control over the commit
message. message.
<!----variables---->
[slack]: https://join.slack.com/t/nuxxapp/shared_invite/zt-fkgoyz5h-CYo5tqAT0CwRZMpuOJYAJA

@ -1,3 +1,4 @@
import json
from rest_framework import generics, status from rest_framework import generics, status
from rest_framework.response import Response from rest_framework.response import Response
@ -11,7 +12,7 @@ class GenerateGenericAPIView(generics.GenericAPIView):
return Response({}, status=status.HTTP_404_NOT_FOUND) return Response({}, status=status.HTTP_404_NOT_FOUND)
def post(self, request, format=None): def post(self, request, format=None):
request_data = request.data request_data = json.loads(request.data)
version = request_data['data'].get('version', '3') version = request_data['data'].get('version', '3')
services = request_data['data'].get('services', None) services = request_data['data'].get('services', None)
volumes = request_data['data'].get('volumes', None) volumes = request_data['data'].get('volumes', None)

@ -71,6 +71,7 @@ def generate(services, volumes, networks, version="3", return_format='yaml'):
s = io.StringIO() s = io.StringIO()
ret_yaml = YAML() ret_yaml = YAML()
ret_yaml.indent(mapping=2, sequence=4, offset=2) ret_yaml.indent(mapping=2, sequence=4, offset=2)
ret_yaml.preserve_quotes = True
ret_yaml.explicit_start = True ret_yaml.explicit_start = True
specified_version = get_version(version) specified_version = get_version(version)
base_version = int(specified_version) base_version = int(specified_version)

@ -78,38 +78,43 @@ export const Canvas: FC<ICanvasProps> = (props) => {
} }
}; };
const onCanvasMouseUpLeave = (e: any) => {
if (dragging) {
const left = _left + e.pageX - _initX;
const top = _top + e.pageY - _initY;
_setLeft(left);
_setTop(top);
setDragging(false);
onCanvasUpdate({
left: left,
top: top
});
}
};
const onCanvasMouseMove = (e: any) => { const onCanvasMouseMove = (e: any) => {
if (!dragging) { if (!dragging) {
return; return;
} }
const styles = { if (e.pageX && e.pageY) {
left: _left + e.pageX - _initX + "px", const styles = {
top: _top + e.pageY - _initY + "px" left: _left + e.pageX - _initX + "px",
}; top: _top + e.pageY - _initY + "px"
};
setStyle(styles);
}
};
setStyle(styles); const onCanvasMouseUpLeave = (e: any) => {
if (dragging) {
if (e.pageX && e.pageY) {
const left = _left + e.pageX - _initX;
const top = _top + e.pageY - _initY;
_setLeft(left);
_setTop(top);
setDragging(false);
onCanvasUpdate({
left: left,
top: top
});
}
}
}; };
const onCanvasMouseDown = (e: any) => { const onCanvasMouseDown = (e: any) => {
_setInitX(e.pageX); if (e.pageX && e.pageY) {
_setInitY(e.pageY); _setInitX(e.pageX);
setDragging(true); _setInitY(e.pageY);
setDragging(true);
}
}; };
useEffect(() => { useEffect(() => {
@ -166,7 +171,7 @@ export const Canvas: FC<ICanvasProps> = (props) => {
<div <div
id={CANVAS_ID} id={CANVAS_ID}
ref={containerCallbackRef} ref={containerCallbackRef}
className="canvas h-full w-full" className="canvas"
style={{ style={{
transformOrigin: "0px 0px 0px", transformOrigin: "0px 0px 0px",
transform: `translate(${translateWidth}px, ${translateHeight}px) scale(${_scale})` transform: `translate(${translateWidth}px, ${translateHeight}px) scale(${_scale})`

@ -79,31 +79,32 @@ const NetworkCreate = (props: INetworkCreateProps) => {
> >
{(formik) => ( {(formik) => (
<> <>
<div className="hidden sm:block"> <div className="border-b border-gray-200 px-4 md:px-8">
<div className="border-b border-gray-200 px-8"> <nav
<nav className="-mb-px flex space-x-8" aria-label="Tabs"> className="-mb-px flex space-x-4 md:space-x-8"
{tabs.map((tab) => ( aria-label="Tabs"
<a >
key={tab.name} {tabs.map((tab) => (
href={tab.href} <a
className={classNames( key={tab.name}
tab.name === openTab href={tab.href}
? "border-indigo-500 text-indigo-600" className={classNames(
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300", tab.name === openTab
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm", ? "border-indigo-500 text-indigo-600"
tab.hidden ? "hidden" : "" : "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",
aria-current={tab.current ? "page" : undefined} tab.hidden ? "hidden" : ""
onClick={(e) => { )}
e.preventDefault(); aria-current={tab.current ? "page" : undefined}
setOpenTab(tab.name); onClick={(e) => {
}} e.preventDefault();
> setOpenTab(tab.name);
{tab.name} }}
</a> >
))} {tab.name}
</nav> </a>
</div> ))}
</nav>
</div> </div>
<div className="relative px-4 py-3 flex-auto"> <div className="relative px-4 py-3 flex-auto">

@ -82,31 +82,32 @@ const NetworkEdit = (props: INetworkEditProps) => {
> >
{(formik) => ( {(formik) => (
<> <>
<div className="hidden sm:block"> <div className="border-b border-gray-200 px-4 md:px-8">
<div className="border-b border-gray-200 px-8"> <nav
<nav className="-mb-px flex space-x-8" aria-label="Tabs"> className="-mb-px flex space-x-4 md:space-x-8"
{tabs.map((tab) => ( aria-label="Tabs"
<a >
key={tab.name} {tabs.map((tab) => (
href={tab.href} <a
className={classNames( key={tab.name}
tab.name === openTab href={tab.href}
? "border-indigo-500 text-indigo-600" className={classNames(
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300", tab.name === openTab
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm", ? "border-indigo-500 text-indigo-600"
tab.hidden ? "hidden" : "" : "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",
aria-current={tab.current ? "page" : undefined} tab.hidden ? "hidden" : ""
onClick={(e) => { )}
e.preventDefault(); aria-current={tab.current ? "page" : undefined}
setOpenTab(tab.name); onClick={(e) => {
}} e.preventDefault();
> setOpenTab(tab.name);
{tab.name} }}
</a> >
))} {tab.name}
</nav> </a>
</div> ))}
</nav>
</div> </div>
<div className="relative px-4 py-3 flex-auto"> <div className="relative px-4 py-3 flex-auto">

@ -7,8 +7,8 @@ import Volumes from "./Volumes";
import Labels from "./Labels"; import Labels from "./Labels";
import { CallbackFunction } from "../../../types"; import { CallbackFunction } from "../../../types";
import { import {
getInitialValues,
getFinalValues, getFinalValues,
getInitialValues,
validationSchema validationSchema
} from "./form-utils"; } from "./form-utils";
@ -48,9 +48,9 @@ const ModalServiceCreate = (props: IModalServiceProps) => {
const { onHide, onAddEndpoint } = props; const { onHide, onAddEndpoint } = props;
const [openTab, setOpenTab] = useState("General"); const [openTab, setOpenTab] = useState("General");
const handleCreate = (values: any, formik: any) => { const handleCreate = (values: any, formik: any) => {
// TODO: This modal should not be aware of endpoints. Seperation of concerns.
onAddEndpoint(getFinalValues(values)); onAddEndpoint(getFinalValues(values));
formik.resetForm(); formik.resetForm();
onHide();
}; };
const initialValues = useMemo(() => getInitialValues(), []); const initialValues = useMemo(() => getInitialValues(), []);
@ -83,38 +83,37 @@ const ModalServiceCreate = (props: IModalServiceProps) => {
<Formik <Formik
initialValues={initialValues} initialValues={initialValues}
enableReinitialize={true} enableReinitialize={true}
onSubmit={(values, formik) => { onSubmit={handleCreate}
handleCreate(values, formik);
}}
validationSchema={validationSchema} validationSchema={validationSchema}
> >
{(formik) => ( {(formik) => (
<> <>
<div className="hidden sm:block"> <div className="border-b border-gray-200 px-4 md:px-8">
<div className="border-b border-gray-200 px-8"> <nav
<nav className="-mb-px flex space-x-8" aria-label="Tabs"> className="-mb-px flex space-x-4 md:space-x-8"
{tabs.map((tab) => ( aria-label="Tabs"
<a >
key={tab.name} {tabs.map((tab) => (
href={tab.href} <a
className={classNames( key={tab.name}
tab.name === openTab href={tab.href}
? "border-indigo-500 text-indigo-600" className={classNames(
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300", tab.name === openTab
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm", ? "border-indigo-500 text-indigo-600"
tab.hidden ? "hidden" : "" : "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",
aria-current={tab.current ? "page" : undefined} tab.hidden ? "hidden" : ""
onClick={(e) => { )}
e.preventDefault(); aria-current={tab.current ? "page" : undefined}
setOpenTab(tab.name); onClick={(e) => {
}} e.preventDefault();
> setOpenTab(tab.name);
{tab.name} }}
</a> >
))} {tab.name}
</nav> </a>
</div> ))}
</nav>
</div> </div>
<div className="relative px-4 py-3 flex-auto"> <div className="relative px-4 py-3 flex-auto">

@ -100,34 +100,32 @@ const ModalServiceEdit = (props: IModalServiceProps) => {
> >
{(formik) => ( {(formik) => (
<> <>
<div className="hidden sm:block"> <div className="border-b border-gray-200 px-4 md:px-8">
<div className="border-b border-gray-200 px-8"> <nav
<nav className="-mb-px flex space-x-4 md:space-x-8"
className="-mb-px flex space-x-8" aria-label="Tabs"
aria-label="Tabs" >
> {tabs.map((tab) => (
{tabs.map((tab) => ( <a
<a key={tab.name}
key={tab.name} href={tab.href}
href={tab.href} className={classNames(
className={classNames( tab.name === openTab
tab.name === openTab ? "border-indigo-500 text-indigo-600"
? "border-indigo-500 text-indigo-600" : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300",
: "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",
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm", tab.hidden ? "hidden" : ""
tab.hidden ? "hidden" : "" )}
)} aria-current={tab.current ? "page" : undefined}
aria-current={tab.current ? "page" : undefined} onClick={(e) => {
onClick={(e) => { e.preventDefault();
e.preventDefault(); setOpenTab(tab.name);
setOpenTab(tab.name); }}
}} >
> {tab.name}
{tab.name} </a>
</a> ))}
))} </nav>
</nav>
</div>
</div> </div>
<div className="relative px-4 py-3 flex-auto"> <div className="relative px-4 py-3 flex-auto">

@ -74,7 +74,7 @@ const Environment = () => {
{ {
name: `environmentVariables[${index}].value`, name: `environmentVariables[${index}].value`,
placeholder: "Value", placeholder: "Value",
required: true, required: false,
type: "text" type: "text"
} }
]} ]}

@ -15,6 +15,9 @@ const Fields = styled("div")`
const ImageNameGroup = styled("div")` const ImageNameGroup = styled("div")`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@media (max-width: 640px) {
flex-direction: column;
}
column-gap: ${({ theme }) => theme.spacing(1)}; column-gap: ${({ theme }) => theme.spacing(1)};
width: 100%; width: 100%;
`; `;
@ -31,6 +34,7 @@ const GroupTitle = styled("h5")`
font-weight: 500; font-weight: 500;
width: 100%; width: 100%;
text-align: left; text-align: left;
margin-bottom: 0.25em;
`; `;
const Records = styled("div")` const Records = styled("div")`
@ -80,12 +84,7 @@ const General = () => {
<Fields> <Fields>
<TextField label="Service name" name="serviceName" required={true} /> <TextField label="Service name" name="serviceName" required={true} />
<ImageNameGroup> <ImageNameGroup>
<TextField <TextField label="Image name" name="imageName" required={true} />
label="Image name"
name="imageName"
required={true}
style={{ minWidth: 400 }}
/>
<TextField label="Image tag" name="imageTag" /> <TextField label="Image tag" name="imageTag" />
</ImageNameGroup> </ImageNameGroup>
<TextField <TextField

@ -70,7 +70,7 @@ const Labels = () => {
{ {
name: `labels[${index}].value`, name: `labels[${index}].value`,
placeholder: "Value", placeholder: "Value",
required: true, required: false,
type: "text" type: "text"
} }
]} ]}

@ -60,8 +60,7 @@ export const validationSchema = yup.object({
), ),
environmentVariables: yup.array( environmentVariables: yup.array(
yup.object({ yup.object({
key: yup.string().required("Key is required"), key: yup.string().required("Key is required")
value: yup.string().required("Value is required")
}) })
), ),
volumes: yup.array( volumes: yup.array(
@ -73,8 +72,7 @@ export const validationSchema = yup.object({
), ),
labels: yup.array( labels: yup.array(
yup.object({ yup.object({
key: yup.string().required("Key is required"), key: yup.string().required("Key is required")
value: yup.string().required("Value is required")
}) })
) )
}); });
@ -110,10 +108,10 @@ export const getInitialValues = (node?: IServiceNodeItem): IEditServiceForm => {
serviceName: node_name, serviceName: node_name,
containerName: container_name, containerName: container_name,
environmentVariables: environment0.map((variable) => { environmentVariables: environment0.map((variable) => {
const [key, value] = variable.split(":"); const [key, value] = variable.split("=");
return { return {
key, key,
value value: value ? value : ""
}; };
}), }),
volumes: volumes0.map((volume) => { volumes: volumes0.map((volume) => {
@ -140,7 +138,7 @@ export const getInitialValues = (node?: IServiceNodeItem): IEditServiceForm => {
return { hostPort, containerPort, protocol } as any; return { hostPort, containerPort, protocol } as any;
}), }),
labels: labels0.map((label) => { labels: labels0.map((label) => {
const [key, value] = label.split(":"); const [key, value] = label.split("=");
return { return {
key, key,
value value
@ -155,7 +153,7 @@ export const getFinalValues = (
): IServiceNodeItem => { ): IServiceNodeItem => {
const { environmentVariables, ports, volumes, labels } = values; const { environmentVariables, ports, volumes, labels } = values;
return lodash.merge( return lodash.mergeWith(
lodash.cloneDeep(previous) || { lodash.cloneDeep(previous) || {
key: "service", key: "service",
type: "SERVICE", type: "SERVICE",
@ -168,25 +166,38 @@ export const getFinalValues = (
node_name: values.serviceName node_name: values.serviceName
}, },
serviceConfig: { serviceConfig: {
image: `${values.imageName}:${values.imageTag}`, image: `${values.imageName}${
values.imageTag ? `:${values.imageTag}` : ""
}`,
container_name: values.containerName, container_name: values.containerName,
environment: environmentVariables.map( environment: environmentVariables.map(
(variable) => `${variable.key}:${variable.value}` (variable) =>
), `${variable.key}${variable.value ? `=${variable.value}` : ""}`
volumes: volumes.map(
(volume) =>
volume.name +
(volume.containerPath ? `:${volume.containerPath}` : "") +
(volume.accessMode ? `:${volume.accessMode}` : "")
), ),
volumes: volumes.length
? volumes.map(
(volume) =>
volume.name +
(volume.containerPath ? `:${volume.containerPath}` : "") +
(volume.accessMode ? `:${volume.accessMode}` : "")
)
: [],
ports: ports.map( ports: ports.map(
(port) => (port) =>
port.hostPort + port.hostPort +
(port.containerPort ? `:${port.containerPort}` : "") + (port.containerPort ? `:${port.containerPort}` : "") +
(port.protocol ? `/${port.protocol}` : "") (port.protocol ? `/${port.protocol}` : "")
), ),
labels: labels.map((label) => `${label.key}:${label.value}`) labels: labels.map(
(label) => `${label.key}${label.value ? `=${label.value}` : ""}`
)
}
},
(obj, src) => {
if (!lodash.isNil(src)) {
return src;
} }
return obj;
} }
) as any; ) as any;
}; };

@ -26,6 +26,7 @@ const CreateVolumeModal = (props: ICreateVolumeModalProps) => {
const handleCreate = useCallback((values: any, formik: any) => { const handleCreate = useCallback((values: any, formik: any) => {
onAddEndpoint(getFinalValues(values)); onAddEndpoint(getFinalValues(values));
formik.resetForm(); formik.resetForm();
onHide();
}, []); }, []);
const initialValues = useMemo(() => getInitialValues(), []); const initialValues = useMemo(() => getInitialValues(), []);
@ -59,31 +60,32 @@ const CreateVolumeModal = (props: ICreateVolumeModalProps) => {
> >
{(formik) => ( {(formik) => (
<> <>
<div className="hidden sm:block"> <div className="border-b border-gray-200 px-4 md:px-8">
<div className="border-b border-gray-200 px-8"> <nav
<nav className="-mb-px flex space-x-8" aria-label="Tabs"> className="-mb-px flex space-x-4 md:space-x-8"
{tabs.map((tab) => ( aria-label="Tabs"
<a >
key={tab.name} {tabs.map((tab) => (
href={tab.href} <a
className={classNames( key={tab.name}
tab.name === openTab href={tab.href}
? "border-indigo-500 text-indigo-600" className={classNames(
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300", tab.name === openTab
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm", ? "border-indigo-500 text-indigo-600"
tab.hidden ? "hidden" : "" : "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",
aria-current={tab.current ? "page" : undefined} tab.hidden ? "hidden" : ""
onClick={(e) => { )}
e.preventDefault(); aria-current={tab.current ? "page" : undefined}
setOpenTab(tab.name); onClick={(e) => {
}} e.preventDefault();
> setOpenTab(tab.name);
{tab.name} }}
</a> >
))} {tab.name}
</nav> </a>
</div> ))}
</nav>
</div> </div>
<div className="relative px-4 py-3 flex-auto"> <div className="relative px-4 py-3 flex-auto">

@ -71,34 +71,32 @@ const EditVolumeModal = (props: IEditVolumeModal) => {
> >
{(formik) => ( {(formik) => (
<> <>
<div className="hidden sm:block"> <div className="border-b border-gray-200 px-4 md:px-8">
<div className="border-b border-gray-200 px-8"> <nav
<nav className="-mb-px flex space-x-4 md:space-x-8"
className="-mb-px flex space-x-8" aria-label="Tabs"
aria-label="Tabs" >
> {tabs.map((tab) => (
{tabs.map((tab) => ( <a
<a key={tab.name}
key={tab.name} href={tab.href}
href={tab.href} className={classNames(
className={classNames( tab.name === openTab
tab.name === openTab ? "border-indigo-500 text-indigo-600"
? "border-indigo-500 text-indigo-600" : "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300",
: "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",
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm", tab.hidden ? "hidden" : ""
tab.hidden ? "hidden" : "" )}
)} aria-current={tab.current ? "page" : undefined}
aria-current={tab.current ? "page" : undefined} onClick={(e) => {
onClick={(e) => { e.preventDefault();
e.preventDefault(); setOpenTab(tab.name);
setOpenTab(tab.name); }}
}} >
> {tab.name}
{tab.name} </a>
</a> ))}
))} </nav>
</nav>
</div>
</div> </div>
<div className="relative px-4 py-3 flex-auto"> <div className="relative px-4 py-3 flex-auto">

@ -70,7 +70,7 @@ const Labels = () => {
{ {
name: `labels[${index}].value`, name: `labels[${index}].value`,
placeholder: "Value", placeholder: "Value",
required: true, required: false,
type: "text" type: "text"
} }
]} ]}

@ -14,8 +14,7 @@ export const validationSchema = yup.object({
.required("Volume name is required"), .required("Volume name is required"),
labels: yup.array( labels: yup.array(
yup.object({ yup.object({
key: yup.string().required("Key is required"), key: yup.string().required("Key is required")
value: yup.string().required("Value is required")
}) })
) )
}); });
@ -44,7 +43,7 @@ export const getInitialValues = (node?: IVolumeNodeItem): IEditVolumeForm => {
entryName: node_name, entryName: node_name,
volumeName: name, volumeName: name,
labels: labels0.map((label) => { labels: labels0.map((label) => {
const [key, value] = label.split(":"); const [key, value] = label.split("=");
return { return {
key, key,
value value
@ -59,7 +58,7 @@ export const getFinalValues = (
): IVolumeNodeItem => { ): IVolumeNodeItem => {
const { labels } = values; const { labels } = values;
return lodash.merge( return lodash.mergeWith(
lodash.cloneDeep(previous) || { lodash.cloneDeep(previous) || {
key: "volume", key: "volume",
type: "VOLUME", type: "VOLUME",
@ -73,8 +72,16 @@ export const getFinalValues = (
}, },
volumeConfig: { volumeConfig: {
name: values.volumeName, name: values.volumeName,
labels: labels.map((label) => `${label.key}:${label.value}`) labels: labels.map(
(label) => `${label.key}${label.value ? `=${label.value}` : ""}`
)
} }
},
(obj, src) => {
if (!lodash.isNil(src)) {
return src;
}
return obj;
} }
) as any; ) as any;
}; };

@ -182,7 +182,7 @@ export default function Project() {
debounce((graphData) => { debounce((graphData) => {
graphData.networks = stateNetworksRef.current; graphData.networks = stateNetworksRef.current;
const flatData = generatePayload(graphData); const flatData = generatePayload(graphData);
generateHttp(flatData) generateHttp(JSON.stringify(flatData))
.then(checkHttpStatus) .then(checkHttpStatus)
.then((data) => { .then((data) => {
if (data["code"].length) { if (data["code"].length) {
@ -231,8 +231,19 @@ export default function Project() {
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 - canvasPosition.left,
top: 30 - canvasPosition.top
};
setNodes({ ...nodes, [clientNodeItem.key]: clientNodeItem }); setNodes({ ...nodes, [clientNodeItem.key]: clientNodeItem });
if (clientNodeItem.type === "VOLUME") {
setVolumeToEdit(clientNodeItem as unknown as IVolumeNodeItem);
}
if (clientNodeItem.type === "SERVICE") {
setServiceToEdit(clientNodeItem as unknown as IServiceNodeItem);
}
}; };
const onCreateNetwork = (values: any) => { const onCreateNetwork = (values: any) => {
@ -446,7 +457,7 @@ export default function Project() {
</form> </form>
</div> </div>
<div className="flex flex-grow relative flex-col md:flex-row"> <div className="flex flex-grow relative">
<div <div
className="w-full overflow-hidden md:w-2/3 z-40" className="w-full overflow-hidden md:w-2/3 z-40"
style={{ height: height - 64 }} style={{ height: height - 64 }}
@ -515,7 +526,8 @@ export default function Project() {
/> />
</div> </div>
</div> </div>
<div className="relative group code-column w-full md:w-1/3">
<div className="group code-column w-1/2 md:w-1/3 absolute top-0 right-0 sm:relative z-40 md:z-30">
<div <div
className={`absolute top-0 left-0 right-0 z-10 flex justify-end p-1 space-x-2 group-hover:visible invisible`} className={`absolute top-0 left-0 right-0 z-10 flex justify-end p-1 space-x-2 group-hover:visible invisible`}
> >
@ -555,8 +567,28 @@ export default function Project() {
</div> </div>
</> </>
); );
} else { }
return <>Something went wrong</>;
if (error) {
return (
<div
className="text-center"
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
minHeight: "calc(100vh - 120px)"
}}
>
<h3 className="mt-2 text-sm font-medium text-gray-900">
Something went wrong...
</h3>
<p className="mt-1 text-sm text-gray-500">
Either this project does not exist, or the link is wrong.
</p>
</div>
);
} }
} }

@ -1,5 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { Link } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { PencilIcon, TrashIcon } from "@heroicons/react/outline"; import { PencilIcon, TrashIcon } from "@heroicons/react/outline";
import { truncateStr } from "../../utils"; import { truncateStr } from "../../utils";
import { IProject } from "../../types"; import { IProject } from "../../types";
@ -15,6 +15,7 @@ const PreviewBlock = (props: IPreviewBlockProps) => {
const [isHovering, setIsHovering] = useState(false); const [isHovering, setIsHovering] = useState(false);
const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false); const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false);
const mutation = useDeleteProject(project.uuid); const mutation = useDeleteProject(project.uuid);
const navigate = useNavigate();
const handleMouseOver = () => { const handleMouseOver = () => {
setIsHovering(true); setIsHovering(true);
@ -24,7 +25,12 @@ const PreviewBlock = (props: IPreviewBlockProps) => {
setIsHovering(false); setIsHovering(false);
}; };
const onDelete = () => { const handleClick = (e: any) => {
navigate(`/projects/${project.uuid}`);
};
const onDelete = (e: any) => {
e.stopPropagation();
setShowDeleteConfirmModal(true); setShowDeleteConfirmModal(true);
}; };
@ -37,8 +43,10 @@ const PreviewBlock = (props: IPreviewBlockProps) => {
<div <div
onMouseOver={handleMouseOver} onMouseOver={handleMouseOver}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
onClick={handleClick}
key={project.id} key={project.id}
className={` className={`
cursor-pointer
relative relative
rounded-lg rounded-lg
dark:bg-gray-900 dark:bg-gray-900
@ -49,7 +57,7 @@ const PreviewBlock = (props: IPreviewBlockProps) => {
flex flex
items-center items-center
space-x-3 space-x-3
hover:border-gray-400 hover:bg-gray-200
`} `}
> >
<div className="flex-1 min-w-0">{truncateStr(project.name, 25)}</div> <div className="flex-1 min-w-0">{truncateStr(project.name, 25)}</div>
@ -57,18 +65,11 @@ const PreviewBlock = (props: IPreviewBlockProps) => {
{isHovering && ( {isHovering && (
<div className="flex space-x-1 absolute top-2 right-2"> <div className="flex space-x-1 absolute top-2 right-2">
<button <button
onClick={() => onDelete()} onClick={onDelete}
className="flex justify-center items-center p-2 hover:bg-gray-100 shadow bg-white rounded-md" className="flex justify-center items-center p-2 hover:bg-gray-100 shadow bg-white rounded-md"
> >
<TrashIcon className="w-3 h-3 text-red-500 hover:text-red-600" /> <TrashIcon className="w-3 h-3 text-red-500 hover:text-red-600" />
</button> </button>
<Link
to={`/projects/${project.uuid}`}
className="flex justify-center items-center p-2 hover:bg-gray-100 shadow bg-white rounded-md"
>
<PencilIcon className="w-3 h-3 text-gray-500 hover:text-gray-600" />
</Link>
</div> </div>
)} )}
</div> </div>

@ -5,6 +5,7 @@ import { IProject } from "../../types";
import Spinner from "../../components/global/Spinner"; import Spinner from "../../components/global/Spinner";
import PreviewBlock from "./PreviewBlock"; import PreviewBlock from "./PreviewBlock";
import { useProjects } from "../../hooks/useProjects"; import { useProjects } from "../../hooks/useProjects";
import { PlusIcon } from "@heroicons/react/outline";
const Projects = () => { const Projects = () => {
const [limit] = useState(PROJECTS_FETCH_LIMIT); const [limit] = useState(PROJECTS_FETCH_LIMIT);
@ -22,22 +23,24 @@ const Projects = () => {
Projects Projects
</h1> </h1>
<Link {data && data.results.length > 0 && (
className="btn-util text-white bg-blue-600 hover:bg-blue-700 sm:w-auto" <Link
to="/projects/new" className="btn-util text-white bg-blue-600 hover:bg-blue-700 sm:w-auto"
> to="/projects/new"
<span>Create new project</span> >
</Link> <span>Create new project</span>
</Link>
)}
</div> </div>
<div className="px-4 sm:px-6 md:px-8"> <div className="px-4 sm:px-6 md:px-8">
{isFetching && ( {(isFetching || isLoading) && (
<div className="flex justify-center items-center mx-auto mt-10"> <div className="flex justify-center items-center mx-auto mt-10">
<Spinner className="w-6 h-6 text-blue-600" /> <Spinner className="w-6 h-6 text-blue-600" />
</div> </div>
)} )}
{!isFetching && ( {!isFetching && !isLoading && (
<> <>
<div className="py-4"> <div className="py-4">
{error && ( {error && (
@ -70,22 +73,23 @@ const Projects = () => {
minHeight: "calc(100vh - 120px)" minHeight: "calc(100vh - 120px)"
}} }}
> >
<img <h3 className="mt-2 text-sm font-medium text-gray-900">
style={{ No projects
width: 400, </h3>
height: "auto" <p className="mt-1 text-sm text-gray-500">
}} Get started by creating a new project.
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 projects.
</p> </p>
<Link <div className="mt-6">
className="btn-util text-white bg-blue-600 hover:bg-blue-700 sm:w-auto mt-3" <Link to="/projects/new" className="btn-util">
to="/projects/new" <span className="flex space-x-1 items-center">
> <PlusIcon
<span>Create new project</span> className="h-3 w-3"
</Link> aria-hidden="true"
/>
<span>New Project</span>
</span>
</Link>
</div>
</div> </div>
)} )}
</div> </div>

@ -28,6 +28,9 @@ const Root = styled("div")`
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-items: flex-start;
column-gap: ${({ theme }) => theme.spacing(2)}; column-gap: ${({ theme }) => theme.spacing(2)};
@media (max-width: 768px) {
column-gap: ${({ theme }) => theme.spacing(1)};
}
`; `;
const RemoveButton = styled(IconButton)``; const RemoveButton = styled(IconButton)``;

@ -12,6 +12,7 @@ export interface ITextFieldProps {
const Root = styled("div")` const Root = styled("div")`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%;
`; `;
const TextField: FunctionComponent<ITextFieldProps> = ( const TextField: FunctionComponent<ITextFieldProps> = (
@ -25,10 +26,7 @@ const TextField: FunctionComponent<ITextFieldProps> = (
return ( return (
<Root> <Root>
{label && ( {label && (
<label <label htmlFor={name} className="lbl-util">
htmlFor={name}
className="block text-xs font-medium text-gray-700"
>
{label + (required ? "*" : "")} {label + (required ? "*" : "")}
</label> </label>
)} )}
@ -37,8 +35,8 @@ const TextField: FunctionComponent<ITextFieldProps> = (
id={name} id={name}
name={name} name={name}
type="text" type="text"
autoComplete="none" autoComplete="off"
className="input-util mt-1" className="input-util"
required={required} required={required}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
onChange={formik.handleChange} onChange={formik.handleChange}

@ -16,7 +16,7 @@ const Search = (props: ISearchProps) => {
action="" action=""
> >
<input <input
autoComplete="false" autoComplete="off"
name="hidden" name="hidden"
type="text" type="text"
className="hidden" className="hidden"

@ -29,15 +29,15 @@ export default function SideBar(props: ISideBarProps) {
return ( return (
<> <>
<div className="md:flex md:w-16 md:flex-col md:fixed md:inset-y-0"> <div className="md:flex md:w-16 md:flex-col md:fixed md:inset-y-0">
<div className="flex flex-col flex-grow pt-5 bg-blue-700 overflow-y-auto"> <div className="flex justify-between flex-col sm:flex-row md:flex-col md:flex-grow md:pt-5 bg-blue-700 overflow-y-auto">
<div className="flex items-center flex-shrink-0 mx-auto"> <div className="flex items-center flex-shrink-0 mx-auto p-2 ">
<Link to={isAuthenticated ? "/" : "projects/new"}> <Link to={isAuthenticated ? "/" : "projects/new"}>
<Logo /> <Logo />
</Link> </Link>
</div> </div>
<div className="mt-5 flex-1 flex flex-col"> <div className="md:mt-5 flex-1 flex flex-col items-center sm:flex-row md:flex-col justify-end">
<nav className="flex-1 px-2 pb-4 space-y-1"> <nav className="md:flex-1 space-y-1">
{navigation.map((item) => ( {navigation.map((item) => (
<a <a
key={item.name} key={item.name}
@ -46,23 +46,23 @@ export default function SideBar(props: ISideBarProps) {
item.current item.current
? "bg-blue-800 text-white" ? "bg-blue-800 text-white"
: "text-blue-100 hover:bg-blue-600", : "text-blue-100 hover:bg-blue-600",
"group flex items-center justify-center px-2 py-2 text-sm font-medium rounded-md" "group flex items-center justify-center p-2 text-sm font-medium rounded-md"
)} )}
> >
<item.icon <item.icon
className="mr-3 md:mr-0 flex-shrink-0 h-5 w-5" className="mr-3 sm:mr-0 flex-shrink-0 h-5 w-5"
aria-hidden="true" aria-hidden="true"
/> />
<span className="md:hidden">{item.name}</span> <span className="sm:hidden">{item.name}</span>
</a> </a>
))} ))}
</nav> </nav>
</div>
<UserMenu <UserMenu
username={userName} username={userName}
current={pathname.includes("profile")} current={pathname.includes("profile")}
/> />
</div>
</div> </div>
</div> </div>
</> </>

@ -19,18 +19,15 @@ export default function UserMenu(props: IUserMenuProps) {
${ ${
current ? "bg-blue-800 text-white" : "text-blue-100 hover:bg-blue-600" current ? "bg-blue-800 text-white" : "text-blue-100 hover:bg-blue-600"
}, },
flex border-t border-blue-800 p-4 w-full hover:cursor-pointer hover:bg-blue-600 flex md:border-t md:border-blue-800 p-4 md:w-full hover:cursor-pointer hover:bg-blue-600
`} `}
> >
<div className="flex items-center mx-auto"> <div className="flex items-center mx-auto">
<UserCircleIcon className="inline-block h-8 w-8 rounded-full text-white" /> <UserCircleIcon className="inline-block h-8 w-8 rounded-full text-white" />
<div className="ml-3 md:ml-0"> <div className="ml-3 sm:ml-0">
<p className="text-base font-medium text-white md:hidden"> <p className="text-sm font-medium text-white sm:hidden">
{username ? <>{username}</> : <>Log in</>} {username ? <>{username}</> : <>Log in</>}
</p> </p>
<p className="text-sm font-medium text-indigo-200 group-hover:text-white md:hidden">
View profile
</p>
</div> </div>
</div> </div>
</div> </div>

@ -8,7 +8,7 @@ const Logo = () => {
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink" xmlnsXlink="http://www.w3.org/1999/xlink"
> >
<title>Nuxx</title> <title>CTK</title>
<g <g
id="Page-1" id="Page-1"
stroke="none" stroke="none"
@ -17,9 +17,9 @@ const Logo = () => {
fillRule="evenodd" fillRule="evenodd"
> >
<g id="Logo" transform="translate(-179.000000, -225.000000)"> <g id="Logo" transform="translate(-179.000000, -225.000000)">
<g id="nuxx-logo-copy" transform="translate(167.000000, 200.000000)"> <g id="ctk-logo-copy" transform="translate(167.000000, 200.000000)">
<g <g
id="nuxx-logo" id="ctk-logo"
transform="translate(98.500000, 95.000000) rotate(-60.000000) translate(-98.500000, -95.000000) translate(33.000000, 20.000000)" transform="translate(98.500000, 95.000000) rotate(-60.000000) translate(-98.500000, -95.000000) translate(33.000000, 20.000000)"
> >
<polygon <polygon

@ -1,3 +1,3 @@
export const API_SERVER_URL = process.env.REACT_APP_API_SERVER; export const API_SERVER_URL = process.env.REACT_APP_API_SERVER;
export const PROJECTS_FETCH_LIMIT = 300; export const PROJECTS_FETCH_LIMIT = 300;
export const LOCAL_STORAGE = "NuxxLocalStorage"; export const LOCAL_STORAGE = "CtkLocalStorage";

@ -16,6 +16,7 @@ body {
.canvas { .canvas {
position: relative; position: relative;
height: 100%; height: 100%;
width: 100%;
} }
.jsplumb-box { .jsplumb-box {
@ -112,6 +113,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;
} }
.lbl-util {
@apply block text-xs font-medium text-gray-700 mb-1
}
.btn-util-red { .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; @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;
} }

@ -1,7 +1,6 @@
import { IGeneratePayload } from "../types";
import { API_SERVER_URL } from "../constants"; import { API_SERVER_URL } from "../constants";
export const generateHttp = (data: IGeneratePayload) => { export const generateHttp = (data: string) => {
return fetch(`${API_SERVER_URL}/generate/`, { return fetch(`${API_SERVER_URL}/generate/`, {
method: "POST", method: "POST",
headers: { headers: {

@ -12,6 +12,11 @@ export const generatePayload = (data: any): IGeneratePayload => {
} }
}; };
Object.keys(networks).forEach((key) => {
base.data.networks[networks[key].canvasConfig.node_name] =
networks[key].networkConfig;
});
Object.keys(nodes).forEach((key) => { Object.keys(nodes).forEach((key) => {
if (nodes[key].type === "SERVICE") { if (nodes[key].type === "SERVICE") {
base.data.services[nodes[key].canvasConfig.node_name] = base.data.services[nodes[key].canvasConfig.node_name] =
@ -24,10 +29,5 @@ export const generatePayload = (data: any): IGeneratePayload => {
} }
}); });
Object.keys(networks).forEach((key) => {
base.data.networks[networks[key].canvasConfig.node_name] =
networks[key].networkConfig;
});
return base; return base;
}; };

Loading…
Cancel
Save