diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c946c60..cbc1320 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,11 +2,6 @@ :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 ## Do you have an improvement? @@ -23,9 +18,4 @@ Do you have an improvement? 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 merged, but rebasing prior to merge gives you better control over the commit -message. - - - - -[slack]: https://join.slack.com/t/nuxxapp/shared_invite/zt-fkgoyz5h-CYo5tqAT0CwRZMpuOJYAJA \ No newline at end of file +message. \ No newline at end of file diff --git a/services/backend/src/api/views/generate.py b/services/backend/src/api/views/generate.py index 9b7907a..9ffeae3 100644 --- a/services/backend/src/api/views/generate.py +++ b/services/backend/src/api/views/generate.py @@ -1,3 +1,4 @@ +import json from rest_framework import generics, status from rest_framework.response import Response @@ -11,7 +12,7 @@ class GenerateGenericAPIView(generics.GenericAPIView): return Response({}, status=status.HTTP_404_NOT_FOUND) def post(self, request, format=None): - request_data = request.data + request_data = json.loads(request.data) version = request_data['data'].get('version', '3') services = request_data['data'].get('services', None) volumes = request_data['data'].get('volumes', None) diff --git a/services/backend/src/api/views/utils.py b/services/backend/src/api/views/utils.py index 8f2322e..ae10d43 100644 --- a/services/backend/src/api/views/utils.py +++ b/services/backend/src/api/views/utils.py @@ -71,6 +71,7 @@ def generate(services, volumes, networks, version="3", return_format='yaml'): s = io.StringIO() ret_yaml = YAML() ret_yaml.indent(mapping=2, sequence=4, offset=2) + ret_yaml.preserve_quotes = True ret_yaml.explicit_start = True specified_version = get_version(version) base_version = int(specified_version) diff --git a/services/frontend/src/components/Canvas/index.tsx b/services/frontend/src/components/Canvas/index.tsx index 96743d6..a9fc101 100644 --- a/services/frontend/src/components/Canvas/index.tsx +++ b/services/frontend/src/components/Canvas/index.tsx @@ -78,38 +78,43 @@ export const Canvas: FC = (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) => { if (!dragging) { return; } - const styles = { - left: _left + e.pageX - _initX + "px", - top: _top + e.pageY - _initY + "px" - }; + if (e.pageX && e.pageY) { + const styles = { + 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) => { - _setInitX(e.pageX); - _setInitY(e.pageY); - setDragging(true); + if (e.pageX && e.pageY) { + _setInitX(e.pageX); + _setInitY(e.pageY); + setDragging(true); + } }; useEffect(() => { @@ -166,7 +171,7 @@ export const Canvas: FC = (props) => {
{ > {(formik) => ( <> -
-
- -
+
+
diff --git a/services/frontend/src/components/Modal/Network/Edit.tsx b/services/frontend/src/components/Modal/Network/Edit.tsx index ede89a5..40cafa8 100644 --- a/services/frontend/src/components/Modal/Network/Edit.tsx +++ b/services/frontend/src/components/Modal/Network/Edit.tsx @@ -82,31 +82,32 @@ const NetworkEdit = (props: INetworkEditProps) => { > {(formik) => ( <> -
- +
diff --git a/services/frontend/src/components/Modal/Service/Create.tsx b/services/frontend/src/components/Modal/Service/Create.tsx index 4b7d59b..07f55e5 100644 --- a/services/frontend/src/components/Modal/Service/Create.tsx +++ b/services/frontend/src/components/Modal/Service/Create.tsx @@ -7,8 +7,8 @@ import Volumes from "./Volumes"; import Labels from "./Labels"; import { CallbackFunction } from "../../../types"; import { - getInitialValues, getFinalValues, + getInitialValues, validationSchema } from "./form-utils"; @@ -48,9 +48,9 @@ const ModalServiceCreate = (props: IModalServiceProps) => { const { onHide, onAddEndpoint } = props; const [openTab, setOpenTab] = useState("General"); const handleCreate = (values: any, formik: any) => { - // TODO: This modal should not be aware of endpoints. Seperation of concerns. onAddEndpoint(getFinalValues(values)); formik.resetForm(); + onHide(); }; const initialValues = useMemo(() => getInitialValues(), []); @@ -83,38 +83,37 @@ const ModalServiceCreate = (props: IModalServiceProps) => { { - handleCreate(values, formik); - }} + onSubmit={handleCreate} validationSchema={validationSchema} > {(formik) => ( <> -
- +
diff --git a/services/frontend/src/components/Modal/Service/Edit.tsx b/services/frontend/src/components/Modal/Service/Edit.tsx index 06c765a..9869d61 100644 --- a/services/frontend/src/components/Modal/Service/Edit.tsx +++ b/services/frontend/src/components/Modal/Service/Edit.tsx @@ -100,34 +100,32 @@ const ModalServiceEdit = (props: IModalServiceProps) => { > {(formik) => ( <> -
- +
diff --git a/services/frontend/src/components/Modal/Service/Environment.tsx b/services/frontend/src/components/Modal/Service/Environment.tsx index 3a07808..d8714e0 100644 --- a/services/frontend/src/components/Modal/Service/Environment.tsx +++ b/services/frontend/src/components/Modal/Service/Environment.tsx @@ -74,7 +74,7 @@ const Environment = () => { { name: `environmentVariables[${index}].value`, placeholder: "Value", - required: true, + required: false, type: "text" } ]} diff --git a/services/frontend/src/components/Modal/Service/General.tsx b/services/frontend/src/components/Modal/Service/General.tsx index 5469120..60b1df8 100644 --- a/services/frontend/src/components/Modal/Service/General.tsx +++ b/services/frontend/src/components/Modal/Service/General.tsx @@ -15,6 +15,9 @@ const Fields = styled("div")` const ImageNameGroup = styled("div")` display: flex; flex-direction: row; + @media (max-width: 640px) { + flex-direction: column; + } column-gap: ${({ theme }) => theme.spacing(1)}; width: 100%; `; @@ -31,6 +34,7 @@ const GroupTitle = styled("h5")` font-weight: 500; width: 100%; text-align: left; + margin-bottom: 0.25em; `; const Records = styled("div")` @@ -80,12 +84,7 @@ const General = () => { - + { { name: `labels[${index}].value`, placeholder: "Value", - required: true, + required: false, type: "text" } ]} diff --git a/services/frontend/src/components/Modal/Service/form-utils.ts b/services/frontend/src/components/Modal/Service/form-utils.ts index f469f1e..e7aa43b 100644 --- a/services/frontend/src/components/Modal/Service/form-utils.ts +++ b/services/frontend/src/components/Modal/Service/form-utils.ts @@ -60,8 +60,7 @@ export const validationSchema = yup.object({ ), environmentVariables: yup.array( yup.object({ - key: yup.string().required("Key is required"), - value: yup.string().required("Value is required") + key: yup.string().required("Key is required") }) ), volumes: yup.array( @@ -73,8 +72,7 @@ export const validationSchema = yup.object({ ), labels: yup.array( yup.object({ - key: yup.string().required("Key is required"), - value: yup.string().required("Value is required") + key: yup.string().required("Key is required") }) ) }); @@ -110,10 +108,10 @@ export const getInitialValues = (node?: IServiceNodeItem): IEditServiceForm => { serviceName: node_name, containerName: container_name, environmentVariables: environment0.map((variable) => { - const [key, value] = variable.split(":"); + const [key, value] = variable.split("="); return { key, - value + value: value ? value : "" }; }), volumes: volumes0.map((volume) => { @@ -140,7 +138,7 @@ export const getInitialValues = (node?: IServiceNodeItem): IEditServiceForm => { return { hostPort, containerPort, protocol } as any; }), labels: labels0.map((label) => { - const [key, value] = label.split(":"); + const [key, value] = label.split("="); return { key, value @@ -155,7 +153,7 @@ export const getFinalValues = ( ): IServiceNodeItem => { const { environmentVariables, ports, volumes, labels } = values; - return lodash.merge( + return lodash.mergeWith( lodash.cloneDeep(previous) || { key: "service", type: "SERVICE", @@ -168,25 +166,38 @@ export const getFinalValues = ( node_name: values.serviceName }, serviceConfig: { - image: `${values.imageName}:${values.imageTag}`, + image: `${values.imageName}${ + values.imageTag ? `:${values.imageTag}` : "" + }`, container_name: values.containerName, environment: environmentVariables.map( - (variable) => `${variable.key}:${variable.value}` - ), - volumes: volumes.map( - (volume) => - volume.name + - (volume.containerPath ? `:${volume.containerPath}` : "") + - (volume.accessMode ? `:${volume.accessMode}` : "") + (variable) => + `${variable.key}${variable.value ? `=${variable.value}` : ""}` ), + volumes: volumes.length + ? volumes.map( + (volume) => + volume.name + + (volume.containerPath ? `:${volume.containerPath}` : "") + + (volume.accessMode ? `:${volume.accessMode}` : "") + ) + : [], ports: ports.map( (port) => port.hostPort + (port.containerPort ? `:${port.containerPort}` : "") + (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; }; diff --git a/services/frontend/src/components/Modal/volume/CreateVolumeModal.tsx b/services/frontend/src/components/Modal/volume/CreateVolumeModal.tsx index 6d017ed..f699905 100644 --- a/services/frontend/src/components/Modal/volume/CreateVolumeModal.tsx +++ b/services/frontend/src/components/Modal/volume/CreateVolumeModal.tsx @@ -26,6 +26,7 @@ const CreateVolumeModal = (props: ICreateVolumeModalProps) => { const handleCreate = useCallback((values: any, formik: any) => { onAddEndpoint(getFinalValues(values)); formik.resetForm(); + onHide(); }, []); const initialValues = useMemo(() => getInitialValues(), []); @@ -59,31 +60,32 @@ const CreateVolumeModal = (props: ICreateVolumeModalProps) => { > {(formik) => ( <> -
- +
diff --git a/services/frontend/src/components/Modal/volume/EditVolumeModal.tsx b/services/frontend/src/components/Modal/volume/EditVolumeModal.tsx index 6c1aba4..c690f24 100644 --- a/services/frontend/src/components/Modal/volume/EditVolumeModal.tsx +++ b/services/frontend/src/components/Modal/volume/EditVolumeModal.tsx @@ -71,34 +71,32 @@ const EditVolumeModal = (props: IEditVolumeModal) => { > {(formik) => ( <> -
- +
diff --git a/services/frontend/src/components/Modal/volume/Labels.tsx b/services/frontend/src/components/Modal/volume/Labels.tsx index b3fbab7..a581862 100644 --- a/services/frontend/src/components/Modal/volume/Labels.tsx +++ b/services/frontend/src/components/Modal/volume/Labels.tsx @@ -70,7 +70,7 @@ const Labels = () => { { name: `labels[${index}].value`, placeholder: "Value", - required: true, + required: false, type: "text" } ]} diff --git a/services/frontend/src/components/Modal/volume/form-utils.ts b/services/frontend/src/components/Modal/volume/form-utils.ts index da2aaf3..ba451f4 100644 --- a/services/frontend/src/components/Modal/volume/form-utils.ts +++ b/services/frontend/src/components/Modal/volume/form-utils.ts @@ -14,8 +14,7 @@ export const validationSchema = yup.object({ .required("Volume name is required"), labels: yup.array( yup.object({ - key: yup.string().required("Key is required"), - value: yup.string().required("Value is required") + key: yup.string().required("Key is required") }) ) }); @@ -44,7 +43,7 @@ export const getInitialValues = (node?: IVolumeNodeItem): IEditVolumeForm => { entryName: node_name, volumeName: name, labels: labels0.map((label) => { - const [key, value] = label.split(":"); + const [key, value] = label.split("="); return { key, value @@ -59,7 +58,7 @@ export const getFinalValues = ( ): IVolumeNodeItem => { const { labels } = values; - return lodash.merge( + return lodash.mergeWith( lodash.cloneDeep(previous) || { key: "volume", type: "VOLUME", @@ -73,8 +72,16 @@ export const getFinalValues = ( }, volumeConfig: { 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; }; diff --git a/services/frontend/src/components/Project/index.tsx b/services/frontend/src/components/Project/index.tsx index f29ea34..dabbb8a 100644 --- a/services/frontend/src/components/Project/index.tsx +++ b/services/frontend/src/components/Project/index.tsx @@ -182,7 +182,7 @@ export default function Project() { debounce((graphData) => { graphData.networks = stateNetworksRef.current; const flatData = generatePayload(graphData); - generateHttp(flatData) + generateHttp(JSON.stringify(flatData)) .then(checkHttpStatus) .then((data) => { if (data["code"].length) { @@ -231,8 +231,19 @@ export default function Project() { values, 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 }); + + if (clientNodeItem.type === "VOLUME") { + setVolumeToEdit(clientNodeItem as unknown as IVolumeNodeItem); + } + + if (clientNodeItem.type === "SERVICE") { + setServiceToEdit(clientNodeItem as unknown as IServiceNodeItem); + } }; const onCreateNetwork = (values: any) => { @@ -446,7 +457,7 @@ export default function Project() {
-
+
-
+ +
@@ -555,8 +567,28 @@ export default function Project() {
); - } else { - return <>Something went wrong; + } + + if (error) { + return ( +
+

+ Something went wrong... +

+

+ Either this project does not exist, or the link is wrong. +

+
+ ); } } diff --git a/services/frontend/src/components/Projects/PreviewBlock.tsx b/services/frontend/src/components/Projects/PreviewBlock.tsx index 8b81916..79f4130 100644 --- a/services/frontend/src/components/Projects/PreviewBlock.tsx +++ b/services/frontend/src/components/Projects/PreviewBlock.tsx @@ -1,5 +1,5 @@ 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 { truncateStr } from "../../utils"; import { IProject } from "../../types"; @@ -15,6 +15,7 @@ const PreviewBlock = (props: IPreviewBlockProps) => { const [isHovering, setIsHovering] = useState(false); const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false); const mutation = useDeleteProject(project.uuid); + const navigate = useNavigate(); const handleMouseOver = () => { setIsHovering(true); @@ -24,7 +25,12 @@ const PreviewBlock = (props: IPreviewBlockProps) => { setIsHovering(false); }; - const onDelete = () => { + const handleClick = (e: any) => { + navigate(`/projects/${project.uuid}`); + }; + + const onDelete = (e: any) => { + e.stopPropagation(); setShowDeleteConfirmModal(true); }; @@ -37,8 +43,10 @@ const PreviewBlock = (props: IPreviewBlockProps) => {
{ flex items-center space-x-3 - hover:border-gray-400 + hover:bg-gray-200 `} >
{truncateStr(project.name, 25)}
@@ -57,18 +65,11 @@ const PreviewBlock = (props: IPreviewBlockProps) => { {isHovering && (
- - - -
)}
diff --git a/services/frontend/src/components/Projects/index.tsx b/services/frontend/src/components/Projects/index.tsx index bdf8551..7ebe1e1 100644 --- a/services/frontend/src/components/Projects/index.tsx +++ b/services/frontend/src/components/Projects/index.tsx @@ -5,6 +5,7 @@ import { IProject } from "../../types"; import Spinner from "../../components/global/Spinner"; import PreviewBlock from "./PreviewBlock"; import { useProjects } from "../../hooks/useProjects"; +import { PlusIcon } from "@heroicons/react/outline"; const Projects = () => { const [limit] = useState(PROJECTS_FETCH_LIMIT); @@ -22,22 +23,24 @@ const Projects = () => { Projects - - Create new project - + {data && data.results.length > 0 && ( + + Create new project + + )}
- {isFetching && ( + {(isFetching || isLoading) && (
)} - {!isFetching && ( + {!isFetching && !isLoading && ( <>
{error && ( @@ -70,22 +73,23 @@ const Projects = () => { minHeight: "calc(100vh - 120px)" }} > - -

- We tried our best, but could not find any projects. +

+ No projects +

+

+ Get started by creating a new project.

- - Create new project - +
+ + + + +
)}
diff --git a/services/frontend/src/components/Record.tsx b/services/frontend/src/components/Record.tsx index 4444a0c..b273fe7 100644 --- a/services/frontend/src/components/Record.tsx +++ b/services/frontend/src/components/Record.tsx @@ -28,6 +28,9 @@ const Root = styled("div")` justify-content: flex-start; align-items: flex-start; column-gap: ${({ theme }) => theme.spacing(2)}; + @media (max-width: 768px) { + column-gap: ${({ theme }) => theme.spacing(1)}; + } `; const RemoveButton = styled(IconButton)``; diff --git a/services/frontend/src/components/global/FormElements/TextField.tsx b/services/frontend/src/components/global/FormElements/TextField.tsx index 89ba3bc..ec02f78 100644 --- a/services/frontend/src/components/global/FormElements/TextField.tsx +++ b/services/frontend/src/components/global/FormElements/TextField.tsx @@ -12,6 +12,7 @@ export interface ITextFieldProps { const Root = styled("div")` display: flex; flex-direction: column; + width: 100%; `; const TextField: FunctionComponent = ( @@ -25,10 +26,7 @@ const TextField: FunctionComponent = ( return ( {label && ( -