diff --git a/services/frontend/src/components/Modal/Service/Edit.tsx b/services/frontend/src/components/Modal/Service/Edit.tsx index 4d0768b..06c765a 100644 --- a/services/frontend/src/components/Modal/Service/Edit.tsx +++ b/services/frontend/src/components/Modal/Service/Edit.tsx @@ -80,7 +80,7 @@ const ModalServiceEdit = (props: IModalServiceProps) => {
-

Update service

+

Edit service

diff --git a/services/frontend/src/components/Modal/Service/Labels.tsx b/services/frontend/src/components/Modal/Service/Labels.tsx index c7e8550..d3ac439 100644 --- a/services/frontend/src/components/Modal/Service/Labels.tsx +++ b/services/frontend/src/components/Modal/Service/Labels.tsx @@ -3,7 +3,7 @@ import { PlusIcon } from "@heroicons/react/outline"; import { Button, styled } from "@mui/joy"; import { useFormikContext } from "formik"; import Record from "../../Record"; -import { IEditServiceForm, IService } from "../../../types"; +import { IEditServiceForm } from "../../../types"; const Root = styled("div")` display: flex; diff --git a/services/frontend/src/components/Modal/Service/form-utils.ts b/services/frontend/src/components/Modal/Service/form-utils.ts index 51f4be0..f469f1e 100644 --- a/services/frontend/src/components/Modal/Service/form-utils.ts +++ b/services/frontend/src/components/Modal/Service/form-utils.ts @@ -1,6 +1,7 @@ import type { IEditServiceForm, IServiceNodeItem } from "../../../types"; import * as yup from "yup"; import lodash from "lodash"; +import { checkArray } from "../../../utils/forms"; const initialValues: IEditServiceForm = { imageName: "", @@ -96,15 +97,6 @@ export const getInitialValues = (node?: IServiceNodeItem): IEditServiceForm => { labels } = serviceConfig; - const checkArray = (array: any, name: string): T => { - if (!Array.isArray(array)) { - throw new Error( - `Looks like we encountered a bug. The current implementation expects "${name}" to be an array.` - ); - } - return array as unknown as T; - }; - const environment0: string[] = checkArray(environment, "environment"); const volumes0: string[] = checkArray(volumes, "volumes"); const ports0: string[] = checkArray(ports, "ports"); diff --git a/services/frontend/src/components/Modal/Volume/General.tsx b/services/frontend/src/components/Modal/Volume/General.tsx deleted file mode 100644 index 88b8f14..0000000 --- a/services/frontend/src/components/Modal/Volume/General.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import TextField from "../../global/FormElements/TextField"; - -const General = () => { - return ( - <> - - - - ); -}; - -export default General; diff --git a/services/frontend/src/components/Modal/Volume/Labels.tsx b/services/frontend/src/components/Modal/Volume/Labels.tsx deleted file mode 100644 index 710b9e5..0000000 --- a/services/frontend/src/components/Modal/Volume/Labels.tsx +++ /dev/null @@ -1,4 +0,0 @@ -const Labels = () => { - return <>; -}; -export default Labels; diff --git a/services/frontend/src/components/Modal/Volume/Create.tsx b/services/frontend/src/components/Modal/volume/CreateVolumeModal.tsx similarity index 66% rename from services/frontend/src/components/Modal/Volume/Create.tsx rename to services/frontend/src/components/Modal/volume/CreateVolumeModal.tsx index fd75afe..6d017ed 100644 --- a/services/frontend/src/components/Modal/Volume/Create.tsx +++ b/services/frontend/src/components/Modal/volume/CreateVolumeModal.tsx @@ -1,58 +1,34 @@ -import { useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { Formik } from "formik"; -import * as yup from "yup"; import { XIcon } from "@heroicons/react/outline"; + +import { + getFinalValues, + getInitialValues, + tabs, + validationSchema +} from "./form-utils"; import General from "./General"; import Labels from "./Labels"; -import { - topLevelVolumeConfigInitialValues, - volumeConfigCanvasInitialValues -} from "../../../utils"; import { CallbackFunction } from "../../../types"; -interface IModalVolumeCreate { +interface ICreateVolumeModalProps { onHide: CallbackFunction; onAddEndpoint: CallbackFunction; } -const ModalVolumeCreate = (props: IModalVolumeCreate) => { +const classNames = (...classes: string[]) => classes.filter(Boolean).join(" "); + +const CreateVolumeModal = (props: ICreateVolumeModalProps) => { const { onHide, onAddEndpoint } = props; const [openTab, setOpenTab] = useState("General"); - const handleCreate = (values: any, formik: any) => { - onAddEndpoint(values); + + const handleCreate = useCallback((values: any, formik: any) => { + onAddEndpoint(getFinalValues(values)); formik.resetForm(); - }; - const validationSchema = yup.object({ - canvasConfig: yup.object({ - node_name: yup - .string() - .max(256, "volume name should be 256 characters or less") - .required("volume name is required") - }), - volumeConfig: yup.object({ - name: yup - .string() - .max(256, "name should be 256 characters or less") - .required("name is required") - }) - }); - const tabs = [ - { - name: "General", - href: "#", - current: true, - hidden: false - }, - { - name: "Labels", - href: "#", - current: false, - hidden: false - } - ]; - const classNames = (...classes: string[]) => { - return classes.filter(Boolean).join(" "); - }; + }, []); + + const initialValues = useMemo(() => getInitialValues(), []); return (
@@ -64,7 +40,7 @@ const ModalVolumeCreate = (props: IModalVolumeCreate) => {
-

Create top level volume

+

Add top level volume

{ - handleCreate(values, formik); - }} + onSubmit={handleCreate} validationSchema={validationSchema} > {(formik) => ( @@ -133,9 +95,7 @@ const ModalVolumeCreate = (props: IModalVolumeCreate) => { @@ -150,4 +110,4 @@ const ModalVolumeCreate = (props: IModalVolumeCreate) => { ); }; -export default ModalVolumeCreate; +export default CreateVolumeModal; diff --git a/services/frontend/src/components/Modal/Volume/Edit.tsx b/services/frontend/src/components/Modal/volume/EditVolumeModal.tsx similarity index 68% rename from services/frontend/src/components/Modal/Volume/Edit.tsx rename to services/frontend/src/components/Modal/volume/EditVolumeModal.tsx index 9eaf6e6..6c1aba4 100644 --- a/services/frontend/src/components/Modal/Volume/Edit.tsx +++ b/services/frontend/src/components/Modal/volume/EditVolumeModal.tsx @@ -1,63 +1,30 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Formik } from "formik"; -import * as yup from "yup"; import { XIcon } from "@heroicons/react/outline"; import General from "./General"; import Labels from "./Labels"; +import type { CallbackFunction, IVolumeNodeItem } from "../../../types"; import { - CallbackFunction, - ICanvasConfig, - IVolumeNodeItem, - IVolumeTopLevel -} from "../../../types"; + getFinalValues, + getInitialValues, + tabs, + validationSchema +} from "./form-utils"; -interface IModalVolumeEdit { +interface IEditVolumeModal { node: IVolumeNodeItem; onHide: CallbackFunction; onUpdateEndpoint: CallbackFunction; } -const ModalVolumeEdit = (props: IModalVolumeEdit) => { +const classNames = (...classes: string[]) => { + return classes.filter(Boolean).join(" "); +}; + +const EditVolumeModal = (props: IEditVolumeModal) => { const { node, onHide, onUpdateEndpoint } = props; const [openTab, setOpenTab] = useState("General"); const [selectedNode, setSelectedNode] = useState(); - const handleUpdate = (values: any) => { - const updated = { ...selectedNode }; - updated.canvasConfig = values.canvasConfig; - updated.volumeConfig = values.volumeConfig; - onUpdateEndpoint(updated); - }; - const validationSchema = yup.object({ - canvasConfig: yup.object({ - node_name: yup - .string() - .max(256, "volume name should be 256 characters or less") - .required("volume name is required") - }), - volumeConfig: yup.object({ - name: yup - .string() - .max(256, "name should be 256 characters or less") - .required("name is required") - }) - }); - const tabs = [ - { - name: "General", - href: "#", - current: true, - hidden: false - }, - { - name: "Labels", - href: "#", - current: false, - hidden: false - } - ]; - const classNames = (...classes: string[]) => { - return classes.filter(Boolean).join(" "); - }; useEffect(() => { if (node) { @@ -65,6 +32,15 @@ const ModalVolumeEdit = (props: IModalVolumeEdit) => { } }, [node]); + const handleUpdate = (values: any) => { + onUpdateEndpoint(getFinalValues(values, selectedNode)); + }; + + const initialValues = useMemo( + () => getInitialValues(selectedNode), + [selectedNode] + ); + return (
@@ -75,9 +51,7 @@ const ModalVolumeEdit = (props: IModalVolumeEdit) => {
-

- Update top level volumes -

+

Edit top level volumes

@@ -163,4 +126,4 @@ const ModalVolumeEdit = (props: IModalVolumeEdit) => { ); }; -export default ModalVolumeEdit; +export default EditVolumeModal; diff --git a/services/frontend/src/components/Modal/volume/General.tsx b/services/frontend/src/components/Modal/volume/General.tsx new file mode 100644 index 0000000..d2985ae --- /dev/null +++ b/services/frontend/src/components/Modal/volume/General.tsx @@ -0,0 +1,20 @@ +import { styled } from "@mui/joy"; + +import TextField from "../../global/FormElements/TextField"; + +const Root = styled("div")` + display: flex; + flex-direction: column; + row-gap: ${({ theme }) => theme.spacing(1)}; +`; + +const General = () => { + return ( + + + + + ); +}; + +export default General; diff --git a/services/frontend/src/components/Modal/volume/Labels.tsx b/services/frontend/src/components/Modal/volume/Labels.tsx new file mode 100644 index 0000000..b3fbab7 --- /dev/null +++ b/services/frontend/src/components/Modal/volume/Labels.tsx @@ -0,0 +1,98 @@ +import { useCallback } from "react"; +import { PlusIcon } from "@heroicons/react/outline"; +import { Button, styled } from "@mui/joy"; +import { useFormikContext } from "formik"; +import Record from "../../Record"; +import { IEditServiceForm } from "../../../types"; + +const Root = styled("div")` + display: flex; + flex-direction: column; + align-items: center; +`; + +const Records = styled("div")` + display: flex; + flex-direction: column; + row-gap: ${({ theme }) => theme.spacing(1)}; +`; + +const AddButton = styled(Button)` + width: 140px; + margin-top: ${({ theme }) => theme.spacing(2)}; +`; + +const Description = styled("p")` + margin-top: ${({ theme }) => theme.spacing(2)}; + text-align: center; + color: #7a7a7a; + font-size: 14px; +`; + +const Labels = () => { + const formik = useFormikContext(); + const { labels } = formik.values; + + const handleNewLabel = useCallback(() => { + formik.setFieldValue(`labels[${labels.length}]`, { + key: "", + value: "" + }); + }, [formik]); + + const handleRemoveLabel = useCallback( + (index: number) => { + const newLabels = labels.filter( + (_: unknown, currentIndex: number) => currentIndex != index + ); + formik.setFieldValue(`labels`, newLabels); + }, + [formik] + ); + + const emptyLabels = labels && labels.length === 0 ? true : false; + + return ( + + {!emptyLabels && ( + + {labels.map((_: unknown, index: number) => ( + + ))} + + )} + {emptyLabels && ( + + This volume does not have any labels. +
+ Click "+ New label" to add a new label. +
+ )} + + + + New label + +
+ ); +}; + +export default Labels; diff --git a/services/frontend/src/components/Modal/volume/form-utils.ts b/services/frontend/src/components/Modal/volume/form-utils.ts new file mode 100644 index 0000000..da2aaf3 --- /dev/null +++ b/services/frontend/src/components/Modal/volume/form-utils.ts @@ -0,0 +1,95 @@ +import lodash from "lodash"; +import * as yup from "yup"; +import { IEditVolumeForm, IVolumeNodeItem } from "../../../types"; +import { checkArray } from "../../../utils/forms"; + +export const validationSchema = yup.object({ + entryName: yup + .string() + .max(256, "Entry name should be 256 characters or less") + .required("Entry name is required"), + volumeName: yup + .string() + .max(256, "Volume name should be 256 characters or less") + .required("Volume name is required"), + labels: yup.array( + yup.object({ + key: yup.string().required("Key is required"), + value: yup.string().required("Value is required") + }) + ) +}); + +const initialValues: IEditVolumeForm = { + entryName: "unknown", + volumeName: "unknown", + labels: [] +}; + +export const getInitialValues = (node?: IVolumeNodeItem): IEditVolumeForm => { + if (!node) { + return { + ...initialValues + }; + } + + const { canvasConfig, volumeConfig } = node; + const { node_name = "" } = canvasConfig; + const { name = "", labels } = volumeConfig; + + const labels0: string[] = checkArray(labels, "labels"); + + return { + ...initialValues, + entryName: node_name, + volumeName: name, + labels: labels0.map((label) => { + const [key, value] = label.split(":"); + return { + key, + value + }; + }) + }; +}; + +export const getFinalValues = ( + values: IEditVolumeForm, + previous?: IVolumeNodeItem +): IVolumeNodeItem => { + const { labels } = values; + + return lodash.merge( + lodash.cloneDeep(previous) || { + key: "volume", + type: "VOLUME", + inputs: [], + outputs: [], + config: {} + }, + { + canvasConfig: { + node_name: values.entryName + }, + volumeConfig: { + name: values.volumeName, + labels: labels.map((label) => `${label.key}:${label.value}`) + } + } + ) as any; +}; + +export const tabs = [ + { + name: "General", + href: "#", + current: true, + hidden: false + }, + { + name: "Labels", + href: "#", + current: false, + hidden: false + } +]; diff --git a/services/frontend/src/components/Project/index.tsx b/services/frontend/src/components/Project/index.tsx index 497a1df..f29ea34 100644 --- a/services/frontend/src/components/Project/index.tsx +++ b/services/frontend/src/components/Project/index.tsx @@ -9,8 +9,7 @@ import { IServiceNodeItem, IVolumeNodeItem, IServiceNodePosition, - IProject, - INetworkTopLevel + IProject } from "../../types"; import eventBus from "../../events/eventBus"; import { useMutation } from "react-query"; @@ -37,8 +36,8 @@ import ModalConfirmDelete from "../Modal/ConfirmDelete"; import ModalServiceCreate from "../Modal/Service/Create"; import ModalServiceEdit from "../Modal/Service/Edit"; import ModalNetwork from "../Modal/Network"; -import ModalVolumeCreate from "../Modal/Volume/Create"; -import ModalVolumeEdit from "../Modal/Volume/Edit"; +import CreateVolumeModal from "../Modal/volume/CreateVolumeModal"; +import EditVolumeModal from "../Modal/volume/EditVolumeModal"; import CodeEditor from "../CodeEditor"; export default function Project() { @@ -329,7 +328,7 @@ export default function Project() { ) : null} {showVolumesModal ? ( - setShowVolumesModal(false)} onAddEndpoint={(values: any) => onAddEndpoint(values)} /> @@ -361,7 +360,7 @@ export default function Project() { ) : null} {volumeToEdit ? ( - setVolumeToEdit(null)} onUpdateEndpoint={(values: any) => onUpdateEndpoint(values)} diff --git a/services/frontend/src/types/index.ts b/services/frontend/src/types/index.ts index 2f3f875..059af69 100644 --- a/services/frontend/src/types/index.ts +++ b/services/frontend/src/types/index.ts @@ -377,3 +377,12 @@ export interface IEditServiceForm { value: string; }[]; } + +export interface IEditVolumeForm { + entryName: string; + volumeName: string; + labels: { + key: string; + value: string; + }[]; +} diff --git a/services/frontend/src/utils/forms.ts b/services/frontend/src/utils/forms.ts new file mode 100644 index 0000000..931a359 --- /dev/null +++ b/services/frontend/src/utils/forms.ts @@ -0,0 +1,8 @@ +export const checkArray = (array: any, name: string): T => { + if (!Array.isArray(array)) { + throw new Error( + `Looks like we encountered a bug. The current implementation expects "${name}" to be an array.` + ); + } + return array as unknown as T; +};