From aaf64ca6085b13b8d70c15114128455277b98e31 Mon Sep 17 00:00:00 2001 From: Ben Gazzard Date: Fri, 9 Feb 2024 17:24:40 +0000 Subject: [PATCH] feat: enclave builder tweaks (#2142) ## Description: This PR implements a small amount of immediate feedback received for the experimental enclave builder ui. Specifically: * Change text on 'add artifact' button * Adjust the layout of service nodes to save some space, using tabs instead * Support for previewing starlark. This change also adds a small check in the `eclaveList.cy.ts` ete test, to try to debug flakey ci runs that @h4ck3rk3y has been impacted by. ### Screenshots ![image](https://github.com/kurtosis-tech/kurtosis/assets/4419574/12b4efa3-f21d-4489-bd39-ad674df6035d) ![image](https://github.com/kurtosis-tech/kurtosis/assets/4419574/947cd8ad-db0a-4ff0-a47b-a0a466ae5ca9) ## Is this change user facing? YES (experimental flag) ## References (if applicable): * direct request on slack --- .../web/cypress/e2e/enclaveList.cy.ts | 2 + .../components/form/DictArgumentInput.tsx | 2 +- .../components/modals/EnclaveBuilderModal.tsx | 41 ++- .../enclaveBuilder/KurtosisArtifactNode.tsx | 6 +- .../enclaveBuilder/KurtosisServiceNode.tsx | 274 ++++++++++-------- .../input/FileTreeArgumentInput.tsx | 4 +- .../{input => }/modals/EditFileModal.tsx | 0 .../{input => }/modals/NewFileModal.tsx | 4 +- .../modals/ViewStarlarkModal.tsx | 39 +++ 9 files changed, 234 insertions(+), 138 deletions(-) rename enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/{input => }/modals/EditFileModal.tsx (100%) rename enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/{input => }/modals/NewFileModal.tsx (92%) create mode 100644 enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/modals/ViewStarlarkModal.tsx diff --git a/enclave-manager/web/cypress/e2e/enclaveList.cy.ts b/enclave-manager/web/cypress/e2e/enclaveList.cy.ts index a8530221f9..73bc64ac5a 100644 --- a/enclave-manager/web/cypress/e2e/enclaveList.cy.ts +++ b/enclave-manager/web/cypress/e2e/enclaveList.cy.ts @@ -21,6 +21,8 @@ describe("Enclave List", () => { // Update the postgres instance cy.contains("button", "Edit").click(); + cy.contains("Loading").parents(".chakra-modal__content").contains("Error").should("not.exist"); + cy.contains("Loading").should("not.exist"); cy.focusInputWithLabel("Max CPU").type("1024"); cy.contains("button", "Update").click(); cy.contains("Script completed", { timeout: 30 * 1000 }); diff --git a/enclave-manager/web/packages/app/src/emui/enclaves/components/form/DictArgumentInput.tsx b/enclave-manager/web/packages/app/src/emui/enclaves/components/form/DictArgumentInput.tsx index 18deb429ce..4a8f53e793 100644 --- a/enclave-manager/web/packages/app/src/emui/enclaves/components/form/DictArgumentInput.tsx +++ b/enclave-manager/web/packages/app/src/emui/enclaves/components/form/DictArgumentInput.tsx @@ -50,7 +50,7 @@ export const DictArgumentInput = ({ {fields.map((field, i) => ( - + (); + const [currentStarlarkPreview, setCurrentStarlarkPreview] = useState(); const { nodes: initialNodes, @@ -121,10 +123,14 @@ export const EnclaveBuilderModal = ({ isOpen, onClose, existingEnclave }: Enclav } }; + const handlePreview = () => { + setCurrentStarlarkPreview(visualiserRef.current?.getStarlark() || "Unable to render"); + }; + return ( null} closeOnEsc={false}> - + {isDefined(existingEnclave) ? `Editing ${existingEnclave.name}` : "Build a new Enclave"} @@ -147,12 +153,18 @@ export const EnclaveBuilderModal = ({ isOpen, onClose, existingEnclave }: Enclav + + setCurrentStarlarkPreview(undefined)} + starlark={currentStarlarkPreview} + /> ); }; @@ -225,7 +237,8 @@ const Visualiser = forwardRef( addNodes({ id, position: getNewNodePosition(), - width: 600, + width: 650, + style: { width: "650px" }, type: "serviceNode", data: {}, }); @@ -238,6 +251,7 @@ const Visualiser = forwardRef( id, position: getNewNodePosition(), width: 600, + style: { width: "400px" }, type: "artifactNode", data: {}, }); @@ -257,6 +271,27 @@ const Visualiser = forwardRef( }); }, [setEdges, data]); + // Remove the resizeObserver error + useEffect(() => { + const errorHandler = (e: any) => { + if ( + e.message.includes( + "ResizeObserver loop completed with undelivered notifications" || "ResizeObserver loop limit exceeded", + ) + ) { + const resizeObserverErr = document.getElementById("webpack-dev-server-client-overlay"); + if (resizeObserverErr) { + resizeObserverErr.style.display = "none"; + } + } + }; + window.addEventListener("error", errorHandler); + + return () => { + window.removeEventListener("error", errorHandler); + }; + }, []); + useImperativeHandle( ref, () => ({ @@ -275,7 +310,7 @@ const Visualiser = forwardRef( Add Service Node diff --git a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisArtifactNode.tsx b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisArtifactNode.tsx index 58c62bdab6..ea3722fd38 100644 --- a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisArtifactNode.tsx +++ b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisArtifactNode.tsx @@ -39,7 +39,7 @@ export const KurtosisArtifactNode = memo( flexDirection={"column"} height={"100%"} borderRadius={"8px"} - p={selected ? "8px" : "10px"} + p={"10px"} bg={"gray.600"} borderWidth={"3px"} outline={"3px solid transparent"} @@ -92,8 +92,8 @@ export const KurtosisArtifactNode = memo( className={"nodrag nowheel"} gap={"8px"} > - name={"artifactName"} label={"Artifact Name"}> - + name={"artifactName"} label={"Artifact Name"} isRequired> + diff --git a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisServiceNode.tsx b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisServiceNode.tsx index 8f164c2325..6c16611d97 100644 --- a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisServiceNode.tsx +++ b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/KurtosisServiceNode.tsx @@ -1,4 +1,4 @@ -import { Flex, Grid, GridItem, IconButton, Text } from "@chakra-ui/react"; +import { Flex, Grid, GridItem, IconButton, Tab, TabList, TabPanel, TabPanels, Tabs, Text } from "@chakra-ui/react"; import { memo, useMemo } from "react"; import { FormProvider, useForm } from "react-hook-form"; import { FiTrash } from "react-icons/fi"; @@ -54,7 +54,7 @@ export const KurtosisServiceNode = memo( flexDirection={"column"} height={"100%"} borderRadius={"8px"} - p={selected ? "8px" : "10px"} + p={"10px"} bg={"gray.600"} borderWidth={"3px"} outline={"3px solid transparent"} @@ -101,135 +101,155 @@ export const KurtosisServiceNode = memo( - name={"serviceName"} label={"Service Name"} isRequired> - { - if (typeof val !== "string") { - return "Value should be a string"; - } - if (!val.match(/^[a-z]([-a-z0-9]{0,61}[a-z0-9])?$/)) { - return ( - "Service names must adhere to the RFC 1035 standard, specifically implementing this regex and" + - " be 1-63 characters long: ^[a-z]([-a-z0-9]{0,61}[a-z0-9])?$. This means the service name must " + - "only contain lowercase alphanumeric characters or '-', and must start with a lowercase alphabet " + - "and end with a lowercase alphanumeric" - ); - } - }} - /> - - name={"image"} label={"Container Image"} isRequired> - { - if (typeof val !== "string") { - return "Value should be a string"; - } - if ( - !val.match( - /^(?[\w.\-_]+((?::\d+|)(?=\/[a-z0-9._-]+\/[a-z0-9._-]+))|)(?:\/|)(?[a-z0-9.\-_]+(?:\/[a-z0-9.\-_]+|))(:(?[\w.\-_]{1,127})|)$/gim, - ) - ) { - return "Value does not look like a docker image"; - } - }} - /> - - name={"env"} label={"Environment Variables"}> - - name={"env"} - KeyFieldComponent={StringArgumentInput} - ValueFieldComponent={MentionStringArgumentInput} - /> - - name={"ports"} label={"Ports"}> - ( - - - - {...props} - size={"sm"} - placeholder={"Port Name (eg postgres)"} - name={`${props.name as `ports.${number}`}.portName`} - /> - - - - {...props} - size={"sm"} - placeholder={"Application Protocol (eg postgresql)"} - name={`${props.name as `ports.${number}`}.applicationProtocol`} - /> - - - - {...props} - options={["TCP", "UDP"]} - name={`${props.name as `ports.${number}`}.transportProtocol`} - /> - - - - {...props} - name={`${props.name as `ports.${number}`}.port`} - size={"sm"} - /> - - - )} - createNewValue={(): KurtosisPort => ({ - portName: "", - applicationProtocol: "", - transportProtocol: "TCP", - port: 0, - })} - /> - - - name={"files"} - label={"Files"} - helperText={"Choose where to mount artifacts on this services filesystem"} - > - ( - - - - {...props} - size={"sm"} - placeholder={"/some/path"} - name={`${props.name as `files.${number}`}.mountPoint`} - /> - - - - options={artifactVariableOptions} - {...props} - size={"sm"} - placeholder={"Select an Artifact"} - name={`${props.name as `files.${number}`}.artifactName`} - /> - - - )} - createNewValue={(): KurtosisFileMount => ({ - mountPoint: "", - artifactName: "", - })} - /> - + + name={"serviceName"} label={"Service Name"} isRequired> + { + if (typeof val !== "string") { + return "Value should be a string"; + } + if (!val.match(/^[a-z]([-a-z0-9]{0,61}[a-z0-9])?$/)) { + return ( + "Service names must adhere to the RFC 1035 standard, specifically implementing this regex and" + + " be 1-63 characters long: ^[a-z]([-a-z0-9]{0,61}[a-z0-9])?$. This means the service name must " + + "only contain lowercase alphanumeric characters or '-', and must start with a lowercase alphabet " + + "and end with a lowercase alphanumeric" + ); + } + }} + /> + + name={"image"} label={"Container Image"} isRequired> + { + if (typeof val !== "string") { + return "Value should be a string"; + } + if ( + !val.match( + /^(?[\w.\-_]+((?::\d+|)(?=\/[a-z0-9._-]+\/[a-z0-9._-]+))|)(?:\/|)(?[a-z0-9.\-_]+(?:\/[a-z0-9.\-_]+|))(:(?[\w.\-_]{1,127})|)$/gim, + ) + ) { + return "Value does not look like a docker image"; + } + }} + /> + + + + + Environment Variables + Ports + Files + + + + + name={"env"} label={"Environment Variables"}> + + name={"env"} + KeyFieldComponent={StringArgumentInput} + ValueFieldComponent={MentionStringArgumentInput} + /> + + + + name={"ports"} label={"Ports"}> + ( + + + + {...props} + size={"sm"} + placeholder={"Port Name (eg postgres)"} + name={`${props.name as `ports.${number}`}.portName`} + /> + + + + {...props} + size={"sm"} + placeholder={"Application Protocol (eg postgresql)"} + name={`${props.name as `ports.${number}`}.applicationProtocol`} + /> + + + + {...props} + options={["TCP", "UDP"]} + name={`${props.name as `ports.${number}`}.transportProtocol`} + /> + + + + {...props} + name={`${props.name as `ports.${number}`}.port`} + size={"sm"} + /> + + + )} + createNewValue={(): KurtosisPort => ({ + portName: "", + applicationProtocol: "", + transportProtocol: "TCP", + port: 0, + })} + /> + + + + + name={"files"} + label={"Files"} + helperText={"Choose where to mount artifacts on this services filesystem"} + > + ( + + + + {...props} + size={"sm"} + placeholder={"/some/path"} + name={`${props.name as `files.${number}`}.mountPoint`} + /> + + + + options={artifactVariableOptions} + {...props} + size={"sm"} + placeholder={"Select an Artifact"} + name={`${props.name as `files.${number}`}.artifactName`} + /> + + + )} + createNewValue={(): KurtosisFileMount => ({ + mountPoint: "", + artifactName: "", + })} + /> + + + + diff --git a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/input/FileTreeArgumentInput.tsx b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/input/FileTreeArgumentInput.tsx index dd9c68a5c4..dc9cb78434 100644 --- a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/input/FileTreeArgumentInput.tsx +++ b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/input/FileTreeArgumentInput.tsx @@ -4,8 +4,8 @@ import { useMemo, useState } from "react"; import { Controller } from "react-hook-form"; import { FiPlus } from "react-icons/fi"; import { KurtosisFormInputProps } from "../../../form/types"; -import { EditFileModal } from "./modals/EditFileModal"; -import { NewFileModal } from "./modals/NewFileModal"; +import { EditFileModal } from "../modals/EditFileModal"; +import { NewFileModal } from "../modals/NewFileModal"; type FileTreeArgumentInputProps = KurtosisFormInputProps; diff --git a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/input/modals/EditFileModal.tsx b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/modals/EditFileModal.tsx similarity index 100% rename from enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/input/modals/EditFileModal.tsx rename to enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/modals/EditFileModal.tsx diff --git a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/input/modals/NewFileModal.tsx b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/modals/NewFileModal.tsx similarity index 92% rename from enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/input/modals/NewFileModal.tsx rename to enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/modals/NewFileModal.tsx index 36f71b1c3b..db206cc665 100644 --- a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/input/modals/NewFileModal.tsx +++ b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/modals/NewFileModal.tsx @@ -10,8 +10,8 @@ import { ModalOverlay, } from "@chakra-ui/react"; import { FormProvider, useForm } from "react-hook-form"; -import { KurtosisFormControl } from "../../../../form/KurtosisFormControl"; -import { StringArgumentInput } from "../../../../form/StringArgumentInput"; +import { KurtosisFormControl } from "../../../form/KurtosisFormControl"; +import { StringArgumentInput } from "../../../form/StringArgumentInput"; type NewFileModalProps = { isOpen: boolean; diff --git a/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/modals/ViewStarlarkModal.tsx b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/modals/ViewStarlarkModal.tsx new file mode 100644 index 0000000000..3b408ce5ac --- /dev/null +++ b/enclave-manager/web/packages/app/src/emui/enclaves/components/modals/enclaveBuilder/modals/ViewStarlarkModal.tsx @@ -0,0 +1,39 @@ +import { + Button, + Flex, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, +} from "@chakra-ui/react"; +import { FileDisplay, isDefined } from "kurtosis-ui-components"; + +type ViewStarlarkModalProps = { + isOpen: boolean; + onClose: () => void; + starlark?: string; +}; +export const ViewStarlarkModal = ({ isOpen, onClose, starlark }: ViewStarlarkModalProps) => { + return ( + + + + Previewing Starlark + + + {isDefined(starlark) && } + + + + + + + + + ); +};