Skip to content

Commit

Permalink
feat: enforce enclave builder validation (#2144)
Browse files Browse the repository at this point in the history
## Description:
This pr is a minor improvement to the enclave builder - it now blocks
running until all nodes have valid data.

### Screenshot


![image](https://github.com/kurtosis-tech/kurtosis/assets/4419574/6bf11be2-e606-4ee4-a521-d69abb045797)


## Is this change user facing?
YES (experimental)
  • Loading branch information
Dartoxian committed Feb 9, 2024
1 parent 0eae9fc commit 5dcdd9e
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 30 deletions.
Expand Up @@ -3,16 +3,20 @@ import {
Button,
ButtonGroup,
Flex,
ListItem,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Text,
Tooltip,
UnorderedList,
} from "@chakra-ui/react";
import Dagre from "@dagrejs/dagre";
import { isDefined, KurtosisAlert, RemoveFunctions, stringifyError } from "kurtosis-ui-components";
import { isDefined, KurtosisAlert, KurtosisAlertModal, RemoveFunctions, stringifyError } from "kurtosis-ui-components";
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
import { FiPlusCircle } from "react-icons/fi";
import { useNavigate } from "react-router-dom";
Expand Down Expand Up @@ -53,13 +57,9 @@ type EnclaveBuilderModalProps = {
existingEnclave?: RemoveFunctions<EnclaveFullInfo>;
};

export const EnclaveBuilderModal = ({ isOpen, onClose, existingEnclave }: EnclaveBuilderModalProps) => {
const navigator = useNavigate();
const visualiserRef = useRef<VisualiserImperativeAttributes | null>(null);
const { createEnclave, runStarlarkScript } = useEnclavesContext();
const [isLoading, setIsLoading] = useState(false);
export const EnclaveBuilderModal = (props: EnclaveBuilderModalProps) => {
const variableContextKey = useRef(0);
const [error, setError] = useState<string>();
const [currentStarlarkPreview, setCurrentStarlarkPreview] = useState<string>();

const {
nodes: initialNodes,
Expand All @@ -70,7 +70,7 @@ export const EnclaveBuilderModal = ({ isOpen, onClose, existingEnclave }: Enclav
edges: Edge<any>[];
data: Record<string, KurtosisNodeData>;
} => {
const parseResult = getInitialGraphStateFromEnclave<KurtosisNodeData>(existingEnclave);
const parseResult = getInitialGraphStateFromEnclave<KurtosisNodeData>(props.existingEnclave);
if (parseResult.isErr) {
setError(parseResult.error);
return { nodes: [], edges: [], data: {} };
Expand All @@ -81,7 +81,65 @@ export const EnclaveBuilderModal = ({ isOpen, onClose, existingEnclave }: Enclav
.filter(([id, data]) => parseResult.value.nodes.some((node) => node.id === id))
.reduce((acc, [id, data]) => ({ ...acc, [id]: data }), {} as Record<string, KurtosisNodeData>),
};
}, [existingEnclave]);
}, [props.existingEnclave]);

useEffect(() => {
if (!props.isOpen) {
variableContextKey.current += 1;
}
}, [props.isOpen]);

if (isDefined(error)) {
return (
<KurtosisAlertModal
title={"Error"}
content={error}
isOpen={true}
onClose={() => {
setError(undefined);
props.onClose();
}}
/>
);
}

return (
<VariableContextProvider key={variableContextKey.current} initialData={initialData}>
<EnclaveBuilderModalImpl {...props} initialNodes={initialNodes} initialEdges={initialEdges} />
</VariableContextProvider>
);
};

type EnclaveBuilderModalImplProps = EnclaveBuilderModalProps & {
initialNodes: Node[];
initialEdges: Edge[];
};
const EnclaveBuilderModalImpl = ({
isOpen,
onClose,
existingEnclave,
initialNodes,
initialEdges,
}: EnclaveBuilderModalImplProps) => {
const navigator = useNavigate();
const visualiserRef = useRef<VisualiserImperativeAttributes | null>(null);
const { createEnclave, runStarlarkScript } = useEnclavesContext();
const { data } = useVariableContext();
const dataIssues = useMemo(
() =>
Object.values(data)
.filter((nodeData) => !nodeData.isValid)
.map(
(nodeData) =>
`${nodeData.type} ${
(nodeData.type === "artifact" ? nodeData.artifactName : nodeData.serviceName) || "with no name"
} has invalid data`,
),
[data],
);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string>();
const [currentStarlarkPreview, setCurrentStarlarkPreview] = useState<string>();

const handleRun = async () => {
if (!isDefined(visualiserRef.current)) {
Expand Down Expand Up @@ -137,26 +195,46 @@ export const EnclaveBuilderModal = ({ isOpen, onClose, existingEnclave }: Enclav
<ModalCloseButton />
<ModalBody paddingInline={"0"}>
{isDefined(error) && <KurtosisAlert message={error} />}
<VariableContextProvider initialData={initialData}>
<ReactFlowProvider>
<Visualiser
ref={visualiserRef}
initialNodes={initialNodes}
initialEdges={initialEdges}
existingEnclave={existingEnclave}
/>
</ReactFlowProvider>
</VariableContextProvider>
<ReactFlowProvider>
<Visualiser
ref={visualiserRef}
initialNodes={initialNodes}
initialEdges={initialEdges}
existingEnclave={existingEnclave}
/>
</ReactFlowProvider>
</ModalBody>
<ModalFooter>
<ButtonGroup>
<Button onClick={onClose} isDisabled={isLoading}>
Close
</Button>
<Button onClick={handlePreview}>Preview</Button>
<Button onClick={handleRun} colorScheme={"green"} isLoading={isLoading} loadingText={"Run"}>
Run
</Button>
<Button onClick={handlePreview}>Preview</Button>
<Tooltip
label={
dataIssues.length === 0 ? undefined : (
<Flex flexDirection={"column"}>
<Text>There are data issues that must be addressed before this enclave can run:</Text>
<UnorderedList>
{dataIssues.map((issue, i) => (
<ListItem key={i}>{issue}</ListItem>
))}
</UnorderedList>
</Flex>
)
}
>
<Button
onClick={handleRun}
colorScheme={"green"}
isLoading={isLoading}
loadingText={"Run"}
isDisabled={dataIssues.length > 0}
>
Run
</Button>
</Tooltip>
</ButtonGroup>
</ModalFooter>
</ModalContent>
Expand Down Expand Up @@ -194,6 +272,8 @@ const getLayoutedElements = <T extends object>(nodes: Node<T>[], edges: Edge<any
};
};

const nodeTypes = { serviceNode: KurtosisServiceNode, artifactNode: KurtosisArtifactNode };

type VisualiserImperativeAttributes = {
getStarlark: () => string;
};
Expand All @@ -212,8 +292,6 @@ const Visualiser = forwardRef<VisualiserImperativeAttributes, VisualiserProps>(
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes || []);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges || []);

const nodeTypes = useMemo(() => ({ serviceNode: KurtosisServiceNode, artifactNode: KurtosisArtifactNode }), []);

const onLayout = useCallback(() => {
const layouted = getLayoutedElements(nodes, edges);

Expand Down
Expand Up @@ -185,6 +185,14 @@ export const KurtosisServiceNode = memo(
size={"sm"}
placeholder={"Application Protocol (eg postgresql)"}
name={`${props.name as `ports.${number}`}.applicationProtocol`}
validate={(val) => {
if (typeof val !== "string") {
return "Value should be a string";
}
if (val.includes(" ")) {
return "Application protocol cannot include spaces";
}
}}
/>
</GridItem>
<GridItem>
Expand Down Expand Up @@ -212,6 +220,7 @@ export const KurtosisServiceNode = memo(
/>
</KurtosisFormControl>
</TabPanel>
<TabPanel></TabPanel>
<TabPanel>
<KurtosisFormControl<KurtosisServiceNodeData>
name={"files"}
Expand Down
Expand Up @@ -9,6 +9,7 @@ import {
ModalHeader,
ModalOverlay,
} from "@chakra-ui/react";
import { isDefined } from "kurtosis-ui-components";
import { FormProvider, useForm } from "react-hook-form";
import { KurtosisFormControl } from "../../../form/KurtosisFormControl";
import { StringArgumentInput } from "../../../form/StringArgumentInput";
Expand Down Expand Up @@ -41,7 +42,18 @@ export const NewFileModal = ({ isOpen, onClose, onConfirm }: NewFileModalProps)
helperText={"Enter the full file name for this file (including its path)"}
isRequired
>
<StringArgumentInput name={"fileName"} placeholder={"/some/path/to/file.txt"} />
<StringArgumentInput
name={"fileName"}
placeholder={"/some/path/to/file.txt"}
validate={(v?: string) => {
if (!isDefined(v)) {
return "input must be defined";
}
if (!v.startsWith("/")) {
return "File paths must start with a /";
}
}}
/>
</KurtosisFormControl>
</ModalBody>
<ModalFooter>
Expand Down
Expand Up @@ -11,15 +11,16 @@ import {
ModalOverlay,
Text,
} from "@chakra-ui/react";
import { isDefined } from "./utils";

type KurtosisAlertModalProps = {
title: string;
content: string;
isOpen: boolean;
isLoading?: boolean;
onClose: () => void;
onConfirm: () => void;
confirmText: string;
onConfirm?: () => void;
confirmText?: string;
confirmButtonProps?: ButtonProps;
};

Expand Down Expand Up @@ -47,9 +48,11 @@ export const KurtosisAlertModal = ({
<Button color={"gray.100"} onClick={onClose} isDisabled={isLoading}>
Dismiss
</Button>
<Button onClick={onConfirm} {...confirmButtonProps} isLoading={isLoading}>
{confirmText}
</Button>
{isDefined(onConfirm) && (
<Button onClick={onConfirm} {...confirmButtonProps} isLoading={isLoading}>
{confirmText}
</Button>
)}
</Flex>
</ModalFooter>
</ModalContent>
Expand Down

0 comments on commit 5dcdd9e

Please sign in to comment.